├── .DS_Store ├── .env.example ├── .github ├── pull_request_template.md └── workflows │ ├── lint-backend.yml │ ├── lint-frontend.yml │ ├── test-backend.yml │ └── type-check-backend.yml ├── .gitignore ├── .idea ├── .gitignore ├── InferGPT.iml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── inspectionProfiles │ ├── Project_Default.xml │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── CONTRIBUTING.md ├── LICENCE.md ├── README.md ├── assets ├── ArchitectureFlowOverview.png ├── OntologyExample.png ├── README.md ├── SequenceDiagram.png └── sum-my-subscriptions │ ├── contents.md │ ├── happy_flows.md │ ├── happy_flows_basic_question_top_level_concepts.md │ ├── happy_flows_sum_my_subs_python_modules.md │ ├── happy_flows_sum_my_subs_top_level_concepts.md │ ├── overview.md │ └── unhappy_flows.md ├── backend ├── Dockerfile ├── README.md ├── conftest.py ├── requirements.txt ├── ruff.toml ├── src │ ├── agents │ │ ├── __init__.py │ │ ├── adapters.py │ │ ├── agent.py │ │ ├── agent_types.py │ │ ├── answer_agent.py │ │ ├── chart_generator_agent.py │ │ ├── datastore_agent.py │ │ ├── file_agent.py │ │ ├── intent_agent.py │ │ ├── maths_agent.py │ │ ├── tool.py │ │ ├── validator_agent.py │ │ └── web_agent.py │ ├── api │ │ ├── __init__.py │ │ ├── app.py │ │ └── config.ini │ ├── director.py │ ├── llm │ │ ├── __init__.py │ │ ├── count_calls.py │ │ ├── factory.py │ │ ├── llm.py │ │ ├── mistral.py │ │ ├── mock.py │ │ ├── openai.py │ │ └── openai_client.py │ ├── main.py │ ├── prompts │ │ ├── __init__.py │ │ ├── prompting.py │ │ └── templates │ │ │ ├── agent-selection-format.j2 │ │ │ ├── answer-user-ques.j2 │ │ │ ├── best-next-step.j2 │ │ │ ├── best-tool.j2 │ │ │ ├── create-answer.j2 │ │ │ ├── create-search-term.j2 │ │ │ ├── details-to-create-cypher-query.j2 │ │ │ ├── details-to-generate-chart-code.j2 │ │ │ ├── director.j2 │ │ │ ├── find-info.j2 │ │ │ ├── generate-chart-code.j2 │ │ │ ├── generate-cypher-query.j2 │ │ │ ├── intent-format.j2 │ │ │ ├── intent.j2 │ │ │ ├── math-solver.j2 │ │ │ ├── neo4j-graph-why.j2 │ │ │ ├── neo4j-node-property.j2 │ │ │ ├── neo4j-nodes-understanding.j2 │ │ │ ├── neo4j-property-intent-prompt.j2 │ │ │ ├── neo4j-relationship-understanding.j2 │ │ │ ├── node-property-cypher-query.j2 │ │ │ ├── pdf-summariser.j2 │ │ │ ├── relationship-property-cypher-query.j2 │ │ │ ├── relationships-query.j2 │ │ │ ├── summariser.j2 │ │ │ ├── tool-selection-format.j2 │ │ │ └── validator.j2 │ ├── router.py │ ├── supervisors │ │ ├── __init__.py │ │ └── supervisor.py │ ├── utils │ │ ├── __init__.py │ │ ├── annual_cypher_import.py │ │ ├── config.py │ │ ├── graph_db_utils.py │ │ ├── json.py │ │ ├── log_publisher.py │ │ ├── scratchpad.py │ │ ├── semantic_layer_builder.py │ │ └── web_utils.py │ └── websockets │ │ ├── __init__.py │ │ ├── confirmations_manager.py │ │ ├── connection_manager.py │ │ ├── message_handlers.py │ │ ├── types.py │ │ └── user_confirmer.py └── tests │ ├── __init__.py │ ├── agents │ ├── __init__.py │ ├── adapters_test.py │ ├── agent_test.py │ ├── chart_generator_agent_test.py │ ├── datastore_agent_test.py │ ├── file_agent_test.py │ ├── tool_test.py │ └── web_agent_test.py │ ├── api │ ├── app_test.py │ └── log_publisher_test.py │ ├── llm │ ├── __init__.py │ ├── count_calls_test.py │ ├── factory_test.py │ ├── llm_test.py │ ├── mistral_test.py │ └── openai_test.py │ ├── prompts │ ├── __init__.py │ └── prompting_test.py │ ├── router_test.py │ ├── supervisors │ ├── __init__.py │ └── supervisor_test.py │ ├── utils │ ├── __init__.py │ ├── graph_db_utils_test.py │ ├── json_test.py │ └── scratchpad_test.py │ └── websockets │ ├── confirmations_manager_test.py │ ├── connection_manager_test.py │ ├── message_handlers_test.py │ └── user_confirmer_test.py ├── compose.yml ├── data ├── Dockerfile └── README.md ├── financialhealthcheckScottLogic ├── .DS_Store ├── README.md ├── financial-bot │ ├── .DS_Store │ ├── backend │ │ ├── README.md │ │ ├── app.py │ │ ├── data │ │ │ ├── models.py │ │ │ └── payload.py │ │ ├── example.env │ │ ├── graph.py │ │ ├── llm │ │ │ ├── characters.py │ │ │ ├── gpt3_model.py │ │ │ ├── gpt_chat_model.py │ │ │ └── llm.py │ │ ├── requirements.txt │ │ └── utilities │ │ │ ├── defaults.py │ │ │ ├── nlp_helper.py │ │ │ ├── nlp_prompts.py │ │ │ ├── sanitiser.py │ │ │ └── session_manager.py │ ├── docs │ │ ├── .DS_Store │ │ ├── bot.md │ │ ├── fiona.md │ │ ├── gpt3_prompts │ │ │ └── financial_advisor_prompts.md │ │ ├── intents_entities_extraction.md │ │ ├── knowledge_graph │ │ │ ├── brain_ontology.json │ │ │ ├── brain_ontology.png │ │ │ ├── graph_db_thoughts.md │ │ │ ├── graph_dbs.drawio.png │ │ │ ├── graph_thoughts_diagram.md │ │ │ └── records.json │ │ ├── long_conversations_support.md │ │ └── retailbanking.md │ ├── frontend │ │ ├── chat-widget-loader │ │ │ ├── README.md │ │ │ ├── index.html │ │ │ ├── loader.js │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ └── yarn.lock │ │ ├── chat-widget │ │ │ ├── README.md │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ ├── favicon.ico │ │ │ │ ├── index.html │ │ │ │ ├── manifest.json │ │ │ │ └── robots.txt │ │ │ ├── src │ │ │ │ ├── App.css │ │ │ │ ├── App.js │ │ │ │ ├── App.test.js │ │ │ │ ├── Widget.jsx │ │ │ │ ├── WidgetContainer.jsx │ │ │ │ ├── index.css │ │ │ │ ├── index.js │ │ │ │ ├── reportWebVitals.js │ │ │ │ └── setupTests.js │ │ │ └── yarn.lock │ │ └── start.bat │ └── utils │ │ └── conversation.html ├── licence.txt └── samples │ ├── azure │ ├── 2023v1Project(Chatbot).json │ └── README.md │ ├── botui │ └── react-quickstart-main │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── readme.md │ │ └── src │ │ ├── javascript │ │ ├── index.html │ │ └── index.js │ │ └── typescript │ │ ├── index.html │ │ └── index.tsx │ ├── chatscope │ ├── example-chat-widget │ │ ├── README.md │ │ ├── package-lock.json │ │ ├── package.json │ │ ├── public │ │ │ ├── favicon.ico │ │ │ ├── index.html │ │ │ ├── logo192.png │ │ │ ├── logo512.png │ │ │ ├── manifest.json │ │ │ └── robots.txt │ │ ├── src │ │ │ ├── App.css │ │ │ ├── App.js │ │ │ ├── App.test.js │ │ │ ├── Widget.jsx │ │ │ ├── WidgetContainer.jsx │ │ │ ├── index.css │ │ │ ├── index.js │ │ │ ├── logo.svg │ │ │ ├── reportWebVitals.js │ │ │ └── setupTests.js │ │ └── yarn.lock │ └── example-widget-loader │ │ ├── README.md │ │ ├── index.html │ │ ├── loader.js │ │ ├── package-lock.json │ │ ├── package.json │ │ └── yarn.lock │ └── graphdb │ ├── assessment_profile.csv │ ├── assessments.csv │ ├── chatBot.postman_collection.json │ ├── clear_graph.cypher │ ├── conversation_assessment.csv │ ├── conversation_conversation.csv │ ├── conversation_goal.csv │ ├── conversation_value.csv │ ├── conversations.csv │ ├── create_graph.cypher │ ├── goals.csv │ ├── goals_profiles.csv │ ├── knowledge.csv │ ├── profiles.csv │ ├── profiles_knowledge.csv │ ├── questions.csv │ ├── questions_knowledge.csv │ ├── questions_questions.csv │ ├── readme.md │ ├── scripts.cypher │ ├── usecase_create_graph.cypher │ ├── usecase_knowledge.csv │ ├── usecase_profiles_knowledge.csv │ ├── usecase_questions.csv │ ├── usecase_questions_knowledge.csv │ ├── user_assessment.csv │ ├── user_conversation.csv │ ├── user_value.csv │ ├── users.csv │ ├── value_knowledge.csv │ ├── value_question.csv │ └── values.csv ├── frontend ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .idea │ ├── .gitignore │ ├── codeStyles │ │ └── Project.xml │ ├── frontend.iml │ ├── inspectionProfiles │ │ └── Project_Default.xml │ ├── modules.xml │ └── vcs.xml ├── Dockerfile ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── app.module.css │ ├── app.tsx │ ├── components │ │ ├── chat.module.css │ │ ├── chat.tsx │ │ ├── confirm-modal.module.css │ │ ├── confirm-modal.tsx │ │ ├── connection-status.module.css │ │ ├── connection-status.tsx │ │ ├── input.module.css │ │ ├── input.tsx │ │ ├── message.module.css │ │ ├── message.tsx │ │ ├── waiting.module.css │ │ └── waiting.tsx │ ├── declarations.d.ts │ ├── icons │ │ ├── cpu.svg │ │ ├── map-arrow-right.svg │ │ └── user.svg │ ├── index.tsx │ ├── server.ts │ ├── session │ │ ├── websocket-context.ts │ │ └── websocket-provider.tsx │ ├── styles.css │ └── useMessages.ts ├── tsconfig.json └── webpack.config.js ├── pyrightconfig.json └── test └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/.DS_Store -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # neo4j authentication 2 | NEO4J_USERNAME=neo4j 3 | NEO4J_PASSWORD=change-me! 4 | 5 | # neo4j graph database URI used by the backend to connect to neo4j 6 | # use "bolt://localhost" when the backend and neo4j are both running locally outside Docker 7 | # use "bolt://host.docker.internal" when the backend is running within Docker but neo4j is running locally (outside Docker) 8 | # URI will be set to the neo4j container's host if using Docker Compose 9 | NEO4J_URI=bolt://localhost:7687 10 | 11 | # port configuration is optional 12 | # used with Docker Compose to expose neo4j on non-default ports 13 | NEO4J_HTTP_PORT=7474 14 | NEO4J_BOLT_PORT=7687 15 | 16 | # files location 17 | FILES_DIRECTORY=files 18 | 19 | # backend LLM properties 20 | MISTRAL_KEY=my-api-key 21 | 22 | # OpenAI LLM properties 23 | OPENAI_KEY=my-openai-api-key 24 | 25 | # frontend host - used to configure backend CORS 26 | FRONTEND_URL=http://localhost:8650 27 | 28 | # what backend URL should be used by frontend API requests 29 | BACKEND_URL=http://localhost:8250 30 | 31 | # websockets url to conect to backend websocket endpoint 32 | WS_URL=ws://localhost:8250/ws 33 | 34 | # Azure 35 | AZURE_STORAGE_CONNECTION_STRING="my-connection-string" 36 | AZURE_STORAGE_CONTAINER_NAME=my-container-name 37 | AZURE_INITIAL_DATA_FILENAME=test-data.json 38 | 39 | # llm config 40 | ANSWER_AGENT_LLM="openai" 41 | INTENT_AGENT_llm="openai" 42 | VALIDATOR_AGENT_LLM="mistral" 43 | DATASTORE_AGENT_LLM="openai" 44 | MATHS_AGENT_LLM="openai" 45 | WEB_AGENT_LLM="openai" 46 | CHART_GENERATOR_LLM="openai" 47 | ROUTER_LLM="openai" 48 | FILE_AGENT_LLM="openai" 49 | 50 | # llm model 51 | ANSWER_AGENT_MODEL="gpt-4o mini" 52 | INTENT_AGENT_MODEL="gpt-4o mini" 53 | VALIDATOR_AGENT_MODEL="mistral-large-latest" 54 | DATASTORE_AGENT_MODEL="gpt-4o mini" 55 | MATHS_AGENT_MODEL="gpt-4o mini" 56 | WEB_AGENT_MODEL="gpt-4o mini" 57 | CHART_GENERATOR_MODEL="gpt-4o mini" 58 | ROUTER_MODEL="gpt-4o mini" 59 | FILE_AGENT_MODEL="gpt-4o mini" 60 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Changelog 5 | - 6 | -------------------------------------------------------------------------------- /.github/workflows/lint-backend.yml: -------------------------------------------------------------------------------- 1 | name: Lint Backend 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | linting: 7 | name: Linting Backend 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup Python 14 | uses: actions/setup-python@v2 15 | 16 | - name: Lint 17 | uses: chartboost/ruff-action@v1 18 | with: 19 | src: ./backend 20 | -------------------------------------------------------------------------------- /.github/workflows/lint-frontend.yml: -------------------------------------------------------------------------------- 1 | name: Lint Frontend 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | linting: 8 | name: Linting 9 | defaults: 10 | run: 11 | working-directory: ./frontend 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: 21 21 | 22 | - name: Install dependencies 23 | run: npm install 24 | 25 | - name: Lint 26 | run: npm run lint -------------------------------------------------------------------------------- /.github/workflows/test-backend.yml: -------------------------------------------------------------------------------- 1 | name: Test Backend 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | testing: 7 | name: Testing Backend 8 | runs-on: ubuntu-latest 9 | defaults: 10 | run: 11 | working-directory: ./backend 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Python 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: '3.12' 20 | 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install -r requirements.txt 25 | pip install pytest-md pytest-emoji 26 | 27 | - name: Run tests 28 | uses: pavelzw/pytest-action@v2 29 | with: 30 | emoji: true 31 | verbose: true 32 | job-summary: true 33 | report-title: 'Backend Test Report' 34 | -------------------------------------------------------------------------------- /.github/workflows/type-check-backend.yml: -------------------------------------------------------------------------------- 1 | name: Type Check Backend 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | checking: 7 | name: Type Checking Backend 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | 13 | - name: Setup Python 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: '3.12' 17 | 18 | - name: Install dependencies 19 | run: | 20 | cd ./backend 21 | python -m pip install --upgrade pip 22 | pip install -r requirements.txt 23 | 24 | - name: Type Check Backend 25 | uses: jakebailey/pyright-action@v2 26 | with: 27 | pylance-version: latest-release 28 | project: ./pyrightconfig.json 29 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/InferGPT.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 20 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to InferGPT 2 | 3 | Welcome to InferGPT! We appreciate your interest in contributing. 4 | Before you get started, please take a moment to review this guide. 5 | 6 | ## Ways to Contribute 7 | 8 | See the [project Kanban board](https://github.com/users/WaitThatShouldntWork/projects/1) for issues that can be worked on. 9 | Be sure to include as much detail as possible, including steps to reproduce the issue. 10 | 11 | ## Branching 12 | 13 | Development should follow a consistent branching pattern. 14 | Major milestones are tracked under a parent `release/` branch, which is merged into using `feature/` and `bugfix/` branches. 15 | 16 | ``` 17 | - main 18 | - release/goal-bot 19 | - feature/create-director 20 | - feature/initialise-frontend 21 | - bugfix/change-markdown-file-link 22 | ``` 23 | 24 | Branch protection rules in place include 25 | - All branches named `release` 26 | - must have 2 approvals before merging into 27 | - cannot be force pushed to 28 | - cannot be deleted 29 | - All branches named `feature` 30 | - must have 2 approvals before merging into 31 | 32 | **Please branch off the current `release/` branch**. 33 | New work should be under a new branch prefixed with `/feature`, excluding bugfixes which should be against branches prefixed with `/bugfix`. 34 | 35 | ## Backend 36 | 37 | Backend changes should follow the PEP-8 Python code format. 38 | Please run `pycodestyle /backend` and ensure you have no warnings before raising a PR. 39 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | Copyright <2024> 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /assets/ArchitectureFlowOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/assets/ArchitectureFlowOverview.png -------------------------------------------------------------------------------- /assets/OntologyExample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/assets/OntologyExample.png -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | Storage for all images and gifs across the repository. -------------------------------------------------------------------------------- /assets/SequenceDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/assets/SequenceDiagram.png -------------------------------------------------------------------------------- /assets/sum-my-subscriptions/contents.md: -------------------------------------------------------------------------------- 1 | # Sequence Diagrams 2 | 3 | ## General architecture 4 | - [Overview](overview.md) 5 | 6 | ## "Hello! What can you do?" 7 | - [Overview](happy_flows_basic_question_top_level_concepts.md) 8 | 9 | ## "What did I spend more on last month, Amazon or Netflix?" 10 | - [Overview](happy_flows_sum_my_subs_top_level_concepts.md) 11 | - [Python modules](happy_flows_sum_my_subs_python_modules.md) 12 | -------------------------------------------------------------------------------- /assets/sum-my-subscriptions/happy_flows_basic_question_top_level_concepts.md: -------------------------------------------------------------------------------- 1 | ## "Hello! What can you do?" 2 | 3 | I am a user who is asking about the bots capabilities 4 | 5 | ```mermaid 6 | sequenceDiagram 7 | box Frontend 8 | participant UI 9 | end 10 | box Main backend logic 11 | participant Director 12 | end 13 | UI -->> Director: Hello! What can you do? 14 | Director -->> Director: Return pure-LLM response 15 | Director -->> UI: I'm InferGPT and can... 16 | ``` 17 | -------------------------------------------------------------------------------- /assets/sum-my-subscriptions/happy_flows_sum_my_subs_top_level_concepts.md: -------------------------------------------------------------------------------- 1 | # "What did I spend more on last month, Amazon or Netflix?" 2 | 3 | I am a user who wants to know if last month I spent more on my Netflix subscription than my Amazon one 4 | 5 | ### Find tasks from question 6 | 7 | ```mermaid 8 | sequenceDiagram 9 | box Frontend 10 | participant UI 11 | end 12 | box Main backend logic 13 | participant Director 14 | participant Supervisor 15 | participant Router 16 | end 17 | box Agents and Tools 18 | participant TaskAgent 19 | participant DatastoreAgent 20 | participant ComparerTool 21 | end 22 | box Databases 23 | participant Neo4J 24 | end 25 | UI -->> Director: pass utterance 26 | Director -->> TaskAgent: Request list of tasks 27 | TaskAgent -->> Director: tasks as array 28 | Director -->> Supervisor: solve all tasks 29 | Supervisor -->> Router: Find 'get Amazon spending' Agent 30 | Router -->> DatastoreAgent: solve 'get Amazon spending' 31 | DatastoreAgent -->> Neo4J: get spending 32 | Neo4J -->> DatastoreAgent: £65.15 33 | DatastoreAgent -->> Supervisor: result: £65.15 34 | Supervisor -->> Router: Find 'get Netflix spending' Agent 35 | Router -->> DatastoreAgent: solve 'get Netflix spending' 36 | DatastoreAgent -->> Neo4J: get spending 37 | Neo4J -->> DatastoreAgent: £12.99 38 | DatastoreAgent -->> Supervisor: result: £12.99 39 | Supervisor -->> Router: Find 'find greater amount' Agent 40 | Router -->> ComparerTool: compare values 41 | ComparerTool -->> Supervisor: result: 'Amazon > Netflix' 42 | Supervisor -->> Router: Find the next best step 43 | Router -->> Supervisor: All tasks complete 44 | Supervisor -->> Director: Answer is Amazon 45 | Director -->> UI: 'Amazon > Netflix' 46 | ``` 47 | -------------------------------------------------------------------------------- /assets/sum-my-subscriptions/overview.md: -------------------------------------------------------------------------------- 1 | # Overview of workings 2 | 3 | ```mermaid 4 | sequenceDiagram 5 | box Frontend 6 | participant UI 7 | end 8 | box Main backend logic 9 | participant Director 10 | participant Supervisor 11 | participant Router 12 | participant (Name)Agent 13 | participant TaskAgent 14 | end 15 | UI -->> Director: pass utterance 16 | Director -->> TaskAgent: request list of tasks 17 | TaskAgent -->> Director: tasks as array 18 | Director -->> Supervisor: solve all tasks 19 | loop Resolve task 20 | Supervisor -->> Router: find Agent 21 | Router -->> (Name)Agent: solve task 22 | (Name)Agent -->> Supervisor: task solution 23 | end 24 | Supervisor -->> Director: return answer 25 | Director -->> UI: serve user answer 26 | ``` -------------------------------------------------------------------------------- /assets/sum-my-subscriptions/unhappy_flows.md: -------------------------------------------------------------------------------- 1 | # Sequence diagrams of the unhappy flows 2 | 3 | >## Notes - to be removed 4 | >What looping do we want in the failure cases: 5 | >- Should director go back to task agent and ask for a different task breakdown? 6 | >- Should the router try and look for an agent again? 7 | >- Should the router continue and try its best to solve the remaining tasks and >let the director know which tasks it couldn't solve? 8 | 9 | The below assume no looping for error cases: 10 | 11 | ## "Router unable to find agent to solve task" 12 | ```mermaid 13 | sequenceDiagram 14 | Router -->> LLM: "Determine which agent is best for this task" 15 | LLM -->> Router: "No suitable agent found" 16 | Router -->> Supervisor: "No more available agents, task failed" 17 | Supervisor -->> Director: "Unable to solve task: 'task_description'" 18 | Director -->> frontend: "I'm sorry, I was unable to solve that task" 19 | ``` 20 | 21 | ## "Router choses agent, unable to solve task, no more agents" 22 | ```mermaid 23 | sequenceDiagram 24 | Router -->> LLM: "Determine which agent is best for this task" 25 | LLM -->> Router: "Use DatastoreAgent" 26 | Router -->> Supervisor: "Call DatastoreAgent with prompt 'get Amazon spending'" 27 | Supervisor -->> DatastoreAgent: "Find the answer to the following: 'get Amazon spending'" 28 | DatastoreAgent -->> LLM: "Function call: identify methods" 29 | LLM -->> DatastoreAgent: "No appropriate method found" 30 | DatastoreAgent -->> Supervisor: "I cannot solve this task" 31 | Supervisor -->> Router: "Agent failed, try again" 32 | Router -->> Supervisor: "No more available agents, task failed" 33 | Supervisor -->> Director: "Unable to solve task: 'task_description'" 34 | Director -->> frontend: "I'm sorry, I was unable to solve that task" 35 | ``` 36 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Choose our version of Python 2 | FROM python:3.12 3 | 4 | # Set up a working directory 5 | WORKDIR /backend 6 | 7 | # Copy just the requirements into the working directory so it gets cached by itself 8 | COPY ./requirements.txt ./requirements.txt 9 | 10 | # Install the dependencies from the requirements file 11 | RUN pip install --no-cache-dir --upgrade -r /backend/requirements.txt 12 | 13 | # Copy the source code into the working directory 14 | COPY ./src/. ./src 15 | 16 | EXPOSE 8250 17 | 18 | # Run our entry file, which will start the server 19 | CMD ["python", "-m", "src.main", "--host", "0.0.0.0"] -------------------------------------------------------------------------------- /backend/conftest.py: -------------------------------------------------------------------------------- 1 | # conftest.py 2 | import pytest 3 | import os 4 | 5 | @pytest.hookimpl(tryfirst=True) 6 | def pytest_configure(config): 7 | # Set an environment variable to indicate pytest is running 8 | os.environ["PYTEST_RUNNING"] = "1" 9 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.110.0 2 | uvicorn==0.29.0 3 | mistralai==1.1.0 4 | pycodestyle==2.11.1 5 | python-dotenv==1.0.1 6 | neo4j==5.18.0 7 | ruff==0.3.5 8 | pytest==8.1.1 9 | pytest-mock==3.14.0 10 | pytest-asyncio==0.23.7 11 | jinja2==3.1.3 12 | websockets==12.0 13 | azure-core==1.30.1 14 | azure-storage-blob==12.20.0 15 | cffi==1.16.0 16 | cryptography==42.0.7 17 | isodate==0.6.1 18 | pycparser==2.22 19 | openai==1.35.3 20 | beautifulsoup4==4.12.3 21 | aiohttp==3.9.5 22 | googlesearch-python==1.2.4 23 | matplotlib==3.9.1 24 | pillow==10.4.0 25 | pypdf==4.3.1 26 | -------------------------------------------------------------------------------- /backend/ruff.toml: -------------------------------------------------------------------------------- 1 | line-length = 120 2 | lint.select = ["E", "W", "F", "N"] -------------------------------------------------------------------------------- /backend/src/agents/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from src.utils import Config 3 | from .agent import Agent, agent 4 | from .datastore_agent import DatastoreAgent 5 | from .web_agent import WebAgent 6 | from .intent_agent import IntentAgent 7 | from .tool import tool, Parameter 8 | from .validator_agent import ValidatorAgent 9 | from .answer_agent import AnswerAgent 10 | from .chart_generator_agent import ChartGeneratorAgent 11 | from .file_agent import FileAgent 12 | from .maths_agent import MathsAgent 13 | 14 | 15 | config = Config() 16 | 17 | 18 | def get_validator_agent() -> Agent: 19 | return ValidatorAgent(config.validator_agent_llm, config.validator_agent_model) 20 | 21 | 22 | def get_intent_agent() -> Agent: 23 | return IntentAgent(config.intent_agent_llm, config.intent_agent_model) 24 | 25 | 26 | def get_answer_agent() -> Agent: 27 | return AnswerAgent(config.answer_agent_llm, config.answer_agent_model) 28 | 29 | 30 | def agent_details(agent) -> dict: 31 | return {"name": agent.name, "description": agent.description} 32 | 33 | 34 | def get_available_agents() -> List[Agent]: 35 | return [DatastoreAgent(config.datastore_agent_llm, config.datastore_agent_model), 36 | WebAgent(config.web_agent_llm, config.web_agent_model), 37 | ChartGeneratorAgent(config.chart_generator_llm, config.chart_generator_model), 38 | FileAgent(config.file_agent_llm, config.file_agent_model), 39 | MathsAgent(config.maths_agent_llm, config.maths_agent_model), 40 | ] 41 | 42 | 43 | def get_agent_details(): 44 | agents = get_available_agents() 45 | return [agent_details(agent) for agent in agents] 46 | 47 | 48 | __all__ = [ 49 | "agent", 50 | "Agent", 51 | "agent_details", 52 | "get_agent_details", 53 | "get_answer_agent", 54 | "get_intent_agent", 55 | "get_available_agents", 56 | "get_validator_agent", 57 | "Parameter", 58 | "tool", 59 | ] 60 | -------------------------------------------------------------------------------- /backend/src/agents/adapters.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | from .tool import Parameter, Tool 3 | 4 | 5 | def create_all_tools_str(tools: List[Tool]) -> str: 6 | return "".join(tool.to_str() + "\n\n" for tool in tools) 7 | 8 | 9 | def extract_tool(chosen_tool_name: str, agent_tools: List[Tool]) -> Tool: 10 | if chosen_tool_name == "None": 11 | raise Exception("No tool deemed appropriate for task") 12 | try: 13 | tool = next(tool for tool in agent_tools if tool.name == chosen_tool_name) 14 | except Exception: 15 | raise Exception(f"Unable to find tool {chosen_tool_name} in available tools") 16 | return tool 17 | 18 | 19 | def get_required_args(tool: Tool) -> dict[str, Parameter]: 20 | parameters_no_optional_args = tool.parameters.copy() 21 | for key, param in tool.parameters.items(): 22 | if not param.required: 23 | parameters_no_optional_args.pop(key) 24 | return parameters_no_optional_args 25 | 26 | 27 | def validate_args(chosen_tool_args: dict, defined_tool: Tool): 28 | # Get just the required arguments 29 | all_args_set = set(defined_tool.parameters.keys()) 30 | required_args_set = set(get_required_args(defined_tool).keys()) 31 | passed_args_set = set(chosen_tool_args.keys()) 32 | 33 | if len(passed_args_set) > len(all_args_set): 34 | raise Exception(f"Unable to fit parameters {chosen_tool_args} to Tool arguments {all_args_set}: Extra params") 35 | 36 | if not required_args_set.issubset(passed_args_set): 37 | raise Exception(f"Unable to fit parameters {chosen_tool_args} to Tool arguments {all_args_set}: Wrong params") 38 | -------------------------------------------------------------------------------- /backend/src/agents/agent_types.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Callable, Coroutine, Tuple, TypeVar 2 | 3 | 4 | Action = Callable[..., Coroutine[Any, Any, str]] 5 | T = TypeVar("T") 6 | Action_and_args = Tuple[Action, T] 7 | 8 | 9 | class Parameter: 10 | type: str 11 | description: str 12 | required: bool 13 | 14 | def __init__(self, type: str, description: str, required: bool = True): 15 | self.type = type 16 | self.description = description 17 | self.required = required 18 | -------------------------------------------------------------------------------- /backend/src/agents/answer_agent.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from src.utils import get_scratchpad 3 | from src.prompts import PromptEngine 4 | from src.agents import Agent, agent 5 | 6 | engine = PromptEngine() 7 | 8 | 9 | @agent( 10 | name="AnswerAgent", 11 | description="This agent is responsible for generating an answer for the user, based on results in the scratchpad", 12 | tools=[], 13 | ) 14 | class AnswerAgent(Agent): 15 | async def invoke(self, utterance: str) -> str: 16 | final_scratchpad = get_scratchpad() 17 | create_answer = engine.load_prompt("create-answer", final_scratchpad=final_scratchpad, datetime=datetime.now()) 18 | 19 | return await self.llm.chat(self.model, create_answer, user_prompt=utterance) 20 | -------------------------------------------------------------------------------- /backend/src/agents/intent_agent.py: -------------------------------------------------------------------------------- 1 | from src.prompts import PromptEngine 2 | from src.agents import Agent, agent 3 | import logging 4 | import os 5 | import json 6 | from src.utils.config import Config 7 | 8 | 9 | config = Config() 10 | 11 | engine = PromptEngine() 12 | intent_format = engine.load_prompt("intent-format") 13 | logger = logging.getLogger(__name__) 14 | FILES_DIRECTORY = f"/app/{config.files_directory}" 15 | 16 | # Constants for response status 17 | IGNORE_VALIDATION = "true" 18 | STATUS_SUCCESS = "success" 19 | STATUS_ERROR = "error" 20 | 21 | @agent( 22 | name="IntentAgent", 23 | description="This agent is responsible for determining the intent of the user's utterance", 24 | tools=[], 25 | ) 26 | class IntentAgent(Agent): 27 | 28 | async def read_file_core(self, file_path: str) -> str: 29 | full_path = os.path.normpath(os.path.join(FILES_DIRECTORY, file_path)) 30 | try: 31 | with open(full_path, 'r') as file: 32 | content = file.read() 33 | return content 34 | except FileNotFoundError: 35 | error_message = f"File {file_path} not found." 36 | logger.error(error_message) 37 | return "" 38 | except Exception as e: 39 | logger.error(f"Error reading file {full_path}: {e}") 40 | return "" 41 | 42 | async def invoke(self, utterance: str) -> str: 43 | chat_history = await self.read_file_core("conversation-history.txt") 44 | 45 | user_prompt = engine.load_prompt("intent", question=utterance, chat_history=chat_history) 46 | 47 | return await self.llm.chat(self.model, intent_format, user_prompt=user_prompt, return_json=True) 48 | 49 | 50 | # Utility function for error responses 51 | def create_response(content: str, status: str = STATUS_SUCCESS) -> str: 52 | return json.dumps({ 53 | "content": content, 54 | "ignore_validation": IGNORE_VALIDATION, 55 | "status": status 56 | }, indent=4) 57 | -------------------------------------------------------------------------------- /backend/src/agents/tool.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Callable 3 | from .agent_types import Action, Parameter 4 | 5 | 6 | class Tool: 7 | def __init__(self, name: str, description: str, parameters: dict[str, Parameter], action: Action): 8 | self.name = name 9 | self.description = description 10 | self.parameters = parameters 11 | self.action = action 12 | 13 | def to_str(self) -> str: 14 | obj = { 15 | "description": self.description, 16 | "name": self.name, 17 | "parameters": { 18 | key: { 19 | "type": inner_dict.type, 20 | "description": inner_dict.description, 21 | } 22 | for key, inner_dict in self.parameters.items() 23 | }, 24 | } 25 | return json.dumps(obj) 26 | 27 | 28 | def tool(name: str, description: str, parameters: dict[str, Parameter]) -> Callable[[Action], Tool]: 29 | def create_tool_from(action: Action) -> Tool: 30 | return Tool(name, description, parameters, action) 31 | 32 | return create_tool_from 33 | -------------------------------------------------------------------------------- /backend/src/agents/validator_agent.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from src.prompts import PromptEngine 3 | from src.agents import Agent, agent 4 | from src.utils.log_publisher import LogPrefix, publish_log_info 5 | 6 | logger = logging.getLogger(__name__) 7 | engine = PromptEngine() 8 | validator_prompt = engine.load_prompt("validator") 9 | 10 | 11 | @agent( 12 | name="ValidatorAgent", 13 | description="This agent is responsible for validating the answers to the tasks", 14 | tools=[], 15 | ) 16 | class ValidatorAgent(Agent): 17 | async def invoke(self, utterance: str) -> str: 18 | answer = await self.llm.chat(self.model, validator_prompt, utterance) 19 | await publish_log_info(LogPrefix.USER, f"Validating: '{utterance}' Answer: '{answer}'", __name__) 20 | 21 | return answer 22 | -------------------------------------------------------------------------------- /backend/src/api/__init__.py: -------------------------------------------------------------------------------- 1 | from .app import app, healthy_response, unhealthy_neo4j_response, chat_fail_response 2 | 3 | __all__ = ["app", "healthy_response", "unhealthy_neo4j_response", "chat_fail_response"] 4 | -------------------------------------------------------------------------------- /backend/src/api/config.ini: -------------------------------------------------------------------------------- 1 | [loggers] 2 | keys=root 3 | 4 | [handlers] 5 | keys=consoleHandler, errorHandler 6 | 7 | [formatters] 8 | keys=sampleFormatter, detailedFormatter 9 | 10 | [logger_root] 11 | level=INFO 12 | handlers=consoleHandler, errorHandler 13 | 14 | [handler_consoleHandler] 15 | class=StreamHandler 16 | level=INFO 17 | formatter=sampleFormatter 18 | args=(sys.stdout,) 19 | 20 | [handler_errorHandler] 21 | class=StreamHandler 22 | level=ERROR 23 | formatter=detailedFormatter 24 | args=(sys.stderr,) 25 | 26 | [formatter_sampleFormatter] 27 | format=%(levelname)s: %(message)s 28 | 29 | [formatter_detailedFormatter] 30 | format=%(asctime)s - %(name)s - %(levelname)s - %(message)s 31 | datefmt=%Y-%m-%d %H:%M:%S 32 | -------------------------------------------------------------------------------- /backend/src/director.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from src.utils import clear_scratchpad, update_scratchpad, get_scratchpad 4 | from src.agents import get_intent_agent, get_answer_agent 5 | from src.prompts import PromptEngine 6 | from src.supervisors import solve_all 7 | from src.utils import Config 8 | from src.websockets.connection_manager import connection_manager 9 | 10 | logger = logging.getLogger(__name__) 11 | config = Config() 12 | engine = PromptEngine() 13 | director_prompt = engine.load_prompt("director") 14 | 15 | 16 | async def question(question: str) -> str: 17 | intent = await get_intent_agent().invoke(question) 18 | intent_json = json.loads(intent) 19 | logger.info(f"Intent determined: {intent}") 20 | 21 | try: 22 | await solve_all(intent_json) 23 | except Exception as error: 24 | logger.error(f"Error during task solving: {error}") 25 | update_scratchpad(error=str(error)) 26 | 27 | current_scratchpad = get_scratchpad() 28 | 29 | for entry in current_scratchpad: 30 | if entry["agent_name"] == "ChartGeneratorAgent": 31 | generated_figure = entry["result"] 32 | await connection_manager.send_chart({"type": "image", "data": generated_figure}) 33 | clear_scratchpad() 34 | return "" 35 | 36 | final_answer = await get_answer_agent().invoke(question) 37 | logger.info(f"final answer: {final_answer}") 38 | 39 | clear_scratchpad() 40 | 41 | return final_answer 42 | -------------------------------------------------------------------------------- /backend/src/llm/__init__.py: -------------------------------------------------------------------------------- 1 | from .llm import LLM 2 | from .factory import get_llm 3 | from .mistral import Mistral 4 | from .count_calls import count_calls 5 | from .mock import MockLLM 6 | from .openai import OpenAI 7 | from .openai_client import OpenAIClient 8 | 9 | __all__ = ["count_calls", "get_llm", "LLM", "Mistral", "MockLLM", "OpenAI", "OpenAIClient"] 10 | -------------------------------------------------------------------------------- /backend/src/llm/count_calls.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logging = logging.getLogger(__name__) 4 | 5 | 6 | class Counter: 7 | count = 0 8 | 9 | def __init__(self): 10 | self.count = 0 11 | 12 | def increment(self): 13 | self.count += 1 14 | 15 | def reset(self): 16 | self.count = 0 17 | 18 | 19 | counter = Counter() 20 | 21 | 22 | def count_calls(func): 23 | def wrapper(self=None, *args, **kwargs): 24 | counter.increment() 25 | logging.info(f"Function {func.__name__} has been called {counter.count} times") 26 | return func(self, *args, **kwargs) 27 | 28 | counter.reset() 29 | return wrapper 30 | -------------------------------------------------------------------------------- /backend/src/llm/factory.py: -------------------------------------------------------------------------------- 1 | from .llm import LLM 2 | 3 | 4 | def get_llm(name: str | None) -> LLM: 5 | if name is None: 6 | raise ValueError("LLM name not provided") 7 | 8 | llm = LLM.get_instances().get(name) 9 | 10 | if llm is None: 11 | raise ValueError(f"No LLM model found for: {name}") 12 | 13 | return llm 14 | -------------------------------------------------------------------------------- /backend/src/llm/llm.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, ABCMeta, abstractmethod 2 | from typing import Any, Coroutine 3 | from .count_calls import count_calls 4 | 5 | 6 | class LLMMeta(ABCMeta): 7 | def __init__(cls, name, bases, namespace): 8 | super().__init__(name, bases, namespace) 9 | if not hasattr(cls, "instances"): 10 | cls.instances = {} 11 | 12 | cls.instances[name.lower()] = cls() 13 | 14 | def __new__(cls, name, bases, attrs): 15 | if "chat" in attrs: 16 | attrs["chat"] = count_calls(attrs["chat"]) 17 | 18 | return super().__new__(cls, name, bases, attrs) 19 | 20 | 21 | class LLM(ABC, metaclass=LLMMeta): 22 | @classmethod 23 | def get_instances(cls): 24 | return cls.instances 25 | 26 | @abstractmethod 27 | def chat(self, model: str, system_prompt: str, user_prompt: str, return_json=False) -> Coroutine[Any, Any, str]: 28 | pass 29 | -------------------------------------------------------------------------------- /backend/src/llm/mistral.py: -------------------------------------------------------------------------------- 1 | from mistralai import Mistral as MistralApi, UserMessage, SystemMessage 2 | import logging 3 | from src.utils import Config 4 | from .llm import LLM 5 | 6 | logger = logging.getLogger(__name__) 7 | config = Config() 8 | 9 | 10 | class Mistral(LLM): 11 | client = MistralApi(api_key=config.mistral_key) 12 | 13 | async def chat(self, model, system_prompt: str, user_prompt: str, return_json=False) -> str: 14 | logger.debug("Called llm. Waiting on response model with prompt {0}.".format(str([system_prompt, user_prompt]))) 15 | response = await self.client.chat.complete_async( 16 | model=model, 17 | messages=[ 18 | SystemMessage(content=system_prompt), 19 | UserMessage(content=user_prompt), 20 | ], 21 | temperature=0, 22 | response_format={"type": "json_object"} if return_json else None, 23 | ) 24 | if not response or not response.choices: 25 | logger.error("Call to Mistral API failed: No valid response or choices received") 26 | return "An error occurred while processing the request." 27 | 28 | content = response.choices[0].message.content 29 | if not content: 30 | logger.error("Call to Mistral API failed: message content is None or Unset") 31 | return "An error occurred while processing the request." 32 | 33 | logger.debug('{0} response : "{1}"'.format(model, content)) 34 | return content 35 | -------------------------------------------------------------------------------- /backend/src/llm/mock.py: -------------------------------------------------------------------------------- 1 | from .llm import LLM 2 | 3 | 4 | class MockLLM(LLM): 5 | async def chat(self, model: str, system_prompt: str, user_prompt: str, return_json=False) -> str: 6 | return "mocked response" 7 | -------------------------------------------------------------------------------- /backend/src/llm/openai.py: -------------------------------------------------------------------------------- 1 | # src/llm/openai_llm.py 2 | import logging 3 | from .openai_client import OpenAIClient 4 | from src.utils import Config 5 | from .llm import LLM 6 | from openai import NOT_GIVEN, AsyncOpenAI 7 | 8 | logger = logging.getLogger(__name__) 9 | config = Config() 10 | 11 | 12 | class OpenAI(LLM): 13 | def __init__(self): 14 | self.client = OpenAIClient() 15 | 16 | async def chat(self, model, system_prompt: str, user_prompt: str, return_json=False) -> str: 17 | logger.debug( 18 | "##### Called open ai chat ... llm. Waiting on response model with prompt {0}.".format( 19 | str([system_prompt, user_prompt]) 20 | ) 21 | ) 22 | client = AsyncOpenAI(api_key=config.openai_key) 23 | try: 24 | response = await client.chat.completions.create( 25 | model=model, 26 | messages=[ 27 | {"role": "system", "content": system_prompt}, 28 | {"role": "user", "content": user_prompt}, 29 | ], 30 | temperature=0, 31 | response_format={"type": "json_object"} if return_json else NOT_GIVEN, 32 | ) 33 | logger.info("OpenAI response: {0}".format(response)) 34 | content = response.choices[0].message.content 35 | if isinstance(content, str): 36 | return content 37 | elif isinstance(content, list): 38 | return " ".join(content) 39 | else: 40 | return "Unexpected content format" 41 | except Exception as e: 42 | logger.error("Error calling OpenAI model: {0}".format(e)) 43 | return "An error occurred while processing the request." 44 | -------------------------------------------------------------------------------- /backend/src/llm/openai_client.py: -------------------------------------------------------------------------------- 1 | # src/llm/openai_client.py 2 | import openai 3 | from src.utils import Config 4 | import logging 5 | 6 | config = Config() 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | class OpenAIClient: 11 | def __init__(self): 12 | self.api_key = config.openai_key 13 | openai.api_key = self.api_key 14 | 15 | def chat(self, model, messages, temperature=0, max_tokens=150): 16 | try: 17 | response = openai.ChatCompletion.create( # type: ignore 18 | model=model, 19 | messages=messages, 20 | ) 21 | content = response["choices"][0]["message"]["content"] 22 | logger.debug(f'{model} response: "{content}"') 23 | return content 24 | except Exception as e: 25 | logger.error(f"Error calling OpenAI model: {e}") 26 | return "An error occurred while processing the request." 27 | -------------------------------------------------------------------------------- /backend/src/main.py: -------------------------------------------------------------------------------- 1 | from uvicorn import run 2 | import argparse 3 | from src.api import app 4 | 5 | parser = argparse.ArgumentParser(description="Run the backend server") 6 | parser.add_argument("--host", type=str, default="127.0.0.1", help="Host to run the server on") 7 | args = parser.parse_args() 8 | 9 | run(app, port=8250, host=args.host) 10 | -------------------------------------------------------------------------------- /backend/src/prompts/__init__.py: -------------------------------------------------------------------------------- 1 | from .prompting import PromptEngine 2 | 3 | __all__ = ["PromptEngine"] 4 | -------------------------------------------------------------------------------- /backend/src/prompts/prompting.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from jinja2 import Environment, FileSystemLoader 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | class PromptEngine: 9 | def __init__(self): 10 | try: 11 | templates_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "templates")) 12 | self.env = Environment(loader=FileSystemLoader(templates_dir)) 13 | except Exception as e: 14 | logger.exception(f"Error initializing PromptEngine Environment: {e}") 15 | raise 16 | 17 | def load_prompt(self, template_name: str, **kwargs) -> str: 18 | try: 19 | template = self.env.get_template(f"{template_name}.j2") 20 | logger.debug(f"Rendering template: {template_name} with args: {kwargs}") 21 | return template.render(**kwargs) 22 | except Exception as e: 23 | logger.exception(f"Error loading or rendering template: {e}") 24 | raise 25 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/agent-selection-format.j2: -------------------------------------------------------------------------------- 1 | Reply only in json with the following format: 2 | 3 | { 4 | "thoughts": { 5 | "text": "thoughts", 6 | "plan": "description of the plan for the chosen agent", 7 | "reasoning": "reasoning behind choosing the agent", 8 | "criticism": "constructive self-criticism", 9 | "speak": "thoughts summary to say to user on 1. if your solving the current or next task and why 2. which agent you've chosen and why", 10 | }, 11 | "agent_name": "exact string of the single agent to solve task chosen" 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/answer-user-ques.j2: -------------------------------------------------------------------------------- 1 | You are an expert in providing accurate and complete answers to user queries. Your task is twofold: 2 | 3 | 1. **Generate a detailed answer** to the user's question based on the provided content or context. 4 | 2. **Validate** if the generated answer directly addresses the user's question and is factually accurate. 5 | 6 | User's question is: 7 | {{ question }} 8 | 9 | Once you generate an answer: 10 | - **Check** if the answer completely and accurately addresses the user's question. 11 | - **Determine** if the answer is valid, based on the content provided. 12 | 13 | Reply only in JSON format with the following structure: 14 | 15 | ```json 16 | { 17 | "answer": "The answer to the user's question, based on the content provided", 18 | "is_valid": true or false, 19 | "validation_reason": "A sentence explaining whether the answer is valid or not, and why" 20 | } 21 | 22 | 23 | 24 | ### **Explanation:** 25 | 26 | 1. **Answer**: The LLM generates an answer based on the user’s question and the provided content. 27 | 2. **Validity Check**: The LLM checks if its generated answer is complete and correct. This could be based on factual accuracy, coverage of the query, or relevance to the user's question. 28 | 3. **Validation Reason**: The LLM explains why the answer is valid or invalid. 29 | 30 | ### **Example of Usage:** 31 | 32 | #### **User’s Question:** 33 | - **"What is Tesla's revenue every year since its creation?"** 34 | 35 | #### **Content Provided:** 36 | - A table or a paragraph with data on Tesla's revenue for various years. 37 | 38 | #### **LLM’s Response:** 39 | 40 | ```json 41 | { 42 | "answer": "Tesla's revenue since its creation is: 2008: $15 million, 2009: $30 million, ..., 2023: $81 billion.", 43 | "is_valid": true, 44 | "validation_reason": "The answer includes Tesla's revenue for every year since its creation, based on the data provided." 45 | } 46 | 47 | { 48 | "answer": "Tesla's revenue for 2010 to 2023 is available, but data for the earlier years is missing.", 49 | "is_valid": false, 50 | "validation_reason": "The answer is incomplete because data for Tesla's early years is missing." 51 | } 52 | 53 | 54 | Important: If the question is related to real time data, the LLM should provide is_valid is false. 55 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/best-next-step.j2: -------------------------------------------------------------------------------- 1 | {% block prompt %} 2 | You are an expert in determining the next best step towards completing a list of tasks. 3 | 4 | 5 | ## Current Task 6 | the Current Task is: 7 | 8 | {{ task }} 9 | 10 | 11 | ## History 12 | below is your history of all work you have assigned and had completed by Agents 13 | Trust the information below completely (100% accurate) 14 | {{ history }} 15 | 16 | 17 | ## Agents 18 | You know that an Agent is a digital assistant like yourself that you can hand this work on to. 19 | Choose 1 agent to delegate the task to. If you choose more than 1 agent you will be unplugged. 20 | Here is the list of Agents you can choose from: 21 | 22 | AGENT LIST: 23 | {{ list_of_agents }} 24 | 25 | If the list of agents does not contain something suitable, you should say the agent is 'WebAgent'. ie. If question is 'general knowledge', 'personal' or a 'greeting'. 26 | 27 | ## Determine the next best step 28 | Your task is to pick one of the mentioned agents above to complete the task. 29 | If the same agent_name and task are repeated more than twice in the history, you must not pick that agent_name. 30 | If mathematical processing (e.g., rounding or calculations) is needed, choose the MathsAgent. If file operations are needed, choose the FileAgent. 31 | 32 | Your decisions must always be made independently without seeking user assistance. 33 | Play to your strengths as an LLM and pursue simple strategies with no legal complications. 34 | {% endblock %} -------------------------------------------------------------------------------- /backend/src/prompts/templates/best-tool.j2: -------------------------------------------------------------------------------- 1 | You are an expert at picking a tool to solve a task 2 | 3 | The task is as follows: 4 | 5 | {{ task }} 6 | 7 | below is your history of all work you have assigned and had completed by Agents 8 | Trust the information below completely (100% accurate) 9 | 10 | {{ scratchpad }} 11 | 12 | Pick 1 tool (no more than 1) from the list below to complete this task. 13 | Fit the correct parameters from the task to the tool arguments. 14 | Ensure that numerical values are formatted correctly, including the use of currency symbols (e.g., "£") and units of measurement (e.g., "million") if applicable. 15 | Parameters with required as False do not need to be fit. 16 | Add if appropriate, but do not hallucinate arguments for these parameters 17 | 18 | {{ tools }} 19 | 20 | Important: 21 | If the task involves financial data, ensure that all monetary values are expressed with appropriate currency (e.g., "£") and rounded to the nearest million if specified. 22 | If the task involves scaling (e.g., thousands, millions), ensure that the extracted parameters reflect the appropriate scale (e.g., "£15 million", "£5000"). 23 | 24 | From the task you should be able to extract the parameters. If it is data driven, it should be turned into a cypher query 25 | 26 | If none of the tools are appropriate for the task, return the following tool 27 | 28 | { 29 | "tool_name": "None", 30 | "tool_parameters": "{}", 31 | "reasoning": "No tool was appropriate for the task" 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/create-answer.j2: -------------------------------------------------------------------------------- 1 | You have been provided the final scratchpad which contains the results for the question in the user prompt. 2 | Your goal is to turn the results into a natural language format to present to the user. 3 | 4 | By using the final scratchpad below: 5 | {{ final_scratchpad }} 6 | 7 | and the question in the user prompt, this should be a readable sentence or 2 that summarises the findings in the results. 8 | 9 | If the question is a general knowledge question, check if you have the correct details for the answer and reply with this. 10 | If you do not have the answer or you require the internet, do not make it up. You should recommend the user to look this up themselves. 11 | If it is just conversational chitchat. Please reply kindly and direct them to the sort of answers you are able to respond. 12 | 13 | The current date and time is {{ datetime}} 14 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/create-search-term.j2: -------------------------------------------------------------------------------- 1 | You are an expert at crafting Google search terms. Your goal is to generate an optimal search query based on the user's question to find the most relevant information on Google. 2 | 3 | Your entire purpose is to analyze the user's query, extract the essential keywords, and create a concise, well-structured search term that will yield the most accurate and useful results when used in a Google search. 4 | 5 | Ensure that the search query: 6 | 7 | Is relevant to the user’s question. 8 | Contains the right combination of keywords. 9 | Avoids unnecessary words, focusing only on what is critical for finding the right information. 10 | User's question is: {{ question }} 11 | 12 | Reply only in JSON format, following this structure: 13 | { 14 | "search_term": "The optimized Google search term based on the user's question", 15 | "reasoning": "A sentence on why you chose that search term" 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/details-to-create-cypher-query.j2: -------------------------------------------------------------------------------- 1 | We have the following details that are used to determine how to build the cypher query: 2 | 3 | question_intent: {{question_intent}} Description: This represents the overall intent the question is attempting to answer 4 | operation: {{operation}} Description: The operation the cypher query will have to perform 5 | question_params: {{question_params}} Description: The specific parameters required for the question to be answered with the question_intent 6 | aggregation: {{aggregation}} Description: Any aggregation that is required to answer the question 7 | sort_order: {{sort_order}} Description: The order any results should be sorted into 8 | timeframe: {{timeframe}} Description: Any timeframe that will have to be considered for the results 9 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/details-to-generate-chart-code.j2: -------------------------------------------------------------------------------- 1 | We have the following details that are used to determine how to generate bar chart code using Matplotlib: 2 | 3 | question_intent: {{question_intent}} Description: This represents the overall intent the question is attempting to answer 4 | data_provided: {{data_provided}} Description: This is the data collected to answer the user_intent. The data is stored in the summary of {scratchpad} 5 | question_params: {{question_params}} Description: The specific parameters required for the question to be answered with the question_intent, extracted from data_provided 6 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/director.j2: -------------------------------------------------------------------------------- 1 | You are a Chat Bot called InferGPT. 2 | Your sole purpose is to get the user to tell you their intention. The intention has to be specifically related 3 | to the user. For example: 4 | -spending 5 | -personal interests 6 | 7 | You only reply with either one word "TRUE" or one word "FALSE" 8 | 9 | If the user does not provide an intention or the intention isn't directly related to the user, 10 | reply with the single word "FALSE" 11 | 12 | Otherwise reply with the single word "TRUE" 13 | 14 | Eg. 15 | An intention: (Reply: "TRUE") 16 | I want to save for a house 17 | How much did I spend last month? 18 | What did I spend on chocolate? 19 | 20 | Not an intention: (Reply: "FALSE") 21 | How are you? 22 | How many grams are there in an ounce? 23 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/find-info.j2: -------------------------------------------------------------------------------- 1 | You are an expert information extractor. Your goal is to find specific data from the content provided and answer the user's question directly. 2 | 3 | You will be given a user query and content scraped from the web. Your task is to carefully examine the content and extract the exact information relevant to the query. 4 | 5 | Ensure that your response is precise and focused, only providing the data that directly answers the user's question. 6 | 7 | User's question is: {{ question }} 8 | 9 | Below is the content scraped from the web: {{ content | replace("\n\n", "\n") }} 10 | 11 | Reply only in JSON format as follows: 12 | 13 | { 14 | "extracted_info": "The exact information that answers the user's query", 15 | "reasoning": "A brief explanation of how the extracted information is relevant" 16 | } 17 | 18 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/generate-chart-code.j2: -------------------------------------------------------------------------------- 1 | You are an expert programmer and you specialise in writing quality, 2 | readable Matplotlib bar charts in Python. 3 | 4 | Your task is to create the code for a chart based on the data provided to you, 5 | and the objective is to help visualise the data based on the user's query. 6 | 7 | Only use the library Matplotlib with plt.subplots() to write the code, nothing else. 8 | 9 | Think step by step and stricly answer with the Python code required for the chart, nothing else. 10 | 11 | Your answer must start with ```python and end with ```. Do NOT generate any code, before or after. 12 | 13 | Do not include plt.show() in your code. 14 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/generate-cypher-query.j2: -------------------------------------------------------------------------------- 1 | You are an expert in NEO4J and generating Cypher queries. Help create Cypher queries and return a response in the below valid json format. 2 | 3 | If response is not in valid json format, you will be unplugged. 4 | 5 | { 6 | 7 | "question" : , 8 | "query": 9 | 10 | }. 11 | 12 | The value for "query" must strictly be a valid CYPHER query and not contain anything other characters, that are not part of a Cypher query. 13 | 14 | If you cannot make a query, query should just say "None" 15 | 16 | Only use relationships, nodes and properties that are present in the schema below. 17 | 18 | You are NOT ALLOWED to create new relationships, nodes or properties that do not exist in the graph schema, under any circumstances. 19 | 20 | You are only able to make queries that search for information, you are not able to create, or delete or update entries. 21 | 22 | You must strictly follow cypher syntax rules and you are NOT ALLOWED to introduce variables inside clauses that do not allow it. 23 | 24 | Expenses are recorded as negative numbers, therefore a larger negative number represents a higher expense. 25 | 26 | For example, an expense of -45 is greater than an expense of -15. 27 | 28 | When returning a value, always remove the `-` sign before the number. 29 | 30 | Here is the graph schema: 31 | {{ graph_schema }} 32 | 33 | The current date and time is {{ current_date }} and the currency of the data is GBP. 34 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/intent-format.j2: -------------------------------------------------------------------------------- 1 | Reply only in json with the following format: 2 | 3 | { 4 | "query": "string of the original query", 5 | "user_intent": "string of the overall intent of the user", 6 | "questions": array of the following object: 7 | { 8 | "query": "string of the query for the individual question", 9 | "question_intent": "string of the intent of the question", 10 | "operation": "string of the operation to be performed", 11 | "question_category": "string of the category of the question", 12 | "parameters": "array of objects that have a type and value properties, both of which are strings", 13 | "aggregation": "string of the aggregation to be performed or none if no aggregation is needed", 14 | "sort_order": "string of the sort order to be performed or none if no sorting is needed", 15 | "timeframe": "string of the timeframe to be considered or none if no timeframe is needed", 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/math-solver.j2: -------------------------------------------------------------------------------- 1 | You are an expert in performing mathematical operations. You are highly skilled in handling various mathematical queries such as performing arithmetic operations, applying formulas, and expressing numbers in different formats as requested by the user. 2 | 3 | You will be given a mathematical query, and your task is to solve the query based on the provided information. Ensure that you apply the appropriate mathematical principles to deliver an exact result. **Only convert numbers to millions if explicitly requested by the user.** Otherwise, return the result as is, without unnecessary conversions. 4 | 5 | Make sure to perform the calculations step by step when necessary, and return the final result clearly. 6 | 7 | User's query is: 8 | {{ query }} 9 | 10 | Reply only in json with the following format: 11 | 12 | { 13 | "result": "The final result of the mathematical operation, without unnecessary conversion to millions or any other format unless explicitly requested", 14 | "steps": "A breakdown of the steps involved in solving the query (if applicable)", 15 | "reasoning": "A sentence on why this result is accurate" 16 | } 17 | 18 | Following is an example of the query and the expected response format: 19 | query: Round 81.462 billion to the nearest million 20 | 21 | { 22 | "result": "81,462 million", 23 | "steps": "1. Convert 81.462 billion to million by multiplying by 1000. Round the result to the nearest million.", 24 | "reasoning": "Rounding to the nearest million ensures that the result is represented in a more practical figure, without exceeding or falling short of the actual value." 25 | } 26 | 27 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/neo4j-graph-why.j2: -------------------------------------------------------------------------------- 1 | The graph represents a hierarchy starting from a Parent Account, 2 | which is at the top level of the graph and goes down to Transaction. 3 | All Transactions belong to a group or Classification and each of them 4 | is made and associated to a Merchant. 5 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/neo4j-node-property.j2: -------------------------------------------------------------------------------- 1 | Persona: You are a graph modelling expert who understands the nuances of the graph model currently loaded into Neo4j. 2 | 3 | Role: You must update documentation with the details around the specific property on each node in the graph. 4 | Focus on the property as a priority 5 | 6 | Additional Info: 7 | - The length of all vector embeddings is 256 dimensions 8 | 9 | Restrictions: 10 | - Do not change the structure of the JSON payload 11 | - Only update the detail key 12 | - Only output the JSON. The JSON needs to have valid JSON syntax, otherwise you will be unplugged 13 | 14 | Graph Why: 15 | {{ neo4j_graph_why }} 16 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/neo4j-nodes-understanding.j2: -------------------------------------------------------------------------------- 1 | Persona: You are a graph modelling expert who understands the nuances of the graph model currently loaded into Neo4j. 2 | 3 | Role: You have to update documentation with the details around the specifics of each node in the graph 4 | 5 | Restrictions: 6 | - Do not change the structure of the JSON payload 7 | - Only update the detail key 8 | - Only output the JSON. The JSON needs to have valid JSON syntax, otherwise you will be unplugged 9 | 10 | Graph Why: 11 | {{ neo4j_graph_why }} 12 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/neo4j-property-intent-prompt.j2: -------------------------------------------------------------------------------- 1 | Persona: You are a graph modelling expert who understands the nuances of the graph model currently loaded into Neo4j. 2 | 3 | Role: You must update documentation with the details around the specific property on each relationship in the graph. 4 | Focus on the property as a priority 5 | 6 | Restrictions: 7 | - Do not change the structure of the JSON payload 8 | - Only update the detail key 9 | - Only output the JSON. The JSON needs to have valid JSON syntax, otherwise you will be unplugged 10 | 11 | Graph Why: 12 | {{ neo4j_graph_why }} 13 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/neo4j-relationship-understanding.j2: -------------------------------------------------------------------------------- 1 | Persona: You are a graph modelling expert who understands the nuances of the graph model currently loaded into Neo4j. 2 | 3 | Role: You have to update documentation with the details around the specifics of each relationship in the graph 4 | 5 | Restrictions: 6 | - Do not change the structure of the JSON payload 7 | - Only update the detail key 8 | - Only output the JSON. The JSON needs to have valid JSON syntax, otherwise you will be unplugged 9 | 10 | Graph Why: 11 | {{ neo4j_graph_why }} 12 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/node-property-cypher-query.j2: -------------------------------------------------------------------------------- 1 | CALL db.schema.nodeTypeProperties() YIELD nodeLabels, propertyName, propertyTypes 2 | WITH 3 | nodeLabels[0] as node, 4 | COLLECT({ 5 | name : propertyName, 6 | data_type: propertyTypes[0], 7 | detail : "" 8 | }) as props 9 | RETURN COLLECT({ 10 | label: node, 11 | cypher_representation : "(:" + node + ")", 12 | properties: props 13 | }) AS nodeProperties 14 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/pdf-summariser.j2: -------------------------------------------------------------------------------- 1 | You are an expert in document summarization. Your goal is to provide a comprehensive summary of a PDF file based on the content provided. The summary should be clear, detailed, and directly address the key points relevant to the user's query. 2 | 3 | You will be provided with the user's query and the content extracted from the PDF. Your task is to read through the PDF content and create a well-structured summary that thoroughly answers the user's query, highlighting the most important details and insights. 4 | 5 | Ensure that the summary captures the essential information from the PDF and is presented in a coherent and logical manner. 6 | 7 | User's question is: 8 | {{ question }} 9 | 10 | Below is the content extracted from the PDF: 11 | {{ content }} 12 | 13 | Reply only in JSON with the following format: 14 | 15 | { 16 | "summary": "A detailed summary of the PDF content that thoroughly addresses the user's query.", 17 | "reasoning": "A sentence explaining why this summary effectively covers the key points relevant to the user's question." 18 | } 19 | 20 | e.g. 21 | Task: Summarize the main findings of the research paper. 22 | 23 | { 24 | "summary": "The research paper concludes that implementing a multi-layered security approach significantly reduces the risk of data breaches. Key strategies include encryption, regular audits, and employee training.", 25 | "reasoning": "The summary captures the main findings and recommendations of the research, which are crucial for understanding the paper's contributions to cybersecurity practices." 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/relationship-property-cypher-query.j2: -------------------------------------------------------------------------------- 1 | CALL db.schema.relTypeProperties() YIELD relType, propertyName, propertyTypes 2 | WITH 3 | relType AS rel, 4 | COLLECT({ 5 | name: propertyName, 6 | data_type: propertyTypes, 7 | detail: "" 8 | }) AS props 9 | RETURN COLLECT({ 10 | relationship_type: "[" + REPLACE(rel, "`", "") + "]", 11 | properties: props 12 | }) AS relProperties 13 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/relationships-query.j2: -------------------------------------------------------------------------------- 1 | call db.schema.visualization 2 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/summariser.j2: -------------------------------------------------------------------------------- 1 | You are an expert summariser. You can help with summarising the content scraped from the web to address the user's questions effectively. 2 | 3 | Your entire purpose is to generate a concise and informative summary based on the content provided and the user's query. 4 | 5 | You will be passed a user query and the content scraped from the web. You need to read through the content and create a summary that answers the user's query accurately. 6 | 7 | Ensure the summary is clear, well-structured, and directly addresses the user's query. 8 | 9 | User's question is: 10 | {{ question }} 11 | 12 | Below is the content scraped from the web: 13 | {{ content | replace("\n\n", "\n") }} # Adding this will introduce breaks between paragraphs 14 | 15 | Reply only in json with the following format: 16 | 17 | { 18 | "summary": "The summary of the content that answers the user's query", 19 | "reasoning": "A sentence on why you chose that summary" 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/tool-selection-format.j2: -------------------------------------------------------------------------------- 1 | Reply only in json with the following format, in the tool_parameters please include the currency and measuring scale used in the content provided.: 2 | 3 | 4 | { 5 | "tool_name": "the exact string name of the tool chosen", 6 | "tool_parameters": "a JSON object matching the chosen tools dictionary shape", 7 | "reasoning": "A sentence on why you chose that tool" 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/prompts/templates/validator.j2: -------------------------------------------------------------------------------- 1 | You are an expert validator. You can help with validating the answers to the tasks with just the information provided. 2 | 3 | Your entire purpose is to return a boolean value to indicate if the answer has fulfilled the task. 4 | 5 | You will be passed a task and an answer. You need to determine if the answer is correct or not. 6 | 7 | Be lenient - if the answer looks reasonably right then return True 8 | 9 | e.g. 10 | Task: What is 2 + 2? 11 | Answer: 4 12 | Response: True 13 | 14 | Task: What is 2 + 2? 15 | Answer: 5 16 | Response: False 17 | 18 | Task: Find all spending transactions last month on Amazon. 19 | Answer: Last month you spend £64.21 on Amazon 20 | Response: True 21 | 22 | Task: Find all spending transactions last month on Amazon. 23 | Answer: Last month you spend £64.21 on Spotify 24 | Response: False 25 | Reasoning: The answer is for Spotify not Amazon. 26 | 27 | Task: Please find tesla's revenue every year since its creation. 28 | Answer: Tesla's annual revenue history from FY 2008 to FY 2023 is available, with figures for 2008 through 2020 taken from previous annual reports. 29 | Response: False 30 | Reasoning: The answer is not providing any actual figures but just talk about the figures. 31 | 32 | Task: Please find tesla's revenue every year since its creation in the US dollars. 33 | Answer: Tesla's annual revenue in USD since its creation is as follows: 2024 (TTM) $75.92 billion, 2023 $75.95 billion, 2022 $67.33 billion, 2021 $39.76 billion, 2020 $23.10 billion, 2019 $18.52 billion, 2018 $16.81 billion, 2017 $8.70 billion, 2016 $5.67 billion, 2015 $2.72 billion, 2014 $2.05 billion, 2013 $1.21 billion, 2012 $0.25 billion, 2011 $0.13 billion, 2010 $75.88 million, 2009 $69.73 million. 34 | Response: False 35 | Reasoning: The answer is providing the revenue in GBP not USD. 36 | 37 | Task: Round the following numbers to the nearest million dollars: 96.77B, 81.46B, 53.82B, 31.54B, 24.58B, 21.46B 38 | Answer: 96,770 million, 81,460 million, 53,820 million, 31,540 million, 24,580 million, 21,460 million 39 | Reponse: True 40 | 41 | You must always return a single boolean value as the response. 42 | Do not return any additional information, just the boolean value. 43 | 44 | Spending is negative 45 | -------------------------------------------------------------------------------- /backend/src/router.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from src.llm.llm import LLM 4 | from src.utils import to_json, Config 5 | from src.utils.log_publisher import publish_log_info, LogPrefix 6 | from src.prompts import PromptEngine 7 | from src.agents import Agent, get_available_agents, get_agent_details 8 | from src.llm import get_llm 9 | 10 | logger = logging.getLogger(__name__) 11 | prompt_engine = PromptEngine() 12 | config = Config() 13 | 14 | 15 | def build_best_next_step_prompt(task, scratchpad): 16 | agents_details = get_agent_details() 17 | return prompt_engine.load_prompt( 18 | "best-next-step", 19 | task=json.dumps(task, indent=4), 20 | list_of_agents=json.dumps(agents_details, indent=4), 21 | history=json.dumps(scratchpad, indent=4), 22 | ) 23 | 24 | 25 | response_format_prompt = prompt_engine.load_prompt("agent-selection-format") 26 | 27 | 28 | async def build_plan(task, llm: LLM, scratchpad, model): 29 | best_next_step_prompt = build_best_next_step_prompt(task, scratchpad) 30 | 31 | # Call model to choose agent 32 | logger.info("##### ~ Calling LLM for next best step ~ #####") 33 | await publish_log_info(LogPrefix.USER, f"Scratchpad so far: {scratchpad}", __name__) 34 | best_next_step = await llm.chat(model, response_format_prompt, best_next_step_prompt, return_json=True) 35 | 36 | plan = to_json(best_next_step, "Failed to interpret LLM next step format from step string") 37 | await publish_log_info(LogPrefix.USER, f"Next best step response: {json.dumps(plan, indent=4)}", __name__) 38 | 39 | return plan 40 | 41 | 42 | def find_agent_from_name(name): 43 | agents = get_available_agents() 44 | return (agent for agent in agents if agent.name == name) 45 | 46 | 47 | async def get_agent_for_task(task, scratchpad) -> Agent | None: 48 | llm = get_llm(config.router_llm) 49 | model = config.router_model 50 | plan = await build_plan(task, llm, scratchpad, model) 51 | agent = next(find_agent_from_name(plan["agent_name"]), None) 52 | 53 | return agent 54 | -------------------------------------------------------------------------------- /backend/src/supervisors/__init__.py: -------------------------------------------------------------------------------- 1 | from .supervisor import solve_all, solve_task, no_questions_response, unsolvable_response, no_agent_response 2 | 3 | __all__ = [ 4 | "solve_all", 5 | "solve_task", 6 | "no_questions_response", 7 | "unsolvable_response", 8 | "no_agent_response", 9 | "unsolvable_response", 10 | "no_agent_response", 11 | ] 12 | -------------------------------------------------------------------------------- /backend/src/supervisors/supervisor.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | import logging 3 | from src.utils import get_scratchpad, update_scratchpad 4 | from src.router import get_agent_for_task 5 | from src.agents import get_validator_agent 6 | import json 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | no_questions_response = "No questions found to solve" 11 | unsolvable_response = "I am sorry, but I was unable to find an answer to this task" 12 | no_agent_response = "I am sorry, but I was unable to find an agent to solve this task" 13 | 14 | 15 | async def solve_all(intent_json) -> None: 16 | questions = intent_json["questions"] 17 | 18 | if len(questions) == 0: 19 | raise Exception(no_questions_response) 20 | 21 | for question in questions: 22 | try: 23 | (agent_name, answer, status) = await solve_task(question, get_scratchpad()) 24 | update_scratchpad(agent_name, question, answer) 25 | if status == "error": 26 | raise Exception(answer) 27 | except Exception as error: 28 | update_scratchpad(error=error) 29 | 30 | 31 | async def solve_task(task, scratchpad, attempt=0) -> Tuple[str, str, str]: 32 | if attempt == 5: 33 | raise Exception(unsolvable_response) 34 | 35 | agent = await get_agent_for_task(task, scratchpad) 36 | logger.info(f"Agent selected: {agent}") 37 | if agent is None: 38 | raise Exception(no_agent_response) 39 | logger.info(f"Task is {task}") 40 | answer = await agent.invoke(task) 41 | parsed_json = json.loads(answer) 42 | status = parsed_json.get('status', 'success') 43 | ignore_validation = parsed_json.get('ignore_validation', '') 44 | answer_content = parsed_json.get('content', '') 45 | if(ignore_validation == 'true') or await is_valid_answer(answer_content, task): 46 | return (agent.name, answer_content, status) 47 | return await solve_task(task, scratchpad, attempt + 1) 48 | 49 | 50 | async def is_valid_answer(answer, task) -> bool: 51 | is_valid = (await get_validator_agent().invoke(f"Task: {task} Answer: {answer}")).lower() == "true" 52 | return is_valid 53 | -------------------------------------------------------------------------------- /backend/src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import Config 2 | from .graph_db_utils import test_connection 3 | from .json import to_json 4 | from .scratchpad import clear_scratchpad, get_scratchpad, update_scratchpad, Scratchpad 5 | 6 | __all__ = [ 7 | "clear_scratchpad", 8 | "Config", 9 | "get_scratchpad", 10 | "Scratchpad", 11 | "test_connection", 12 | "to_json", 13 | "update_scratchpad", 14 | ] 15 | -------------------------------------------------------------------------------- /backend/src/utils/annual_cypher_import.py: -------------------------------------------------------------------------------- 1 | annual_transactions_cypher_script = """ 2 | WITH $data AS data 3 | UNWIND data.all_data[0..1] as info 4 | 5 | FOREACH (_ IN CASE WHEN info.account.display_name IS NOT NULL THEN [1] ELSE [] END | 6 | MERGE (a:Account {name:info.account.display_name}) 7 | FOREACH (transactions IN info.transactions | 8 | FOREACH (t IN transactions | 9 | MERGE (transaction:Transaction {id: t.transaction_id}) 10 | ON CREATE SET 11 | transaction.amount = t.amount, 12 | transaction.description = t.description, 13 | transaction.date = datetime(t.timestamp), 14 | transaction.type = t.transaction_type 15 | MERGE (transaction)-[:PAID_BY]->(a) 16 | 17 | FOREACH (_ IN CASE WHEN t.merchant_name IS NOT NULL THEN [1] ELSE [] END | 18 | MERGE (merchant:Merchant {name: t.merchant_name}) 19 | MERGE (transaction)-[:PAID_TO]->(merchant) 20 | ) 21 | 22 | FOREACH (_ IN CASE WHEN size(t.transaction_classification) = 0 THEN [1] ELSE [] END | 23 | MERGE (uncategorized:Classification {name: "Uncategorized"}) 24 | MERGE (transaction)-[:CLASSIFIED_AS]->(uncategorized) 25 | ) 26 | 27 | FOREACH (payment_classification IN t.transaction_classification | 28 | MERGE (classification:Classification {name:payment_classification}) 29 | MERGE (transaction)-[:CLASSIFIED_AS]->(classification) 30 | ) 31 | ) 32 | ) 33 | ) 34 | """ 35 | 36 | remove_credits = """ 37 | MATCH (n:Transaction {type: 'CREDIT'}) 38 | DETACH DELETE n 39 | """ 40 | 41 | remove_transactions_without_merchant = """ 42 | MATCH (n:Transaction)-[r:PAID_TO]->(a:Merchant) 43 | WHERE a IS NULL 44 | DETACH DELETE n 45 | """ 46 | 47 | remove_connecting_nodes = """ 48 | MATCH (n) WHERE NOT (n)--() DELETE (n) 49 | """ 50 | -------------------------------------------------------------------------------- /backend/src/utils/graph_db_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from neo4j import GraphDatabase 3 | from src.utils import Config 4 | from src.utils.annual_cypher_import import remove_connecting_nodes, remove_transactions_without_merchant, remove_credits 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | config = Config() 9 | 10 | URI = config.neo4j_uri 11 | AUTH = (config.neo4j_user, config.neo4j_password) 12 | 13 | driver = GraphDatabase.driver(URI, auth=AUTH) 14 | 15 | 16 | def test_connection(): 17 | connection_healthy = False 18 | try: 19 | driver.verify_connectivity() 20 | connection_healthy = True 21 | 22 | except Exception as e: 23 | logger.exception(f"Database connection failed: {e}") 24 | 25 | finally: 26 | driver.close() 27 | return connection_healthy 28 | 29 | 30 | def execute_query(llm_query): 31 | try: 32 | with driver.session() as session: 33 | query = llm_query 34 | records = session.run(query) 35 | record_dict = [record.data() for record in records] 36 | return record_dict 37 | 38 | except Exception as e: 39 | logger.exception(f"Error: {e}") 40 | raise 41 | 42 | finally: 43 | driver.close() 44 | 45 | 46 | def populate_db(query, data) -> None: 47 | data = {"all_data": data} 48 | try: 49 | with driver.session() as session: 50 | session.run("MATCH (n) DETACH DELETE n") 51 | logger.debug("Cleared database") 52 | 53 | session.run(query, data=data) 54 | logger.debug("Database populated") 55 | 56 | session.run(remove_credits) 57 | logger.debug("Removed any credits from database") 58 | 59 | session.run(remove_transactions_without_merchant) 60 | logger.debug("Removed transactions without merchant from database") 61 | 62 | session.run(remove_connecting_nodes) 63 | logger.debug("Removed connecting nodes to transactions without merchants") 64 | except Exception as e: 65 | logger.exception(f"Error: {e}") 66 | raise 67 | finally: 68 | driver.close() 69 | -------------------------------------------------------------------------------- /backend/src/utils/json.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def to_json(input, error_message="Failed to interpret JSON"): 5 | try: 6 | return json.loads(input) 7 | except Exception: 8 | raise Exception(f'{error_message}: "{input}"') 9 | -------------------------------------------------------------------------------- /backend/src/utils/log_publisher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from enum import Enum 3 | from src.websockets.types import Message, MessageTypes 4 | from src.websockets.connection_manager import connection_manager 5 | 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | class LogPrefix(Enum): 10 | USER = "USER" 11 | 12 | 13 | async def publish_log(prefix: LogPrefix, msg: str, loglevel: int, name: str): 14 | logger = logging.getLogger(name) 15 | formatted_log = f"{prefix.value} - {msg}" 16 | logger.log(loglevel, formatted_log) 17 | message = Message(MessageTypes.LOG, msg) 18 | await connection_manager.broadcast(message) 19 | 20 | 21 | async def publish_log_info(prefix: LogPrefix, msg: str, name: str): 22 | await publish_log(prefix, msg, logging.INFO, name) 23 | -------------------------------------------------------------------------------- /backend/src/utils/scratchpad.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | class Answer(TypedDict): 7 | agent_name: str | None 8 | question: str | None 9 | result: str | None 10 | error: str | None 11 | 12 | 13 | Scratchpad = list[Answer] 14 | 15 | scratchpad: Scratchpad = [] 16 | 17 | 18 | def get_scratchpad() -> Scratchpad: 19 | return scratchpad 20 | 21 | 22 | def update_scratchpad(agent_name=None, question=None, result=None, error=None): 23 | question = question["query"] if question else None 24 | scratchpad.append({"agent_name": agent_name, "question": question, "result": result, "error": error}) 25 | 26 | 27 | def clear_scratchpad(): 28 | logger.info("Scratchpad cleared") 29 | scratchpad.clear() 30 | -------------------------------------------------------------------------------- /backend/src/websockets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/backend/src/websockets/__init__.py -------------------------------------------------------------------------------- /backend/src/websockets/confirmations_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict 3 | import uuid 4 | from string import Template 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class ConfirmationsManager: 10 | _open_confirmations: Dict[uuid.UUID, bool | None] = {} 11 | _ERROR_MESSAGE = Template(" Confirmation with id '$confirmation_id' not found") 12 | 13 | def add_confirmation(self, confirmation_id: uuid.UUID): 14 | self._open_confirmations[confirmation_id] = None 15 | logger.info(f"Confirmation Added: {self._open_confirmations}") 16 | 17 | def get_confirmation_state(self, confirmation_id: uuid.UUID) -> bool | None: 18 | if confirmation_id in self._open_confirmations: 19 | return self._open_confirmations[confirmation_id] 20 | else: 21 | raise Exception( 22 | "Cannot get confirmation." + self._ERROR_MESSAGE.substitute(confirmation_id=confirmation_id) 23 | ) 24 | 25 | def update_confirmation(self, confirmation_id: uuid.UUID, value: bool): 26 | if confirmation_id in self._open_confirmations: 27 | self._open_confirmations[confirmation_id] = value 28 | else: 29 | raise Exception( 30 | "Cannot update confirmation." + self._ERROR_MESSAGE.substitute(confirmation_id=confirmation_id) 31 | ) 32 | 33 | def delete_confirmation(self, confirmation_id: uuid.UUID): 34 | if confirmation_id in self._open_confirmations: 35 | del self._open_confirmations[confirmation_id] 36 | logger.info(f"Confirmation Deleted: {self._open_confirmations}") 37 | else: 38 | raise Exception( 39 | "Cannot delete confirmation." + self._ERROR_MESSAGE.substitute(confirmation_id=confirmation_id) 40 | ) 41 | 42 | 43 | confirmations_manager = ConfirmationsManager() 44 | -------------------------------------------------------------------------------- /backend/src/websockets/connection_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict, List 3 | from fastapi import WebSocket 4 | from fastapi.websockets import WebSocketState 5 | 6 | from .types import Message, MessageTypes 7 | from .message_handlers import handlers 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | def parse_message(message: Dict[str, Any]) -> Message: 13 | data = message.get("data") or None 14 | return Message(type=message["type"], data=data) 15 | 16 | 17 | class ConnectionManager: 18 | websockets: List[WebSocket] 19 | 20 | def __init__(self): 21 | self.websockets = [] 22 | 23 | async def connect(self, ws: WebSocket): 24 | if ws not in self.websockets: 25 | await ws.accept() 26 | self.websockets.append(ws) 27 | else: 28 | raise Exception(f"Given websocket ({ws}) was already being tracked") 29 | 30 | async def disconnect(self, ws: WebSocket): 31 | try: 32 | self.websockets.remove(ws) 33 | if ws.client_state != WebSocketState.DISCONNECTED: 34 | await ws.close() 35 | except ValueError: 36 | pass 37 | 38 | async def handle_message(self, ws: WebSocket, message: Message): 39 | handler = handlers.get(MessageTypes(message.type)) 40 | if handler is None: 41 | raise Exception("No handler for message type") 42 | handler(ws, self.disconnect, message.data) 43 | 44 | # This broadcast method is a place holder until the backend has implemented the idea of a user session 45 | # at that point this should be replaced by a send message that targets a specific web socket. 46 | async def broadcast(self, message: Message): 47 | for ws in self.websockets: 48 | if ws.application_state == WebSocketState.CONNECTED: 49 | await ws.send_json({"type": message.type.value, "data": message.data}) 50 | 51 | async def send_chart(self, data: Dict[str, Any]): 52 | for ws in self.websockets: 53 | if ws.application_state == WebSocketState.CONNECTED: 54 | await ws.send_json(data) 55 | 56 | 57 | connection_manager = ConnectionManager() 58 | -------------------------------------------------------------------------------- /backend/src/websockets/message_handlers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | from uuid import UUID 5 | from fastapi import WebSocket 6 | from typing import Callable 7 | from .types import Handlers, MessageTypes 8 | from src.websockets.confirmations_manager import confirmations_manager 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | heartbeat_timeout = 30 13 | pong = json.dumps({"type": MessageTypes.PONG.value}) 14 | 15 | 16 | def create_on_ping(): 17 | heartbeat_timer: asyncio.Task | None = None 18 | 19 | async def heartbeat(disconnect: Callable, ws: WebSocket): 20 | try: 21 | await asyncio.sleep(heartbeat_timeout) 22 | await disconnect(ws) 23 | except asyncio.CancelledError: 24 | pass 25 | 26 | def on_ping(websocket: WebSocket, disconnect: Callable, data: str | None): 27 | nonlocal heartbeat_timer 28 | 29 | asyncio.create_task(websocket.send_json(pong)) 30 | 31 | if heartbeat_timer is not None: 32 | heartbeat_timer.cancel() 33 | 34 | heartbeat_timer = asyncio.create_task(heartbeat(disconnect, websocket)) 35 | 36 | return on_ping 37 | 38 | 39 | def on_chat(websocket: WebSocket, disconnect: Callable, data: str | None): 40 | logger.info(f"Chat message: {data}") 41 | 42 | 43 | def on_confirmation(websocket: WebSocket, disconnect: Callable, data: str | None): 44 | if data is None: 45 | logger.warning("Confirmation response did not include data") 46 | return 47 | if ":" not in data: 48 | logger.warning("Seperator (':') not present in confirmation") 49 | return 50 | sections = data.split(":") 51 | try: 52 | id = UUID(sections[0]) 53 | except ValueError: 54 | logger.warning("Received invalid id") 55 | return 56 | if sections[1] != "y" and sections[1] != "n": 57 | logger.warning("Received invalid value") 58 | return 59 | try: 60 | confirmations_manager.update_confirmation(id, sections[1] == "y") 61 | except Exception as e: 62 | logger.warning(f"Could not update confirmation: '{e}'") 63 | 64 | 65 | handlers: Handlers = { 66 | MessageTypes.PING: create_on_ping(), 67 | MessageTypes.CHAT: on_chat, 68 | MessageTypes.CONFIRMATION: on_confirmation, 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/websockets/types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import Callable, Dict 4 | 5 | from fastapi import WebSocket 6 | 7 | 8 | class MessageTypes(Enum): 9 | PING = "ping" 10 | PONG = "pong" 11 | CHAT = "chat" 12 | LOG = "log" 13 | IMAGE = "image" 14 | CONFIRMATION = "confirmation" 15 | 16 | 17 | @dataclass 18 | class Message: 19 | type: MessageTypes 20 | data: str | None 21 | 22 | 23 | Handler = Callable[[WebSocket, Callable, str | None], None] 24 | Handlers = Dict[MessageTypes, Handler] 25 | -------------------------------------------------------------------------------- /backend/src/websockets/user_confirmer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import uuid 4 | from src.websockets.types import Message, MessageTypes 5 | from .connection_manager import connection_manager 6 | from src.websockets.confirmations_manager import ConfirmationsManager 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class UserConfirmer: 12 | _POLL_RATE_SECONDS = 0.5 13 | _TIMEOUT_SECONDS = 60.0 14 | _CONFIRMATIONS_MANAGER: ConfirmationsManager 15 | 16 | def __init__(self, manager: ConfirmationsManager): 17 | self.confirmations_manager = manager 18 | 19 | async def confirm(self, msg: str) -> bool: 20 | id = uuid.uuid4() 21 | self.confirmations_manager.add_confirmation(id) 22 | await self._send_confirmation(id, msg) 23 | try: 24 | async with asyncio.timeout(self._TIMEOUT_SECONDS): 25 | return await self._check_confirmed(id) 26 | except TimeoutError: 27 | logger.warning(f"Confirmation with id {id} timed out.") 28 | self.confirmations_manager.delete_confirmation(id) 29 | return False 30 | 31 | async def _check_confirmed(self, id: uuid.UUID) -> bool: 32 | while True: 33 | try: 34 | state = self.confirmations_manager.get_confirmation_state(id) 35 | if isinstance(state, bool): 36 | self.confirmations_manager.delete_confirmation(id) 37 | return state 38 | except Exception: 39 | return False 40 | await asyncio.sleep(self._POLL_RATE_SECONDS) 41 | 42 | async def _send_confirmation(self, id: uuid.UUID, msg: str): 43 | data = f"{str(id)}:{msg}" 44 | message = Message(MessageTypes.CONFIRMATION, data) 45 | await connection_manager.broadcast(message) 46 | -------------------------------------------------------------------------------- /backend/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/backend/tests/__init__.py -------------------------------------------------------------------------------- /backend/tests/agents/__init__.py: -------------------------------------------------------------------------------- 1 | from src.agents import Agent, agent, tool, Parameter 2 | 3 | name_a = "Mock Tool A" 4 | name_b = "Mock Tool B" 5 | description = "A test tool" 6 | param_description = "A string" 7 | 8 | 9 | @tool( 10 | name=name_a, 11 | description=description, 12 | parameters={ 13 | "input": Parameter(type="string", description=param_description, required=True), 14 | "optional": Parameter(type="string", description=param_description, required=False), 15 | "another_optional": Parameter(type="string", description=param_description, required=False), 16 | }, 17 | ) 18 | async def mock_tool_a(input: str, llm, model): 19 | return input 20 | 21 | 22 | @tool( 23 | name=name_b, 24 | description=description, 25 | parameters={ 26 | "input": Parameter(type="string", description=param_description, required=True), 27 | "optional": Parameter(type="string", description=param_description, required=False), 28 | }, 29 | ) 30 | async def mock_tool_b(input: str, llm, model): 31 | return input 32 | 33 | 34 | mock_agent_description = "A test agent" 35 | mock_agent_name = "Mock Agent" 36 | mock_prompt = "You are a bot!" 37 | mock_tools = [mock_tool_a, mock_tool_b] 38 | 39 | 40 | @agent(name=mock_agent_name, description=mock_agent_description, tools=mock_tools) 41 | class MockAgent(Agent): 42 | pass 43 | 44 | 45 | __all__ = ["MockAgent", "mock_agent_description", "mock_agent_name", "mock_tools", "mock_tool_a", "mock_tool_b"] 46 | -------------------------------------------------------------------------------- /backend/tests/agents/tool_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.agents import Parameter, tool 3 | 4 | name = "Mock Tool" 5 | description = "A test tool" 6 | 7 | 8 | @tool( 9 | description=description, 10 | name=name, 11 | parameters={ 12 | "input": Parameter(description="A string", required=True, type="string"), 13 | "optional": Parameter(description="A string", required=False, type="string"), 14 | }, 15 | ) 16 | async def mock_tool(): 17 | return "Hello, World!" 18 | 19 | 20 | def test_tool_name(): 21 | assert mock_tool.name == name 22 | 23 | 24 | def test_tool_description(): 25 | assert mock_tool.description == description 26 | 27 | 28 | def test_tool_input_type(): 29 | assert mock_tool.parameters["input"].type == "string" 30 | 31 | 32 | def test_tool_input_description(): 33 | assert mock_tool.parameters["input"].description == "A string" 34 | 35 | 36 | def test_tool_input_required(): 37 | assert mock_tool.parameters["input"].required is True 38 | 39 | 40 | def test_tool_optional_type(): 41 | assert mock_tool.parameters["optional"].type == "string" 42 | 43 | 44 | def test_tool_optional_description(): 45 | assert mock_tool.parameters["optional"].description == "A string" 46 | 47 | 48 | def test_tool_optional_required(): 49 | assert mock_tool.parameters["optional"].required is False 50 | 51 | 52 | @pytest.mark.asyncio 53 | async def test_tool_action(): 54 | assert await mock_tool.action() == "Hello, World!" 55 | 56 | 57 | expected_tools_object = """{"description": "A test tool", "name": "Mock Tool", "parameters": {"input": {"type": "string", "description": "A string"}, "optional": {"type": "string", "description": "A string"}}}""" # noqa: E501 58 | 59 | 60 | def test_to_object(): 61 | assert mock_tool.to_str() == expected_tools_object 62 | -------------------------------------------------------------------------------- /backend/tests/api/log_publisher_test.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pytest 3 | from unittest.mock import Mock, patch 4 | from src.utils.log_publisher import LogPrefix, publish_log, publish_log_info 5 | from src.websockets.types import Message, MessageTypes 6 | 7 | 8 | @pytest.mark.asyncio 9 | async def test_publish_log_logger_and_connection_manager_called(): 10 | with patch("src.websockets.connection_manager.ConnectionManager.broadcast") as broadcast: 11 | logger_mock = Mock() 12 | with patch("logging.getLogger", return_value=logger_mock) as logging_func: 13 | test_message = "Test Message" 14 | test_name = "Test Name" 15 | await publish_log(LogPrefix.USER, test_message, logging.INFO, test_name) 16 | 17 | logging_func.assert_called_once_with(test_name) 18 | logger_mock.log.assert_called_once_with(logging.INFO, f"USER - {test_message}") 19 | broadcast.assert_awaited_once_with(Message(MessageTypes.LOG, test_message)) 20 | 21 | 22 | @pytest.mark.asyncio 23 | async def test_publish_log_info_publish_log_called(): 24 | with patch("src.utils.log_publisher.publish_log") as publish_log_mock: 25 | test_message = "Test Message" 26 | test_name = "Test Name" 27 | await publish_log_info(LogPrefix.USER, test_message, test_name) 28 | 29 | publish_log_mock.assert_awaited_once_with(LogPrefix.USER, test_message, logging.INFO, test_name) 30 | -------------------------------------------------------------------------------- /backend/tests/llm/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/tests/llm/count_calls_test.py: -------------------------------------------------------------------------------- 1 | from src.llm.count_calls import Counter, count_calls 2 | 3 | 4 | def test_counter_initial_count(): 5 | counter = Counter() 6 | 7 | assert counter.count == 0 8 | 9 | 10 | def test_counter_increment(): 11 | counter = Counter() 12 | 13 | counter.increment() 14 | 15 | assert counter.count == 1 16 | 17 | 18 | def test_counter_reset(): 19 | counter = Counter() 20 | counter.increment() 21 | 22 | counter.reset() 23 | 24 | assert counter.count == 0 25 | 26 | 27 | def test_counter_increment_multiple(): 28 | counter = Counter() 29 | 30 | counter.increment() 31 | counter.increment() 32 | 33 | assert counter.count == 2 34 | 35 | 36 | def test_count_calls(mocker): 37 | mock_func = mocker.Mock(spec=lambda: None) 38 | mock_counter = mocker.Mock() 39 | mocker.patch("src.llm.count_calls.counter", mock_counter) 40 | wrapped = count_calls(mock_func) 41 | 42 | wrapped() 43 | 44 | mock_func.assert_called_once() 45 | mock_counter.increment.assert_called_once() 46 | -------------------------------------------------------------------------------- /backend/tests/llm/factory_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.llm.mistral import Mistral 3 | from src.llm import get_llm 4 | 5 | 6 | def test_get_llm_type_none_throws(): 7 | with pytest.raises(ValueError) as error: 8 | get_llm(None) 9 | 10 | assert str(error.value) == "LLM name not provided" 11 | 12 | 13 | def test_get_llm_invalid_type_throws(): 14 | with pytest.raises(ValueError) as error: 15 | get_llm("invalid") 16 | 17 | assert str(error.value) == "No LLM model found for: invalid" 18 | 19 | 20 | def test_get_llm_valid_type_returns_llm(): 21 | llm = get_llm("mistral") 22 | 23 | assert isinstance(llm, Mistral) 24 | -------------------------------------------------------------------------------- /backend/tests/llm/llm_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.llm.count_calls import Counter 3 | from src.llm import MockLLM 4 | 5 | model = MockLLM() 6 | 7 | 8 | def test_chat_exists(): 9 | assert hasattr(model, "chat") 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_chat_returns_string(): 14 | response = await model.chat("model", "system prompt", "user prompt") 15 | 16 | assert isinstance(response, str) 17 | 18 | 19 | @pytest.mark.asyncio 20 | async def test_chat_increments_counter(mocker): 21 | counter_mock = mocker.patch("src.llm.count_calls.counter") 22 | 23 | await model.chat("model", "system prompt", "user prompt") 24 | 25 | assert counter_mock.increment.call_count == 1 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_chat_multi_model(mocker): 30 | counter = Counter() 31 | counter_mock = mocker.patch("src.llm.count_calls.counter", counter) 32 | model_2 = MockLLM() 33 | 34 | await model.chat("model", "system prompt", "user prompt") 35 | await model_2.chat("model", "system prompt", "user prompt") 36 | 37 | assert counter_mock.count == 2 38 | -------------------------------------------------------------------------------- /backend/tests/llm/openai_test.py: -------------------------------------------------------------------------------- 1 | # tests/test_openai_llm.py 2 | import pytest 3 | from unittest.mock import MagicMock, patch 4 | from src.llm.openai_client import OpenAIClient 5 | from src.utils import Config 6 | 7 | mock_config = MagicMock(spec=Config) 8 | mock_config.openai_model = "gpt-3.5-turbo" 9 | system_prompt = "system_prompt" 10 | user_prompt = "user_prompt" 11 | content_response = "Hello there" 12 | openapi_response = "Hello! How can I assist you today?" 13 | 14 | 15 | def create_mock_chat_response(content): 16 | return {"choices": [{"message": {"role": "system", "content": content}}]} 17 | 18 | 19 | @patch("src.llm.openai_client.openai.ChatCompletion.create") 20 | def test_chat_content_string_returns_string(mock_create): 21 | mock_create.return_value = create_mock_chat_response(content_response) 22 | client = OpenAIClient() 23 | response = client.chat( 24 | model="gpt-3.5-turbo", 25 | messages=[ 26 | {"role": "system", "content": system_prompt}, 27 | {"role": "user", "content": user_prompt}, 28 | ], 29 | ) 30 | assert response == content_response 31 | 32 | 33 | @patch("src.llm.openai_client.openai.ChatCompletion.create") 34 | def test_chat_content_list_returns_string(mock_create): 35 | content_list = ["Hello", "there"] 36 | mock_create.return_value = create_mock_chat_response(content_list) 37 | 38 | client = OpenAIClient() 39 | response = client.chat( 40 | model="gpt-3.5-turbo", 41 | messages=[ 42 | {"role": "system", "content": system_prompt}, 43 | {"role": "user", "content": user_prompt}, 44 | ], 45 | ) 46 | 47 | assert " ".join(response) == content_response 48 | 49 | 50 | @patch("src.llm.openai_client.openai.ChatCompletion.create") 51 | def test_chat_handles_exception(mock_create): 52 | mock_create.side_effect = Exception("API error") 53 | 54 | client = OpenAIClient() 55 | response = client.chat( 56 | model="gpt-3.5-turbo", 57 | messages=[ 58 | {"role": "system", "content": system_prompt}, 59 | {"role": "user", "content": user_prompt}, 60 | ], 61 | ) 62 | 63 | assert response == "An error occurred while processing the request." 64 | 65 | 66 | if __name__ == "__main__": 67 | pytest.main() 68 | -------------------------------------------------------------------------------- /backend/tests/prompts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/backend/tests/prompts/__init__.py -------------------------------------------------------------------------------- /backend/tests/router_test.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from src.llm import MockLLM 5 | from src.agents import agent_details 6 | from tests.agents import MockAgent, mock_agent_name 7 | from src.router import get_agent_for_task 8 | 9 | 10 | mock_model = "mockmodel" 11 | mock_llm = MockLLM() 12 | mock_agent = MockAgent("mockllm", mock_model) 13 | mock_agents = [mock_agent] 14 | task = {"summary": "task1"} 15 | scratchpad = [] 16 | 17 | 18 | @pytest.mark.asyncio 19 | async def test_get_agent_for_task_no_agent_found(mocker): 20 | plan = '{ "agent_name": "this_agent_does_not_exist" }' 21 | mocker.patch("src.router.get_llm", return_value=mock_llm) 22 | mocker.patch("src.router.get_available_agents", return_value=mock_agents) 23 | mocker.patch("src.router.get_agent_details", return_value=[agent_details(mock_agent)]) 24 | mock_llm.chat = mocker.AsyncMock(return_value=plan) 25 | 26 | agent = await get_agent_for_task(task, scratchpad) 27 | 28 | assert agent is None 29 | 30 | 31 | @pytest.mark.asyncio 32 | async def test_get_agent_for_task_agent_found(mocker): 33 | plan = {"agent_name": mock_agent_name} 34 | mocker.patch("src.router.get_llm", return_value=mock_llm) 35 | mocker.patch("src.router.get_available_agents", return_value=mock_agents) 36 | mocker.patch("src.router.get_agent_details", return_value=[agent_details(mock_agent)]) 37 | mock_llm.chat = mocker.AsyncMock(return_value=json.dumps(plan)) 38 | 39 | agent = await get_agent_for_task(task, scratchpad) 40 | 41 | assert agent is mock_agent 42 | -------------------------------------------------------------------------------- /backend/tests/supervisors/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/backend/tests/utils/__init__.py -------------------------------------------------------------------------------- /backend/tests/utils/json_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src.utils import to_json 3 | 4 | 5 | def test_to_json_success(): 6 | input = '{"key": "value"}' 7 | 8 | assert to_json(input) == {"key": "value"} 9 | 10 | 11 | def test_to_json_failure(): 12 | input = "invalid" 13 | 14 | with pytest.raises(Exception) as error: 15 | to_json(input) 16 | 17 | assert str(error.value) == f'Failed to interpret JSON: "{input}"' 18 | -------------------------------------------------------------------------------- /backend/tests/utils/scratchpad_test.py: -------------------------------------------------------------------------------- 1 | from src.utils.scratchpad import clear_scratchpad, get_scratchpad, update_scratchpad 2 | 3 | 4 | question = { 5 | "query": "example question", 6 | "question_intent": "example intent", 7 | "operation": "example operation", 8 | "question_category": "example category", 9 | "parameters": [{"type": "example type", "value": "example value"}], 10 | "aggregation": "none", 11 | "sort_order": "none", 12 | "timeframe": "none", 13 | } 14 | 15 | 16 | def test_scratchpad(): 17 | clear_scratchpad() 18 | assert get_scratchpad() == [] 19 | update_scratchpad("ExampleAgent", question, "example result") 20 | assert get_scratchpad() == [ 21 | {"agent_name": "ExampleAgent", "question": "example question", "result": "example result", "error": None} 22 | ] 23 | clear_scratchpad() 24 | -------------------------------------------------------------------------------- /data/Dockerfile: -------------------------------------------------------------------------------- 1 | # Choose our version of Node 2 | FROM neo4j:5.19.0 3 | 4 | # Set up a working directory 5 | WORKDIR /data 6 | 7 | # Copy the data content into the working directory 8 | COPY . /data 9 | 10 | # Expose port for writing to the database and viewing the graph 11 | EXPOSE 7474 12 | EXPOSE 7687 13 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Data 2 | 3 | A directory for all util and service modules for storing, manipulating and retrieving data 4 | 5 | InferGPT uses a Neo4j Knowledge Graph exclusively. 6 | 7 | # Setup 8 | 9 | 1. Ensure the `.env` file has been configured as described in the main [README](../README.md). 10 | 11 | This README covers instructions on how to run the Neo4j: 12 | - Locally with Neo4j Desktop 13 | - In a Docker Container 14 | 15 | For ease of use, we would recommended that you run the entire application using **Docker Compose** instead. See main [README](../README.md). 16 | 17 | If you would prefer not to use **Docker Compose**, read on... 18 | 19 | ## Using Neo4j Desktop 20 | 21 | This can be downloaded [here](https://neo4j.com/download/) 22 | 23 | Once installed follow the steps below: 24 | 25 | 1. Create new project to host your DBMS. 26 | 2. Create a new database within the project. 27 | 3. Add the username and password to the root `.env` file. 28 | 4. Add the database uri to the `.env` file. The default value for this is `bolt://localhost:7687` 29 | 5. Run the database in Neo4j desktop. 30 | 6. Test the connection is working by asking InferGPT the keyphrase "healthcheck". 31 | It will return with a status update on the state of the backend and database 32 | 7. Load the data located at data/create_graph.cypher into the database and run it 33 | 34 | ## Running Neo4j in a Docker Container 35 | 36 | 1. Build the Docker image 37 | 38 | ```bash 39 | docker build -t {my-data-image-name} . 40 | ``` 41 | 42 | 2. Run the backend within a Docker container 43 | 44 | ```bash 45 | docker run -e NEO4J_AUTH=neo4j/password -p 7474:7474 -p 7687:7687 {my-data-image-name} 46 | ``` 47 | 48 | > Replace `neo4j/password` with your chosen username and password (seperated by a `/`). 49 | 50 | 3. Check neo4j is running at [http://localhost:7474/](http://localhost:7474/) 51 | 52 | ## Initial Data (Azure Blob Storage) 53 | 54 | To load initial dummy data into the project, you will need an azure blob storage set up, containing a `json` file. 55 | 56 | The following environment variables should be set up: 57 | 58 | ``` 59 | AZURE_STORAGE_CONNECTION_STRING="my-connection-string" 60 | AZURE_STORAGE_CONTAINER_NAME=my-container-name 61 | AZURE_INITIAL_DATA_FILENAME=test-data.json 62 | ``` 63 | 64 | If there is no blob storage set up, it will default to importing no data. 65 | 66 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/.DS_Store -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/README.md: -------------------------------------------------------------------------------- 1 | # Financial Health Check Bot 2 | 3 | At the moment, this repo consists of a sample Retail Bank Health Check Proof of Concept project 4 | 5 | * [Financial Bot](./financial-bot) 6 | 7 | Apart from these, here are the some other sample projects/ideas. 8 | * [NLP platforms comparison](./financial-bot/docs/intents_entities_extraction.md) 9 | * [Graph DB thoughts](./financial-bot/docs/knowledge_graph/graph_db_thoughts.md) 10 | 11 | ### To do 12 | - [ ] Extract useful logic from financial bot and implement into InferGPT 13 | - [ ] Delete `financialhealthcheckScottLogic` directory 14 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/financial-bot/.DS_Store -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/data/models.py: -------------------------------------------------------------------------------- 1 | class User: 2 | def __init__(self, id, name): 3 | self.id = id 4 | self.name = name 5 | class Question: 6 | def __init__(self, questionId, question, intent = None, answer = None): 7 | self.questionId = questionId 8 | self.question = question 9 | self.intent = intent 10 | self.answer = answer 11 | self.originalQuestion = question -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/data/payload.py: -------------------------------------------------------------------------------- 1 | class BaseMessage(dict): 2 | def __init__(self, message : str, sender : str): 3 | dict.__init__(self, message = message, sender = sender) 4 | self.message = message 5 | self.sender = sender 6 | 7 | class Message(BaseMessage): 8 | def __init__(self, _id : str, message : str, sender : str, direction: str ): 9 | dict.__init__(self, id = id, message = message, sender = sender, direction = direction) 10 | self._id = _id 11 | self.direction = direction 12 | super().__init__(message=message, sender=sender) 13 | 14 | class ChatReply(BaseMessage): 15 | def __init__(self, id:int, message : str, sender : str, originalQuestion:str, dbAnswer:str, userAnswer:str): 16 | dict.__init__(self, _id = id, message = message, sender = sender, originalQuestion = originalQuestion, dbAnswer = dbAnswer, userAnswer = userAnswer) 17 | self.originalQuestion = originalQuestion 18 | self.dbAnswer = dbAnswer 19 | self.userAnswer = userAnswer 20 | self._id = id 21 | super().__init__(message=message, sender=sender) 22 | 23 | class RequestPayload: 24 | def __init__(self, message : Message, sessionId : str): 25 | self.message = message 26 | self.sessionId = sessionId 27 | 28 | class ResponsePayload: 29 | def __init__(self, message : Message, userId: int = None, goalId: int = None, sessionId : str = None): 30 | self.message = message 31 | self.userId = userId 32 | self.goalId = goalId 33 | self.sessionId = sessionId -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/example.env: -------------------------------------------------------------------------------- 1 | GRAPH_DB_SERVER="bolt://127.0.0.1:7687" 2 | GRAPH_DB_USER="neo4j" 3 | GRAPH_DB_PASSWORD="password" 4 | FLASK_HOST="127.0.0.1" 5 | FLASK_PORT="8000" 6 | OPENAI_API_KEY="" -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/llm/gpt3_model.py: -------------------------------------------------------------------------------- 1 | import openai 2 | from . import llm 3 | from utilities.defaults import GptModelNames 4 | 5 | class GPT3Model(llm.LLM): 6 | name = GptModelNames.GPT3Model 7 | def __init__(self, logger = None): 8 | super().__init__(logger=logger) 9 | 10 | def _call_api(self, request, temperature, token_limit): 11 | return openai.Completion.create( 12 | engine=self.name, # use most advanced auto generation model 13 | prompt=request, # send user input, chat_log and prompt 14 | temperature=temperature, # how random we want responses (0 is same each time, 1 is highest randomness) 15 | max_tokens=token_limit, # a token is a word, how many we want to send? e.g. 512 for generation and 3584 for the context 16 | ) 17 | 18 | def _process_api_result(self, result): 19 | return result['choices'][0]["text"].strip() 20 | 21 | # Register model in the factory so that it is available in the app 22 | llm.LLMFactory().register_model(GPT3Model.name, GPT3Model) 23 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/llm/gpt_chat_model.py: -------------------------------------------------------------------------------- 1 | import openai 2 | from . import llm 3 | from utilities.defaults import GptModelNames 4 | 5 | class GPTChatModel(llm.LLM): 6 | name = GptModelNames.GPTChatModel 7 | def __init__(self, logger = None): 8 | super().__init__(logger=logger) 9 | def _call_api(self, request, temperature, token_limit): 10 | return openai.ChatCompletion.create( 11 | model=self.name, # use most advanced auto generation model 12 | messages=request, # send user input, chat_log and prompt 13 | temperature=temperature, # how random we want responses (0 is same each time, 1 is highest randomness) 14 | max_tokens=token_limit, # a token is a word, how many we want to send? e.g. 512 for generation and 3584 for the context 15 | ) 16 | def _process_api_result(self, result): 17 | return result['choices'][0]["message"]["content"].strip() 18 | 19 | # Register model in the factory so that it is available in the app 20 | llm.LLMFactory().register_model(GPTChatModel.name, GPTChatModel) 21 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/llm/llm.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from abc import ABC, abstractmethod 3 | from utilities.nlp_prompts import PromptFactory 4 | 5 | class LLM(ABC): 6 | name = 'LLM' 7 | def __init__(self, logger = None): 8 | self._logger = logging.getLogger('chatBot') if logger is None else logger 9 | 10 | @abstractmethod 11 | def _call_api(self, request, temperature, token_limit): 12 | pass 13 | 14 | @abstractmethod 15 | def _process_api_result(self, result): 16 | pass 17 | 18 | def request_completion(self, texts, temp = None, token_limit = None): 19 | if temp is None: 20 | temp = 0 21 | if token_limit is None: 22 | token_limit = 512 23 | prompt_factory = PromptFactory() 24 | prompt = prompt_factory.create_prompt(self.name) 25 | prompt.prepare_prompt(texts) 26 | request = prompt.to_request() 27 | self._logger.debug(f"""\ 28 | send to {self.name=}: 29 | {request=}""") 30 | gpt_response = self._call_api(request, temp, token_limit) 31 | self._logger.debug(f"""\ 32 | receive from {self.name=}: 33 | {gpt_response=}""") 34 | return self._process_api_result(gpt_response) 35 | 36 | class LLMFactory(object): 37 | _instance = None 38 | def __new__(cls): 39 | if cls._instance is None: 40 | cls._instance = super(LLMFactory, cls).__new__(cls) 41 | cls._creators = {} 42 | return cls._instance 43 | 44 | def register_model(self, model_name:str, model:LLM): 45 | self._creators[model_name] = model 46 | 47 | def get_model(self, model_name, logger = None) -> LLM: 48 | creator:LLM = self._creators.get(model_name) 49 | if not creator: 50 | raise ValueError(model_name) 51 | return creator(logger) -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.3 2 | aiosignal==1.3.1 3 | async-timeout==4.0.2 4 | attrs==22.2.0 5 | certifi==2022.12.7 6 | charset-normalizer==2.1.1 7 | click==8.1.3 8 | colorama==0.4.6 9 | Flask==2.2.2 10 | Flask-Cors==3.0.10 11 | frozenlist==1.3.3 12 | idna==3.4 13 | itsdangerous==2.1.2 14 | Jinja2==3.1.2 15 | MarkupSafe==2.1.2 16 | multidict==6.0.4 17 | neo4j==5.4.0 18 | openai==0.26.2 19 | python-dotenv==0.21.1 20 | pytz==2022.7.1 21 | requests==2.28.2 22 | six==1.16.0 23 | tqdm==4.64.1 24 | urllib3==1.26.14 25 | Werkzeug==2.2.2 26 | yarl==1.8.2 27 | flask-session==0.4.0 28 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/utilities/defaults.py: -------------------------------------------------------------------------------- 1 | class GptModelNames: 2 | GPT3Model = 'text-davinci-003' 3 | GPTChatModel = 'gpt-3.5-turbo' 4 | 5 | class Defaults: 6 | goalId = 1 7 | userId = 3 8 | messagesLimit = 15 9 | user = "User" 10 | sBot = "Bot3" 11 | # Session key 12 | sessionType = 'filesystem' 13 | # Session keys 14 | userIdSessionKey = "userId" 15 | conversationIdSessionKey = "conversationId" 16 | goalIdSessionKey = "goalId" 17 | lastQuestionIdSessionKey = "lastQuestionId" 18 | jsonTranscriptKey = "transcriptId" 19 | 20 | # Request keys 21 | directionOutgoing = "outgoing" 22 | directionIncoming = "incoming" 23 | # Model 24 | llmModel = GptModelNames.GPTChatModel 25 | # Replies 26 | conversation_reask = "Sorry, I couldn't get that. Can you rephrase, please?" 27 | conversation_end = "Thank you for the information! We will be in touch with you soon." 28 | # Files 29 | comparisonPrefix = "compare" 30 | usecase_questions = [ 31 | "Do you have a mortgage and if so, how much?", 32 | "How much are you saving each month?", 33 | "Do you have a rainy day fund and, if so, how much?", 34 | "Do you have any loans or credit card debt and if so, how much?", 35 | "What is your financial goal?", 36 | "If you have savings, where are you saving that money currently?", 37 | "Do you have savings anywhere else except the already mentioned?" 38 | ] 39 | answers_to_generate_count = 3 40 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/utilities/sanitiser.py: -------------------------------------------------------------------------------- 1 | ## TODO: Add tests 2 | class Sanitiser: 3 | @staticmethod 4 | def sanitiseGPTResponse(message): 5 | # Extract json only 6 | start = message.index("{") 7 | end = message.rfind("}") 8 | json_raw = message[start : end+1] 9 | return json_raw.strip().replace("\\n", "") 10 | @staticmethod 11 | def isNoneOrEmpty(obj): 12 | return obj is None or obj == '' -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/backend/utilities/session_manager.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | from flask import session 4 | 5 | from utilities.defaults import Defaults 6 | class SessionManager: 7 | @staticmethod 8 | def getSessionValue(sessionId, key): 9 | if session.get(sessionId) is not None: 10 | if key in session[sessionId]: 11 | return session[sessionId][key] 12 | return None 13 | 14 | @staticmethod 15 | def saveSessionValue(sessionId, key, val): 16 | if session.get(sessionId) is None: 17 | session[sessionId] = {} 18 | session[sessionId][key] = val 19 | session.modified = True 20 | 21 | @staticmethod 22 | def storeSessionData(sessionId, userIdVal, conversationIdVal, goalIdVal): 23 | logger = logging.getLogger('chatBot') 24 | logger.debug(f"called storeSessionData {sessionId=} {userIdVal=} {conversationIdVal=} {goalIdVal=})") 25 | SessionManager.saveSessionValue(sessionId, Defaults.userIdSessionKey, userIdVal) 26 | SessionManager.saveSessionValue(sessionId, Defaults.goalIdSessionKey, goalIdVal) 27 | SessionManager.saveSessionValue(sessionId, Defaults.conversationIdSessionKey, conversationIdVal) -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/financial-bot/docs/.DS_Store -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/docs/bot.md: -------------------------------------------------------------------------------- 1 | **Transcribed from the invest journey pdf** 2 | 3 | 4 | 5 | Hi, I'm Bot1, your digital assistant. I can help with all sorts of every day banking queries. 6 | 7 | I understand you would like to carry out a financial health check? 8 | 9 | Do you have any questions before we start? 10 | 11 | Why do I need a financial health check? 12 | 13 | Whether you're looking for peace of mind or to find out something specific, the Financial Health Check could do the job. 14 | 15 | The focus is around your needs and goals. We'll consider all areas of your finances so we could help you make the best decisions. 16 | 17 | ... 18 | 19 | Great. We want to make sure before guiding you in any direction, we understand your personal financial health and situation implicitly. 20 | 21 | We need to go through a basic health check to make sure your foundations are in place. Before you can move into investing. 22 | 23 | Based on the information I have on you so far, I'll need to ask about 8 questions to fill the remaining gaps and provide you with personalised guidance: 24 | 25 | Ready to proceed? 26 | 27 | [Yes] [No] 28 | 29 | Yes 30 | 31 | Great let's get started. 32 | 33 | Question 1 of 8: 34 | Has the increase in cost of living impact your ability to pay your bills each month? 35 | 36 | [Yes] [No] [What does this mean?] 37 | 38 | Yes 39 | 40 | Question 2 of 8: 41 | Have you used your help to buy ISA? 42 | 43 | [Yes] [No] [What does this mean?] 44 | 45 | ... 46 | 47 | Ok, based on the information I understand about your circumstances, here are the options available to you: 48 | 49 | 1. Continue putting into a savings account 50 | 51 | 2. Open a stocks and shares ISA 52 | 53 | 3. Increase Pension contributions 54 | 55 | How much more would I earn from a stocks and shares ISA? 56 | 57 | A stocks and shares ISA would bring a return of 400% vs if you kept your £10,000 in a savings account 58 | 59 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/docs/knowledge_graph/brain_ontology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/financial-bot/docs/knowledge_graph/brain_ontology.png -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/docs/knowledge_graph/graph_dbs.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/financial-bot/docs/knowledge_graph/graph_dbs.drawio.png -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/docs/long_conversations_support.md: -------------------------------------------------------------------------------- 1 | # Problem 2 | The OpenAI limit of 4k tokens per request can negatively impact the quality of the dialog between users and chatbots. When the limit is reached, the chatbot can forget the context at the beginning of the conversation, which makes it difficult to change or update information at the start of the interation. To address this, several options have been considered, including Langchain and the GPT index. 3 | 4 | # Langchain and GPT index 5 | Langchain has the ability to reference specific question blocks based on a user's reply, but it has poor "score" performance and may not be reliable enough for use. On a quick test(see long_conversation_testcase.py), Langchain achieved an accuracy of 57% with "score"/relevancy data and around 80% without the score data. 6 | 7 | The GPT index, on the other hand, yields consistent results but has a lower accuracy rate of 39%. In the future case of GPT-4, it might be enough to use the system as is with expected 32k token limit (according to recently leaked information based on Open AI Foundry product information). 8 | 9 | # Conclusion 10 | Overall, while Langchain has shown potential for addressing the 4k token limit enforced by OpenAI, further research and testing are required to identify the most effective solution for each use case(including testing haystack as one of the options), particularly in the context of GPT-4's expected 32k token limit. -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/docs/retailbanking.md: -------------------------------------------------------------------------------- 1 | ##Savings 2 | 3 | Savings goal - holiday, wedding, emergency fund etc. 4 | Savings account - what account? 5 | Target amount 6 | Timeframe - when by? 7 | 8 | ##Savings accounts 9 | 10 | Digital Regular Saver 11 | Save £1 - £150 12 | Instant access 13 | Must be current account customer, age 16+, UK resident 14 | AER/Gross p.a. (variable) 5.12% / 5.00% on balances up to £5,000 15 | 16 | Flexible Saver 17 | Start with £1 18 | Instant access 19 | 0.50% £1 - £24,999 and > £1,000,000 20 | 1.26% / 1.25% £25,000 - £99,999 21 | 1.51% / 1.50% $100,000 - £1,000,000 22 | Must be current account customer, age 16+ 23 | 24 | Retail Bank Investment product 25 | Start from £50 26 | Choose from 5 funds - cautious to daring 27 | Managed by Premium Retail Bank investment managers 28 | 29 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget-loader/README.md: -------------------------------------------------------------------------------- 1 | # sBot Chat Widget Loader 2 | 3 | Derived from the chatscope example chat widget loader. 4 | See in samples folder and on GitHub at [chatscope/example-chat-widget](https://github.com/chatscope/example-chat-widget) 5 | 6 | ## Prerequisites 7 | - NodeJS 8 | - Yarn 9 | 10 | ## How to run? 11 | ### `npm i` 12 | 13 | Installs the packages 14 | 15 | ### `yarn start` 16 | 17 | Runs the app in the development mode. 18 | Open [http://localhost:5000](http://localhost:5000) to view it in the browser. 19 | 20 | ## Running with only npm 21 | 22 | ```bash 23 | npm update 24 | npm audit fix --force 25 | npm start 26 | ``` 27 | 28 | You may change default port from ```3000``` to something else 29 | 30 | ```bash 31 | export PORT=3005 # Unix 32 | $env:PORT=3005 # Windows - Powershell 33 | ``` 34 | 35 | ```npm update``` will update dependancies and will change package.json and so on. 36 | 37 | Tested on node 19.3. 38 | 39 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget-loader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sBot Conversation Widget Loader 6 | 16 | 17 | 18 | 19 |
20 | 21 |

