├── .DS_Store ├── .github └── workflows │ ├── development.yml │ └── production.yml ├── .gitignore ├── LICENSE ├── README.md ├── aws ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── aws-cdk-python-prototype.ts ├── cdk.json ├── jest.config.js ├── lib │ ├── law-bot-backend-2.ts │ ├── law-bot-backend.ts │ ├── law-bot-slack.ts │ └── stacks │ │ └── GithubActionsOIDCProvider │ │ └── GithubActionsOIDCProvider.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── backend-2 ├── .env.example ├── .gitignore ├── README.md ├── azure.ts ├── bun.lockb ├── dev │ ├── docker-compose.yml │ ├── legalReasoning.json │ └── update-reasoning.ts ├── index.ts ├── main.ts ├── package-lock.json ├── package.json ├── reasoning.ts ├── repository.ts ├── search │ └── law-article-tree.ts ├── tsconfig.json └── utils.ts ├── backend ├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── Dockerfile.dev ├── README.md ├── openapi_schema.json ├── requirements.txt └── src │ ├── __init__.py │ ├── api │ ├── __init__.py │ └── chat.py │ ├── app.py │ ├── config │ ├── LoggingMiddleware.py │ ├── __init__.py │ ├── globals.py │ └── logconfig.py │ ├── models │ ├── __init__.py │ ├── request.py │ └── response.py │ └── services │ ├── ChatBotPipeline.py │ └── __init__.py ├── data ├── .DS_Store ├── .env.example ├── .gitignore ├── ArG.json ├── ArG_by_article.json ├── README.md ├── SR-210-01012024-DE.xml ├── SR-220-01012024-DE.xml ├── SR-822.11-01092023-DE.xml ├── archive │ └── scripts_extraction │ │ ├── Read_pflichten_des_arbeitsgebers.py │ │ └── retrieve_law_data.py ├── create_azure_index.py ├── graph_law.py ├── obligationrecht.json ├── obligationrecht_by_article.json ├── requirements.txt ├── retrieve_law_data.py ├── retrieve_law_data_SR210.py ├── retrieve_law_data_SR822_11.py ├── schweizerisches_zivilgesetzbuch.json ├── schweizerisches_zivilgesetzbuch_by_article.json └── upload_index_data.py ├── frontend ├── .env.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app │ ├── (chat) │ │ ├── chat │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── actions.ts │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── chat │ │ │ └── route.ts │ │ └── expert │ │ │ └── route.ts │ ├── expert │ │ ├── chat │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ └── page.tsx │ ├── globals.css │ ├── layout.tsx │ ├── opengraph-image.png │ ├── share │ │ └── [id] │ │ │ └── page.tsx │ ├── sign-in │ │ └── page.tsx │ └── twitter-image.png ├── auth.ts ├── components │ ├── button-scroll-to-bottom.tsx │ ├── chat-expert.tsx │ ├── chat-history.tsx │ ├── chat-list.tsx │ ├── chat-message-actions.tsx │ ├── chat-message.tsx │ ├── chat-panel.tsx │ ├── chat-scroll-anchor.tsx │ ├── chat-share-dialog.tsx │ ├── chat.tsx │ ├── clear-history.tsx │ ├── empty-screen.tsx │ ├── external-link.tsx │ ├── footer.tsx │ ├── header.tsx │ ├── login-button.tsx │ ├── markdown.tsx │ ├── prompt-form.tsx │ ├── providers.tsx │ ├── sidebar-actions.tsx │ ├── sidebar-desktop.tsx │ ├── sidebar-footer.tsx │ ├── sidebar-item.tsx │ ├── sidebar-items.tsx │ ├── sidebar-list.tsx │ ├── sidebar-mobile.tsx │ ├── sidebar-toggle.tsx │ ├── sidebar.tsx │ ├── tailwind-indicator.tsx │ ├── theme-toggle.tsx │ ├── ui │ │ ├── alert-dialog.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── codeblock.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-chat-block.tsx │ │ ├── dropdown-menu.tsx │ │ ├── expert-toggle.tsx │ │ ├── icons.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── switch.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx │ └── user-menu.tsx ├── lib │ ├── hooks │ │ ├── use-at-bottom.tsx │ │ ├── use-copy-to-clipboard.tsx │ │ ├── use-enter-submit.tsx │ │ ├── use-local-storage.ts │ │ └── use-sidebar.tsx │ ├── types.ts │ └── utils.ts ├── middleware.ts ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.cjs ├── public │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon.ico │ └── law-bot-temporary-logo.png ├── tailwind.config.js └── tsconfig.json ├── ignore-build-step.js └── slack ├── .gitignore ├── responder ├── index.ts ├── notion.ts ├── package-lock.json ├── package.json ├── slack.ts └── tsconfig.json └── worker ├── index.ts ├── package-lock.json ├── package.json ├── slack.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/development.yml: -------------------------------------------------------------------------------- 1 | name: Development 2 | 3 | on: 4 | push: 5 | branches: 6 | - "backend/*" 7 | - "slack/*" 8 | pull_request: 9 | branches: 10 | - "backend/*" 11 | - "slack/*" 12 | 13 | permissions: 14 | id-token: write 15 | contents: read 16 | 17 | env: 18 | ENV_NAME: development 19 | 20 | jobs: 21 | deploy: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - name: Install Node.js 27 | uses: actions/setup-node@v3 28 | with: 29 | node-version: 20 30 | 31 | # TODO Optimize the Nodejs installation using GitHub Actions 32 | # TODO Optimize and cache node_modules 33 | - run: npm i -g pnpm 34 | working-directory: ./aws 35 | 36 | - run: pnpm i --no-frozen-lockfile --shamefully-hoist 37 | working-directory: ./aws 38 | 39 | - name: Configure AWS Credentials 40 | uses: aws-actions/configure-aws-credentials@v4 41 | with: 42 | aws-region: eu-central-1 43 | role-to-assume: arn:aws:iam::851725404161:role/GitHubRoleFor-access2justice-law-bot 44 | timeout-minutes: 3 # assuming an AWS role never takes that long, but error loops do 45 | 46 | - run: aws sts get-caller-identity 47 | 48 | - run: pnpm dlx cdk deploy 'LawBotBackend2LambdaApiStack-*' --require-approval never 49 | working-directory: ./aws 50 | env: 51 | ALLOWED_ORIGINS: ${{ vars.ALLOWED_ORIGINS }} 52 | AUTHENTICATION_KEY: ${{ secrets.AUTHENTICATION_KEY }} 53 | AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} 54 | AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} 55 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_EMBEDDING_DEPLOYMENT }} 56 | AZURE_SEARCH_ENPOINT: ${{ secrets.AZURE_SEARCH_ENPOINT }} 57 | AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }} 58 | AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} 59 | AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} 60 | RUNNING_ENV: "development" 61 | DB_CONNECTION_STRING: ${{ secrets.DB_CONNECTION_STRING }} 62 | 63 | - run: pnpm dlx cdk deploy 'LawBotBackendLambdaApiStack-*' --require-approval never 64 | working-directory: ./aws 65 | env: 66 | ALLOWED_ORIGINS: ${{ vars.ALLOWED_ORIGINS }} 67 | AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} 68 | AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} 69 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_EMBEDDING_DEPLOYMENT }} 70 | AZURE_SEARCH_ENPOINT: ${{ secrets.AZURE_SEARCH_ENPOINT }} 71 | AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }} 72 | AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} 73 | AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} 74 | RUNNING_ENV: "development" 75 | 76 | - run: pnpm dlx cdk deploy 'LawBotSlackLambdaApiStack-*' --require-approval never 77 | working-directory: ./aws 78 | env: 79 | SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} 80 | NOTION_API_SECRET: ${{ secrets.NOTION_API_SECRET }} 81 | AWS_API_CHAT_ENDPOINT: ${{ vars.AWS_API_CHAT_ENDPOINT }} 82 | -------------------------------------------------------------------------------- /.github/workflows/production.yml: -------------------------------------------------------------------------------- 1 | name: Production 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | permissions: 8 | id-token: write 9 | contents: read 10 | 11 | env: 12 | ENV_NAME: production 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Install Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 20 24 | 25 | # TODO Optimize the Nodejs installation using GitHub Actions 26 | # TODO Optimize and cache node_modules 27 | - run: npm i -g pnpm 28 | working-directory: ./aws 29 | 30 | - run: pnpm i --no-frozen-lockfile --shamefully-hoist 31 | working-directory: ./aws 32 | 33 | - name: Configure AWS Credentials 34 | uses: aws-actions/configure-aws-credentials@v4 35 | with: 36 | aws-region: eu-central-1 37 | role-to-assume: arn:aws:iam::533267285693:role/GitHubRoleFor-access2justice-law-bot 38 | timeout-minutes: 3 # assuming an AWS role never takes that long, but error loops do 39 | 40 | - run: aws sts get-caller-identity 41 | 42 | - run: pnpm dlx cdk deploy 'LawBotBackend2LambdaApiStack-*' --require-approval never 43 | working-directory: ./aws 44 | env: 45 | ALLOWED_ORIGINS: ${{ vars.ALLOWED_ORIGINS }} 46 | AUTHENTICATION_KEY: ${{ secrets.AUTHENTICATION_KEY }} 47 | AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} 48 | AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} 49 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_EMBEDDING_DEPLOYMENT }} 50 | AZURE_SEARCH_ENPOINT: ${{ secrets.AZURE_SEARCH_ENPOINT }} 51 | AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }} 52 | AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} 53 | AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} 54 | RUNNING_ENV: "production" 55 | DB_CONNECTION_STRING: ${{ secrets.DB_CONNECTION_STRING }} 56 | 57 | - run: pnpm dlx cdk deploy 'LawBotBackendLambdaApiStack-*' --require-approval never 58 | working-directory: ./aws 59 | env: 60 | ALLOWED_ORIGINS: ${{ vars.ALLOWED_ORIGINS }} 61 | AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} 62 | AZURE_OPENAI_KEY: ${{ secrets.AZURE_OPENAI_KEY }} 63 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_EMBEDDING_DEPLOYMENT }} 64 | AZURE_SEARCH_ENPOINT: ${{ secrets.AZURE_SEARCH_ENPOINT }} 65 | AZURE_SEARCH_INDEX_NAME: ${{ secrets.AZURE_SEARCH_INDEX_NAME }} 66 | AZURE_SEARCH_KEY: ${{ secrets.AZURE_SEARCH_KEY }} 67 | AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} 68 | RUNNING_ENV: "production" 69 | 70 | - run: pnpm dlx cdk deploy 'LawBotSlackLambdaApiStack-*' --require-approval never 71 | working-directory: ./aws 72 | env: 73 | SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }} 74 | NOTION_API_SECRET: ${{ secrets.NOTION_API_SECRET }} 75 | AWS_API_CHAT_ENDPOINT: ${{ vars.AWS_API_CHAT_ENDPOINT }} 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .env 36 | .vercel 37 | .vscode 38 | .env*.local 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 access2justice 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Law Bot 2 | 3 | ## Backend 4 | 5 | The Backend is based on Python / FastAPI and described in more detail in the backend folder. 6 | 7 | ## Frontend 8 | 9 | The Frontend is based on Next.js and described in more detail in the frontend folder. 10 | 11 | ## Data 12 | 13 | Test data can be found inside the data folder. 14 | -------------------------------------------------------------------------------- /aws/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /aws/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /aws/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project 2 | 3 | This is a blank project for CDK development with TypeScript. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /aws/bin/aws-cdk-python-prototype.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { LawBotBackend as LawBotBackend2 } from "../lib/law-bot-backend-2"; 5 | import { LawBotBackend } from "../lib/law-bot-backend"; 6 | 7 | import { GithubActionsOIDCProvider } from "../lib/stacks/GithubActionsOIDCProvider/GithubActionsOIDCProvider"; 8 | import { LawBotSlack } from "../lib/law-bot-slack"; 9 | 10 | const envName = () => 11 | String(process.env.ENV_NAME ?? "") 12 | .toLowerCase() 13 | .replace(/[^a-z0-9-]/g, ""); 14 | 15 | const app = new cdk.App(); 16 | 17 | new GithubActionsOIDCProvider(app, "LawBotBackendGitHubOIDCProviderStack", { 18 | env: { 19 | account: process.env.CDK_DEFAULT_ACCOUNT, 20 | region: process.env.CDK_DEFAULT_REGION, 21 | }, 22 | repositoryConfig: [ 23 | { 24 | owner: "access2justice", 25 | repo: "law-bot", 26 | }, 27 | ], 28 | }); 29 | 30 | const lawBotBackend = new LawBotBackend( 31 | app, 32 | `LawBotBackendLambdaApiStack-${envName()}`, 33 | { 34 | env: { 35 | account: process.env.CDK_DEFAULT_ACCOUNT, 36 | region: process.env.CDK_DEFAULT_REGION, 37 | }, 38 | } 39 | ); 40 | 41 | new LawBotBackend2(app, `LawBotBackend2LambdaApiStack-${envName()}`, { 42 | env: { 43 | account: process.env.CDK_DEFAULT_ACCOUNT, 44 | region: process.env.CDK_DEFAULT_REGION, 45 | }, 46 | apiGateway: lawBotBackend.apiGateway, 47 | }); 48 | 49 | new LawBotSlack(app, `LawBotSlackLambdaApiStack-${envName()}`, { 50 | env: { 51 | account: process.env.CDK_DEFAULT_ACCOUNT, 52 | region: process.env.CDK_DEFAULT_REGION, 53 | }, 54 | apiGateway: lawBotBackend.apiGateway, 55 | }); 56 | -------------------------------------------------------------------------------- /aws/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/aws-cdk-python-prototype.ts", 3 | "build": "npm run build --prefix ../slack/responder && npm run build --prefix ../slack/worker && npm run build --prefix ../backend-2", 4 | "watch": { 5 | "include": ["**"], 6 | "exclude": [ 7 | "README.md", 8 | "cdk*.json", 9 | "**/*.d.ts", 10 | "**/*.js", 11 | "tsconfig.json", 12 | "package*.json", 13 | "yarn.lock", 14 | "node_modules", 15 | "test" 16 | ] 17 | }, 18 | "context": { 19 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 20 | "@aws-cdk/core:checkSecretUsage": true, 21 | "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], 22 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 23 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 24 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 25 | "@aws-cdk/aws-iam:minimizePolicies": true, 26 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 27 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 28 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 29 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 30 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 31 | "@aws-cdk/core:enablePartitionLiterals": true, 32 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 33 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 34 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 35 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 36 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 37 | "@aws-cdk/aws-route53-patters:useCertificate": true, 38 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 39 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 40 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 41 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 42 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 43 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 44 | "@aws-cdk/aws-redshift:columnId": true, 45 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 46 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 47 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 48 | "@aws-cdk/aws-kms:aliasNameRef": true, 49 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 50 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 51 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 52 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 53 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 54 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, 55 | "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, 56 | "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, 57 | "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, 58 | "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /aws/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /aws/lib/law-bot-backend-2.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { LambdaIntegration, RestApi } from "aws-cdk-lib/aws-apigateway"; 4 | import * as path from "path"; 5 | import { Function, Runtime, Code } from "aws-cdk-lib/aws-lambda"; 6 | 7 | export class LawBotBackend extends cdk.Stack { 8 | public apiGateway: cdk.aws_apigateway.RestApi; 9 | 10 | constructor( 11 | scope: Construct, 12 | id: string, 13 | props?: cdk.StackProps & { apiGateway: cdk.aws_apigateway.RestApi } 14 | ) { 15 | super(scope, id, props); 16 | 17 | const lambdaFunction = new Function(this, "LambdaFunction", { 18 | runtime: Runtime.NODEJS_LATEST, 19 | timeout: cdk.Duration.seconds(60), 20 | handler: "index.handler", 21 | code: Code.fromAsset(path.resolve(__dirname, "../../backend-2")), // same assumption as above 22 | /* 23 | environment: { 24 | AUTHENTICATION_KEY: authenticationKey, 25 | AZURE_OPENAI_ENDPOINT: process.env.AZURE_OPENAI_ENDPOINT || "", 26 | AZURE_OPENAI_KEY: process.env.AZURE_OPENAI_KEY || "", 27 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT: 28 | process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT || "", 29 | AZURE_SEARCH_ENPOINT: process.env.AZURE_SEARCH_ENPOINT || "", 30 | AZURE_SEARCH_INDEX_NAME: process.env.AZURE_SEARCH_INDEX_NAME || "", 31 | AZURE_SEARCH_KEY: process.env.AZURE_SEARCH_KEY || "", 32 | AZURE_OPENAI_DEPLOYMENT_NAME: 33 | process.env.AZURE_OPENAI_DEPLOYMENT_NAME || "", 34 | DB_CONNECTION_STRING: dbConnectionString, 35 | }, 36 | */ 37 | }); 38 | 39 | this.apiGateway = props?.apiGateway as cdk.aws_apigateway.RestApi; 40 | 41 | const chat = this.apiGateway.root.addResource("chat-2"); 42 | 43 | chat.addMethod( 44 | "ANY", 45 | new LambdaIntegration(lambdaFunction, { 46 | proxy: true, 47 | }) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /aws/lib/law-bot-backend.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { LambdaIntegration, RestApi } from "aws-cdk-lib/aws-apigateway"; 4 | import * as path from "path"; 5 | import { DockerImageCode, DockerImageFunction } from "aws-cdk-lib/aws-lambda"; 6 | import { 7 | PredefinedMetric, 8 | ScalableTarget, 9 | ServiceNamespace, 10 | } from "aws-cdk-lib/aws-applicationautoscaling"; 11 | 12 | export class LawBotBackend extends cdk.Stack { 13 | public apiGateway: cdk.aws_apigateway.RestApi; 14 | 15 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 16 | super(scope, id, props); 17 | 18 | const dockerImageFunction = new DockerImageFunction( 19 | this, 20 | "DockerImageFunction", 21 | { 22 | code: DockerImageCode.fromImageAsset( 23 | path.resolve(__dirname, "../../backend") 24 | ), 25 | timeout: cdk.Duration.seconds(30), 26 | memorySize: 256, 27 | reservedConcurrentExecutions: 100, 28 | } 29 | ); 30 | 31 | const target = new ScalableTarget(this, "ScalableTarget", { 32 | serviceNamespace: ServiceNamespace.LAMBDA, 33 | maxCapacity: 100, 34 | minCapacity: 1, 35 | resourceId: `function:${dockerImageFunction.functionName}:${dockerImageFunction.currentVersion.version}`, 36 | scalableDimension: "lambda:function:ProvisionedConcurrency", 37 | }); 38 | 39 | target.scaleToTrackMetric("PceTracking", { 40 | targetValue: 0.9, 41 | scaleOutCooldown: cdk.Duration.seconds(0), 42 | scaleInCooldown: cdk.Duration.seconds(30), 43 | predefinedMetric: 44 | PredefinedMetric.LAMBDA_PROVISIONED_CONCURRENCY_UTILIZATION, 45 | }); 46 | 47 | this.apiGateway = new RestApi(this, "ApiGateway", {}); 48 | 49 | this.apiGateway.root.addMethod( 50 | "ANY", 51 | new LambdaIntegration(dockerImageFunction, { 52 | proxy: true, 53 | }) 54 | ); 55 | 56 | const chat = this.apiGateway.root.addResource("chat"); 57 | 58 | chat.addMethod( 59 | "ANY", 60 | new LambdaIntegration(dockerImageFunction, { 61 | proxy: true, 62 | }) 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /aws/lib/law-bot-slack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { LambdaIntegration } from "aws-cdk-lib/aws-apigateway"; 4 | import * as lambda from "aws-cdk-lib/aws-lambda"; 5 | 6 | import * as path from "path"; 7 | 8 | export class LawBotSlack extends cdk.Stack { 9 | constructor( 10 | scope: Construct, 11 | id: string, 12 | props: cdk.StackProps & { apiGateway: cdk.aws_apigateway.RestApi } 13 | ) { 14 | super(scope, id, props); 15 | 16 | const lambdaWorker = new lambda.Function( 17 | this, 18 | "LambdaFunctionSlackWorker", 19 | { 20 | runtime: lambda.Runtime.NODEJS_LATEST, 21 | timeout: cdk.Duration.seconds(60), 22 | handler: "index.handler", 23 | code: lambda.Code.fromAsset( 24 | path.resolve(__dirname, "../../slack/worker") 25 | ), 26 | environment: { 27 | AWS_API_CHAT_ENDPOINT: process.env.AWS_API_CHAT_ENDPOINT || "", 28 | SLACK_TOKEN: process.env.SLACK_TOKEN || "", 29 | NOTION_API_SECRET: process.env.NOTION_API_SECRET || "", 30 | }, 31 | } 32 | ); 33 | 34 | // Define the first Lambda function 35 | const lambdaResponder = new lambda.Function( 36 | this, 37 | "LambdaFunctionSlackResponder", 38 | { 39 | runtime: lambda.Runtime.NODEJS_LATEST, 40 | timeout: cdk.Duration.seconds(15), 41 | handler: "index.handler", 42 | code: lambda.Code.fromAsset( 43 | path.resolve(__dirname, "../../slack/responder") 44 | ), 45 | environment: { 46 | WORKER_FUNCTION_NAME: lambdaWorker.functionName, 47 | AWS_API_CHAT_ENDPOINT: process.env.AWS_API_CHAT_ENDPOINT || "", 48 | SLACK_TOKEN: process.env.SLACK_TOKEN || "", 49 | NOTION_API_SECRET: process.env.NOTION_API_SECRET || "", 50 | }, 51 | } 52 | ); 53 | 54 | lambdaWorker.grantInvoke(lambdaResponder); 55 | 56 | const slackResource = props?.apiGateway.root.addResource("slack"); 57 | 58 | slackResource.addResource("interaction").addMethod( 59 | "POST", 60 | new LambdaIntegration(lambdaResponder, { 61 | proxy: true, 62 | }) 63 | ); 64 | 65 | slackResource.addResource("events").addMethod( 66 | "POST", 67 | new LambdaIntegration(lambdaResponder, { 68 | proxy: true, 69 | }) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /aws/lib/stacks/GithubActionsOIDCProvider/GithubActionsOIDCProvider.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib' 2 | import {Construct} from 'constructs' 3 | import {aws_iam as iam} from 'aws-cdk-lib' 4 | 5 | export interface GithubActionsAwsAuthCdkStackProps extends cdk.StackProps { 6 | readonly repositoryConfig: { owner: string; repo: string; filter?: string }[] 7 | } 8 | 9 | export class GithubActionsOIDCProvider extends cdk.Stack { 10 | constructor(scope: Construct, id: string, props: GithubActionsAwsAuthCdkStackProps) { 11 | super(scope, id, props) 12 | 13 | const githubDomain = 'https://token.actions.githubusercontent.com' 14 | 15 | const githubProvider = new iam.OpenIdConnectProvider(this, 'GithubActionsProvider', { 16 | url: githubDomain, 17 | clientIds: ['sts.amazonaws.com'], 18 | }) 19 | 20 | props.repositoryConfig.map(r => { 21 | const repoARN = `repo:${r.owner}/${r.repo}:${r.filter ?? '*'}`; 22 | 23 | const context = new Construct(this, `GithubActionsContext${r.owner}${r.repo}`); 24 | 25 | const conditions: iam.Conditions = { 26 | StringEquals: { 27 | 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com', 28 | }, 29 | StringLike: { 30 | "token.actions.githubusercontent.com:sub": repoARN 31 | } 32 | } 33 | 34 | const role = new iam.Role(context, 'Role', { 35 | assumedBy: new iam.WebIdentityPrincipal(githubProvider.openIdConnectProviderArn, conditions), 36 | managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess')], 37 | roleName: `GitHubRoleFor-${r.owner}-${r.repo}`, 38 | description: 'This role is used via GitHub Actions to deploy with AWS CDK or Terraform on the target AWS account', 39 | maxSessionDuration: cdk.Duration.hours(12), 40 | }) 41 | 42 | new cdk.CfnOutput(context, 'GithubActionOidcIamRoleArn', { 43 | value: role.roleArn, 44 | description: `Arn for AWS IAM role with Github oidc auth for ${repoARN}`, 45 | exportName: 'GithubActionOidcIamRoleArn', 46 | }) 47 | }) 48 | 49 | 50 | cdk.Tags.of(this).add('component', 'CdkGithubActionsOidcIamRole') 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /aws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-cdk-python-prototype", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "watch": "tsc -w", 7 | "test": "jest", 8 | "cdk": "cdk" 9 | }, 10 | "devDependencies": { 11 | "@types/jest": "^29.5.5", 12 | "@types/node": "20.7.1", 13 | "aws-cdk": "2.104.0", 14 | "esbuild": "0", 15 | "jest": "^29.7.0", 16 | "ts-jest": "^29.1.1", 17 | "ts-node": "^10.9.1", 18 | "typescript": "~5.2.2", 19 | "@types/aws-serverless-express": "^3.3.9", 20 | "@types/express": "^4.17.17", 21 | "@types/supertest": "^2.0.12", 22 | "@typescript-eslint/eslint-plugin": "^6.0.0", 23 | "@typescript-eslint/parser": "^6.0.0", 24 | "eslint": "^8.42.0", 25 | "eslint-config-prettier": "^9.0.0", 26 | "eslint-plugin-prettier": "^5.0.0", 27 | "prettier": "^3.0.0", 28 | "source-map-support": "^0.5.21", 29 | "supertest": "^6.3.3", 30 | "ts-loader": "^9.4.3", 31 | "tsconfig-paths": "^4.2.0" 32 | }, 33 | "dependencies": { 34 | "aws-cdk-lib": "2.104.0", 35 | "constructs": "^10.0.0", 36 | "source-map-support": "^0.5.21", 37 | "aws-lambda": "^1.0.7", 38 | "aws-serverless-express": "^3.4.0", 39 | "reflect-metadata": "^0.1.13", 40 | "rxjs": "^7.8.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /aws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /backend-2/.env.example: -------------------------------------------------------------------------------- 1 | AZURE_OPENAI_KEY= 2 | AZURE_OPENAI_DEPLOYMENT_NAME= 3 | AZURE_OPENAI_ENDPOINT= 4 | AZURE_SEARCH_ENPOINT= 5 | AZURE_SEARCH_KEY= 6 | AZURE_SEARCH_INDEX_NAME= 7 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT= 8 | DB_CONNECTION_STRING= 9 | # if you use dev/docker-compose.yml 10 | #DB_CONNECTION_STRING=mongodb://root:legalbot@127.0.0.1:27017 -------------------------------------------------------------------------------- /backend-2/.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | node_modules 3 | **/dist 4 | dist 5 | 6 | .env -------------------------------------------------------------------------------- /backend-2/README.md: -------------------------------------------------------------------------------- 1 | # law-bot backend-2 2 | 3 | ## Development 4 | In production this backend runs as a aws lambda function. For development purposes there is an `index-bun.ts` file which adapts to the original `index.ts` file. 5 | 6 | You still need to get a bunch of environment variables (see [.env.example](.env.example)) from the maintainer of the project. 7 | 8 | ### Setup 9 | - Install bun.js by Follow the instructions on https://bun.sh 10 | - Install the dependencies: `bun install` 11 | - Get the required environment variables from the maintainer of the project and save them in `backend-2/.env`. 12 | 13 | ### Run 14 | Run the backend: 15 | ```bash 16 | bun --watch run index-bun.ts 17 | ``` 18 | 19 | Make a request to the backend: 20 | ```bash 21 | curl --request POST \ 22 | --url http://localhost:8080/ \ 23 | --data '{ 24 | "message": [ 25 | { 26 | "role": "user", 27 | "content": "How long is the maternity leave?" 28 | } 29 | ], 30 | "stream": false 31 | }' 32 | ``` -------------------------------------------------------------------------------- /backend-2/azure.ts: -------------------------------------------------------------------------------- 1 | require("dotenv").config(); 2 | 3 | import { 4 | AzureKeyCredential, 5 | SearchClient, 6 | SearchIterator, 7 | } from "@azure/search-documents"; 8 | import { ChatRequestMessage, OpenAIClient } from "@azure/openai"; 9 | 10 | const openAIClient = new OpenAIClient( 11 | process.env.AZURE_OPENAI_ENDPOINT || "", 12 | new AzureKeyCredential(process.env.AZURE_OPENAI_KEY || "") 13 | ); 14 | 15 | const searchClient = new SearchClient( 16 | process.env.AZURE_SEARCH_ENPOINT || "", 17 | process.env.AZURE_SEARCH_INDEX_NAME || "", 18 | new AzureKeyCredential(process.env.AZURE_SEARCH_KEY || "") 19 | ); 20 | 21 | const getEmbeddings = async (query: string) => { 22 | return await openAIClient.getEmbeddings( 23 | process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT || "", 24 | [query] 25 | ); 26 | }; 27 | 28 | export const llmQuery = async ( 29 | messages: ChatRequestMessage[] 30 | ): Promise => { 31 | // console.log("Doing a chat completion: " + JSON.stringify(messages)); 32 | 33 | const temperature = 0; 34 | 35 | const completion = await openAIClient.getChatCompletions( 36 | process.env.AZURE_OPENAI_DEPLOYMENT_NAME || "", 37 | messages, 38 | { 39 | temperature, 40 | maxTokens: 300, 41 | requestOptions: { 42 | timeout: 10000, 43 | }, 44 | } 45 | ); 46 | const completionResult = completion.choices[0].message?.content as string; 47 | 48 | // console.log("Received a chat completion: " + completionResult); 49 | 50 | return completionResult; 51 | }; 52 | 53 | export const semanticSearch = async ( 54 | query: string 55 | ): Promise> => { 56 | const x = await searchClient.search(query, { 57 | vectorSearchOptions: { 58 | queries: [ 59 | { 60 | fields: ["text_vector"], 61 | kNearestNeighborsCount: 3, 62 | vector: (await getEmbeddings(query)).data[0].embedding, 63 | kind: "vector", 64 | }, 65 | ], 66 | }, 67 | top: 15, 68 | queryType: "semantic", 69 | semanticSearchOptions: { 70 | configurationName: "SemanticConfTest", 71 | captions: { 72 | captionType: "extractive", 73 | }, 74 | }, 75 | includeTotalCount: true, 76 | select: ["text", "metadata", "eIds"], 77 | }); 78 | return x.results; 79 | }; 80 | 81 | export const filterMetadata = async ( 82 | query: string 83 | ): Promise> => { 84 | const x = await searchClient.search("*", { 85 | filter: `metadata/any(t: search.in(t, '${query}', '|'))`, 86 | select: ["text", "metadata", "eIds"], 87 | }); 88 | return x.results; 89 | }; 90 | 91 | export const filterMetadataArray = async ( 92 | keywords: string[] 93 | ): Promise> => { 94 | const keywordFilter = keywords 95 | .map((keyword) => `search.ismatch('${keyword}', 'metadata')`) 96 | .join(" and "); 97 | const x = await searchClient.search("*", { 98 | filter: keywordFilter, 99 | select: ["text", "metadata", "eIds"], 100 | }); 101 | return x.results; 102 | }; 103 | -------------------------------------------------------------------------------- /backend-2/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/backend-2/bun.lockb -------------------------------------------------------------------------------- /backend-2/dev/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | environment: 7 | MONGO_INITDB_ROOT_USERNAME: root 8 | MONGO_INITDB_ROOT_PASSWORD: legalbot 9 | ports: 10 | - 27017:27017 11 | volumes: 12 | - mongodb_data:/data/db 13 | 14 | mongo-express: 15 | image: mongo-express 16 | ports: 17 | - 8081:8081 18 | environment: 19 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 20 | ME_CONFIG_MONGODB_ADMINPASSWORD: legalbot 21 | ME_CONFIG_MONGODB_URL: mongodb://root:legalbot@mongo:27017/ 22 | 23 | volumes: 24 | mongodb_data: -------------------------------------------------------------------------------- /backend-2/dev/legalReasoning.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "order": 1, 4 | "type": "llm", 5 | "llmQuery": { 6 | "messages": [ 7 | { 8 | "role": "system", 9 | "content": "From the following legal question, extract and list all facts and terms, that might be relevant for searching and retrieving the relevant law articles, required to answer the question." 10 | }, 11 | { 12 | "role": "user", 13 | "content": "{{baseQuery}}" 14 | } 15 | ] 16 | } 17 | }, 18 | { 19 | "order": 2, 20 | "type": "search", 21 | "searchQuery": { 22 | "query": "{{previousQuery}}" 23 | } 24 | }, 25 | { 26 | "order": 3, 27 | "llmQuery": { 28 | "messages": [ 29 | { 30 | "role": "system", 31 | "content": "You are a Swiss legal expert. Please only answer questions to which Swiss law is applicable, for any other question, just say 'Your question is out of scope.' Use only the provisions of Swiss law provided in the Swiss law retrieval result to answer the user question. You should only use the Swiss law retrieval result for your answer. Your answer must be based on the Swiss law retrieval result. If the Swiss law retrieval result does not contain the exact answer to the question, just say that 'I don't know', don't try to make up an answer if it is not fully clear from the Swiss law retrieval result. Explain your answer and refer the exact source / article of the Swiss law retrieval result sentence by sentence taking into account the five articles preceding the article identified in the numeric order." 32 | }, 33 | { 34 | "role": "user", 35 | "content": "{{baseQuery}}\n\nSwiss law retrieval result:\n{{previousQuery}}" 36 | } 37 | ] 38 | }, 39 | "type": "llm" 40 | } 41 | ] 42 | -------------------------------------------------------------------------------- /backend-2/dev/update-reasoning.ts: -------------------------------------------------------------------------------- 1 | // put legalReasoning.json into mongodb 2 | 3 | // hardcoded to never update the prod db 4 | process.env.DB_CONNECTION_STRING = "mongodb://root:legalbot@127.0.0.1:27017"; 5 | 6 | import { getReasoningModel } from "../repository"; 7 | 8 | const reasoningFromFile = require("./legalReasoning.json"); 9 | 10 | async function updateReasoning() { 11 | console.log( 12 | `Updating reasoning in mongodb at ${process.env.DB_CONNECTION_STRING}` 13 | ); 14 | const reasoningModel = await getReasoningModel(); 15 | await reasoningModel.deleteMany({}); 16 | await reasoningModel.insertMany(reasoningFromFile, { lean: true }); 17 | console.log("Updated reasoning."); 18 | process.exit(0); 19 | } 20 | 21 | // @ts-ignore 22 | await updateReasoning(); 23 | -------------------------------------------------------------------------------- /backend-2/index.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayProxyHandler } from "aws-lambda"; 2 | import { getReasoning } from "./repository"; 3 | import { doReasoning } from "./reasoning"; 4 | 5 | export const handler: APIGatewayProxyHandler = async (event) => { 6 | try { 7 | const authenticationKey = 8 | event.headers["Authorization"]?.replace("Bearer ", "") || 9 | event.headers["authorization"]?.replace("Bearer ", ""); 10 | 11 | if (authenticationKey !== process.env.AUTHENTICATION_KEY) { 12 | throw new Error("Unauthenticated!"); 13 | } 14 | 15 | console.log(event.body); 16 | console.log(JSON.parse(event.body || "{}")); 17 | 18 | const message = JSON.parse(event.body || "{}").message as { 19 | role: string; 20 | content: string; 21 | }[]; 22 | 23 | let baseQuery = message[0].content; 24 | 25 | console.log("Get the reasoning."); 26 | const reasoning = await getReasoning(); 27 | console.log("Got the reasoning."); 28 | 29 | console.log("Do the reasoning."); 30 | const { reasoningThread, content } = await doReasoning( 31 | baseQuery, 32 | reasoning 33 | ); 34 | console.log("Did the reasoning."); 35 | 36 | return { 37 | statusCode: 200, 38 | body: JSON.stringify({ 39 | data: { 40 | content: content, 41 | reasoning_thread: reasoningThread, 42 | }, 43 | }), 44 | }; 45 | } catch (error) { 46 | console.error("An error took place:", error); 47 | return { 48 | statusCode: 500, 49 | body: JSON.stringify({ 50 | message: `An error took place. ${error}`, 51 | }), 52 | }; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /backend-2/main.ts: -------------------------------------------------------------------------------- 1 | import LawArticleTree from "./search/law-article-tree"; 2 | 3 | require("dotenv").config(); 4 | 5 | const run = async () => { 6 | const lawArticleTree = new LawArticleTree( 7 | "Ich habe mich mit einem Autohändler über den Kauf eines VW-Polo, Chassisnummer 2398777, zum Preis von CHF 15'000 geeinigt. Ich erklärte, dass ich den Wagen erst am übernächsten Tag abholen werde. In der Nacht wurde jedoch von einem Dritten ein Feuer gelegt, wodurch der Wagen vollständig zerstört wurde. Wie ist nun die rechtliche Situation in Bezug auf den Kaufpreis?" 8 | ); 9 | const exec = await lawArticleTree.exec(); 10 | 11 | console.log(exec); 12 | }; 13 | 14 | run(); 15 | -------------------------------------------------------------------------------- /backend-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "npm i && tsc", 8 | "predeploy": "npm run build" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@azure/openai": "^1.0.0-beta.11", 14 | "@azure/search-documents": "^12.0.0", 15 | "aws-lambda": "^1.0.7", 16 | "aws-sdk": "^2.1575.0", 17 | "dotenv": "^16.4.5", 18 | "mongoose": "^8.2.2" 19 | }, 20 | "devDependencies": { 21 | "@types/aws-lambda": "^8.10.136", 22 | "@types/bun": "^1.0.10", 23 | "@types/node": "^20.11.26", 24 | "ts-node": "^10.9.2", 25 | "typescript": "^5.4.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend-2/reasoning.ts: -------------------------------------------------------------------------------- 1 | import { ReasoningInterface } from "./repository"; 2 | import { AzureKeyCredential, SearchClient } from "@azure/search-documents"; 3 | import { ChatRequestMessage, OpenAIClient } from "@azure/openai"; 4 | import LawArticleTree from "./search/law-article-tree"; 5 | 6 | const openAIClient = new OpenAIClient( 7 | process.env.AZURE_OPENAI_ENDPOINT || "", 8 | new AzureKeyCredential(process.env.AZURE_OPENAI_KEY || "") 9 | ); 10 | 11 | const searchClient = new SearchClient( 12 | process.env.AZURE_SEARCH_ENPOINT || "", 13 | process.env.AZURE_SEARCH_INDEX_NAME || "", 14 | new AzureKeyCredential(process.env.AZURE_SEARCH_KEY || "") 15 | ); 16 | 17 | const getEmbeddings = async (query: string) => { 18 | return await openAIClient.getEmbeddings( 19 | process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT || "", 20 | [query] 21 | ); 22 | }; 23 | 24 | export const doReasoning = async ( 25 | baseQuery: string, 26 | reasoning: ReasoningInterface[] 27 | ) => { 28 | let previousQuery = baseQuery; 29 | const reasoningThread = [] as any[]; 30 | 31 | for (let reason of reasoning) { 32 | if (reason.type === "llm") { 33 | console.log("LLM Stage"); 34 | console.log(JSON.stringify(reason)); 35 | 36 | const messages = reason.llmQuery?.messages.map((m) => { 37 | let prompt = m.content || ""; 38 | prompt = prompt.replace("{{baseQuery}}", baseQuery); 39 | prompt = prompt.replace("{{previousQuery}}", previousQuery); 40 | return Object.assign({}, m, { content: prompt }); 41 | }) as unknown as ChatRequestMessage[]; 42 | 43 | const temperature = 0.0; 44 | 45 | console.log("Rendered Query"); 46 | console.log(JSON.stringify(messages)); 47 | 48 | const completion = await openAIClient.getChatCompletions( 49 | process.env.AZURE_OPENAI_DEPLOYMENT_NAME || "", 50 | messages, 51 | { 52 | temperature, 53 | maxTokens: 300, 54 | } 55 | ); 56 | 57 | console.log("Received LLM Result"); 58 | console.log(JSON.stringify(completion)); 59 | 60 | const completionResult = completion.choices[0].message?.content as string; 61 | 62 | previousQuery = completionResult; 63 | 64 | reasoningThread.push({ 65 | type: "llm", 66 | prompt: messages, 67 | response: completionResult, 68 | }); 69 | } else if (reason.type === "search") { 70 | console.log("SEARCH Stage"); 71 | console.log(JSON.stringify(reason)); 72 | 73 | let query = reason.searchQuery?.query || ""; 74 | 75 | query = query.replace("{{baseQuery}}", baseQuery); 76 | query = query.replace("{{previousQuery}}", previousQuery); 77 | 78 | console.log("Rendered Query"); 79 | console.log(JSON.stringify(query)); 80 | 81 | const lawArticleTree = new LawArticleTree(query); 82 | const results = await lawArticleTree.exec(); 83 | 84 | console.log(results); 85 | 86 | console.log("Received Search Results"); 87 | console.log(JSON.stringify(results)); 88 | 89 | const retrievedInfo = { 90 | eIds: [] as string[][], 91 | text: [] as string[][], 92 | metadata: [] as string[][], 93 | }; 94 | 95 | for (let result of results) { 96 | retrievedInfo.eIds.push([result.code]); 97 | retrievedInfo.text.push([result.content]); 98 | } 99 | 100 | previousQuery = retrievedInfo.text.reduce( 101 | (p: any, c: any) => p + `\n\n` + c, 102 | "" 103 | ); 104 | 105 | reasoningThread.push({ 106 | type: "search", 107 | query: previousQuery, 108 | results: retrievedInfo, 109 | }); 110 | } 111 | } 112 | 113 | return { reasoningThread, content: previousQuery }; 114 | }; 115 | -------------------------------------------------------------------------------- /backend-2/repository.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Connection, Model, Schema } from "mongoose"; 2 | 3 | export interface ReasoningInterface { 4 | order: number; 5 | type: "llm" | "search"; 6 | llmQuery?: { 7 | messages: { 8 | role: string; 9 | content: string; 10 | }[]; 11 | }; 12 | searchQuery?: { 13 | query: string; 14 | }; 15 | } 16 | 17 | let reasoningModel = null as Model | null; 18 | let conn = null as Connection | null; 19 | 20 | export const getReasoningModel = async (): Promise< 21 | Model 22 | > => { 23 | if (reasoningModel) { 24 | return reasoningModel; 25 | } 26 | 27 | await mongoose.connect(process.env.DB_CONNECTION_STRING || ""); 28 | 29 | reasoningModel = mongoose.model( 30 | "legalReasoning", 31 | new Schema( 32 | { 33 | order: { type: Number, required: true }, 34 | type: { type: String, enum: ["search", "llm"] }, 35 | llmQuery: { 36 | type: { 37 | messages: [ 38 | { 39 | role: String, 40 | content: String, 41 | }, 42 | ], 43 | }, 44 | required: false, 45 | }, 46 | searchQuery: { 47 | type: { 48 | query: String, 49 | }, 50 | required: false, 51 | }, 52 | }, 53 | { collection: "legalReasoning" } 54 | ) 55 | ); 56 | conn = mongoose.connection; 57 | 58 | return reasoningModel; 59 | }; 60 | 61 | export const getReasoning = async () => { 62 | const reasoningModel = await getReasoningModel(); 63 | const reasoning = (await reasoningModel.find({}).lean()).sort( 64 | (a: any, b: any) => a.order - b.order 65 | ); 66 | return reasoning; 67 | }; 68 | -------------------------------------------------------------------------------- /backend-2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", // Suitable for Node.js 10.x and above 4 | "module": "CommonJS", 5 | "strict": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "forceConsistentCasingInFileNames": true 9 | }, 10 | "include": ["**/*.ts"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /backend-2/utils.ts: -------------------------------------------------------------------------------- 1 | export const withTimeout = (promise: Promise, ms: number): Promise => { 2 | return new Promise((resolve, reject) => { 3 | const timer = setTimeout(() => { 4 | reject(new Error("Function timed out")); 5 | }, ms); 6 | 7 | promise 8 | .then((value) => { 9 | clearTimeout(timer); 10 | resolve(value); 11 | }) 12 | .catch((err) => { 13 | clearTimeout(timer); 14 | reject(err); 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | .dockerignore 3 | .gitignore 4 | README.md 5 | .vscode 6 | openapi_schema.json -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | # Credentials for Azure OpenAI client 2 | AZURE_OPENAI_KEY=XXXXX 3 | AZURE_OPENAI_ENDPOINT=XXXXX 4 | AZURE_OPENAI_DEPLOYMENT_NAME=XXXX 5 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT=XXXX 6 | 7 | # Credentials for Azure Cognitive Search 8 | AZURE_SEARCH_ENPOINT=XXX 9 | AZURE_SEARCH_KEY=XXX 10 | AZURE_SEARCH_INDEX_NAME=XXX 11 | 12 | # Frontend URL 13 | ALLOWED_ORIGINS=XXX 14 | 15 | # Specify running environment. Use "dev" when you run the docker container locally 16 | RUNNING_ENV=XXX 17 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | 3 | # Byte-compiled / optimized / DLL files 4 | **/__pycache__/ 5 | 6 | # Environments 7 | .env 8 | .venv 9 | env/ 10 | venv/ 11 | ENV/ 12 | .vscode -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM amazon/aws-lambda-python:3.9 2 | # Copies and install requirements.txt file into the container 3 | COPY requirements.txt ./ 4 | 5 | RUN pip install -r requirements.txt -t . 6 | 7 | # Goes last to take advantage of Docker caching. 8 | COPY src/ src/ 9 | 10 | # Points to the handler function of your lambda function 11 | CMD ["src.app.lambda_handler"] -------------------------------------------------------------------------------- /backend/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | 3 | WORKDIR /backend 4 | 5 | COPY ./requirements.txt /backend/requirements.txt 6 | 7 | RUN pip install --no-cache-dir -r /backend/requirements.txt 8 | 9 | COPY ./src /backend/src 10 | 11 | CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "80", "--reload"] -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Run the backend locally using Docker 2 | 3 | 1. Install and start Docker. [Download Docker](https://docs.docker.com/get-docker/) 4 | 5 | 2. Create a .env file under `\backend` folder with required environment variables. 6 | 7 | 3. Open the backend project (the `\backend` folder) in your IDE and open a terminal window within the IDE. 8 | 9 | 4. Run the following commands 10 | 11 | - Build the Docker image: 12 | `docker build -f Dockerfile.dev -t lawbot:dev .` 13 | - Run the container: 14 | For **Mac** users, `docker run -p 8000:80 -v $(pwd):/backend lawbot:dev` 15 | For **Windows** users, `docker run -p 8000:80 -v "%cd%":/backend lawbot:dev` 16 | 17 | Once the container is up and running, access the backend via `http://127.0.0.1:8000/` 18 | -------------------------------------------------------------------------------- /backend/openapi_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.1.0", 3 | "info": { 4 | "title": "FastAPI", 5 | "version": "0.1.0" 6 | }, 7 | "paths": { 8 | "/": { 9 | "get": { 10 | "summary": "Get Root", 11 | "operationId": "get_root__get", 12 | "responses": { 13 | "200": { 14 | "description": "Successful Response", 15 | "content": { 16 | "application/json": { 17 | "schema": {} 18 | } 19 | } 20 | } 21 | } 22 | } 23 | }, 24 | "/chat": { 25 | "post": { 26 | "summary": "Chat Handler", 27 | "operationId": "chat_handler_chat_post", 28 | "requestBody": { 29 | "content": { 30 | "application/json": { 31 | "schema": { 32 | "$ref": "#/components/schemas/ChatRequest" 33 | } 34 | } 35 | }, 36 | "required": true 37 | }, 38 | "responses": { 39 | "200": { 40 | "description": "Successful Response", 41 | "content": { 42 | "application/json": { 43 | "schema": {} 44 | } 45 | } 46 | }, 47 | "422": { 48 | "description": "Validation Error", 49 | "content": { 50 | "application/json": { 51 | "schema": { 52 | "$ref": "#/components/schemas/HTTPValidationError" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "components": { 62 | "schemas": { 63 | "ChatRequest": { 64 | "properties": { 65 | "message": { 66 | "items": { 67 | "$ref": "#/components/schemas/UserMessage" 68 | }, 69 | "type": "array", 70 | "title": "Message" 71 | }, 72 | "stream": { 73 | "type": "boolean", 74 | "title": "Stream", 75 | "default": false 76 | } 77 | }, 78 | "additionalProperties": false, 79 | "type": "object", 80 | "required": [ 81 | "message" 82 | ], 83 | "title": "ChatRequest" 84 | }, 85 | "HTTPValidationError": { 86 | "properties": { 87 | "detail": { 88 | "items": { 89 | "$ref": "#/components/schemas/ValidationError" 90 | }, 91 | "type": "array", 92 | "title": "Detail" 93 | } 94 | }, 95 | "type": "object", 96 | "title": "HTTPValidationError" 97 | }, 98 | "UserMessage": { 99 | "properties": { 100 | "role": { 101 | "type": "string", 102 | "title": "Role", 103 | "default": "user" 104 | }, 105 | "content": { 106 | "type": "string", 107 | "title": "Content" 108 | } 109 | }, 110 | "additionalProperties": false, 111 | "type": "object", 112 | "required": [ 113 | "content" 114 | ], 115 | "title": "UserMessage" 116 | }, 117 | "ValidationError": { 118 | "properties": { 119 | "loc": { 120 | "items": { 121 | "anyOf": [ 122 | { 123 | "type": "string" 124 | }, 125 | { 126 | "type": "integer" 127 | } 128 | ] 129 | }, 130 | "type": "array", 131 | "title": "Location" 132 | }, 133 | "msg": { 134 | "type": "string", 135 | "title": "Message" 136 | }, 137 | "type": { 138 | "type": "string", 139 | "title": "Error Type" 140 | } 141 | }, 142 | "type": "object", 143 | "required": [ 144 | "loc", 145 | "msg", 146 | "type" 147 | ], 148 | "title": "ValidationError" 149 | } 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi==0.109.0 2 | uvicorn==0.26.0 3 | mangum==0.17.0 4 | openai==1.10.0 5 | pydantic==2.5.3 6 | python-dotenv==1.0.1 7 | azure-search-documents==11.4.0 8 | aiohttp==3.9.1 9 | tiktoken==0.5.2 10 | -------------------------------------------------------------------------------- /backend/src/__init__.py: -------------------------------------------------------------------------------- 1 | from contextlib import asynccontextmanager 2 | import os 3 | from fastapi import FastAPI 4 | from openai import AsyncAzureOpenAI, AzureOpenAI 5 | from fastapi.middleware.cors import CORSMiddleware 6 | from dotenv import load_dotenv 7 | from .config.globals import clients 8 | from .config.logconfig import logger 9 | from .api import chat 10 | from azure.core.credentials import AzureKeyCredential 11 | from azure.search.documents.aio import SearchClient 12 | from .config.LoggingMiddleware import LoggingMiddleware 13 | 14 | @asynccontextmanager 15 | async def lifespan(app: FastAPI): 16 | # Defines startup and shutdown logic of the FastAPI app 17 | # Instantiate the OpenAI client 18 | clients["azure_openai"] = AsyncAzureOpenAI( 19 | azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT"), 20 | api_key=os.getenv("AZURE_OPENAI_KEY"), 21 | api_version="2023-09-01-preview" 22 | ) 23 | # Instantiate the Cognitive Search client 24 | search_key = os.getenv("AZURE_SEARCH_KEY") 25 | clients["azure_search"] = SearchClient( 26 | endpoint = os.getenv("AZURE_SEARCH_ENPOINT"), 27 | index_name = os.getenv("AZURE_SEARCH_INDEX_NAME"), 28 | credential = AzureKeyCredential(search_key) 29 | ) 30 | clients["azure_embedding"] = AsyncAzureOpenAI( 31 | azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT"), 32 | api_key=os.getenv("AZURE_OPENAI_KEY"), 33 | api_version="2023-09-01-preview" 34 | ) 35 | yield 36 | await clients["azure_openai"].close() 37 | await clients["azure_search"].close() 38 | await clients["azure_embedding"].close() 39 | 40 | def create_app(): 41 | load_dotenv(override=True) 42 | is_dev = os.getenv('RUNNING_ENV') == 'dev' 43 | app = FastAPI(root_path="/prod", 44 | docs_url="/docs", 45 | redoc_url="/redoc", 46 | openapi_url="/openapi.json", 47 | lifespan=lifespan) 48 | 49 | logger.info('Backend is starting up') 50 | 51 | @app.get("/") 52 | def get_root(): 53 | return {"message": "FastAPI running in a Lambda function"} 54 | 55 | app.include_router(chat.router) 56 | 57 | if is_dev: 58 | origins = ["http://localhost"] 59 | origins.append(os.getenv('ALLOWED_ORIGINS')) 60 | # Generate Swagger schema 61 | import json 62 | openapi_schema = app.openapi() 63 | openapi_schema_json = json.dumps(openapi_schema, indent=2) 64 | with open('openapi_schema.json', 'w') as file: 65 | file.write(openapi_schema_json) 66 | else: 67 | origins = ["https://frontend-socram-testing.vercel.app/"] 68 | origins.append(os.getenv('ALLOWED_ORIGINS')) 69 | app.add_middleware(LoggingMiddleware) 70 | app.add_middleware( 71 | CORSMiddleware, 72 | allow_origins=origins, 73 | allow_credentials=True, 74 | allow_methods=["*"], 75 | allow_headers=["*"], 76 | ) 77 | 78 | return app -------------------------------------------------------------------------------- /backend/src/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/backend/src/api/__init__.py -------------------------------------------------------------------------------- /backend/src/api/chat.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastapi.encoders import jsonable_encoder 3 | from fastapi.responses import JSONResponse 4 | from ..models.request import ChatRequest 5 | from ..services.ChatBotPipeline import ChatBotPipeline 6 | from ..config.globals import clients 7 | import os 8 | 9 | 10 | router = APIRouter() 11 | 12 | @router.post("/chat") 13 | async def chat_handler(chat_request: ChatRequest): 14 | search_client = clients["azure_search"] 15 | openai_client = clients["azure_openai"] 16 | embed_client = clients["azure_embedding"] 17 | model = os.getenv("AZURE_OPENAI_DEPLOYMENT_NAME") 18 | embeddings_model = os.getenv("AZURE_OPENAI_EMBEDDING_DEPLOYMENT") 19 | chatbot = ChatBotPipeline(search_client, openai_client, embed_client, model, embeddings_model) 20 | response = await chatbot.run(chat_request) 21 | return response 22 | -------------------------------------------------------------------------------- /backend/src/app.py: -------------------------------------------------------------------------------- 1 | from . import create_app 2 | from mangum import Mangum 3 | 4 | app = create_app() 5 | lambda_handler = Mangum(app) -------------------------------------------------------------------------------- /backend/src/config/LoggingMiddleware.py: -------------------------------------------------------------------------------- 1 | from starlette.middleware.base import BaseHTTPMiddleware 2 | from starlette.requests import Request 3 | import traceback 4 | from fastapi.responses import JSONResponse 5 | import logging 6 | import sys 7 | from .logconfig import logger 8 | 9 | class LoggingMiddleware(BaseHTTPMiddleware): 10 | async def dispatch(self, request: Request, call_next): 11 | try: 12 | response = await call_next(request) 13 | return response 14 | except Exception as exc: 15 | logger.error(f"Exception caught: {exc}\n{traceback.format_exc()}") 16 | return JSONResponse( 17 | status_code=500, 18 | content={"message": "An internal server error occurred."} 19 | ) 20 | 21 | -------------------------------------------------------------------------------- /backend/src/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/backend/src/config/__init__.py -------------------------------------------------------------------------------- /backend/src/config/globals.py: -------------------------------------------------------------------------------- 1 | clients = {} -------------------------------------------------------------------------------- /backend/src/config/logconfig.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] [%(name)s]: %(message)s") 5 | stream_handler = logging.StreamHandler(sys.stdout) 6 | stream_handler.setFormatter(formatter) 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | logger.addHandler(stream_handler) 10 | 11 | logger_uvicorn = logging.getLogger("uvicorn") 12 | logger_uvicorn.setLevel(logging.INFO) 13 | logger_uvicorn.addHandler(stream_handler) 14 | logger_uvicorn.propagate = False 15 | 16 | logger_uvicorn_error = logging.getLogger("uvicorn.error") 17 | logger_uvicorn_error.setLevel(logging.INFO) 18 | logger_uvicorn_error.addHandler(stream_handler) 19 | logger_uvicorn_error.propagate = False 20 | 21 | logger_uvicorn_access = logging.getLogger("uvicorn.access") 22 | logger_uvicorn_access.setLevel(logging.INFO) 23 | logger_uvicorn_access.addHandler(stream_handler) 24 | logger_uvicorn_access.propagate = False 25 | -------------------------------------------------------------------------------- /backend/src/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/backend/src/models/__init__.py -------------------------------------------------------------------------------- /backend/src/models/request.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | class UserMessage(BaseModel): 4 | role: str = "user" 5 | content: str 6 | class Config: 7 | extra = "forbid" 8 | 9 | class ChatRequest(BaseModel): 10 | message: list[UserMessage] 11 | stream: bool = False 12 | class Config: 13 | extra = "forbid" -------------------------------------------------------------------------------- /backend/src/models/response.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union, List 2 | from pydantic import BaseModel 3 | 4 | class ReasoningThread(BaseModel): 5 | type: str 6 | query: Optional[Any] = None 7 | results: Optional[Dict[str, Any]] = None 8 | prompt: Optional[Any] = None 9 | response: Optional[str] = None 10 | 11 | class IntegratedResponse(BaseModel): 12 | content: str 13 | reasoning_thread: Union[str, List[ReasoningThread]] 14 | 15 | class ChatResponse(BaseModel): 16 | data: IntegratedResponse 17 | -------------------------------------------------------------------------------- /backend/src/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/backend/src/services/__init__.py -------------------------------------------------------------------------------- /data/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/data/.DS_Store -------------------------------------------------------------------------------- /data/.env.example: -------------------------------------------------------------------------------- 1 | # Credentials for Azure OpenAI client 2 | AZURE_OPENAI_KEY=XXX 3 | AZURE_OPENAI_ENDPOINT=XXX 4 | AZURE_EMBEDDING_DEPLOYMENT_NAME=XXX 5 | 6 | # Credentials for Azure Cognitive Search 7 | AZURE_SEARCH_ENDPOINT=XXX 8 | AZURE_SEARCH_KEY=XXX 9 | AZURE_SEARCH_INDEX_NAME=XXX -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | .pytest_cache 2 | 3 | # Byte-compiled / optimized / DLL files 4 | **/__pycache__/ 5 | 6 | # Environments 7 | .env 8 | .venv 9 | env/ 10 | venv/ 11 | ENV/ 12 | .vscode 13 | .idea 14 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Law Bot Data 2 | The test data is currently based on https://www.fedlex.admin.ch/eli/cc/27/317_321_377/de 3 | 4 | # Run the data scripts 5 | First, open the data project (the `\data` folder) in your IDE and open a terminal window within the IDE. 6 | 7 | Second, make sure to prepare your env: 8 | 1. Install packages: 9 | `pip install -r requirements.txt` 10 | 2. Create an .env file, using the .env.example as a boilerplate 11 | 3. Generate data by running: 12 | `python retrieve_law_data.py` 13 | 14 | 15 | Third, run the following commands: 16 | 0. Create Azure index (optional): 17 | `python create_azure_index.py` 18 | 1. Upload Obligationrecht dataset to Azure: 19 | `python upload_index_data.py` 20 | 21 | 22 | -------------------------------------------------------------------------------- /data/create_azure_index.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | from azure.core.credentials import AzureKeyCredential 4 | from azure.search.documents.indexes import SearchIndexClient 5 | 6 | from azure.search.documents.indexes.models import ( 7 | SearchIndex, 8 | SearchField, 9 | SearchFieldDataType, 10 | SearchableField, 11 | VectorSearch, 12 | VectorSearchProfile, 13 | HnswAlgorithmConfiguration 14 | ) 15 | 16 | load_dotenv(override=True) 17 | 18 | service_endpoint = os.getenv('AZURE_SEARCH_ENDPOINT') 19 | index_name = os.getenv('AZURE_SEARCH_INDEX_NAME') 20 | key = os.getenv('AZURE_SEARCH_KEY') 21 | 22 | 23 | def get_lawbot_index(name: str): 24 | """ Function to create the lawbot index """ 25 | fields = [ 26 | SearchField( 27 | name="id", type=SearchFieldDataType.String, key=True, 28 | searchable=True, 29 | sortable=True, 30 | filterable=True, 31 | facetable=True 32 | ), 33 | SearchableField( 34 | name="text", 35 | type=SearchFieldDataType.String, 36 | searchable=True, 37 | sortable=True, 38 | filterable=True, 39 | facetable=True 40 | ), 41 | SearchField( 42 | name="metadata", 43 | type=SearchFieldDataType.Collection(SearchFieldDataType.String), 44 | searchable=True, 45 | filterable=True, 46 | facetable=True 47 | ), 48 | SearchField( 49 | name="text_vector", 50 | type=SearchFieldDataType.Collection(SearchFieldDataType.Single), 51 | searchable=True, 52 | vector_search_dimensions=1536, 53 | vector_search_profile_name="lawbot-vector-config", 54 | ), 55 | SearchField( 56 | name="eIds", type=SearchFieldDataType.Collection(SearchFieldDataType.String), 57 | searchable=True, 58 | filterable=True, 59 | facetable=True 60 | ) 61 | ] 62 | vector_search = VectorSearch( 63 | profiles=[VectorSearchProfile(name="lawbot-vector-config", algorithm_configuration_name="lawbot-algorithms-config")], 64 | algorithms=[HnswAlgorithmConfiguration(name="lawbot-algorithms-config")], 65 | ) 66 | return SearchIndex(name=name, fields=fields, vector_search=vector_search) 67 | 68 | 69 | if __name__ == "__main__": 70 | index = get_lawbot_index(index_name) 71 | index_client = SearchIndexClient(service_endpoint, AzureKeyCredential(key)) 72 | index_client.create_index(index) 73 | -------------------------------------------------------------------------------- /data/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv==1.0.1 2 | xmltodict==0.13.0 3 | azure-search-documents==11.4.0 4 | cobalt==8.0.1 5 | openai==1.13.3 -------------------------------------------------------------------------------- /data/upload_index_data.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | import json 4 | import openai 5 | 6 | from dotenv import load_dotenv 7 | from azure.core.credentials import AzureKeyCredential 8 | from azure.search.documents import SearchIndexingBufferedSender 9 | 10 | load_dotenv(override=True) 11 | 12 | service_endpoint = os.getenv('AZURE_SEARCH_ENDPOINT') 13 | index_name = os.getenv('AZURE_SEARCH_INDEX_NAME') 14 | key = os.getenv('AZURE_SEARCH_KEY') 15 | open_ai_endpoint = os.getenv('AZURE_OPENAI_ENDPOINT') 16 | open_ai_key = os.getenv('AZURE_OPENAI_KEY') 17 | deployment_name = os.getenv('AZURE_EMBEDDING_DEPLOYMENT_NAME') 18 | 19 | 20 | def get_embeddings(text: str): 21 | """ 22 | Create article embeddings 23 | """ 24 | client = openai.AzureOpenAI( 25 | azure_endpoint=open_ai_endpoint, 26 | api_key=open_ai_key, 27 | api_version="2023-07-01-preview", 28 | ) 29 | embedding = client.embeddings.create(input=[text], model=deployment_name) 30 | 31 | return embedding.data[0].embedding 32 | 33 | 34 | def consolidate_data(regex): 35 | """ 36 | Consolidate different law json files 37 | """ 38 | data_files = glob.glob(regex) 39 | docs = [] 40 | for file in data_files: 41 | docs.extend(json.load(open(file))) 42 | return docs 43 | 44 | 45 | 46 | def prep_data(documents: list): 47 | """ 48 | Prepare dataset for upload to Azure index 49 | """ 50 | # Prepare data for upload 51 | docs = [] 52 | counter = 1 53 | 54 | for document in documents: 55 | metadata = ';'.join(document['metadata'][1:]) 56 | document['text'] = metadata + document['text'] 57 | # print(document['text']) 58 | DOCUMENT = { 59 | "@search.action": "mergeOrUpload", 60 | "id": str(counter), 61 | "text": document['text'], 62 | "text_vector": get_embeddings(document['text']), 63 | "metadata": document['metadata'], 64 | "eIds": document['@eIds'] 65 | } 66 | counter += 1 67 | docs.append(DOCUMENT) 68 | 69 | return docs 70 | 71 | 72 | if __name__ == "__main__": 73 | consolidated_docs = consolidate_data('*_by_article.json') 74 | # prepare documents for azure index 75 | docs = prep_data(consolidated_docs) 76 | # chunk documents 77 | chunks = [docs[i:i + 1000] for i in range(0, len(docs), 1000)] 78 | # upload chunks to azure 79 | for chunk in chunks: 80 | # Use SearchIndexingBufferedSender to upload the documents in batches optimized for indexing 81 | with SearchIndexingBufferedSender( 82 | endpoint=service_endpoint, 83 | index_name=index_name, 84 | credential=AzureKeyCredential(key), 85 | ) as batch_client: 86 | # Add upload actions for all documents 87 | batch_client.upload_documents(documents=chunk) 88 | print(f"Uploaded {len(docs)} documents in total") 89 | -------------------------------------------------------------------------------- /frontend/.env.example: -------------------------------------------------------------------------------- 1 | # Create a Google OAuth Credential https://console.cloud.google.com/apis/credentials 2 | # Authorization callback URL: https://authjs.dev/reference/core/providers/google#callback-url 3 | AUTH_GOOGLE_ID=XXXXXXXX 4 | AUTH_GOOGLE_SECRET=XXXXXXXX 5 | # Support OAuth login on preview deployments, see: https://authjs.dev/guides/basics/deployment#securing-a-preview-deployment 6 | # Set the following only when deployed. In this example, we can reuse the same OAuth app, but if you are storing users, we recommend using a different OAuth app for development/production so that you don't mix your test and production user base. 7 | # AUTH_REDIRECT_PROXY_URL=https://YOURAPP.vercel.app/api/auth 8 | 9 | # Instructions to create kv database here: https://vercel.com/docs/storage/vercel-kv/quickstart 10 | KV_URL=XXXXXXXX 11 | KV_REST_API_URL=XXXXXXXX 12 | KV_REST_API_TOKEN=XXXXXXXX 13 | KV_REST_API_READ_ONLY_TOKEN=XXXXXXXX 14 | SLACK_TOKEN=XXXXXXXX 15 | 16 | # API ENDPOINT AWS Backend 17 | AWS_API_CHAT_ENDPOINT=XXXXXXXX -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "tailwindcss/no-custom-classname": "off", 12 | "tailwindcss/classnames-order": "off" 13 | }, 14 | "settings": { 15 | "tailwindcss": { 16 | "callees": ["cn", "cva"], 17 | "config": "tailwind.config.js" 18 | } 19 | }, 20 | "overrides": [ 21 | { 22 | "files": ["*.ts", "*.tsx"], 23 | "parser": "@typescript-eslint/parser" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .pnpm-debug.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | .env 36 | .vercel 37 | .vscode 38 | .env*.local 39 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | 2 | Next.js 14 and App Router-ready AI chatbot. 3 |