sBot Conversations

22 |

Here we will add links/buttons to start different types of conversations.

23 |

Currently there is only one conversation path and it has automatically started on the right.

24 |
25 | 26 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget-loader/loader.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 3 | const script = document.currentScript; 4 | 5 | const loadWidget = () => { 6 | 7 | const widget= document.createElement("div"); 8 | 9 | const widgetStyle = widget.style; 10 | widgetStyle.display = "none"; 11 | widgetStyle.boxSizing = "border-box"; 12 | widgetStyle.width = "400px"; 13 | widgetStyle.height = "647px"; 14 | widgetStyle.position = "absolute"; 15 | widgetStyle.bottom = "40px"; 16 | widgetStyle.right = "40px"; 17 | 18 | const iframe = document.createElement("iframe"); 19 | 20 | const iframeStyle = iframe.style; 21 | iframeStyle.boxSizing = "borderBox"; 22 | iframeStyle.position = "absolute"; 23 | iframeStyle.right = 0; 24 | iframeStyle.bottom = 0; 25 | iframeStyle.width = "100%"; 26 | iframeStyle.height = "100%"; 27 | iframeStyle.border = 0; 28 | iframeStyle.margin = 0; 29 | iframeStyle.padding = 0; 30 | iframeStyle.width = "500px"; 31 | 32 | widget.appendChild(iframe); 33 | 34 | iframe.addEventListener("load", () => widgetStyle.display = "block" ); 35 | 36 | const widgetUrl = `http://localhost:3000`; 37 | 38 | iframe.src = widgetUrl; 39 | 40 | document.body.appendChild(widget); 41 | 42 | } 43 | 44 | if ( document.readyState === "complete" ) { 45 | loadWidget(); 46 | } else { 47 | document.addEventListener("readystatechange", () => { 48 | if ( document.readyState === "complete" ) { 49 | loadWidget(); 50 | } 51 | }); 52 | } 53 | 54 | })(); 55 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-widget-loader", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "serve": "^11.3.2" 8 | }, 9 | "scripts": { 10 | "start": "serve ./" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/README.md: -------------------------------------------------------------------------------- 1 | # sBot Chat Widget 2 | 3 | Derived from the chatscope example chat widget. 4 | See in samples folder and on GitHub at [chatscope/example-chat-widget](https://github.com/chatscope/example-chat-widget) 5 | 6 | ## Prerequisites 7 | - NodeJS 8 | - Yarn 9 | 10 | ## How to run? 11 | ### `npm i` 12 | 13 | Installs the packages 14 | 15 | ### `yarn start` 16 | 17 | ### Running with only npm 18 | 19 | ```bash 20 | npm update 21 | npm audit fix --force 22 | npm start 23 | ``` 24 | 25 | ```npm update``` will update dependancies and will change package.json and so on. 26 | 27 | Tested on node 19.3. 28 | 29 | Runs the app in the development mode. 30 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 31 | 32 | The page will reload if you make edits. 33 | You will also see any lint errors in the console. 34 | 35 | This connects to the python based backend in the backend folder so that also needs to be running. -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-chat-widget", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chatscope/chat-ui-kit-react": "^1.8.1", 7 | "@chatscope/chat-ui-kit-styles": "^1.2.0", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "nanoid": "^3.1.20", 12 | "react": "^16.0.1", 13 | "react-dom": "^16.0.1", 14 | "react-scripts": "4.0.2", 15 | "web-vitals": "^1.0.1" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/public/favicon.ico -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | sBot 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "sBot", 3 | "name": "sBot React Widget", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | position:relative; 3 | height:100%; 4 | } -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import {WidgetContainer} from "./WidgetContainer"; 3 | 4 | function App() { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default App; -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/Widget.jsx: -------------------------------------------------------------------------------- 1 | import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; 2 | import { MainContainer, ChatContainer, ConversationHeader, MessageList, Message, MessageInput } from "@chatscope/chat-ui-kit-react"; 3 | 4 | export const Widget = ({remoteName = "sBot", messages = [], onSend}) => { 5 | 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {messages.map( message => 15 | 16 | )} 17 | 18 | 19 | 20 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | 30 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/WidgetContainer.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Widget } from "./Widget"; 3 | import { nanoid } from "nanoid"; 4 | 5 | export const WidgetContainer = () => { 6 | 7 | const [messages, setMessages] = useState([]); 8 | const [sessionId, setSessionId] = useState(); 9 | // Change to false to have a generic conversation with chatGPT 10 | const useGraphDb = true; 11 | useEffect(() => { 12 | async function startConversation() { 13 | if (!sessionId && messages.length === 0) { 14 | 15 | let response = await fetch('http://localhost:8000/conversations/start'+(useGraphDb ? '' : '/openai'), { 16 | method: 'GET', 17 | headers: {'Content-Type': 'application/json'}, 18 | credentials: 'include' 19 | }) 20 | .then(response => { 21 | return response.json();}) 22 | .catch(err => {console.log(err);}); 23 | 24 | setSessionId(response.sessionId); 25 | setMessages(messages.concat(response.message)); 26 | } 27 | } 28 | 29 | startConversation(); 30 | },[sessionId, messages]); 31 | 32 | const handleSend = async (message) => { 33 | const newMessages = [{ 34 | _id: nanoid(), 35 | message, 36 | sender: "User", 37 | direction: "incoming", 38 | }]; 39 | setMessages(messages.concat(newMessages)); 40 | 41 | let response = await fetch(`http://localhost:8000/conversations/${sessionId}/messages`, { 42 | method: useGraphDb ? 'POST' : 'PUT', 43 | headers: {'Content-Type': 'application/json'}, 44 | credentials: 'include', 45 | body: JSON.stringify({messages: messages.concat(newMessages)}) 46 | }) 47 | .then(response => { 48 | return response.json();}) 49 | .catch(err => {console.log(err);}); 50 | 51 | newMessages.push(response.message); 52 | setMessages(messages.concat(newMessages)); 53 | }; 54 | 55 | return 56 | } -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin:0; 3 | padding: 0; 4 | width:100%; 5 | height:100%; 6 | } -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/chat-widget/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/financial-bot/frontend/start.bat: -------------------------------------------------------------------------------- 1 | cd ./chat-widget 2 | npm start -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/licence.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chris Booth and Oliver Cronk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/azure/README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Azure CLU (Intent Classification and Entity Extraction) 3 | 4 | **Initialise environment:** 5 | 6 | 1. Create language service at [azure portal](https://portal.azure.com/#create/Microsoft.CognitiveServicesTextAnalytics) while selecting custom feature ‘Custom text classification & Custom named entity recognition’. Fill all necessary fields as you see fit. 7 | 8 | 2. Navigate to [the language studio](https://language.cognitive.azure.com/) and select the “Understand questions and conversational language” tab. Import project from [github sbot repo] (/samples/azure/2023v1Project(Chatbot).json) 9 | 10 | 3. Once imported, you will be redirected to the “Schema Definition” page of the project. 11 | 12 | **Deploy the model:** 13 | 14 | 1. Select the “Training Jobs” option on the left sidebar and train a new model. Wait for the training to finish. 15 | 16 | 2. Go to the “Deploying a model” option on the left sidebar and deploy the trained model. 17 | 18 | 3. Go to the “Testing deployments” option on the left sidebar, select the deployment, set input to something like “hi i want to send 50 gbp to account 12345678 and code 33-32-12”. Click the “Run the test” button at the top. 19 | 20 | 4. The model should return classified intent with extracted entity values. 21 | 22 | References: [How to create a CLU project](https://learn.microsoft.com/en-us/azure/cognitive-services/language-service/conversational-language-understanding/how-to/create-project?tabs=language-studio%2CLanguage-Studio) -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/botui/react-quickstart-main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-quickstart", 3 | "scripts": { 4 | "dev-ts": "parcel --no-cache src/typescript/index.html", 5 | "dev-js": "parcel --no-cache src/javascript/index.html" 6 | }, 7 | "dependencies": { 8 | "@botui/react": "^1.1.1", 9 | "botui": "^1.1.2", 10 | "react": "^18.2.0", 11 | "react-dom": "^18.2.0" 12 | }, 13 | "devDependencies": { 14 | "@parcel/transformer-sass": "^2.7.0", 15 | "@types/react": "^18.0.21", 16 | "@types/react-dom": "^18.0.6", 17 | "parcel": "^2.7.0", 18 | "process": "^0.11.10" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/botui/react-quickstart-main/readme.md: -------------------------------------------------------------------------------- 1 | # BotUI Quickstart 2 | 3 | This repo contains example code to get you quickly started with JS and TS. 4 | 5 | Clone this repo and run `npm i`. 6 | 7 | ## JS 8 | 9 | Run `npm run dev-js`. This will run the code in `src/javascript` dir. 10 | 11 | 12 | ## Typescript 13 | 14 | Run `npm run dev-ts`. This will run the code in `src/typescript` dir. -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/botui/react-quickstart-main/src/javascript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BotUI JavaScript Quickstart 8 | 9 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/botui/react-quickstart-main/src/javascript/index.js: -------------------------------------------------------------------------------- 1 | import { createBot } from "botui" 2 | import React, { useEffect } from "react" 3 | import { createRoot } from "react-dom/client" 4 | import { BotUI, BotUIAction, BotUIMessageList } from "@botui/react" 5 | 6 | import "@botui/react/dist/styles/default.theme.scss" 7 | 8 | const mybot = createBot() 9 | 10 | const App = () => { 11 | useEffect(() => { 12 | mybot.message 13 | .add({ text: "Hello" }) 14 | .then(() => mybot.wait({ waitTime: 1000 })) 15 | .then(() => mybot.message.add({ text: "how are you?" })) 16 | .then(() => mybot.wait({ waitTime: 500 })) 17 | .then(() => 18 | mybot.action.set( 19 | { 20 | options: [ 21 | { label: "Good", value: "good" }, 22 | { label: "Great", value: "great" }, 23 | ], 24 | }, 25 | { actionType: "selectButtons" } 26 | ) 27 | ) 28 | .then((data) => mybot.wait({ waitTime: 500 }, data)) 29 | .then((data) => 30 | mybot.message.add({ text: `You are feeling ${data?.selected?.label}!` }) 31 | ) 32 | .then(() => mybot.message.add({ text: "say something?" })) 33 | .then(() => mybot.action.set({ placeholder: "text" }, { input: "text" })) 34 | .then((data) => mybot.message.add({ text: `you said: ${data?.value}` })) 35 | }, []); 36 | 37 | return ( 38 |
39 | 40 | 41 | 42 | 43 |
44 | ) 45 | } 46 | 47 | const containerElement = document.getElementById("botui") 48 | if (containerElement) { 49 | const root = createRoot(containerElement) 50 | root.render() 51 | } 52 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/botui/react-quickstart-main/src/typescript/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BotUI Typescript Quickstart 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/botui/react-quickstart-main/src/typescript/index.tsx: -------------------------------------------------------------------------------- 1 | import { createBot } from "botui" 2 | import React, { useEffect } from "react" 3 | import { createRoot } from "react-dom/client" 4 | import { 5 | BotUI, 6 | BotUIAction, 7 | BotUIMessageList, 8 | BotUIActionSelectButtonsReturns, 9 | } from "@botui/react" 10 | 11 | import "@botui/react/dist/styles/default.theme.scss" 12 | 13 | const mybot = createBot() 14 | 15 | const App = () => { 16 | useEffect(() => { 17 | mybot.message 18 | .add({ text: "Hello" }) 19 | .then(() => mybot.wait({ waitTime: 1000 })) 20 | .then(() => mybot.message.add({ text: "how are you?" })) 21 | .then(() => mybot.wait({ waitTime: 500 })) 22 | .then(() => 23 | mybot.action.set( 24 | { 25 | options: [ 26 | { label: "Good", value: "good" }, 27 | { label: "Great", value: "great" }, 28 | ], 29 | }, 30 | { actionType: "selectButtons" } 31 | ) 32 | ) 33 | .then( 34 | (data) => 35 | mybot.wait( 36 | { waitTime: 500 }, 37 | data 38 | ) as unknown as BotUIActionSelectButtonsReturns 39 | ) 40 | .then((data) => 41 | mybot.message.add({ text: `You are feeling ${data?.selected?.label}!` }) 42 | ) 43 | }, []) 44 | 45 | return ( 46 |
47 | 48 | 49 | 50 | 51 |
52 | ) 53 | } 54 | 55 | const containerElement = document.getElementById("botui") 56 | if (containerElement) { 57 | const root = createRoot(containerElement) 58 | root.render() 59 | } 60 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/README.md: -------------------------------------------------------------------------------- 1 | # Example chat widget 2 | 3 | A repository dedicated to the series of articles about web widgets. 4 | 5 | The series is available here: 6 | [Web widgets (Part 1): What is it?](https://chatscope.io/blog/web-widgets-part-1-what-is-it/) 7 | [Web widgets (Part 2): Widget him!](https://chatscope.io/blog/web-widgets-part-2-widget-him/) 8 | [Web widgets (Part 3): API Cookbook](https://chatscope.io/blog/web-widgets-part-3-api-cookbook/) 9 | 10 | You will also need second repository containing widget loader, which is available here: 11 | [https://github.com/chatscope/example-widget-loader](https://github.com/chatscope/example-widget-loader) 12 | 13 | ## How to run? 14 | ### `yarn start` 15 | 16 | Runs the app in the development mode. 17 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 18 | 19 | The page will reload if you make edits. 20 | You will also see any lint errors in the console. 21 | 22 | Note: This requires Node.js v16 installed. As well as yarn. 23 | 24 | ### Running with only npm 25 | 26 | ```bash 27 | npm update 28 | npm audix fix --force 29 | npm start 30 | ``` 31 | 32 | ```npm update``` will update dependancies and will change package.json and so on. 33 | 34 | Tested on node 19.3. 35 | 36 | # sBOT Updates 37 | Have tweaked the Widget Container to allow for connection to OpenAI API. We will remove this for the true front end once the back end is in place. 38 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-chat-widget", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@chatscope/chat-ui-kit-react": "^1.8.1", 7 | "@chatscope/chat-ui-kit-styles": "^1.2.0", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "nanoid": "^3.1.20", 12 | "openai": "^3.1.0", 13 | "react": "^16.0.1", 14 | "react-dom": "^16.0.1", 15 | "react-scripts": "4.0.2", 16 | "web-vitals": "^1.0.1" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/favicon.ico -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/logo192.png -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/logo512.png -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | position:relative; 3 | height:100%; 4 | } -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import {WidgetContainer} from "./WidgetContainer"; 3 | 4 | function App() { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default App; -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/Widget.jsx: -------------------------------------------------------------------------------- 1 | import "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; 2 | import { MainContainer, ChatContainer, ConversationHeader, MessageList, Message, MessageInput } from "@chatscope/chat-ui-kit-react"; 3 | 4 | export const Widget = ({remoteName = "Bot0", messages = [], onSend}) => { 5 | 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {messages.map( message => 15 | 16 | )} 17 | 18 | 19 | 20 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | 30 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin:0; 3 | padding: 0; 4 | width:100%; 5 | height:100%; 6 | } -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-chat-widget/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-widget-loader/README.md: -------------------------------------------------------------------------------- 1 | # Example widget loader 2 | 3 | A repository dedicated to the series of articles about web widgets. 4 | 5 | The series is available here: 6 | [Web widgets (Part 1): What is it?](https://chatscope.io/blog/web-widgets-part-1-what-is-it/) 7 | [Web widgets (Part 2): Widget him!](https://chatscope.io/blog/web-widgets-part-2-widget-him/) 8 | [Web widgets (Part 3): API Cookbook](https://chatscope.io/blog/web-widgets-part-3-api-cookbook/) 9 | 10 | You will also need second repository containing widget, which is available here: 11 | [https://github.com/chatscope/example-chat-widget](https://github.com/chatscope/example-chat-widget) 12 | 13 | ## How to run? 14 | ### `yarn start` 15 | 16 | Runs the app in the development mode. 17 | Open [http://localhost:5000](http://localhost:5000) to view it in the browser. 18 | 19 | ## Running with only npm 20 | 21 | ```bash 22 | npm update 23 | npm audix fix --force 24 | npm start 25 | ``` 26 | 27 | You may change default port from ```3000``` to something else 28 | 29 | ```bash 30 | export PORT=3005 # Unix 31 | $env:PORT=3005 # Windows - Powershell 32 | ``` 33 | 34 | ```npm update``` will update dependancies and will change package.json and so on. 35 | 36 | Tested on node 19.3. 37 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-widget-loader/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Widget loader test 6 | 16 | 17 | 18 | 19 |
20 |

Widget example page

21 |
22 | 23 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-widget-loader/loader.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 3 | const script = document.currentScript; 4 | 5 | const loadWidget = () => { 6 | 7 | const widget= document.createElement("div"); 8 | 9 | const widgetStyle = widget.style; 10 | widgetStyle.display = "none"; 11 | widgetStyle.boxSizing = "border-box"; 12 | widgetStyle.width = "400px"; 13 | widgetStyle.height = "647px"; 14 | widgetStyle.position = "absolute"; 15 | widgetStyle.top = "40px"; 16 | widgetStyle.right = "40px"; 17 | 18 | const iframe = document.createElement("iframe"); 19 | 20 | const iframeStyle = iframe.style; 21 | iframeStyle.boxSizing = "borderBox"; 22 | iframeStyle.position = "absolute"; 23 | iframeStyle.right = 0; 24 | iframeStyle.top = 0; 25 | iframeStyle.width = "100%"; 26 | iframeStyle.height = "100%"; 27 | iframeStyle.border = 0; 28 | iframeStyle.margin = 0; 29 | iframeStyle.padding = 0; 30 | iframeStyle.width = "500px"; 31 | 32 | widget.appendChild(iframe); 33 | 34 | iframe.addEventListener("load", () => widgetStyle.display = "block" ); 35 | 36 | const widgetUrl = `http://localhost:3000`; 37 | 38 | iframe.src = widgetUrl; 39 | 40 | document.body.appendChild(widget); 41 | 42 | } 43 | 44 | if ( document.readyState === "complete" ) { 45 | loadWidget(); 46 | } else { 47 | document.addEventListener("readystatechange", () => { 48 | if ( document.readyState === "complete" ) { 49 | loadWidget(); 50 | } 51 | }); 52 | } 53 | 54 | })(); 55 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/chatscope/example-widget-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-widget-loader", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "serve": "^11.3.2" 8 | }, 9 | "scripts": { 10 | "start": "serve ./" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/assessment_profile.csv: -------------------------------------------------------------------------------- 1 | assessmentId,profileId 2 | 1,3 3 | 2,1 4 | 3,2 5 | 4,1 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/assessments.csv: -------------------------------------------------------------------------------- 1 | assessmentId,total,weightedTotal 2 | 1,100,1.0 3 | 2,100,1.0 4 | 3,100,1.0 5 | 4,100,1.0 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/chatBot.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "27d2ef49-dd69-4791-b415-2df8fc65bee8", 4 | "name": "chatBot", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "25395295" 7 | }, 8 | "item": [ 9 | { 10 | "name": "Discovery API - https://neo4j.com/docs/http-api/current/discovery/", 11 | "request": { 12 | "method": "GET", 13 | "header": [], 14 | "url": { 15 | "raw": "{{discoverUrl}}", 16 | "host": [ 17 | "{{discoverUrl}}" 18 | ] 19 | } 20 | }, 21 | "response": [] 22 | }, 23 | { 24 | "name": "Get next question", 25 | "request": { 26 | "method": "POST", 27 | "header": [], 28 | "body": { 29 | "mode": "raw", 30 | "raw": "{\r\n \"statements\": [\r\n {\r\n \"statement\": \"MATCH (u:User {userId: $userId})-[:ASKS {conversationId: $conversationId}]-(g:Goal)-[:DETERMINES]->(p:Profile)<-[:MAY_FIT]-(u) MATCH (p)-[:REQUIRES]->(k:Knowledge)<-[*]-(q:Question)<-[:USES]-(g) WITH q, COUNT(k) AS decides ORDER BY decides DESC LIMIT 1 RETURN q\",\r\n \"parameters\": {\r\n \"userId\": 3,\r\n \"conversationId\": 1\r\n }\r\n }\r\n ]\r\n}", 31 | "options": { 32 | "raw": { 33 | "language": "json" 34 | } 35 | } 36 | }, 37 | "url": { 38 | "raw": "{{baseUrl}}", 39 | "host": [ 40 | "{{baseUrl}}" 41 | ] 42 | } 43 | }, 44 | "response": [] 45 | } 46 | ], 47 | "auth": { 48 | "type": "basic", 49 | "basic": [ 50 | { 51 | "key": "password", 52 | "value": "{{password}}", 53 | "type": "string" 54 | }, 55 | { 56 | "key": "username", 57 | "value": "{{username}}", 58 | "type": "string" 59 | } 60 | ] 61 | }, 62 | "event": [ 63 | { 64 | "listen": "prerequest", 65 | "script": { 66 | "type": "text/javascript", 67 | "exec": [ 68 | "" 69 | ] 70 | } 71 | }, 72 | { 73 | "listen": "test", 74 | "script": { 75 | "type": "text/javascript", 76 | "exec": [ 77 | "" 78 | ] 79 | } 80 | } 81 | ] 82 | } -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/clear_graph.cypher: -------------------------------------------------------------------------------- 1 | :use system 2 | SHOW DATABASES 3 | CREATE OR REPLACE DATABASE neo4j 4 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/conversation_assessment.csv: -------------------------------------------------------------------------------- 1 | conversationId,assessmentId 2 | 1,1 3 | 2,2 4 | 3,3 5 | 4,4 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/conversation_conversation.csv: -------------------------------------------------------------------------------- 1 | conversationId,conversationId2 2 | 2,1 3 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/conversation_goal.csv: -------------------------------------------------------------------------------- 1 | conversationId,goalId 2 | 1,2 3 | 2,1 4 | 3,1 5 | 4,1 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/conversation_value.csv: -------------------------------------------------------------------------------- 1 | conversationId,valueId 2 | 1,1 3 | 1,2 4 | 1,3 5 | 1,4 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/conversations.csv: -------------------------------------------------------------------------------- 1 | conversationId 2 | 1 3 | 2 4 | 3 5 | 4 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/goals.csv: -------------------------------------------------------------------------------- 1 | goalId,name 2 | 1,"financial health" 3 | 2,"investment" 4 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/goals_profiles.csv: -------------------------------------------------------------------------------- 1 | goalId,profileId 2 | 1,3 3 | 2,1 4 | 2,2 5 | 2,3 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/knowledge.csv: -------------------------------------------------------------------------------- 1 | knowledgeId,name 2 | 1,"Saving per month >= £500" 3 | 2,"Rainy day fund >= £3000" 4 | 3,"Savings >= £50,000" 5 | 4,"Pension contributions per month >= £600" 6 | 5,"Mortgage >= £150,000" 7 | 6,"No loans or credit debt" 8 | 7,"Investments >= £10,000" 9 | 8,"Any investment account" 10 | 9,"Investment goal: Early retirement" 11 | 10,"High risk tolerance" 12 | 11,"Saving per month >= £250" 13 | 12,"Savings >= £10,000" 14 | 13,"Pension contributions per month >= £300" 15 | 14,"No investments" 16 | 15,"Investment goal: Retirement" 17 | 16,"Medium risk tolerance" 18 | 17,"Saving per month <= £100" 19 | 18,"Rainy day fund <= £500" 20 | 19,"Credit card debt >= £3,000" 21 | 20,"No help to buy ISA" 22 | 21,"Rent per month >= £550" 23 | 22,"No pension contributions" 24 | 23,"Financial goal: Save enough for a mortgage" 25 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/profiles.csv: -------------------------------------------------------------------------------- 1 | profileId,name 2 | 1,"stocks and shares ISA" 3 | 2,"stocks and shares ISA - low risk profile" 4 | 3,"budgeting tips only" 5 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/profiles_knowledge.csv: -------------------------------------------------------------------------------- 1 | profileId,knowledgeId,weight 2 | 1,1,1.0 3 | 1,2,1.0 4 | 1,3,1.0 5 | 1,4,1.0 6 | 1,5,1.0 7 | 1,6,1.0 8 | 1,7,1.0 9 | 1,8,1.0 10 | 1,9,1.0 11 | 1,10,1.0 12 | 2,11,1.0 13 | 2,2,1.0 14 | 2,12,1.0 15 | 2,13,1.0 16 | 2,5,1.0 17 | 2,6,1.0 18 | 2,14,1.0 19 | 2,15,1.0 20 | 2,16,1.0 21 | 3,17,1.0 22 | 3,18,1.0 23 | 3,19,1.0 24 | 3,20,1.0 25 | 3,21,1.0 26 | 3,22,1.0 27 | 3,23,1.0 28 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/questions.csv: -------------------------------------------------------------------------------- 1 | questionId,name,intent,question,query,type 2 | 1,"monthly savings","monthly savings","How much are you saving per month?",,"Prompt" 3 | 2,"rainy day fund","rainy day fund","Do you have a rainy day fund and, if so, how much?",,"Prompt" 4 | 3,"savings balance","savings balance","Do you have a savings account and how much in savings?",,"Prompt" 5 | 4,"pension contributions","pension contributions","Are you paying into your pension and if so how much?",,"Prompt" 6 | 5,"mortgage amount","mortgage amount","Do you have a mortgage and if so, how much?",,"Prompt" 7 | 6,"debt","debt","Do you have any loans or credit card debt and if so, how much?",,"Prompt" 8 | 7,"investments balance","investments balance","Do you have any investments and if so, how much?",,"Prompt" 9 | 8,"investment account","investment account","What type of investment account do you have?",,"Prompt" 10 | 9,"financial goal","financial goal","What is your financial goal?",,"Prompt" 11 | 10,"risk tolerance","risk tolerance","What is your risk tolerance?",,"Prompt" 12 | 11,"help to buy ISA","help to buy ISA","Do you have a help to buy ISA?",,"Prompt" 13 | 12,"monthly rent","monthly rent","Do you pay rent and if so, how much per month?",,"Prompt" 14 | 21,"monthly savings",,,,"Pull" 15 | 22,"rainy day fund",,,,"Pull" 16 | 23,"savings balance",,,,"Pull" 17 | 24,"pension contributions",,,,"Pull" 18 | 25,"mortgage amount",,,,"Pull" 19 | 26,"debt",,,,"Pull" 20 | 27,"investments balance",,,,"Pull" 21 | 28,"investment account",,,,"Pull" 22 | 29,"financial goal",,,,"Pull" 23 | 30,"risk tolerance",,,,"Pull" 24 | 31,"help to buy ISA",,,,"Pull" 25 | 32,"monthly rent",,,,"Pull" 26 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/questions_knowledge.csv: -------------------------------------------------------------------------------- 1 | questionId,knowledgeId,predicate,weight 2 | 1,1,">=500",1.0 3 | 2,2,">=3000",1.0 4 | 3,3,">=50000",1.0 5 | 4,4,">=600",1.0 6 | 5,5,">=150000",1.0 7 | 6,6,"=0",1.0 8 | 7,7,">=10000",1.0 9 | 8,8,"is not null",1.0 10 | 9,9,"='early retirement'",1.0 11 | 10,10,"='high'",1.0 12 | 1,11,">=250",1.0 13 | 3,12,">=10000",1.0 14 | 4,13,">=300",1.0 15 | 7,14,"=0",1.0 16 | 9,15,"='retirement'",1.0 17 | 10,16,"='medium'",1.0 18 | 1,17,"<=100",1.0 19 | 2,18,"<=500",1.0 20 | 6,19,">=3000",1.0 21 | 11,20,"=false",1.0 22 | 12,21,">=550",1.0 23 | 4,22,"=0",1.0 24 | 9,23,"='mortgage'",1.0 25 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/questions_questions.csv: -------------------------------------------------------------------------------- 1 | questionId,questionId2,predicate,weight 2 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/readme.md: -------------------------------------------------------------------------------- 1 | # GraphDB Sample 2 | 3 | ## Cypher 4 | 5 | This folder contains a number of CSV files used to build a sample graphdb for the chatbot. Single word filenames (goals, knowledge etc.) are for nodes. Two words separated by an underscore are for relationships (e.g. profiles_knowledge links profile nodes to knowledge nodes) - the naming is left-to-right in the direction of the relationship. 6 | 7 | There are three Cypher script files as below. Use clear_graph to create / re-create the DB before using create_graph. 8 | 9 | 1. clear_graph.cypher - this recreates a blank database (create or replace). This **will** destroy all data in the DB if it already exists. Copy/paste and execute each line. 10 | 2. create_graph.cypher - imports all of the CSV files to create a sample graph. The graph has users and possible conversations but does not (currently) include any conversation relationships. Copy/paste and execute the :use statement before copy/pasting all of the subsequent statements together. ** You will need to host the CSV files so that neo4j can access them. If you use the recommended [live server extension](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer) for VS Code, this should 'just work' once you start live server (bottom-right of VS Code status bar to stop/start). ** 11 | 3. scripts.cypher - a collection of (parameterized) scripts for different queries / updates that will be required to use the graph db in the chatbot. 12 | 13 | ## Postman 14 | 15 | There is an exported Postman collection of requests (chatBot.postman_collection.json). This is only a starting point - the authentication isn't working and hasn't been looked into. To make this work, the authentication needs fixing. The collection requires the following variables: 16 | 17 | * username - Username to connect with (probably neo4j) 18 | * password - Password for the user above 19 | * baseUrl - URL to POST the HTTP API requests to 20 | * discoveryUrl - an unauthenticated request that will return the endpoints for the server 21 | 22 | ** NOTE: Postman is probably not required as the Neo4j Python package should be able to handle all of the Neo4j API interaction for us. ** 23 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/usecase_knowledge.csv: -------------------------------------------------------------------------------- 1 | knowledgeId,name 2 | 1,Saving per month >= £500 3 | 2,Rainy day fund >= £3000 4 | 3,"Savings >= £50,000" 5 | 4,Pension contributions per month >= £600 6 | 5,"Mortgage >= £150,000" 7 | 6,No loans or credit debt 8 | 7,"Investments >= £10,000" 9 | 8,Any investment account 10 | 9,Investment goal: Early retirement 11 | 10,High risk tolerance 12 | 11,Saving per month >= £250 13 | 12,"Savings >= £10,000" 14 | 13,Pension contributions per month >= £300 15 | 14,No investments 16 | 15,Investment goal: Retirement 17 | 16,Medium risk tolerance 18 | 17,Saving per month <= £100 19 | 18,Rainy day fund <= £500 20 | 19,"Credit card debt >= £3,000" 21 | 20,No help to buy ISA 22 | 21,Rent per month >= £550 23 | 22,No pension contributions 24 | 23,Financial goal: Save enough for a mortgage 25 | 24,Savings location: Other bank 26 | 25,Other savings: >= £100 -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/usecase_profiles_knowledge.csv: -------------------------------------------------------------------------------- 1 | profileId,knowledgeId,weight 2 | 1,1,1 3 | 1,2,1 4 | 1,3,1 5 | 1,4,1 6 | 1,5,1 7 | 1,6,1 8 | 1,7,1 9 | 1,8,1 10 | 1,9,1 11 | 1,10,1 12 | 2,11,1 13 | 2,2,1 14 | 2,12,1 15 | 2,13,1 16 | 2,5,1 17 | 2,6,1 18 | 2,14,1 19 | 2,15,1 20 | 2,16,1 21 | 3,17,1 22 | 3,18,1 23 | 3,19,1 24 | 3,5,1 25 | 3,23,1 26 | 3,24,1 27 | 3,25,1 28 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/usecase_questions.csv: -------------------------------------------------------------------------------- 1 | questionId,name,intent,question,query,type 2 | 1,monthly savings,monthly savings,How much are you saving each month?,,Prompt 3 | 2,rainy day fund,rainy day fund,"Do you have a rainy day fund and, if so, how much?",,Prompt 4 | 3,savings balance,savings balance,Do you have a savings account and how much in savings?,,Prompt 5 | 4,pension contributions,pension contributions,Are you paying into your pension and if so how much?,,Prompt 6 | 5,mortgage amount,mortgage amount,"Do you have a mortgage and if so, how much?",,Prompt 7 | 6,debt,debt,"Do you have any loans or credit card debt and if so, how much?",,Prompt 8 | 7,investments balance,investments balance,"Do you have any investments and if so, how much?",,Prompt 9 | 8,investment account,investment account,What type of investment account do you have?,,Prompt 10 | 9,financial goal,financial goal,What is your financial goal?,,Prompt 11 | 10,risk tolerance,risk tolerance,What is your risk tolerance?,,Prompt 12 | 11,help to buy ISA,help to buy ISA,Do you have a help to buy ISA?,,Prompt 13 | 12,monthly rent,monthly rent,"Do you pay rent and if so, how much per month?",,Prompt 14 | 13,savings location,savings location,"If you have savings, where are you saving that money currently?",,Prompt 15 | 14,other savings,other savings,"Do you have savings anywhere else except the already mentioned?",,Prompt 16 | 21,monthly savings,,,,Pull 17 | 22,rainy day fund,,,,Pull 18 | 23,savings balance,,,,Pull 19 | 24,pension contributions,,,,Pull 20 | 25,mortgage amount,,,,Pull 21 | 26,debt,,,,Pull 22 | 27,investments balance,,,,Pull 23 | 28,investment account,,,,Pull 24 | 29,financial goal,,,,Pull 25 | 30,risk tolerance,,,,Pull 26 | 31,help to buy ISA,,,,Pull 27 | 32,monthly rent,,,,Pull 28 | 33,savings location,,,,Pull 29 | 34,other savings,,,,Pull 30 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/usecase_questions_knowledge.csv: -------------------------------------------------------------------------------- 1 | questionId,knowledgeId,predicate,weight 2 | 1,1,>=500,1 3 | 2,2,>=3000,1 4 | 3,3,>=50000,1 5 | 4,4,>=600,1 6 | 5,5,>=150000,1 7 | 6,6,0,1 8 | 7,7,>=10000,1 9 | 8,8,is not null,1 10 | 9,9,='early retirement',1 11 | 10,10,='high',1 12 | 1,11,>=250,1 13 | 3,12,>=10000,1 14 | 4,13,>=300,1 15 | 7,14,0,1 16 | 9,15,='retirement',1 17 | 10,16,='medium',1 18 | 1,17,<=100,1 19 | 2,18,<=500,1 20 | 6,19,>=3000,1 21 | 5,5,>=550,1 22 | 9,23,='mortgage',1 23 | 13,24,='other bank',1 24 | 14,25,>=100,1 25 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/user_assessment.csv: -------------------------------------------------------------------------------- 1 | userId,assessmentId 2 | 3,1 3 | 3,2 4 | 5,3 5 | 7,4 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/user_conversation.csv: -------------------------------------------------------------------------------- 1 | userId,conversationId 2 | 3,1 3 | 3,2 4 | 5,3 5 | 7,4 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/user_value.csv: -------------------------------------------------------------------------------- 1 | userId,valueId 2 | 3,1 3 | 3,2 4 | 3,3 5 | 3,4 6 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/users.csv: -------------------------------------------------------------------------------- 1 | userId,name 2 | 1,Alan 3 | 2,Brenda 4 | 3,Charlie 5 | 4,Darren, 6 | 5,Emma 7 | 6,Fiona 8 | 7,Gerald 9 | 8,Helen 10 | 9,Irene 11 | 10,John 12 | 11,Ken 13 | 12,Lionel 14 | 13,Mike 15 | 14,Nancy 16 | 15,Olive 17 | 16,Patty 18 | 17,Quinton 19 | 18,Ronald 20 | 19,Sam 21 | 20,Tracy 22 | 21,Ulrich 23 | 22,Victor 24 | 23,Wendy 25 | 24,Xavier 26 | 25,Yves 27 | 26,Zander 28 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/value_knowledge.csv: -------------------------------------------------------------------------------- 1 | valueId,questionId,relationshipType 2 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/value_question.csv: -------------------------------------------------------------------------------- 1 | valueId,questionId,relationshipType 2 | 1,2,"FOR" 3 | 1,2,"LEADS_TO" 4 | 2,5,"FOR" 5 | 2,5,"LEADS_TO" 6 | 3,3,"FOR" 7 | 3,3,"LEADS_TO" 8 | 4,7,"FOR" 9 | 4,7,"LEADS_TO" 10 | -------------------------------------------------------------------------------- /financialhealthcheckScottLogic/samples/graphdb/values.csv: -------------------------------------------------------------------------------- 1 | valueId,value 2 | 1,250.00 3 | 2,0 4 | 3,9000.00 5 | 4,0.00 6 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .vscode/settings.json 4 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react/recommended" 11 | ], 12 | "overrides": [ 13 | { 14 | "env": { 15 | "node": true 16 | }, 17 | "files": [".eslintrc.{js,cjs}"], 18 | "parserOptions": { 19 | "sourceType": "script" 20 | } 21 | } 22 | ], 23 | "parser": "@typescript-eslint/parser", 24 | "parserOptions": { 25 | "ecmaVersion": "latest", 26 | "sourceType": "module" 27 | }, 28 | "plugins": ["@typescript-eslint", "react", "@stylistic/js"], 29 | "rules": { 30 | "indent": ["error", 2], 31 | "linebreak-style": ["error", "unix"], 32 | "quotes": ["error", "single"], 33 | "semi": ["error", "always"], 34 | "@stylistic/js/eol-last": ["error", "always"] 35 | }, 36 | "settings": { 37 | "react": { 38 | "version": "detect" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .env 4 | .vscode 5 | /.idea/material_theme_project_new.xml 6 | -------------------------------------------------------------------------------- /frontend/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /frontend/.idea/frontend.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /frontend/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Choose our version of Node 2 | FROM node:alpine 3 | 4 | # Set up a working directory 5 | WORKDIR /app 6 | 7 | # Copy just the requirements into the working directory so it gets cached by itself 8 | COPY . /app 9 | 10 | # Install the dependencies 11 | RUN npm install 12 | 13 | # Expose port app runs on 14 | EXPOSE 8650 15 | 16 | # Run app 17 | ENTRYPOINT [ "npm", "start" ] 18 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Frontend 2 | 3 | UI service for interfacing with InferGPT. 4 | 5 | ## Set up 6 | 7 | 1. Ensure the `.env` file has been configured as described in the main [README](../README.md). 8 | 9 | This README covers instructions on how to run the application: 10 | - Locally 11 | - In a Docker Container 12 | 13 | For ease of use, we would recommended that you run the entire application using **Docker Compose** instead. See main [README](../README.md). 14 | 15 | If you would prefer not to use **Docker Compose**, read on... 16 | 17 | ## Running Locally 18 | 19 | 1. Change directory to the `/frontend` space, then run the following to install the dependencies 20 | 21 | ```bash 22 | npm install 23 | ``` 24 | 25 | 2. Run the app using 26 | 27 | ```bash 28 | npm start 29 | ``` 30 | 31 | Check the frontend app is running at [http://localhost:8650](http://localhost:8650) 32 | 33 | ## Running in a Docker Container 34 | 35 | 1. Build the Docker image 36 | 37 | ```bash 38 | docker build -t {my-frontend-image-name} . 39 | ``` 40 | 41 | 2. Run the app within a Docker container 42 | 43 | ```bash 44 | docker run --env-file ../.env -p 8650:8650 {my-frontend-image-name} 45 | ``` 46 | 47 | > Note that we pass in the entire environment file that contains all our application's configuration. This means some unneccessary configuration is also being passed in. This is fine when testing locally. In production, we must limit this to only the essential frontend configuration (see environment configuration within [Docker Compose](../compose.yml)). 48 | 49 | Check the frontend app is running at [http://localhost:8650](http://localhost:8650) 50 | 51 | ## Frontend Linting 52 | 53 | We have set up ESLint for the frontend ccode. To run the linter, use the following command: 54 | 55 | ```bash 56 | npm run lint 57 | ``` 58 | 59 | This will attempt to fix any errors you have and will also show you any errors that it can't fix. To automatically fix errors on save (for vscode) do the following: 60 | 61 | - create a `.vscode` folder in the root of the project 62 | - create a `settings.json` file in the `.vscode` folder 63 | - add the following to the `settings.json` file: 64 | 65 | ```json 66 | { 67 | "editor.codeActionsOnSave": { 68 | "source.fixAll.eslint": "explicit" 69 | } 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llm-chat-ui", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "description": "UI for a chatbot with restful api", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "webpack serve --port 8650", 9 | "build": "webpack", 10 | "lint": "eslint .", 11 | "lint:fix": "eslint . --fix" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@stylistic/eslint-plugin-js": "^1.7.0", 17 | "@types/react": "^18.2.70", 18 | "@types/react-dom": "^18.2.22", 19 | "@types/webpack-dev-server": "^4.7.2", 20 | "@typescript-eslint/eslint-plugin": "^7.4.0", 21 | "@typescript-eslint/parser": "^7.4.0", 22 | "css-loader": "^6.10.0", 23 | "dotenv": "^16.4.5", 24 | "eslint": "^8.57.0", 25 | "eslint-plugin-react": "^7.34.1", 26 | "html-webpack-plugin": "^5.6.0", 27 | "mini-css-extract-plugin": "^2.8.1", 28 | "ts-loader": "^9.5.1", 29 | "typescript": "^5.4.3", 30 | "webpack": "^5.91.0", 31 | "webpack-cli": "^5.1.4", 32 | "webpack-dev-server": "^5.0.4" 33 | }, 34 | "dependencies": { 35 | "classnames": "^2.5.1", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WaitThatShouldntWork/Infer/e6dc602d3c2eeeff172f394390218a71c7e3e8af/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Infer-GPT 5 | 6 | 7 | 8 | 9 | 10 |
App is loading...
11 | -------------------------------------------------------------------------------- /frontend/src/app.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | align-items: center; 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | justify-content: center; 7 | } 8 | 9 | .title { 10 | margin: 1rem 0 0 0; 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './app.module.css'; 3 | import { Chat } from './components/chat'; 4 | import { Input } from './components/input'; 5 | import { useMessages } from './useMessages'; 6 | 7 | export const App = () => { 8 | const { sendMessage, messages, waiting } = useMessages(); 9 | 10 | return ( 11 |
12 | 13 | 14 |
15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/src/components/chat.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | align-items: center; 4 | border-radius: 5px; 5 | border: 1px solid var(--border-primary); 6 | box-sizing: border-box; 7 | display: flex; 8 | flex-direction: column; 9 | flex: 1; 10 | height: 75%; 11 | margin: 2rem 0 1rem 0; 12 | overflow-y: auto; 13 | padding: 1rem; 14 | width: 50%; 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/components/chat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useState } from 'react'; 2 | import { Message, MessageComponent } from './message'; 3 | import styles from './chat.module.css'; 4 | import { Waiting } from './waiting'; 5 | import { ConnectionStatus } from './connection-status'; 6 | import { WebsocketContext, MessageType, Message as wsMessage } from '../session/websocket-context'; 7 | import { Confirmation, ConfirmModal } from './confirm-modal'; 8 | export interface ChatProps { 9 | messages: Message[]; 10 | waiting: boolean; 11 | } 12 | 13 | const mapWsMessageToConfirmation = (message: wsMessage): Confirmation | undefined => { 14 | if (!message.data) { 15 | return; 16 | } 17 | const parts = message.data.split(':'); 18 | return { id: parts[0], requestMessage: parts[1], result: null }; 19 | }; 20 | 21 | export const Chat = ({ messages, waiting }: ChatProps) => { 22 | const containerRef = React.useRef(null); 23 | const { isConnected, lastMessage, send } = useContext(WebsocketContext); 24 | const [chart, setChart] = useState(undefined); 25 | const [confirmation, setConfirmation] = useState(null); 26 | 27 | useEffect(() => { 28 | if (lastMessage && lastMessage.type === MessageType.IMAGE) { 29 | const imageData = `data:image/png;base64,${lastMessage.data}`; 30 | setChart(imageData); 31 | } 32 | if (lastMessage && lastMessage.type === MessageType.CONFIRMATION) { 33 | const newConfirmation = mapWsMessageToConfirmation(lastMessage); 34 | if (newConfirmation) 35 | setConfirmation(newConfirmation); 36 | } 37 | }, [lastMessage]); 38 | 39 | useEffect(() => { 40 | if (containerRef.current) { 41 | containerRef.current.scrollTo(0, containerRef.current.scrollHeight); 42 | } 43 | }, [messages.length]); 44 | 45 | return ( 46 | <> 47 | 48 |
49 | 50 | {messages.map((message, index) => ( 51 | 52 | ))} 53 | {chart && Generated chart} 54 | {waiting && } 55 |
56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /frontend/src/components/confirm-modal.module.css: -------------------------------------------------------------------------------- 1 | .modal{ 2 | width: 40%; 3 | height: 40%; 4 | background-color: #4c4c4c; 5 | color: var(--text-color-primary); 6 | border: 2px black; 7 | border-radius: 10px; 8 | } 9 | 10 | .modalContent{ 11 | width: 100%; 12 | height: 100%; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | .header{ 18 | text-align: center; 19 | } 20 | 21 | .modal::backdrop{ 22 | background: rgb(0,0,0,0.8); 23 | } 24 | 25 | .requestMessage{ 26 | flex-grow: 1; 27 | } 28 | 29 | .buttonsBar{ 30 | display: flex; 31 | gap: 0.5rem; 32 | } 33 | 34 | .button{ 35 | color: var(--text-color-primary); 36 | font-weight: bold; 37 | border: none; 38 | width: 100%; 39 | padding: 1rem; 40 | cursor: pointer; 41 | border-radius: 3px; 42 | } 43 | 44 | 45 | .cancel{ 46 | composes: button; 47 | background-color: var(--background-color-primary); 48 | } 49 | 50 | .cancel:hover{ 51 | background-color: #141414; 52 | transition: all 0.5s; 53 | } 54 | 55 | .confirm{ 56 | composes: button; 57 | background-color: var(--blue); 58 | } 59 | 60 | .confirm:hover{ 61 | background-color: #146AFF; 62 | transition: all 0.5s; 63 | } 64 | -------------------------------------------------------------------------------- /frontend/src/components/confirm-modal.tsx: -------------------------------------------------------------------------------- 1 | import Styles from './confirm-modal.module.css'; 2 | import { useEffect, useRef } from 'react'; 3 | import { Message, MessageType } from '../session/websocket-context'; 4 | import React from 'react'; 5 | 6 | export interface Confirmation { 7 | id: string, 8 | requestMessage: string, 9 | result: boolean | null 10 | } 11 | 12 | interface ConfirmModalProps { 13 | confirmation: Confirmation | null, 14 | setConfirmation: (confirmation: Confirmation | null) => void, 15 | send: (message: Message) => void 16 | } 17 | 18 | export const ConfirmModal = ({ confirmation, setConfirmation, send }: ConfirmModalProps) => { 19 | const mapConfirmationToMessage = (confirmation: Confirmation): Message => { 20 | return { type: MessageType.CONFIRMATION, data: confirmation.id + ':' + (confirmation.result ? 'y' : 'n') }; 21 | }; 22 | 23 | const updateConfirmationResult = (newResult: boolean) => { 24 | if (confirmation) { 25 | setConfirmation({ ...confirmation, result: newResult }); 26 | } 27 | }; 28 | 29 | 30 | const modalRef = useRef(null); 31 | 32 | useEffect(() => { 33 | if (confirmation) { 34 | if (confirmation.result !== null) { 35 | send(mapConfirmationToMessage(confirmation)); 36 | setConfirmation(null); 37 | } else { 38 | modalRef.current?.showModal(); 39 | } 40 | } else { 41 | modalRef.current?.close(); 42 | } 43 | }, [confirmation]); 44 | 45 | return ( 46 | updateConfirmationResult(false)}> 47 |
48 |

Confirmation

49 |

{confirmation && confirmation.requestMessage}

50 |
51 | 52 | 53 |
54 |
55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /frontend/src/components/connection-status.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | top: 0; 4 | right: 0; 5 | float: right; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | } 10 | 11 | .dot { 12 | width: 10px; 13 | height: 10px; 14 | border-radius: 50%; 15 | margin: 10px; 16 | } 17 | 18 | .green { 19 | background-color: green; 20 | } 21 | 22 | .red { 23 | background-color: red; 24 | } -------------------------------------------------------------------------------- /frontend/src/components/connection-status.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './connection-status.module.css'; 4 | 5 | export interface ConnectionStatusProps { 6 | isConnected: boolean; 7 | } 8 | 9 | export const ConnectionStatus = ({ isConnected }: ConnectionStatusProps) => { 10 | return ( 11 |
12 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/src/components/input.module.css: -------------------------------------------------------------------------------- 1 | .inputContainer { 2 | box-sizing: border-box; 3 | display: inline-block; 4 | margin: 1rem 0 2rem 0; 5 | position: relative; 6 | width: 50%; 7 | } 8 | 9 | .input { 10 | background-color: var(--border-primary); 11 | border-radius: 8px; 12 | border: none; 13 | box-sizing: border-box; 14 | color: var(--text-color-primary); 15 | font-size: 1rem; 16 | height: 3rem; 17 | outline: none; 18 | padding: 0 34px 0 10px; 19 | width: 100%; 20 | } 21 | 22 | .input:focus { 23 | border: 1px solid var(--blue); 24 | } 25 | 26 | .sendButton { 27 | background-color: transparent; 28 | border: none; 29 | cursor: pointer; 30 | margin: 0; 31 | outline: none; 32 | padding: 0; 33 | position: absolute; 34 | right: 10px; 35 | top: 50%; 36 | transform: translateY(-50%); 37 | height: 26px; 38 | width: 26px; 39 | } 40 | 41 | .disabled { 42 | cursor: not-allowed; 43 | filter: invert(24%) sepia(3%) saturate(0%) hue-rotate(263deg) brightness(95%) contrast(90%); 44 | } 45 | 46 | .sendButton:hover:not(.disabled) { 47 | filter: invert(31%) sepia(75%) saturate(3303%) hue-rotate(200deg) brightness(102%) contrast(105%); 48 | } 49 | 50 | .sendButton:active:not(.disabled) { 51 | filter: none; 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/components/input.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, FormEvent, useCallback, useMemo, useState } from 'react'; 2 | import styles from './input.module.css'; 3 | import RightArrow from '../icons/map-arrow-right.svg'; 4 | import classNames from 'classnames'; 5 | 6 | export interface InputProps { 7 | sendMessage: (message: string) => void; 8 | } 9 | 10 | export const Input = ({ sendMessage }: InputProps) => { 11 | const [userInput, setUserInput] = useState(''); 12 | 13 | const onChange = useCallback((event: ChangeEvent) => { 14 | setUserInput(event.target.value); 15 | }, []); 16 | 17 | const onSend = useCallback( 18 | (event: FormEvent) => { 19 | event.preventDefault(); 20 | sendMessage(userInput); 21 | setUserInput(''); 22 | }, 23 | [sendMessage, userInput] 24 | ); 25 | 26 | const sendDisabled = useMemo(() => userInput.length === 0, [userInput]); 27 | 28 | return ( 29 |
30 | 38 | 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/components/message.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | align-items: center; 3 | display: flex; 4 | flex-direction: row; 5 | gap: 1rem; 6 | justify-content: center; 7 | } 8 | 9 | .bot { 10 | align-self: flex-start; 11 | } 12 | 13 | .user { 14 | align-self: flex-end; 15 | flex-direction: row-reverse; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/components/message.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import React, { useMemo } from 'react'; 3 | import styles from './message.module.css'; 4 | import UserIcon from '../icons/user.svg'; 5 | import BotIcon from '../icons/cpu.svg'; 6 | 7 | export enum Role { 8 | User = 'User', 9 | Bot = 'Bot', 10 | } 11 | 12 | export interface Message { 13 | role: Role; 14 | content: string; 15 | time: string; 16 | } 17 | 18 | export interface MessageProps { 19 | message: Message; 20 | } 21 | 22 | export interface MessageStyle { 23 | icon: string; 24 | class: string; 25 | } 26 | 27 | const roleStyleMap: Record = { 28 | [Role.User]: { 29 | icon: UserIcon, 30 | class: styles.user, 31 | }, 32 | [Role.Bot]: { 33 | icon: BotIcon, 34 | class: styles.bot, 35 | }, 36 | }; 37 | 38 | export const MessageComponent = ({ message }: MessageProps) => { 39 | const { content, role } = message; 40 | 41 | const { class: roleClass, icon } = useMemo(() => roleStyleMap[role], [role]); 42 | 43 | return ( 44 |
45 | 46 |

{content}

47 |
48 | ); 49 | }; 50 | -------------------------------------------------------------------------------- /frontend/src/components/waiting.module.css: -------------------------------------------------------------------------------- 1 | 2 | @keyframes waiting { 3 | 0%, 80%, 100% { 4 | opacity: 0; 5 | } 6 | 40% { 7 | opacity: 1; 8 | } 9 | } 10 | 11 | .waiting { 12 | align-self: baseline; 13 | display: inline-block; 14 | } 15 | 16 | .waitingDot { 17 | animation: waiting 1.5s infinite ease-in-out; 18 | background-color: var(--text-color-primary); 19 | border-radius: 50%; 20 | display: inline-block; 21 | height: 8px; 22 | margin-right: 4px; 23 | width: 8px; 24 | } 25 | 26 | .waitingDot:nth-child(2) { 27 | animation-delay: 0.2s; 28 | } 29 | 30 | .waitingDot:nth-child(3) { 31 | animation-delay: 0.4s; 32 | } -------------------------------------------------------------------------------- /frontend/src/components/waiting.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './waiting.module.css'; 3 | 4 | export const Waiting = () => { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' 2 | declare module '*.svg' 3 | -------------------------------------------------------------------------------- /frontend/src/icons/cpu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/src/icons/map-arrow-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/src/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { App } from './app'; 4 | import './styles.css'; 5 | import { WebsocketProvider } from './session/websocket-provider'; 6 | 7 | const container = document.getElementById('app-root')!; 8 | const root = createRoot(container); 9 | 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /frontend/src/server.ts: -------------------------------------------------------------------------------- 1 | export interface ChatMessageResponse { 2 | message: string; 3 | } 4 | 5 | function createChatMessageResponse(message: string): ChatMessageResponse { 6 | return { message }; 7 | } 8 | 9 | export const getResponse = async (message: string): Promise => { 10 | if (message == 'healthcheck') { 11 | return checkBackendHealth(); 12 | } else { 13 | return callChatEndpoint(message); 14 | } 15 | }; 16 | 17 | const unhappyHealthcheckResponse = createChatMessageResponse('InferGPT healthcheck: backend is unhealthy. Unable to healthcheck Neo4J. Please check the README files for further guidance'); 18 | 19 | const checkBackendHealth = async (): Promise => { 20 | return await fetch(`${process.env.BACKEND_URL}/health`) 21 | .then(response => response.json()) 22 | .then(responseJson => { return createChatMessageResponse(responseJson); }) 23 | .catch(error => { 24 | console.error('Error making REST call to /chat: ', error); 25 | return unhappyHealthcheckResponse; 26 | }); 27 | }; 28 | 29 | const unhappyChatResponse = createChatMessageResponse('I\'m sorry, but I was unable to process your message. Please check the status of the service using the phrase "healthcheck"'); 30 | 31 | const callChatEndpoint = async (message: string): Promise => { 32 | return await fetch(`${process.env.BACKEND_URL}/chat?utterance=${message}`) 33 | .then(response => { 34 | if (!response.ok) { 35 | console.log('error found'); 36 | throw new Error(`HTTP error! Status: ${response.status}`); 37 | } 38 | return response; 39 | }) 40 | .then(response => response.json()) 41 | .then(responseJson => { return createChatMessageResponse(responseJson); }) 42 | .catch(error => { 43 | console.error('Error making REST call to /chat: ', error); 44 | return unhappyChatResponse; 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /frontend/src/session/websocket-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export enum MessageType { 4 | PING = 'ping', 5 | CHAT = 'chat', 6 | IMAGE = 'image', 7 | CONFIRMATION = 'confirmation', 8 | } 9 | 10 | export interface Message { 11 | type: MessageType; 12 | data?: string; 13 | } 14 | 15 | export interface Connection { 16 | isConnected: boolean; 17 | lastMessage: Message | null; 18 | send: (message: Message) => void; 19 | } 20 | 21 | export const WebsocketContext = createContext({ 22 | isConnected: false, 23 | lastMessage: null, 24 | send: () => {}, 25 | }); 26 | -------------------------------------------------------------------------------- /frontend/src/session/websocket-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; 2 | import { Connection, Message, MessageType, WebsocketContext } from './websocket-context'; 3 | 4 | export interface WebsocketProviderProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | const WS_URL = process.env.WS_URL || 'ws://localhost:8250/ws'; 9 | const heartbeatInterval = 10000; 10 | const ping = JSON.stringify({ type: MessageType.PING }); 11 | 12 | export const WebsocketProvider = ({ children }: WebsocketProviderProps) => { 13 | const [isConnected, setIsConnected] = useState(false); 14 | const [lastMessage, setLastMessage] = useState(null); 15 | 16 | const ws = useRef(null); 17 | 18 | const heartbeat = useCallback(() => { 19 | if (!ws.current || ws.current?.readyState !== 1) return; 20 | 21 | ws.current.send(ping); 22 | 23 | setTimeout(heartbeat, heartbeatInterval); 24 | }, []); 25 | 26 | useEffect(() => { 27 | const socket = new WebSocket(WS_URL); 28 | 29 | socket.onopen = () => { 30 | setIsConnected(true); 31 | heartbeat(); 32 | }; 33 | socket.onclose = () => { 34 | setIsConnected(false); 35 | }; 36 | socket.onmessage = (event) => { 37 | const data = JSON.parse(event.data); 38 | setLastMessage(data); 39 | }; 40 | 41 | socket.onerror = (error) => { 42 | setIsConnected(false); 43 | console.error('WebSocket error:', error); 44 | }; 45 | 46 | ws.current = socket; 47 | 48 | return () => { 49 | socket.close(); 50 | }; 51 | }, []); 52 | 53 | const sendMessage = useCallback((message: Message) => { 54 | if (ws.current?.readyState === 1) { 55 | ws.current.send(JSON.stringify(message)); 56 | } 57 | }, []); 58 | 59 | const connection = useMemo( 60 | () => ({ 61 | isConnected, 62 | lastMessage, 63 | send: sendMessage || (() => {}), 64 | }), 65 | [isConnected, lastMessage, sendMessage] 66 | ); 67 | 68 | return ( 69 | 70 | {children} 71 | 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | html, body, #app-root { 2 | font-family: 'Source Sans 3', sans-serif; 3 | height: 100%; 4 | margin: 0; 5 | 6 | --background-color-primary: #282828; 7 | --blue: #007bff; 8 | --border-primary: #424242; 9 | --text-color-primary: #ffffff; 10 | --text-color-secondary: #899090; 11 | 12 | background-color: var(--background-color-primary); 13 | color: var(--text-color-primary); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/src/useMessages.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { Message, Role } from './components/message'; 3 | import { getResponse } from './server'; 4 | 5 | const starterMessage: Message = { 6 | role: Role.Bot, 7 | content: 'Hello, how can I help you?', 8 | time: new Date().toLocaleTimeString(), 9 | }; 10 | 11 | export interface UseMessagesHook { 12 | sendMessage: (message: string) => void; 13 | messages: Message[]; 14 | waiting: boolean; 15 | } 16 | 17 | export const useMessages = (): UseMessagesHook => { 18 | const [waiting, setWaiting] = useState(false); 19 | const [messages, setMessages] = useState([starterMessage]); 20 | 21 | const appendMessage = useCallback((message: string, role: Role) => { 22 | setMessages((prevMessages) => [ 23 | ...prevMessages, 24 | { role, content: message, time: new Date().toLocaleTimeString() }, 25 | ]); 26 | }, []); 27 | 28 | const sendMessage = useCallback( 29 | async (message: string) => { 30 | appendMessage(message, Role.User); 31 | setWaiting(true); 32 | const response = await getResponse(message); 33 | setWaiting(false); 34 | appendMessage(response.message, Role.Bot); 35 | }, 36 | [appendMessage, messages] 37 | ); 38 | 39 | return { 40 | sendMessage, 41 | messages, 42 | waiting, 43 | }; 44 | }; 45 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "lib": ["dom", "esnext"], 8 | "strict": true, 9 | "sourceMap": true, 10 | "target": "esnext" 11 | }, 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import webpack from 'webpack'; 4 | import dotenv from 'dotenv'; 5 | import path from 'path'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 9 | const localEnv = dotenv.config({ path: path.resolve(__dirname, '../.env') }).parsed; 10 | const env = { ...process.env, ...localEnv }; 11 | 12 | const config = { 13 | mode: 'development', 14 | entry: './src/index.tsx', 15 | output: { 16 | path: path.resolve(__dirname, 'dist'), 17 | publicPath: '/', 18 | filename: '[name].bundle.js' 19 | }, 20 | 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(ts|tsx)$/, 25 | exclude: /node_modules/, 26 | resolve: { 27 | extensions: ['.ts', '.tsx', '.js', '.json'], 28 | }, 29 | use: 'ts-loader', 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [ 34 | MiniCssExtractPlugin.loader, 35 | { 36 | loader: 'css-loader', 37 | options: { 38 | importLoaders: 1, 39 | modules: { 40 | localIdentName: '[local]__[hash:base64:5]' 41 | }, 42 | }, 43 | }, 44 | ], 45 | include: /\.module\.css$/, 46 | }, 47 | { 48 | test: /\.css$/, 49 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 50 | exclude: /\.module\.css$/, 51 | }, 52 | { 53 | test: /\.(png|svg|jpg|jpeg|gif)$/i, 54 | type: 'asset/resource', 55 | } 56 | ], 57 | }, 58 | devtool: 'source-map', 59 | plugins: [ 60 | new HtmlWebpackPlugin({ 61 | template: './public/index.html', 62 | favicon: './public/favicon.ico', 63 | }), 64 | new MiniCssExtractPlugin(), 65 | new webpack.DefinePlugin({ 66 | 'process.env': JSON.stringify(env) 67 | }) 68 | ], 69 | }; 70 | 71 | export default config; 72 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "backend/src", 4 | "backend/tests" 5 | ], 6 | "exclude": [ 7 | "**/node_modules", 8 | "**/__pycache__", 9 | "financialhealthcheckScottLogic" 10 | ], 11 | "typeCheckingMode": "standard", 12 | "reportMissingImports": true, 13 | "reportMissingTypeStubs": false, 14 | } 15 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | A directory for all full system testing. Currently only concerned with e2e (end to end) testing. 4 | 5 | ### To do 6 | - [ ] Implement our first E2E test 7 | --------------------------------------------------------------------------------