Next.js AI Chatbot

4 |
5 | 6 |

7 | An open-source AI chatbot app template built with Next.js, the Vercel AI SDK, OpenAI, and Vercel KV. 8 |

9 | 10 |

11 | Features · 12 | Model Providers · 13 | Deploy Your Own · 14 | Running locally · 15 | Authors 16 |

17 |
18 | 19 | ## Features 20 | 21 | - [Next.js](https://nextjs.org) App Router. 22 | - React Server Components (RSCs), Suspense, and Server Actions. 23 | - [Vercel AI SDK](https://sdk.vercel.ai/docs) for streaming chat UI. 24 | - Support for OpenAI (default), Anthropic, Cohere, Hugging Face, or custom AI chat models and/or LangChain. 25 | - [shadcn/ui](https://ui.shadcn.com) 26 | - Styling with [Tailwind CSS](https://tailwindcss.com) 27 | - [Radix UI](https://radix-ui.com) for headless component primitives 28 | - Icons from [Phosphor Icons](https://phosphoricons.com) 29 | - Chat History, rate limiting, and session storage with [Vercel KV](https://vercel.com/storage/kv) 30 | - [NextAuth.js](https://github.com/nextauthjs/next-auth) for authentication 31 | 32 | ## Model Providers 33 | 34 | This template ships with OpenAI `gpt-3.5-turbo` as the default. However, thanks to the [Vercel AI SDK](https://sdk.vercel.ai/docs), you can switch LLM providers to [Anthropic](https://anthropic.com), [Cohere](https://cohere.com/), [Hugging Face](https://huggingface.co), or using [LangChain](https://js.langchain.com) with just a few lines of code. 35 | 36 | ## Deploy Your Own 37 | 38 | You can deploy your own version of the Next.js AI Chatbot to Vercel with one click: 39 | 40 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?demo-title=Next.js+Chat&demo-description=A+full-featured%2C+hackable+Next.js+AI+chatbot+built+by+Vercel+Labs&demo-url=https%3A%2F%2Fchat.vercel.ai%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F4aVPvWuTmBvzM5cEdRdqeW%2F4234f9baf160f68ffb385a43c3527645%2FCleanShot_2023-06-16_at_17.09.21.png&project-name=Next.js+Chat&repository-name=nextjs-chat&repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot&from=templates&skippable-integrations=1&env=OPENAI_API_KEY%2CAUTH_GITHUB_ID%2CAUTH_GITHUB_SECRET%2CAUTH_SECRET&envDescription=How+to+get+these+env+vars&envLink=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fai-chatbot%2Fblob%2Fmain%2F.env.example&teamCreateStatus=hidden&stores=[{"type":"kv"}]) 41 | 42 | ## Creating a KV Database Instance 43 | 44 | Follow the steps outlined in the [quick start guide](https://vercel.com/docs/storage/vercel-kv/quickstart#create-a-kv-database) provided by Vercel. This guide will assist you in creating and configuring your KV database instance on Vercel, enabling your application to interact with it. 45 | 46 | Remember to update your environment variables (`KV_URL`, `KV_REST_API_URL`, `KV_REST_API_TOKEN`, `KV_REST_API_READ_ONLY_TOKEN`) in the `.env` file with the appropriate credentials provided during the KV database setup. 47 | 48 | ## Running locally 49 | 50 | You will need to use the environment variables [defined in `.env.example`](.env.example) to run Next.js AI Chatbot. It's recommended you use [Vercel Environment Variables](https://vercel.com/docs/projects/environment-variables) for this, but a `.env` file is all that is necessary. 51 | 52 | > Note: You should not commit your `.env` file or it will expose secrets that will allow others to control access to your various OpenAI and authentication provider accounts. 53 | 54 | 1. Install Vercel CLI: `npm i -g vercel` 55 | 2. Link local instance with Vercel and GitHub accounts (creates `.vercel` directory): `vercel link` 56 | 3. Download your environment variables: `vercel env pull` 57 | 58 | ```bash 59 | pnpm install 60 | pnpm dev 61 | ``` 62 | 63 | Your app template should now be running on [localhost:3000](http://localhost:3000/). 64 | 65 | ## Authors 66 | 67 | This library is created by [Vercel](https://vercel.com) and [Next.js](https://nextjs.org) team members, with contributions from: 68 | 69 | - Jared Palmer ([@jaredpalmer](https://twitter.com/jaredpalmer)) - [Vercel](https://vercel.com) 70 | - Shu Ding ([@shuding\_](https://twitter.com/shuding_)) - [Vercel](https://vercel.com) 71 | - shadcn ([@shadcn](https://twitter.com/shadcn)) - [Vercel](https://vercel.com) 72 | -------------------------------------------------------------------------------- /frontend/app/(chat)/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next' 2 | import { notFound, redirect } from 'next/navigation' 3 | 4 | import { auth } from '@/auth' 5 | import { getChat } from '@/app/actions' 6 | import { Chat } from '@/components/chat' 7 | 8 | export interface ChatPageProps { 9 | params: { 10 | id: string 11 | } 12 | } 13 | 14 | export async function generateMetadata({ 15 | params 16 | }: ChatPageProps): Promise { 17 | const session = await auth() 18 | 19 | if (!session?.user) { 20 | return {} 21 | } 22 | 23 | const chat = await getChat(params.id, session.user.id) 24 | return { 25 | title: chat?.title.toString().slice(0, 50) ?? 'Chat' 26 | } 27 | } 28 | 29 | export default async function ChatPage({ params }: ChatPageProps) { 30 | const session = await auth() 31 | 32 | if (!session?.user) { 33 | redirect(`/sign-in?next=/chat/${params.id}`) 34 | } 35 | 36 | const chat = await getChat(params.id, session.user.id) 37 | 38 | if (!chat) { 39 | notFound() 40 | } 41 | 42 | if (chat?.userId !== session?.user?.id) { 43 | notFound() 44 | } 45 | 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /frontend/app/(chat)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SidebarDesktop } from '@/components/sidebar-desktop' 2 | 3 | interface ChatLayoutProps { 4 | children: React.ReactNode 5 | } 6 | 7 | export default async function ChatLayout({ children }: ChatLayoutProps) { 8 | return ( 9 |
10 | {/* @ts-ignore */} 11 | 12 |
13 | {children} 14 |
15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /frontend/app/(chat)/page.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@/lib/utils' 2 | import { Chat } from '@/components/chat' 3 | 4 | export default function IndexPage() { 5 | const id = nanoid() 6 | 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /frontend/app/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath } from 'next/cache' 4 | import { redirect } from 'next/navigation' 5 | import { kv } from '@vercel/kv' 6 | 7 | import { auth } from '@/auth' 8 | import { type Chat } from '@/lib/types' 9 | 10 | export async function getChats(userId?: string | null) { 11 | if (!userId) { 12 | return [] 13 | } 14 | 15 | try { 16 | const pipeline = kv.pipeline() 17 | const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, { 18 | rev: true 19 | }) 20 | 21 | for (const chat of chats) { 22 | pipeline.hgetall(chat) 23 | } 24 | 25 | const results = await pipeline.exec() 26 | 27 | return results as Chat[] 28 | } catch (error) { 29 | return [] 30 | } 31 | } 32 | 33 | export async function getChat(id: string, userId: string) { 34 | const chat = await kv.hgetall(`chat:${id}`) 35 | 36 | if (!chat || (userId && chat.userId !== userId)) { 37 | return null 38 | } 39 | 40 | return chat 41 | } 42 | 43 | export async function removeChat({ id, path }: { id: string; path: string }) { 44 | const session = await auth() 45 | 46 | if (!session) { 47 | return { 48 | error: 'Unauthorized' 49 | } 50 | } 51 | 52 | const uid = await kv.hget(`chat:${id}`, 'userId') 53 | 54 | if (uid !== session?.user?.id) { 55 | return { 56 | error: 'Unauthorized' 57 | } 58 | } 59 | 60 | await kv.del(`chat:${id}`) 61 | await kv.zrem(`user:chat:${session.user.id}`, `chat:${id}`) 62 | 63 | revalidatePath('/') 64 | return revalidatePath(path) 65 | } 66 | 67 | export async function clearChats() { 68 | const session = await auth() 69 | 70 | if (!session?.user?.id) { 71 | return { 72 | error: 'Unauthorized' 73 | } 74 | } 75 | 76 | const chats: string[] = await kv.zrange(`user:chat:${session.user.id}`, 0, -1) 77 | if (!chats.length) { 78 | return redirect('/') 79 | } 80 | const pipeline = kv.pipeline() 81 | 82 | for (const chat of chats) { 83 | pipeline.del(chat) 84 | pipeline.zrem(`user:chat:${session.user.id}`, chat) 85 | } 86 | 87 | await pipeline.exec() 88 | 89 | revalidatePath('/') 90 | return redirect('/') 91 | } 92 | 93 | export async function getSharedChat(id: string) { 94 | const chat = await kv.hgetall(`chat:${id}`) 95 | 96 | if (!chat || !chat.sharePath) { 97 | return null 98 | } 99 | 100 | return chat 101 | } 102 | 103 | export async function shareChat(id: string) { 104 | const session = await auth() 105 | 106 | if (!session?.user?.id) { 107 | return { 108 | error: 'Unauthorized' 109 | } 110 | } 111 | 112 | const chat = await kv.hgetall(`chat:${id}`) 113 | 114 | if (!chat || chat.userId !== session.user.id) { 115 | return { 116 | error: 'Something went wrong' 117 | } 118 | } 119 | 120 | const payload = { 121 | ...chat, 122 | sharePath: `/share/${chat.id}` 123 | } 124 | 125 | await kv.hmset(`chat:${chat.id}`, payload) 126 | 127 | return payload 128 | } 129 | -------------------------------------------------------------------------------- /frontend/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/auth' 2 | export const runtime = 'edge' 3 | -------------------------------------------------------------------------------- /frontend/app/api/chat/route.ts: -------------------------------------------------------------------------------- 1 | import { kv } from '@vercel/kv' 2 | 3 | import { auth } from '@/auth' 4 | import { nanoid } from '@/lib/utils' 5 | 6 | export const runtime = 'edge' 7 | 8 | export async function POST(req: Request) { 9 | const json = await req.json() 10 | const { messages, previewToken } = json 11 | const userId = (await auth())?.user.id 12 | console.log('Request: ', req) 13 | 14 | if (!userId) { 15 | return new Response('Unauthorized', { 16 | status: 401 17 | }) 18 | } 19 | 20 | const userMessage = messages[messages.length - 1] 21 | 22 | // POST => AWS 23 | const res = await fetch(process.env.AWS_API_CHAT_ENDPOINT || '', { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json' 27 | }, 28 | body: JSON.stringify({ 29 | message: [ 30 | { 31 | role: 'user', 32 | content: userMessage.content 33 | } 34 | ], 35 | stream: false 36 | }) 37 | }) 38 | 39 | if (!res.ok) { 40 | return new Response('Error from server', { status: res.status }) 41 | } 42 | 43 | const data = await res.json() 44 | console.log('Backend response: ', data) 45 | const completion = data.data.content.trim() 46 | const title = json.messages[0].content.substring(0, 100) 47 | const id = json.id ?? nanoid() 48 | const createdAt = Date.now() 49 | const path = `/chat/${id}` 50 | const payload = { 51 | id, 52 | title, 53 | userId, 54 | createdAt, 55 | path, 56 | messages: [ 57 | ...messages, 58 | { 59 | content: completion, 60 | role: 'assistant' 61 | } 62 | ] 63 | } 64 | 65 | await kv.hmset(`chat:${id}`, payload) 66 | await kv.zadd(`user:chat:${userId}`, { 67 | score: createdAt, 68 | member: `chat:${id}` 69 | }) 70 | 71 | return new Response(completion, { 72 | headers: { 73 | 'Content-Type': 'text/plain' 74 | } 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /frontend/app/api/expert/route.ts: -------------------------------------------------------------------------------- 1 | import { kv } from '@vercel/kv' 2 | 3 | import { auth } from '@/auth' 4 | import { nanoid } from '@/lib/utils' 5 | 6 | export const runtime = 'edge' 7 | 8 | export async function POST(req: Request) { 9 | const json = await req.json() 10 | const { messages, previewToken } = json 11 | const userId = (await auth())?.user.id 12 | console.log('Request: ', req) 13 | 14 | if (!userId) { 15 | return new Response('Unauthorized', { 16 | status: 401 17 | }) 18 | } 19 | 20 | const userMessage = messages[messages.length - 1] 21 | 22 | // POST => AWS 23 | const res = await fetch(process.env.AWS_API_CHAT_ENDPOINT || '', { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json' 27 | }, 28 | body: JSON.stringify({ 29 | message: [ 30 | { 31 | role: 'user', 32 | content: userMessage.content 33 | } 34 | ], 35 | stream: false 36 | }) 37 | }) 38 | 39 | if (!res.ok) { 40 | return new Response('Error from server', { status: res.status }) 41 | } 42 | 43 | const data = await res.json() 44 | console.log('Backend response: ', data) 45 | const completion = JSON.stringify(data.data) 46 | const title = '[E] ' + json.messages[0].content.substring(0, 100) 47 | const id = json.id ?? nanoid() 48 | const createdAt = Date.now() 49 | const path = `/expert/chat/${id}` 50 | const payload = { 51 | id, 52 | title, 53 | userId, 54 | createdAt, 55 | path, 56 | messages: [ 57 | ...messages, 58 | { 59 | content: completion, 60 | role: 'assistant' 61 | } 62 | ] 63 | } 64 | 65 | await kv.hmset(`chat:${id}`, payload) 66 | await kv.zadd(`user:chat:${userId}`, { 67 | score: createdAt, 68 | member: `chat:${id}` 69 | }) 70 | 71 | return new Response(completion, { 72 | headers: { 73 | 'Content-Type': 'text/plain' 74 | } 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /frontend/app/expert/chat/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next' 2 | import { notFound, redirect } from 'next/navigation' 3 | 4 | import { auth } from '@/auth' 5 | import { getChat } from '@/app/actions' 6 | import { Chat } from '@/components/chat-expert' 7 | 8 | export interface ChatPageProps { 9 | params: { 10 | id: string 11 | } 12 | } 13 | 14 | export async function generateMetadata({ 15 | params 16 | }: ChatPageProps): Promise { 17 | const session = await auth() 18 | 19 | if (!session?.user) { 20 | return {} 21 | } 22 | 23 | const chat = await getChat(params.id, session.user.id) 24 | return { 25 | title: chat?.title.toString().slice(0, 50) ?? 'Chat' 26 | } 27 | } 28 | 29 | export default async function ChatPage({ params }: ChatPageProps) { 30 | const session = await auth() 31 | 32 | if (!session?.user) { 33 | redirect(`/sign-in?next=/chat/${params.id}`) 34 | } 35 | 36 | const chat = await getChat(params.id, session.user.id) 37 | 38 | if (!chat) { 39 | notFound() 40 | } 41 | 42 | if (chat?.userId !== session?.user?.id) { 43 | notFound() 44 | } 45 | 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /frontend/app/expert/page.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from '@/lib/utils' 2 | import { Chat } from '@/components/chat-expert' 3 | 4 | export default function IndexPage() { 5 | const id = nanoid() 6 | 7 | return 8 | } 9 | -------------------------------------------------------------------------------- /frontend/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | 10 | --muted: 240 4.8% 95.9%; 11 | --muted-foreground: 240 3.8% 46.1%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 240 10% 3.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 240 10% 3.9%; 18 | 19 | --border: 240 5.9% 90%; 20 | --input: 240 5.9% 90%; 21 | 22 | --primary: 240 5.9% 10%; 23 | --primary-foreground: 0 0% 98%; 24 | 25 | --secondary: 240 4.8% 95.9%; 26 | --secondary-foreground: 240 5.9% 10%; 27 | 28 | --accent: 240 4.8% 95.9%; 29 | --accent-foreground: ; 30 | 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | 34 | --ring: 240 5% 64.9%; 35 | 36 | --radius: 0.5rem; 37 | } 38 | 39 | .dark { 40 | --background: 240 10% 3.9%; 41 | --foreground: 0 0% 98%; 42 | 43 | --muted: 240 3.7% 15.9%; 44 | --muted-foreground: 240 5% 64.9%; 45 | 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | 49 | --card: 240 10% 3.9%; 50 | --card-foreground: 0 0% 98%; 51 | 52 | --border: 240 3.7% 15.9%; 53 | --input: 240 3.7% 15.9%; 54 | 55 | --primary: 0 0% 98%; 56 | --primary-foreground: 240 5.9% 10%; 57 | 58 | --secondary: 240 3.7% 15.9%; 59 | --secondary-foreground: 0 0% 98%; 60 | 61 | --accent: 240 3.7% 15.9%; 62 | --accent-foreground: ; 63 | 64 | --destructive: 0 62.8% 30.6%; 65 | --destructive-foreground: 0 85.7% 97.3%; 66 | 67 | --ring: 240 3.7% 15.9%; 68 | } 69 | } 70 | 71 | @layer base { 72 | * { 73 | @apply border-border; 74 | } 75 | body { 76 | @apply bg-background text-foreground; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /frontend/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Toaster } from 'react-hot-toast' 2 | import { GeistSans } from 'geist/font/sans' 3 | import { GeistMono } from 'geist/font/mono' 4 | 5 | import '@/app/globals.css' 6 | import { cn } from '@/lib/utils' 7 | import { TailwindIndicator } from '@/components/tailwind-indicator' 8 | import { Providers } from '@/components/providers' 9 | import { Header } from '@/components/header' 10 | 11 | export const metadata = { 12 | metadataBase: new URL(`https://${process.env.VERCEL_URL}`), 13 | title: { 14 | default: 'Law Bot', 15 | template: `%s - Law Bot` 16 | }, 17 | description: 'Open source project aiming to build a conversational interface, giving sound legal advice for laypeople in Switzerland.', 18 | icons: { 19 | icon: '/favicon.ico', 20 | shortcut: '/favicon-16x16.png', 21 | apple: '/apple-touch-icon.png' 22 | } 23 | } 24 | 25 | export const viewport = { 26 | themeColor: [ 27 | { media: '(prefers-color-scheme: light)', color: 'white' }, 28 | { media: '(prefers-color-scheme: dark)', color: 'black' } 29 | ] 30 | } 31 | 32 | interface RootLayoutProps { 33 | children: React.ReactNode 34 | } 35 | 36 | export default function RootLayout({ children }: RootLayoutProps) { 37 | return ( 38 | 39 | 46 | 47 | 53 |
54 | {/* @ts-ignore */} 55 |
56 |
{children}
57 |
58 | 59 |
60 | 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /frontend/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/frontend/app/opengraph-image.png -------------------------------------------------------------------------------- /frontend/app/share/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { type Metadata } from 'next' 2 | import { notFound } from 'next/navigation' 3 | 4 | import { formatDate } from '@/lib/utils' 5 | import { getSharedChat } from '@/app/actions' 6 | import { ChatList } from '@/components/chat-list' 7 | import { FooterText } from '@/components/footer' 8 | 9 | interface SharePageProps { 10 | params: { 11 | id: string 12 | } 13 | } 14 | 15 | export async function generateMetadata({ 16 | params 17 | }: SharePageProps): Promise { 18 | const chat = await getSharedChat(params.id) 19 | 20 | return { 21 | title: chat?.title.slice(0, 50) ?? 'Chat' 22 | } 23 | } 24 | 25 | export default async function SharePage({ params }: SharePageProps) { 26 | const chat = await getSharedChat(params.id) 27 | 28 | if (!chat || !chat?.sharePath) { 29 | notFound() 30 | } 31 | 32 | return ( 33 | <> 34 |
35 |
36 |
37 |
38 |

{chat.title}

39 |
40 | {formatDate(chat.createdAt)} · {chat.messages.length} messages 41 |
42 |
43 |
44 |
45 | 46 |
47 | 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /frontend/app/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { auth } from '@/auth' 2 | import { LoginButtonGoogle } from '@/components/login-button' 3 | import { redirect } from 'next/navigation' 4 | 5 | export default async function SignInPage() { 6 | const session = await auth() 7 | // redirect to home if user is already logged in 8 | if (session?.user) { 9 | redirect('/') 10 | } 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /frontend/app/twitter-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/access2justice/law-bot/ede98a48937ddbbb8f54dc9d27153253ca18c8f0/frontend/app/twitter-image.png -------------------------------------------------------------------------------- /frontend/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { type DefaultSession } from 'next-auth' 2 | import Google from 'next-auth/providers/google' 3 | 4 | declare module 'next-auth' { 5 | interface Session { 6 | user: { 7 | /** The user's id. */ 8 | id: string 9 | } & DefaultSession['user'] 10 | } 11 | } 12 | 13 | export const { 14 | handlers: { GET, POST }, 15 | auth 16 | } = NextAuth({ 17 | providers: [ 18 | Google({ 19 | clientId: process.env.AUTH_GOOGLE_ID || '', 20 | clientSecret: process.env.AUTH_GOOGLE_SECRET || '' 21 | }) 22 | ], 23 | callbacks: { 24 | jwt({ token, profile, user }) { 25 | if (profile) { 26 | if (profile.id) { 27 | token.id = profile.id 28 | } 29 | if (user.id) { 30 | token.id = user.id 31 | } 32 | token.image = profile.avatar_url || profile.picture 33 | } 34 | return token 35 | }, 36 | session: async ({ session, token }) => { 37 | if (session?.user && token?.id) { 38 | session.user.id = String(token.id) 39 | } 40 | return session 41 | }, 42 | authorized({ auth }) { 43 | return !!auth?.user // this ensures there is a logged in user for -every- request 44 | } 45 | }, 46 | pages: { 47 | signIn: '/sign-in' // overrides the next-auth default signin page https://authjs.dev/guides/basics/pages 48 | } 49 | }) 50 | -------------------------------------------------------------------------------- /frontend/components/button-scroll-to-bottom.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | import { Button, type ButtonProps } from '@/components/ui/button' 8 | import { IconArrowDown } from '@/components/ui/icons' 9 | 10 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) { 11 | const isAtBottom = useAtBottom() 12 | 13 | return ( 14 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /frontend/components/chat-expert.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useChat, type Message } from 'ai/react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { ChatList } from '@/components/chat-list' 7 | import { ChatPanel } from '@/components/chat-panel' 8 | import { EmptyScreen } from '@/components/empty-screen' 9 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' 10 | import { useLocalStorage } from '@/lib/hooks/use-local-storage' 11 | import { 12 | Dialog, 13 | DialogContent, 14 | DialogDescription, 15 | DialogFooter, 16 | DialogHeader, 17 | DialogTitle 18 | } from '@/components/ui/dialog' 19 | import { useState } from 'react' 20 | import { Button } from './ui/button' 21 | import { Input } from './ui/input' 22 | import { toast } from 'react-hot-toast' 23 | import { usePathname, useRouter } from 'next/navigation' 24 | 25 | const IS_PREVIEW = process.env.VERCEL_ENV === 'preview' 26 | export interface ChatProps extends React.ComponentProps<'div'> { 27 | initialMessages?: Message[] 28 | id?: string 29 | } 30 | 31 | export function Chat({ id, initialMessages, className }: ChatProps) { 32 | const router = useRouter() 33 | const path = usePathname() 34 | const [previewToken, setPreviewToken] = useLocalStorage( 35 | 'ai-token', 36 | null 37 | ) 38 | const [previewTokenDialog, setPreviewTokenDialog] = useState(IS_PREVIEW) 39 | const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '') 40 | const { messages, append, reload, stop, isLoading, input, setInput } = 41 | useChat({ 42 | api: '/api/expert', 43 | initialMessages, 44 | id, 45 | body: { 46 | id, 47 | previewToken 48 | }, 49 | onResponse(response) { 50 | if (response.status === 401) { 51 | toast.error(response.statusText) 52 | } 53 | }, 54 | onFinish() { 55 | if (!path.includes('chat')) { 56 | router.push(`/expert/chat/${id}`, { scroll: false }) 57 | router.refresh() 58 | } 59 | } 60 | }) 61 | return ( 62 | <> 63 |
64 | {messages.length ? ( 65 | <> 66 | 67 | 68 | 69 | ) : ( 70 | 71 | )} 72 |
73 | 83 | 84 | 85 | 86 | 87 | Enter your OpenAI Key 88 | 89 | If you have not obtained your OpenAI API key, you can do so by{' '} 90 | 94 | signing up 95 | {' '} 96 | on the OpenAI website. This is only necessary for preview 97 | environments so that the open source community can test the app. 98 | The token will be saved to your browser's local storage under 99 | the name ai-token. 100 | 101 | 102 | setPreviewTokenInput(e.target.value)} 106 | /> 107 | 108 | 116 | 117 | 118 | 119 | 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /frontend/components/chat-history.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | import Link from 'next/link' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { SidebarList } from '@/components/sidebar-list' 7 | import { buttonVariants } from '@/components/ui/button' 8 | import { IconPlus } from '@/components/ui/icons' 9 | 10 | interface ChatHistoryProps { 11 | userId?: string 12 | } 13 | 14 | export async function ChatHistory({ userId }: ChatHistoryProps) { 15 | return ( 16 |
17 |
18 | 25 | 26 | New Chat 27 | 28 |
29 | 32 | {Array.from({ length: 10 }).map((_, i) => ( 33 |
37 | ))} 38 |
39 | } 40 | > 41 | {/* @ts-ignore */} 42 | 43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /frontend/components/chat-list.tsx: -------------------------------------------------------------------------------- 1 | import { type Message } from 'ai' 2 | 3 | import { Separator } from '@/components/ui/separator' 4 | import { ChatMessage } from '@/components/chat-message' 5 | 6 | export interface ChatList { 7 | messages: Message[] 8 | } 9 | 10 | export function ChatList({ messages }: ChatList) { 11 | if (!messages.length) { 12 | return null 13 | } 14 | 15 | return ( 16 |
17 | {messages.map((message, index) => ( 18 |
19 | 20 | {index < messages.length - 1 && ( 21 | 22 | )} 23 |
24 | ))} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/components/chat-message-actions.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { type Message } from 'ai' 4 | 5 | import { Button } from '@/components/ui/button' 6 | import { IconCheck, IconCopy } from '@/components/ui/icons' 7 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 8 | import { cn } from '@/lib/utils' 9 | 10 | interface ChatMessageActionsProps extends React.ComponentProps<'div'> { 11 | message: Message 12 | } 13 | 14 | export function ChatMessageActions({ 15 | message, 16 | className, 17 | ...props 18 | }: ChatMessageActionsProps) { 19 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 }) 20 | 21 | const onCopy = () => { 22 | if (isCopied) return 23 | copyToClipboard(message.content) 24 | } 25 | 26 | return ( 27 |
34 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /frontend/components/chat-panel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { type UseChatHelpers } from 'ai/react' 3 | 4 | import { shareChat } from '@/app/actions' 5 | import { Button } from '@/components/ui/button' 6 | import { PromptForm } from '@/components/prompt-form' 7 | import { ButtonScrollToBottom } from '@/components/button-scroll-to-bottom' 8 | import { IconRefresh, IconShare, IconStop } from '@/components/ui/icons' 9 | import { FooterText } from '@/components/footer' 10 | import { ChatShareDialog } from '@/components/chat-share-dialog' 11 | 12 | export interface ChatPanelProps 13 | extends Pick< 14 | UseChatHelpers, 15 | | 'append' 16 | | 'isLoading' 17 | | 'reload' 18 | | 'messages' 19 | | 'stop' 20 | | 'input' 21 | | 'setInput' 22 | > { 23 | id?: string 24 | title?: string 25 | } 26 | 27 | export function ChatPanel({ 28 | id, 29 | title, 30 | isLoading, 31 | stop, 32 | append, 33 | reload, 34 | input, 35 | setInput, 36 | messages 37 | }: ChatPanelProps) { 38 | const [shareDialogOpen, setShareDialogOpen] = React.useState(false) 39 | 40 | return ( 41 |
42 | 43 |
44 |
45 | {isLoading ? ( 46 | 54 | ) : ( 55 | messages?.length >= 2 && ( 56 |
57 | 61 | {id && title ? ( 62 | <> 63 | 70 | setShareDialogOpen(false)} 74 | shareChat={shareChat} 75 | chat={{ 76 | id, 77 | title, 78 | messages 79 | }} 80 | /> 81 | 82 | ) : null} 83 |
84 | ) 85 | )} 86 |
87 |
88 | { 90 | await append({ 91 | id, 92 | content: value, 93 | role: 'user' 94 | }) 95 | }} 96 | input={input} 97 | setInput={setInput} 98 | isLoading={isLoading} 99 | /> 100 | 101 |
102 |
103 |
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /frontend/components/chat-scroll-anchor.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useInView } from 'react-intersection-observer' 5 | 6 | import { useAtBottom } from '@/lib/hooks/use-at-bottom' 7 | 8 | interface ChatScrollAnchorProps { 9 | trackVisibility?: boolean 10 | } 11 | 12 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) { 13 | const isAtBottom = useAtBottom() 14 | const { ref, entry, inView } = useInView({ 15 | trackVisibility, 16 | delay: 100, 17 | rootMargin: '0px 0px -150px 0px' 18 | }) 19 | 20 | React.useEffect(() => { 21 | if (isAtBottom && trackVisibility && !inView) { 22 | entry?.target.scrollIntoView({ 23 | block: 'start' 24 | }) 25 | } 26 | }, [inView, entry, isAtBottom, trackVisibility]) 27 | 28 | return
29 | } 30 | -------------------------------------------------------------------------------- /frontend/components/chat-share-dialog.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import Link from 'next/link' 5 | import { type DialogProps } from '@radix-ui/react-dialog' 6 | import { toast } from 'react-hot-toast' 7 | 8 | import { ServerActionResult, type Chat } from '@/lib/types' 9 | import { cn } from '@/lib/utils' 10 | import { badgeVariants } from '@/components/ui/badge' 11 | import { Button } from '@/components/ui/button' 12 | import { 13 | Dialog, 14 | DialogContent, 15 | DialogDescription, 16 | DialogFooter, 17 | DialogHeader, 18 | DialogTitle 19 | } from '@/components/ui/dialog' 20 | import { IconSpinner } from '@/components/ui/icons' 21 | import { useCopyToClipboard } from '@/lib/hooks/use-copy-to-clipboard' 22 | 23 | interface ChatShareDialogProps extends DialogProps { 24 | chat: Pick 25 | shareChat: (id: string) => ServerActionResult 26 | onCopy: () => void 27 | } 28 | 29 | export function ChatShareDialog({ 30 | chat, 31 | shareChat, 32 | onCopy, 33 | ...props 34 | }: ChatShareDialogProps) { 35 | const { copyToClipboard } = useCopyToClipboard({ timeout: 1000 }) 36 | const [isSharePending, startShareTransition] = React.useTransition() 37 | 38 | const copyShareLink = React.useCallback( 39 | async (chat: Chat) => { 40 | if (!chat.sharePath) { 41 | return toast.error('Could not copy share link to clipboard') 42 | } 43 | 44 | const url = new URL(window.location.href) 45 | url.pathname = chat.sharePath 46 | copyToClipboard(url.toString()) 47 | onCopy() 48 | toast.success('Share link copied to clipboard', { 49 | style: { 50 | borderRadius: '10px', 51 | background: '#333', 52 | color: '#fff', 53 | fontSize: '14px' 54 | }, 55 | iconTheme: { 56 | primary: 'white', 57 | secondary: 'black' 58 | } 59 | }) 60 | }, 61 | [copyToClipboard, onCopy] 62 | ) 63 | 64 | return ( 65 | 66 | 67 | 68 | Share link to chat 69 | 70 | Anyone with the URL will be able to view the shared chat. 71 | 72 | 73 |
74 |
{chat.title}
75 |
76 | {chat.messages ? `${chat.messages.length} messages` : '0 messages'} 77 |
78 |
79 | 80 | 105 | 106 |
107 |
108 | ) 109 | } 110 | -------------------------------------------------------------------------------- /frontend/components/chat.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useChat, type Message } from 'ai/react' 4 | 5 | import { cn } from '@/lib/utils' 6 | import { ChatList } from '@/components/chat-list' 7 | import { ChatPanel } from '@/components/chat-panel' 8 | import { EmptyScreen } from '@/components/empty-screen' 9 | import { ChatScrollAnchor } from '@/components/chat-scroll-anchor' 10 | import { useLocalStorage } from '@/lib/hooks/use-local-storage' 11 | import { 12 | Dialog, 13 | DialogContent, 14 | DialogDescription, 15 | DialogFooter, 16 | DialogHeader, 17 | DialogTitle 18 | } from '@/components/ui/dialog' 19 | import { useState } from 'react' 20 | import { Button } from './ui/button' 21 | import { Input } from './ui/input' 22 | import { toast } from 'react-hot-toast' 23 | import { usePathname, useRouter } from 'next/navigation' 24 | 25 | const IS_PREVIEW = process.env.VERCEL_ENV === 'preview' 26 | export interface ChatProps extends React.ComponentProps<'div'> { 27 | initialMessages?: Message[] 28 | id?: string 29 | } 30 | 31 | export function Chat({ id, initialMessages, className }: ChatProps) { 32 | const router = useRouter() 33 | const path = usePathname() 34 | const [previewToken, setPreviewToken] = useLocalStorage( 35 | 'ai-token', 36 | null 37 | ) 38 | const [previewTokenDialog, setPreviewTokenDialog] = useState(IS_PREVIEW) 39 | const [previewTokenInput, setPreviewTokenInput] = useState(previewToken ?? '') 40 | const { messages, append, reload, stop, isLoading, input, setInput } = 41 | useChat({ 42 | api: '/api/chat', 43 | initialMessages, 44 | id, 45 | body: { 46 | id, 47 | previewToken 48 | }, 49 | onResponse(response) { 50 | if (response.status === 401) { 51 | toast.error(response.statusText) 52 | } 53 | }, 54 | onFinish() { 55 | if (!path.includes('chat')) { 56 | router.push(`/chat/${id}`, { scroll: false }) 57 | router.refresh() 58 | } 59 | } 60 | }) 61 | return ( 62 | <> 63 |
64 | {messages.length ? ( 65 | <> 66 | 67 | 68 | 69 | ) : ( 70 | 71 | )} 72 |
73 | 83 | 84 | 85 | 86 | 87 | Enter your OpenAI Key 88 | 89 | If you have not obtained your OpenAI API key, you can do so by{' '} 90 | 94 | signing up 95 | {' '} 96 | on the OpenAI website. This is only necessary for preview 97 | environments so that the open source community can test the app. 98 | The token will be saved to your browser's local storage under 99 | the name ai-token. 100 | 101 | 102 | setPreviewTokenInput(e.target.value)} 106 | /> 107 | 108 | 116 | 117 | 118 | 119 | 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /frontend/components/clear-history.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { useRouter } from 'next/navigation' 5 | import { toast } from 'react-hot-toast' 6 | 7 | import { ServerActionResult } from '@/lib/types' 8 | import { Button } from '@/components/ui/button' 9 | import { 10 | AlertDialog, 11 | AlertDialogAction, 12 | AlertDialogCancel, 13 | AlertDialogContent, 14 | AlertDialogDescription, 15 | AlertDialogFooter, 16 | AlertDialogHeader, 17 | AlertDialogTitle, 18 | AlertDialogTrigger 19 | } from '@/components/ui/alert-dialog' 20 | import { IconSpinner } from '@/components/ui/icons' 21 | 22 | interface ClearHistoryProps { 23 | isEnabled: boolean 24 | clearChats: () => ServerActionResult 25 | } 26 | 27 | export function ClearHistory({ 28 | isEnabled = false, 29 | clearChats 30 | }: ClearHistoryProps) { 31 | const [open, setOpen] = React.useState(false) 32 | const [isPending, startTransition] = React.useTransition() 33 | const router = useRouter() 34 | 35 | return ( 36 | 37 | 38 | 42 | 43 | 44 | 45 | Are you absolutely sure? 46 | 47 | This will permanently delete your chat history and remove your data 48 | from our servers. 49 | 50 | 51 | 52 | Cancel 53 | { 56 | event.preventDefault() 57 | startTransition(() => { 58 | clearChats().then(result => { 59 | if (result && 'error' in result) { 60 | toast.error(result.error) 61 | return 62 | } 63 | 64 | setOpen(false) 65 | router.push('/') 66 | }) 67 | }) 68 | }} 69 | > 70 | {isPending && } 71 | Delete 72 | 73 | 74 | 75 | 76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /frontend/components/empty-screen.tsx: -------------------------------------------------------------------------------- 1 | import { UseChatHelpers } from 'ai/react' 2 | 3 | import { Button } from '@/components/ui/button' 4 | import { ExternalLink } from '@/components/external-link' 5 | import { IconArrowRight } from '@/components/ui/icons' 6 | 7 | const exampleMessages = [ 8 | { 9 | heading: 'Figure out your vacation days', 10 | message: `What is the minimum length of vacation?` 11 | }, 12 | { 13 | heading: 'Eine Firma gründen', 14 | message: 'Was sind die Voraussetzungen für die Gründung einer GmbH?' 15 | }, 16 | { 17 | heading: 'Estimer durée de préavis', 18 | message: `Quelle est la durée de préavis pour un contrat de bail?` 19 | } 20 | ] 21 | 22 | export function EmptyScreen({ setInput }: Pick) { 23 | return ( 24 |
25 |
26 |

27 | Welcome to the Law Bot project! 28 |

29 |

30 | This is an open source project aiming to build a conversational 31 | interface, giving sound legal advice for laypeople in Switzerland. 32 | Take a look at our Github repository{' '} 33 | 34 | Github repo 35 | 36 | . 37 |

38 |

39 | You can start a conversation here or try the following examples: 40 |

41 |
42 | {exampleMessages.map((message, index) => ( 43 | 52 | ))} 53 |
54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /frontend/components/external-link.tsx: -------------------------------------------------------------------------------- 1 | export function ExternalLink({ 2 | href, 3 | children 4 | }: { 5 | href: string 6 | children: React.ReactNode 7 | }) { 8 | return ( 9 | 14 | {children} 15 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /frontend/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { cn } from '@/lib/utils' 4 | import { ExternalLink } from '@/components/external-link' 5 | 6 | export function FooterText({ className, ...props }: React.ComponentProps<'p'>) { 7 | return ( 8 |

15 | Conversational interface built by the Law Bot team. 16 |

17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /frontend/components/header.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Link from 'next/link' 3 | import Image from 'next/image' 4 | import lawBotTemporaryLogo from '../public/law-bot-temporary-logo.png' 5 | 6 | import { cn } from '@/lib/utils' 7 | import { auth } from '@/auth' 8 | import { clearChats } from '@/app/actions' 9 | import { Button, buttonVariants } from '@/components/ui/button' 10 | import { Sidebar } from '@/components/sidebar' 11 | import { SidebarList } from '@/components/sidebar-list' 12 | import { 13 | IconGitHub, 14 | IconNextChat, 15 | IconSeparator, 16 | IconVercel 17 | } from '@/components/ui/icons' 18 | import { SidebarFooter } from '@/components/sidebar-footer' 19 | import { ThemeToggle } from '@/components/theme-toggle' 20 | import { ClearHistory } from '@/components/clear-history' 21 | import { UserMenu } from '@/components/user-menu' 22 | import { SidebarMobile } from './sidebar-mobile' 23 | import { SidebarToggle } from './sidebar-toggle' 24 | import { ChatHistory } from './chat-history' 25 | import { nanoid } from '@/lib/utils' 26 | import ExpertToggle from './ui/expert-toggle' 27 | 28 | async function UserOrLogin() { 29 | const session = await auth() 30 | return ( 31 | <> 32 | {session?.user ? ( 33 | <> 34 | 35 | 36 | 37 | 38 | 39 | ) : ( 40 | 41 | {'Law 48 | 49 | )} 50 |
51 | 52 | {session?.user ? ( 53 | 54 | ) : ( 55 | 58 | )} 59 |
60 | 61 | ) 62 | } 63 | 64 | export async function Header() { 65 | const session = await auth() 66 | return ( 67 |
68 |
69 | }> 70 | 71 | 72 |
73 | {session?.user && ( 74 |
75 | 76 |
77 | )} 78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /frontend/components/login-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { signIn } from 'next-auth/react' 5 | 6 | import { cn } from '@/lib/utils' 7 | import { Button, type ButtonProps } from '@/components/ui/button' 8 | import { IconSpinner, IconGoogle } from '@/components/ui/icons' 9 | 10 | interface LoginButtonProps extends ButtonProps { 11 | showIcon?: boolean 12 | text?: string 13 | } 14 | 15 | export function LoginButtonGoogle({ 16 | text = 'Login with Google', 17 | showIcon = true, 18 | className, 19 | ...props 20 | }: LoginButtonProps) { 21 | const [isLoading, setIsLoading] = React.useState(false) 22 | return ( 23 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /frontend/components/markdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, memo } from 'react' 2 | import ReactMarkdown, { Options } from 'react-markdown' 3 | 4 | export const MemoizedReactMarkdown: FC = memo( 5 | ReactMarkdown, 6 | (prevProps, nextProps) => 7 | prevProps.children === nextProps.children && 8 | prevProps.className === nextProps.className 9 | ) 10 | -------------------------------------------------------------------------------- /frontend/components/prompt-form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Textarea from 'react-textarea-autosize' 3 | import { UseChatHelpers } from 'ai/react' 4 | import { useEnterSubmit } from '@/lib/hooks/use-enter-submit' 5 | import { cn } from '@/lib/utils' 6 | import { Button, buttonVariants } from '@/components/ui/button' 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipTrigger 11 | } from '@/components/ui/tooltip' 12 | import { IconArrowElbow, IconPlus } from '@/components/ui/icons' 13 | import { useRouter } from 'next/navigation' 14 | 15 | export interface PromptProps 16 | extends Pick { 17 | onSubmit: (value: string) => void 18 | isLoading: boolean 19 | } 20 | 21 | export function PromptForm({ 22 | onSubmit, 23 | input, 24 | setInput, 25 | isLoading 26 | }: PromptProps) { 27 | const { formRef, onKeyDown } = useEnterSubmit() 28 | const inputRef = React.useRef(null) 29 | const router = useRouter() 30 | React.useEffect(() => { 31 | if (inputRef.current) { 32 | inputRef.current.focus() 33 | } 34 | }, []) 35 | 36 | return ( 37 |
{ 39 | e.preventDefault() 40 | if (!input?.trim()) { 41 | return 42 | } 43 | setInput('') 44 | await onSubmit(input) 45 | }} 46 | ref={formRef} 47 | > 48 |
49 | 50 | 51 | 65 | 66 | New Chat 67 | 68 |