├── .dockerignore ├── .env.example ├── .env.template ├── .gitignore ├── .vscode └── launch.json ├── Dockerfile ├── LICENSE ├── README.md ├── _assets ├── logo.svg └── preview.webp ├── build.sh ├── docker-compose.yml ├── package-lock.json ├── package.json ├── src ├── config.ts ├── custom │ ├── cypher-query-validator │ │ ├── BindingCollector.ts │ │ ├── NameRetriever.ts │ │ ├── antlr │ │ │ ├── Cypher.g4 │ │ │ ├── Cypher.interp │ │ │ ├── Cypher.tokens │ │ │ ├── CypherLexer.interp │ │ │ ├── CypherLexer.tokens │ │ │ ├── CypherLexer.ts │ │ │ ├── CypherListener.ts │ │ │ └── CypherParser.ts │ │ ├── index.ts │ │ ├── labelvalidators.ts │ │ └── validate.ts │ └── graph-cypher-qa-chain │ │ ├── chain.ts │ │ └── prompts.ts ├── public │ ├── favicon.ico │ ├── htmx.min.js │ ├── logo.svg │ ├── main.css │ ├── main.js │ ├── prism.css │ └── prism.js ├── routes │ ├── ask.ts │ ├── home.ts │ ├── schema.ts │ ├── validate.ts │ ├── vote-down.ts │ └── vote-up.ts ├── server.ts ├── services │ └── database.ts ├── utils │ ├── database.ts │ └── partials.ts └── views │ ├── index.hbs │ ├── layouts │ └── main.hbs │ └── partials │ ├── database-selector.hbs │ ├── last-prompts.hbs │ ├── notification.hbs │ ├── prompt-input.hbs │ ├── prompt-output.hbs │ ├── schema.hbs │ ├── settings.hbs │ ├── vote-success.hbs │ ├── vote-validate-dialog.hbs │ ├── vote-validate-success.hbs │ └── vote.hbs └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/dist 3 | .git 4 | .gitignore 5 | npm-debug.log 6 | .env -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=sk-a1b2c3d4e5fg6h7i8j9k 2 | DATABASES='[{"uri":"neo4j+s://demo.neo4jlabs.com","name":"movies","username":"movies","password":"movies"},{"uri":"neo4j+s://demo.neo4jlabs.com","name":"stackoverflow","username":"stackoverflow","password":"stackoverflow"}]' 3 | 4 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= 2 | DATABASES= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch TS Program", 8 | "runtimeExecutable": "npm", 9 | "runtimeArgs": ["run-script", "dev"], 10 | "skipFiles": [ 11 | "/**" 12 | ], 13 | "preLaunchTask": "tsc: build - tsconfig.json", 14 | "outFiles": ["${workspaceFolder}/dist/**/*.js"], 15 | "sourceMaps": true, 16 | "console": "integratedTerminal" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | RUN apk add --no-cache bash 4 | 5 | WORKDIR /app 6 | 7 | COPY package*.json ./ 8 | 9 | # Install dependencies 10 | RUN npm install 11 | 12 | # Copy the rest of the application code needed for build 13 | COPY src ./src 14 | COPY build.sh . 15 | COPY tsconfig.json . 16 | 17 | # Build the app 18 | RUN npm run build 19 | 20 | # Expose the port the app runs on 21 | EXPOSE 3001 22 | 23 | # Command to run the app 24 | 25 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anej Gorkič 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 | # text2cypher 2 | 3 | Application to evaluate how well [text-to-cypher](https://python.langchain.com/docs/use_cases/graph/graph_cypher_qa) approach works on your own Neo4j databases. 4 | 5 | Features: 6 | - Multiple input databases 7 | - Collect user feedback on generated cypher queries 8 | - Client-side OpenAI API key override 9 | 10 | ![text2cypher prompt example](_assets/preview.webp) 11 | 12 | 13 | ## Run with Docker 14 | 15 | Setup env: 16 | 17 | `cp .env.template .env` 18 | 19 | `OPENAI_API_KEY` and `DATABASES` env vars are required. `DATABASES` accepts a string containing an array of database connection objects. You can provide multiple databases (see `.env.example`). 20 | 21 | Example database connection object: 22 | 23 | ``` 24 | { 25 | "uri":"neo4j+s://demo.neo4jlabs.com", 26 | "name":"test_db", 27 | "username":"test", 28 | "password":"test" 29 | } 30 | ``` 31 | 32 | Docker compose will read env variables from your `.env` file. 33 | 34 | `docker compose up --build` 35 | 36 | Docker container will spin up on [127.0.0.1:3001](http://127.0.0.1:3001)`. 37 | 38 | ## .env vars 39 | 40 | Check `.env.example` for overview on how to setup `.env` vars. 41 | 42 | | Parameter | Description | 43 | | ------------------------ | -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 44 | | `OPENAI_API_KEY` | Your [OpenAI API key](https://platform.openai.com/api-keys) | 45 | | `DATABASES` | String containing an array of your database connection objects. Example:
`'[{"uri":"neo4j+s://demo.neo4jlabs.com","name":"test_db","username":"test","password":"test"}]'` | 46 | | `FEEDBACK_DATABASE` | [optional] String containing your feedback database connection object. Example:
`'{"uri":"neo4j+s://example.databases.neo4j.io","name":"feedback_db","username":"feedback","password":"feedback"}'` | 47 | | `PROMPT_MAX_LENGTH` | [optional] Maximum allowed prompt length. Default: `300` | 48 | | `PROMPT_MAX_DURATION_MS` | [optional] Maximum duration of prompt request. Default: `10000` | 49 | | `HOST` | [optional] Server host. Default: `127.0.0.1` | 50 | | `PORT` | [optional] Server port for app to run on. Default: `3001` 51 | 52 | ## Build and run with NodeJS 53 | 54 | Setup env: 55 | 56 | `cp .env.template .env` and add missing keys. 57 | 58 | Install dependencies: 59 | 60 | `npm install` 61 | 62 | Build: 63 | 64 | `npm run build` 65 | 66 | Run: 67 | 68 | `cd dist && npm start` 69 | 70 | ## Development 71 | 72 | Setup env: 73 | 74 | `cp .env.template .env` 75 | 76 | and add missing keys. Check `.env.example` for setup. Multiple input `DATABASES` are supported. 77 | 78 | Install dependencies: 79 | 80 | `npm install` 81 | 82 | Run local development server: 83 | 84 | `npm run dev` 85 | 86 | This project is built with [Fastify](https://www.fastify.io/docs/latest/), [Langchain](https://js.langchain.com/docs/get_started/introduction) and [HTMX](https://htmx.org/). 87 | -------------------------------------------------------------------------------- /_assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 92 | 96 | 100 | 104 | 108 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /_assets/preview.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easwee/text2cypher/6445fbd9e2359ee6aae93e8070e15f8b32ac6f68/_assets/preview.webp -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Clean the 'dist' directory 4 | rm -rf dist 5 | 6 | # Compile TypeScript files 7 | tsc --project tsconfig.json 8 | 9 | # Copy static files to 'dist' directory 10 | cp -r src/views dist/views 11 | cp -r src/public dist/public 12 | 13 | echo "Build complete." -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - "127.0.0.1:3001:3001" 7 | env_file: 8 | - .env 9 | environment: 10 | HOST: 0.0.0.0 # binds to docker internal app host 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "text2cypher", 3 | "version": "1.0.0", 4 | "description": "Crowdsourced cypher statement evaluation", 5 | "main": "server.ts", 6 | "scripts": { 7 | "build": "bash build.sh", 8 | "start": "nodemon dist/server.js", 9 | "dev": "nodemon --exec 'ts-node src/server.ts'" 10 | }, 11 | "keywords": [ 12 | "text2cypher", 13 | "neo4j", 14 | "crowdsourcing" 15 | ], 16 | "author": "easwee.net", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@fastify/autoload": "^5.0.0", 20 | "@fastify/cors": "^8.4.1", 21 | "@fastify/env": "^4.3.0", 22 | "@fastify/formbody": "^7.4.0", 23 | "@fastify/sensible": "^5.0.0", 24 | "@fastify/static": "^6.12.0", 25 | "@fastify/view": "^8.2.0", 26 | "@langchain/community": "^0.0.20", 27 | "@langchain/openai": "^0.0.12", 28 | "antlr4": "^4.13.1-patch-1", 29 | "fastify": "^4.0.0", 30 | "fastify-cli": "^5.9.0", 31 | "fastify-plugin": "^4.0.0", 32 | "fluent-json-schema": "^4.2.1", 33 | "handlebars": "^4.7.8", 34 | "langchain": "0.1.6", 35 | "neo4j-driver": "^5.15.0" 36 | }, 37 | "devDependencies": { 38 | "@types/node": "^20.9.5", 39 | "nodemon": "^3.0.3", 40 | "rimraf": "^5.0.5", 41 | "ts-node": "^10.9.2", 42 | "typescript": "^5.3.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | export interface DbConnectionData { 2 | uri: string; 3 | username: string; 4 | password: string; 5 | name?: string; 6 | } 7 | 8 | export interface EnvConfig { 9 | HOST: string; 10 | PORT: number; 11 | OPENAI_API_KEY: string; 12 | PROMPT_MAX_LENGTH: number; 13 | PROMPT_MAX_DURATION_MS: number; 14 | DATABASES: Array; 15 | FEEDBACK_DATABASE: DbConnectionData | null; 16 | } 17 | 18 | export function initConfig(env: any): EnvConfig { 19 | return { 20 | HOST: env.HOST ? env.HOST : "127.0.0.1", 21 | PORT: env.PORT ? parseInt(env.PORT) : 3001, 22 | OPENAI_API_KEY: env.OPENAI_API_KEY || "", 23 | PROMPT_MAX_LENGTH: env.PROMPT_MAX_LENGTH 24 | ? parseInt(env.PROMPT_MAX_LENGTH) 25 | : 300, 26 | PROMPT_MAX_DURATION_MS: env.PROMPT_MAX_DURATION_MS 27 | ? parseInt(env.PROMPT_MAX_DURATION_MS) 28 | : 10000, 29 | DATABASES: env.DATABASES ? JSON.parse(env.DATABASES) : [], 30 | FEEDBACK_DATABASE: env.FEEDBACK_DATABASE 31 | ? JSON.parse(env.FEEDBACK_DATABASE) 32 | : null, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/BindingCollector.ts: -------------------------------------------------------------------------------- 1 | import { Validator } from "./labelvalidators"; 2 | import CypherListener from "./antlr/CypherListener"; 3 | import CypherParser, { 4 | AsMappingContext, 5 | ReturnItemContext, 6 | ReturnItemsContext, 7 | WithClauseContext, 8 | } from "./antlr/CypherParser"; 9 | import { NameRetriever } from "./NameRetriever"; 10 | import { ParseTreeWalker } from "antlr4"; 11 | 12 | export type Binding = { name: string; types: Validator }; 13 | 14 | export class BindingCollector extends CypherListener { 15 | constructor( 16 | private resolve: (variable: string) => Validator, 17 | private oldBindings: Binding[], 18 | private bindings: Binding[], 19 | ) { 20 | super(); 21 | } 22 | 23 | enterAsMapping: (ctx: AsMappingContext) => void = (ctx) => { 24 | const varName = NameRetriever.getName(ctx.variable()); 25 | if (ctx.expression().ruleIndex === CypherParser.RULE_expression) { 26 | const expVar = NameRetriever.getName(ctx.expression()); 27 | if (expVar && varName) { 28 | this.bindings.push({ name: varName, types: this.resolve(expVar) }); 29 | } 30 | } 31 | }; 32 | 33 | enterReturnItems: (ctx: ReturnItemsContext) => void = (ctx) => { 34 | if (ctx.getText().charAt(0) === "*") { 35 | this.bindings.push(...this.oldBindings); 36 | } 37 | }; 38 | 39 | enterReturnItem: (ctx: ReturnItemContext) => void = (ctx) => { 40 | if (!ctx.asMapping()) { 41 | const name = NameRetriever.getName(ctx); 42 | if (name) { 43 | this.bindings.push({ name, types: this.resolve(name) }); 44 | } 45 | } 46 | }; 47 | 48 | static getBindings( 49 | resolve: (variable: string) => Validator, 50 | oldBindings: Binding[], 51 | ctx: WithClauseContext, 52 | ) { 53 | if (ctx) { 54 | const bindings: Binding[] = []; 55 | const nameRetriever = new BindingCollector( 56 | resolve, 57 | oldBindings, 58 | bindings, 59 | ); 60 | ParseTreeWalker.DEFAULT.walk(nameRetriever, ctx); 61 | return bindings; 62 | } else { 63 | return undefined; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/NameRetriever.ts: -------------------------------------------------------------------------------- 1 | import CypherListener from "./antlr/CypherListener"; 2 | import { SymbolicNameContext } from "./antlr/CypherParser"; 3 | import { ParserRuleContext, ParseTreeWalker } from "antlr4"; 4 | 5 | export class NameRetriever extends CypherListener { 6 | public name: string = ""; 7 | public count = 0; 8 | 9 | enterSymbolicName: (ctx: SymbolicNameContext) => void = (ctx) => { 10 | if (ctx.EscapedSymbolicName()) { 11 | const escapedValue = ctx.EscapedSymbolicName().getText(); 12 | this.name = escapedValue 13 | .substring(1, escapedValue.length - 1) 14 | .replace(/''/g, "'"); 15 | } else { 16 | this.name = ctx.getText(); 17 | } 18 | this.count++; 19 | }; 20 | 21 | static getName(root?: ParserRuleContext): string | undefined { 22 | if (root) { 23 | const nameRetriever = new NameRetriever(); 24 | ParseTreeWalker.DEFAULT.walk(nameRetriever, root); 25 | if (nameRetriever.count !== 1) { 26 | return undefined; 27 | } 28 | return nameRetriever.name; 29 | } else { 30 | return undefined; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/antlr/Cypher.g4: -------------------------------------------------------------------------------- 1 | 2 | grammar Cypher; 3 | 4 | cypher: cypherPart ( ';' cypherPart )* ';'? EOF; 5 | 6 | cypherPart: SP? (cypherQuery | cypherConsoleCommand) SP? ; 7 | 8 | cypherConsoleCommand: cypherConsoleCommandName ( SP cypherConsoleCommandParameters )? ; 9 | 10 | cypherConsoleCommandName: ':' symbolicName ('-' symbolicName)*; 11 | 12 | cypherConsoleCommandParameters : cypherConsoleCommandParameter ( SP cypherConsoleCommandParameter )* ; 13 | 14 | cypherConsoleCommandParameter: 15 | url 16 | | json 17 | | arrowExpression 18 | | mapLiteral 19 | | keyValueLiteral 20 | | stringLiteral 21 | | numberLiteral 22 | | booleanLiteral 23 | | subCommand 24 | | commandPath; 25 | 26 | arrowExpression: symbolicName SP? '=>' SP? expression; 27 | 28 | // URL 29 | 30 | url: uri; 31 | 32 | uri: 33 | scheme '://' login? host (':' port)? ('/' path)? urlQuery? frag? ; 34 | 35 | scheme: string; 36 | 37 | host: '/'? (hostname | hostnumber); 38 | 39 | hostname: string ('.' string)*; 40 | 41 | hostnumber: urlDigits '.' urlDigits '.' urlDigits '.' urlDigits; 42 | 43 | port: urlDigits; 44 | 45 | path: string ('/' string)*; 46 | 47 | user: string; 48 | 49 | login: user ':' password '@'; 50 | 51 | password: string; 52 | 53 | frag: ('#' string); 54 | 55 | urlQuery: ('?' search); 56 | 57 | search: searchparameter ('&' searchparameter)*; 58 | 59 | searchparameter: string ('=' (string | urlDigits | UrlHex))?; 60 | 61 | string: symbolicName (('+'| '.')? symbolicName)*? ; 62 | 63 | urlDigits: integerLiteral+; 64 | 65 | // JSON 66 | 67 | json: value; 68 | 69 | obj : '{' SP? pair SP? (',' SP? pair SP?)* '}' 70 | | '{' SP? '}' 71 | ; 72 | 73 | pair : stringLiteral SP? ':' SP? value 74 | ; 75 | 76 | array : '[' SP? value SP? (',' SP? value SP?)* ']' 77 | | '[' SP? ']' 78 | ; 79 | 80 | value : stringLiteral 81 | | numberLiteral 82 | | obj 83 | | array 84 | | booleanLiteral 85 | | NULL 86 | ; 87 | 88 | keyValueLiteral : variable ':' SP ( StringLiteral | numberLiteral | booleanLiteral | symbolicName) ; 89 | 90 | commandPath : ( '/' ( symbolicName | numberLiteral ) )+ '/'? ; 91 | 92 | subCommand : ( symbolicName ( '-' symbolicName )* ) ; 93 | 94 | cypherQuery : queryOptions statement ; 95 | 96 | queryOptions : ( anyCypherOption SP? )* ; 97 | 98 | anyCypherOption : cypherOption 99 | | explain 100 | | profile 101 | ; 102 | 103 | cypherOption : CYPHER ( SP versionNumber )? ( SP configurationOption )* ; 104 | 105 | versionNumber : RegularDecimalReal ; 106 | 107 | explain : EXPLAIN ; 108 | 109 | profile : PROFILE ; 110 | 111 | configurationOption : symbolicName SP? '=' SP? symbolicName ; 112 | 113 | statement : command 114 | | query 115 | | ( CATALOG SP )? systemCommand 116 | ; 117 | 118 | query : regularQuery 119 | | bulkImportQuery 120 | ; 121 | 122 | regularQuery : singleQuery ( SP? union )* ; 123 | 124 | bulkImportQuery : periodicCommitHint SP? loadCSVQuery ; 125 | 126 | singleQuery : clause ( SP? clause )* ; 127 | 128 | periodicCommitHint : USING SP PERIODIC SP COMMIT ( SP integerLiteral )? ; 129 | 130 | loadCSVQuery : loadCSVClause ( SP? clause )* ; 131 | 132 | union : ( UNION SP ALL SP? singleQuery ) 133 | | ( UNION SP? singleQuery ) 134 | ; 135 | 136 | clause : loadCSVClause 137 | | startClause 138 | | matchClause 139 | | unwindClause 140 | | mergeClause 141 | | createClause 142 | | createUniqueClause 143 | | setClause 144 | | deleteClause 145 | | removeClause 146 | | foreachClause 147 | | withClause 148 | | returnClause 149 | | call 150 | | subqueryClause 151 | ; 152 | 153 | command : createIndex 154 | | dropIndex 155 | | createUniqueConstraint 156 | | dropUniqueConstraint 157 | | createNodeKeyConstraint 158 | | dropNodeKeyConstraint 159 | | createNodePropertyExistenceConstraint 160 | | dropNodePropertyExistenceConstraint 161 | | createRelationshipPropertyExistenceConstraint 162 | | dropRelationshipPropertyExistenceConstraint 163 | ; 164 | 165 | systemCommand : multidatabaseCommand 166 | | userCommand 167 | | privilegeCommand 168 | ; 169 | 170 | multidatabaseCommand : showDatabase 171 | | createDatabase 172 | | dropDatabase 173 | | startDatabase 174 | | stopDatabase 175 | ; 176 | 177 | userCommand: showRoles 178 | | createRole 179 | | dropRole 180 | | showUsers 181 | | createUser 182 | | dropUser 183 | | alterUser 184 | ; 185 | 186 | privilegeCommand: showPrivileges 187 | | grantPrivilege 188 | | denyPrivilege 189 | | revokePrivilege 190 | ; 191 | 192 | showRoles: SHOW SP ( ALL SP )? ROLES ( SP WITH SP USERS )? 193 | | SHOW SP ( POPULATED SP )? ROLES ( SP WITH SP USERS )? 194 | ; 195 | 196 | createRole: CREATE SP ROLE SP symbolicName ( SP ifNotExists )? ( SP copyRole )? 197 | | CREATE SP ( orReplace SP )? ROLE SP symbolicName ( SP copyRole )? 198 | ; 199 | 200 | copyRole: AS SP COPY SP OF SP symbolicName ; 201 | 202 | dropRole: DROP SP ROLE SP symbolicName ( SP IF SP EXISTS )? ; 203 | 204 | showUsers: SHOW SP USERS ; 205 | 206 | createUser: CREATE SP USER SP user SP ( SP ifNotExists )? setPassword ( SP setStatus )? 207 | | CREATE SP ( orReplace SP )? USER SP user SP setPassword ( SP setStatus )? 208 | ; 209 | 210 | dropUser: DROP SP USER SP user ( SP ifExists )? ; 211 | 212 | alterUser: ALTER SP CURRENT SP USER SP SET SP PASSWORD SP FROM SP ( password | parameter ) TO SP ( password | parameter ) 213 | | ALTER SP USER SP user SP setPassword ( SP setStatus )? 214 | | ALTER SP USER SP user SP setStatus 215 | ; 216 | 217 | showPrivileges: SHOW SP ( ALL SP )? PRIVILEGES 218 | | SHOW SP ( ROLE SP symbolicName SP )? PRIVILEGES 219 | | SHOW SP ( USER SP user SP )? PRIVILEGES 220 | ; 221 | 222 | grantPrivilege: GRANT SP ROLE SP roles SP TO SP user 223 | | GRANT SP datasbasePrivilege SP ON SP databaseScope SP TO roles 224 | | GRANT SP grantableGraphPrivileges SP ON SP graphScope SP elementScope SP TO roles 225 | | GRANT SP dbmsPrivilege SP ON SP DBMS SP TO roles 226 | ; 227 | 228 | denyPrivilege: DENY SP ROLE SP roles SP TO SP user 229 | | DENY SP datasbasePrivilege SP ON SP databaseScope SP TO roles 230 | | DENY SP grantableGraphPrivileges SP ON SP graphScope SP elementScope SP TO roles 231 | | DENY SP dbmsPrivilege SP ON SP DBMS SP TO roles 232 | ; 233 | 234 | revokePrivilege: REVOKE SP ROLE SP roles SP FROM SP user 235 | | REVOKE ( SP ( GRANT | DENY ) )? revokePart SP FROM SP roles; 236 | 237 | revokePart: datasbasePrivilege SP ON SP databaseScope 238 | | revokeableGraphPrivileges SP ON SP graphScope 239 | | dbmsPrivilege SP ON SP DBMS 240 | ; 241 | 242 | databaseScope: ( DATABASE | DATABASES ) SP '*' 243 | | ( DATABASE | DATABASES ) SP symbolicName ( SP? ',' SP? symbolicName )* 244 | ; 245 | 246 | graphScope: ( GRAPH | GRAPHS ) SP '*' 247 | | ( GRAPH | GRAPHS ) SP symbolicName ( SP? ',' SP? symbolicName )* 248 | ; 249 | 250 | roles: symbolicName ( SP? ',' SP? symbolicName )* ; 251 | 252 | grantableGraphPrivileges: revokeableGraphPrivileges 253 | | MATCH SP '{' SP? propertiesList '}' 254 | ; 255 | 256 | revokeableGraphPrivileges: TRAVERSE 257 | | READ SP '{' SP? propertiesList '}' 258 | | WRITE 259 | ; 260 | 261 | datasbasePrivilege: ACCESS 262 | | START 263 | | STOP 264 | | CREATE SP ( INDEX | INDEXES ) 265 | | DROP SP ( INDEX | INDEXES ) 266 | | ( INDEX | INDEXES ) SP MANAGEMENT 267 | | CREATE SP ( CONSTRAINT | CONSTRAINTS ) 268 | | DROP SP ( CONSTRAINT | CONSTRAINTS ) 269 | | ( CONSTRAINT | CONSTRAINTS ) SP MANAGEMENT 270 | | CREATE SP NEW SP ( NODE SP )? ( LABEL | LABELS ) 271 | | CREATE SP NEW SP ( RELATIONSHIP SP )? ( TYPE | TYPES ) 272 | | CREATE SP NEW SP ( PROPERTY SP )? ( NAME | NAMES ) 273 | | NAME ( SP MANAGEMENT )? 274 | | ALL ( SP ( DATABASE SP )? PRIVILEGES )? 275 | ; 276 | 277 | dbmsPrivilege: ROLE SP MANAGEMENT 278 | | CREATE SP ROLE 279 | | DROP SP ROLE 280 | | ASSIGN SP ROLE 281 | | REMOVE SP ROLE 282 | | SHOW SP ROLE 283 | ; 284 | 285 | elementScope: ( RELATIONSHIP | RELATIONSHIPS ) SP propertiesList ( SP propertyScope )? 286 | | ( NODE | NODES ) SP propertiesList ( SP propertyScope )? 287 | | ( ELEMENT | ELEMENTS ) SP propertiesList ( SP propertyScope )? 288 | ; 289 | 290 | propertiesList: '*' 291 | | symbolicName ( SP? ',' SP? symbolicName)* 292 | ; 293 | 294 | propertyScope: '(' SP? '*' SP? ')' ; 295 | 296 | showDatabase : SHOW SP ( DEFAULT SP )? DATABASE 297 | | SHOW SP DATABASES 298 | ; 299 | 300 | createDatabase: CREATE SP DATABASE SP symbolicName ( SP ifNotExists )? 301 | | CREATE SP ( orReplace SP )? DATABASE SP symbolicName 302 | ; 303 | 304 | dropDatabase: DROP SP DATABASE SP symbolicName ( SP ifExists )? ; 305 | 306 | startDatabase: START SP DATABASE SP symbolicName ; 307 | 308 | stopDatabase: STOP SP DATABASE SP symbolicName ; 309 | 310 | ifNotExists: IF SP NOT SP EXISTS ; 311 | 312 | ifExists: IF SP EXISTS ; 313 | 314 | orReplace: OR SP REPLACE ; 315 | 316 | setPassword: SET SP PASSWORD SP ( password | parameter ) ( SP passwordStatus )? 317 | | SET SP PASSWORD SP passwordStatus 318 | ; 319 | 320 | passwordStatus: CHANGE SP ( NOT SP )? REQUIRED; 321 | 322 | setStatus: SET SP STATUS SP userStatus ; 323 | 324 | userStatus: ACTIVE 325 | | SUSPENDED 326 | ; 327 | 328 | createUniqueConstraint : CREATE SP uniqueConstraint ; 329 | 330 | createNodeKeyConstraint : CREATE SP nodeKeyConstraint ; 331 | 332 | createNodePropertyExistenceConstraint : CREATE SP nodePropertyExistenceConstraint ; 333 | 334 | createRelationshipPropertyExistenceConstraint : CREATE SP relationshipPropertyExistenceConstraint ; 335 | 336 | createIndex : CREATE SP index ; 337 | 338 | dropUniqueConstraint : DROP SP uniqueConstraint ; 339 | 340 | dropNodeKeyConstraint : DROP SP nodeKeyConstraint ; 341 | 342 | dropNodePropertyExistenceConstraint : DROP SP nodePropertyExistenceConstraint ; 343 | 344 | dropRelationshipPropertyExistenceConstraint : DROP SP relationshipPropertyExistenceConstraint ; 345 | 346 | dropIndex : DROP SP index ; 347 | 348 | index : INDEX SP ON SP? nodeLabel SP? '(' SP? propertyKeys SP? ')' ; 349 | 350 | uniqueConstraint : CONSTRAINT SP ON SP? '(' SP? variable nodeLabel SP? ')' SP? ASSERT SP propertyExpression SP IS SP UNIQUE ; 351 | 352 | nodeKeyConstraint : CONSTRAINT SP ON SP? '(' SP? variable nodeLabel SP? ')' SP? ASSERT SP '(' SP? propertyExpressions SP? ')' SP IS SP NODE SP KEY ; 353 | 354 | nodePropertyExistenceConstraint : CONSTRAINT SP ON SP? '(' variable nodeLabel ')' SP? ASSERT SP EXISTS SP? '(' propertyExpression ')' ; 355 | 356 | relationshipPropertyExistenceConstraint : CONSTRAINT SP ON SP? relationshipPatternSyntax SP? ASSERT SP EXISTS SP? '(' propertyExpression ')' ; 357 | 358 | relationshipPatternSyntax : ( '(' SP? ')' dash '[' variable relType ']' dash '(' SP? ')' ) 359 | | ( '(' SP? ')' dash '[' variable relType ']' dash rightArrowHead '(' SP? ')' ) 360 | | ( '(' SP? ')' leftArrowHead dash '[' variable relType ']' dash '(' SP? ')' ) 361 | ; 362 | 363 | loadCSVClause : LOAD SP CSV SP ( WITH SP HEADERS SP )? FROM SP expression SP AS SP variable SP ( FIELDTERMINATOR SP StringLiteral )? ; 364 | 365 | matchClause : ( OPTIONAL SP )? MATCH SP? pattern ( hint )* ( SP? where )? ; 366 | 367 | unwindClause : UNWIND SP? expression SP AS SP variable ; 368 | 369 | mergeClause : MERGE SP? patternPart ( SP mergeAction )* ; 370 | 371 | mergeAction : ( ON SP MATCH SP setClause ) 372 | | ( ON SP CREATE SP setClause ) 373 | ; 374 | 375 | createClause : CREATE SP? pattern ; 376 | 377 | createUniqueClause : CREATE SP UNIQUE SP? pattern ; 378 | 379 | setClause : SET SP? setItem ( SP? ',' SP? setItem )* ; 380 | 381 | setItem : ( propertyExpression SP? '=' SP? expression ) 382 | | ( variable SP? '=' SP? expression ) 383 | | ( variable SP? '+=' SP? expression ) 384 | | ( variable SP? nodeLabels ) 385 | ; 386 | 387 | deleteClause : ( DETACH SP )? DELETE SP? expression ( SP? ',' SP? expression )* ; 388 | 389 | removeClause : REMOVE SP removeItem ( SP? ',' SP? removeItem )* ; 390 | 391 | removeItem : ( variable nodeLabels ) 392 | | propertyExpression 393 | ; 394 | 395 | foreachClause : FOREACH SP? '(' SP? variable SP IN SP expression SP? '|' ( SP clause )+ SP? ')' ; 396 | 397 | withClause : WITH ( SP? DISTINCT )? SP returnBody ( SP? where )? ; 398 | 399 | returnClause : RETURN ( SP? DISTINCT )? SP? returnBody ; 400 | 401 | returnBody : returnItems ( SP order )? ( SP skip )? ( SP limit )? ; 402 | 403 | func : procedureInvocation SP? procedureResults? ; 404 | 405 | returnItems : ( '*' ( SP? ',' SP? returnItem )* ) 406 | | ( returnItem ( SP? ',' SP? returnItem )* ) 407 | | func 408 | ; 409 | 410 | returnItem : asMapping 411 | | expression 412 | ; 413 | 414 | asMapping: ( expression SP AS SP variable ); 415 | 416 | call : CALL SP procedureInvocation SP? procedureResults? ; 417 | 418 | subqueryClause : CALL SP? subquery ; 419 | 420 | subquery : '{' SP? query? SP? '}' ; 421 | 422 | procedureInvocation : procedureInvocationBody SP? procedureArguments? ; 423 | 424 | procedureInvocationBody : namespace procedureName ; 425 | 426 | procedureArguments : '(' SP? expression? ( SP? ',' SP? expression )* SP? ')' ; 427 | 428 | procedureResults : YIELD SP procedureResult ( SP? ',' SP? procedureResult )* (SP where)?; 429 | 430 | procedureResult : aliasedProcedureResult 431 | | simpleProcedureResult ; 432 | 433 | aliasedProcedureResult : procedureOutput SP AS SP variable ; 434 | 435 | simpleProcedureResult : procedureOutput ; 436 | 437 | procedureOutput : symbolicName ; 438 | 439 | order : ORDER SP BY SP sortItem ( SP? ',' SP? sortItem )* ; 440 | 441 | skip : L_SKIP SP expression ; 442 | 443 | limit : LIMIT SP expression ; 444 | 445 | sortItem : expression ( SP? ( ASCENDING | ASC | DESCENDING | DESC ) SP? )? ; 446 | 447 | hint : SP? ( ( USING SP INDEX SP variable nodeLabel SP? '(' SP? propertyKeys SP? ')' ) 448 | | ( USING SP JOIN SP ON SP variable ( SP? ',' SP? variable )* ) 449 | | ( USING SP SCAN SP variable nodeLabel ) ) ; 450 | 451 | startClause : START SP startPoint ( SP? ',' SP? startPoint )* where? ; 452 | 453 | startPoint : variable SP? '=' SP? lookup ; 454 | 455 | lookup : nodeLookup 456 | | relationshipLookup 457 | ; 458 | 459 | nodeLookup : NODE SP? ( identifiedIndexLookup | indexQuery | idLookup ) ; 460 | 461 | relationshipLookup : ( RELATIONSHIP | REL ) ( identifiedIndexLookup | indexQuery | idLookup ) ; 462 | 463 | identifiedIndexLookup : ':' symbolicName '(' symbolicName '=' ( StringLiteral | parameter ) ')' ; 464 | 465 | indexQuery : ':' symbolicName '(' ( StringLiteral | parameter ) ')' ; 466 | 467 | idLookup : '(' ( literalIds | parameter | '*' ) ')' ; 468 | 469 | literalIds : integerLiteral ( SP? ',' SP? integerLiteral )* ; 470 | 471 | where : WHERE SP expression ; 472 | 473 | pattern : patternPart ( SP? ',' SP? patternPart )* ; 474 | 475 | patternPart : ( variable SP? '=' SP? anonymousPatternPart ) 476 | | anonymousPatternPart 477 | ; 478 | 479 | anonymousPatternPart : shortestPathPatternFunction 480 | | patternElement 481 | ; 482 | 483 | patternElement : ( nodePattern ( SP? patternElementChain )* ) 484 | | ( '(' patternElement ')' ) 485 | ; 486 | 487 | nodePattern : '(' SP? ( variable SP? )? ( nodeLabels SP? )? ( properties SP? )? ')' ; 488 | 489 | patternElementChain : relationshipPattern SP? nodePattern ; 490 | 491 | relationshipPattern : relationshipPatternStart SP? relationshipDetail? SP? relationshipPatternEnd; 492 | 493 | relationshipPatternStart : ( leftArrowHead SP? dash ) 494 | | ( dash ) 495 | ; 496 | 497 | relationshipPatternEnd : ( dash SP? rightArrowHead ) 498 | | ( dash ) 499 | ; 500 | 501 | relationshipDetail : '[' SP? ( variable SP? )? ( relationshipTypes SP? )? rangeLiteral? ( properties SP? )? ']' ; 502 | 503 | properties : mapLiteral 504 | | parameter 505 | ; 506 | 507 | relType : ':' SP? relTypeName ; 508 | 509 | relationshipTypeTermAtom : parenthesizedRelationshipTypeTerm 510 | | relTypeName ; 511 | 512 | relationshipTypeTerm : relTypeName 513 | | orRelationshipTypeTerm 514 | | wildcardRelationshipType 515 | | parenthesizedRelationshipTypeTerm 516 | ; 517 | 518 | wildcardRelationshipType : '%' ; 519 | 520 | orRelationshipTypeTerm : andRelationshipTypeTerm ( SP? '|' SP? andRelationshipTypeTerm )* ; 521 | 522 | andRelationshipTypeTerm : notRelationshipTypeTerm ( SP? '&' SP? notRelationshipTypeTerm )* ; 523 | 524 | notRelationshipTypeTerm : inversionToken* SP? relationshipTypeTermAtom ; 525 | 526 | parenthesizedRelationshipTypeTerm : '(' SP? relationshipTypeTerm SP? ')' ; 527 | 528 | relationshipTypes : relationshipType ( SP? '|' relationshipTypeOptionalColon )* ; 529 | 530 | relationshipType : ':' relationshipTypeTerm ; 531 | 532 | inversionToken : '!' ; 533 | 534 | relationshipTypeOptionalColon : ':'? relTypeName ; 535 | 536 | nodeLabels : nodeLabel ( SP? nodeLabel )* ; 537 | 538 | nodeLabel : ':' labelTerm ; 539 | 540 | labelAtom : parenthesizedLabelTerm 541 | | labelName; 542 | 543 | labelTerm : labelName 544 | | orLabelTerm 545 | | wildcardLabel 546 | | parenthesizedLabelTerm 547 | ; 548 | 549 | wildcardLabel : '%' ; 550 | 551 | orLabelTerm : andLabelTerm ( SP? '|' SP? andLabelTerm )* ; 552 | 553 | andLabelTerm : notLabelTerm ( SP? '&' SP? notLabelTerm )* ; 554 | 555 | notLabelTerm : inversionToken* SP? labelAtom ; 556 | 557 | parenthesizedLabelTerm : '(' SP? labelTerm SP? ')' ; 558 | 559 | rangeLiteral : '*' SP? ( integerLiteral SP? )? ( '..' SP? ( integerLiteral SP? )? )? ; 560 | 561 | labelName : symbolicName ; 562 | 563 | relTypeName : symbolicName ; 564 | 565 | expression : orExpression; 566 | 567 | existsSubQuery : EXISTS SP? existsSubQueryBody; 568 | 569 | existsSubQueryBody: subquery | subqueryPatternBody ; 570 | 571 | subqueryPatternBody: '{' SP? pattern ( hint )* ( SP? where )? SP? '}' ; 572 | 573 | orExpression : xorExpression ( SP OR SP xorExpression )* ; 574 | 575 | xorExpression : andExpression ( SP XOR SP andExpression )* ; 576 | 577 | andExpression : notExpression ( SP AND SP notExpression )* ; 578 | 579 | notExpression : ( NOT SP? )* comparisonExpression ; 580 | 581 | comparisonExpression : addOrSubtractExpression ( SP? partialComparisonExpression )* ; 582 | 583 | addOrSubtractExpression : multiplyDivideModuloExpression ( ( SP? '+' SP? multiplyDivideModuloExpression ) | ( SP? '-' SP? multiplyDivideModuloExpression ) )* ; 584 | 585 | multiplyDivideModuloExpression : powerOfExpression ( ( SP? '*' SP? powerOfExpression ) | ( SP? '/' SP? powerOfExpression ) | ( SP? '%' SP? powerOfExpression ) )* ; 586 | 587 | powerOfExpression : unaryAddOrSubtractExpression ( SP? '^' SP? unaryAddOrSubtractExpression )* ; 588 | 589 | unaryAddOrSubtractExpression : ( ( '+' | '-' ) SP? )* stringListNullOperatorExpression ; 590 | 591 | stringListNullOperatorExpression : propertyOrLabelsExpression ( ( SP? '[' expression ']' ) | ( SP? '[' expression? '..' expression? ']' ) | ( ( ( SP? '=~' ) | ( SP IN ) | ( SP STARTS SP WITH ) | ( SP ENDS SP WITH ) | ( SP CONTAINS ) ) SP? propertyOrLabelsExpression ) | ( SP IS SP NULL ) | ( SP IS SP NOT SP NULL ) )* ; 592 | 593 | propertyOrLabelsExpression : atom ( SP? ( propertyLookup | nodeLabels ) )* ; 594 | 595 | filterFunction : ( filterFunctionName SP? '(' SP? filterExpression SP? ')' ) ; 596 | 597 | filterFunctionName : FILTER ; 598 | 599 | existsFunction : ( existsFunctionName SP? '(' SP? expression SP? ')' ) ; 600 | 601 | existsFunctionName: EXISTS ; 602 | 603 | allFunction : ( allFunctionName SP? '(' SP? filterExpression SP? ')' ) ; 604 | 605 | allFunctionName : ALL ; 606 | 607 | anyFunction : ( anyFunctionName SP? '(' SP? filterExpression SP? ')' ) ; 608 | 609 | anyFunctionName : ANY ; 610 | 611 | noneFunction : ( noneFunctionName SP? '(' SP? filterExpression SP? ')' ) ; 612 | 613 | noneFunctionName : NONE ; 614 | 615 | singleFunction : ( singleFunctionName SP? '(' SP? filterExpression SP? ')' ) ; 616 | 617 | singleFunctionName : SINGLE ; 618 | 619 | extractFunction : ( extractFunctionName SP? '(' SP? filterExpression ( SP? '|' SP? expression )? SP? ')' ) ; 620 | 621 | extractFunctionName : EXTRACT ; 622 | 623 | reduceFunction : ( reduceFunctionName SP? '(' SP? variable SP? '=' SP? expression SP? ',' SP? idInColl SP? '|' SP? expression SP? ')' ); 624 | 625 | reduceFunctionName : REDUCE ; 626 | 627 | shortestPathPatternFunction : ( shortestPathFunctionName SP? '(' SP? patternElement SP? ')' ) 628 | | ( allShortestPathFunctionName SP? '(' SP? patternElement SP? ')' ) 629 | ; 630 | 631 | shortestPathFunctionName : SHORTESTPATH ; 632 | 633 | allShortestPathFunctionName : ALLSHORTESTPATHS ; 634 | 635 | atom : literal 636 | | parameter 637 | | caseExpression 638 | | ( COUNT SP? '(' SP? '*' SP? ')' ) 639 | | listComprehension 640 | | patternComprehension 641 | | filterFunction 642 | | extractFunction 643 | | reduceFunction 644 | | allFunction 645 | | anyFunction 646 | | noneFunction 647 | | singleFunction 648 | | existsFunction 649 | | shortestPathPatternFunction 650 | | relationshipsPattern 651 | | parenthesizedExpression 652 | | functionInvocation 653 | | existsSubQuery 654 | | variable 655 | ; 656 | 657 | literal : numberLiteral 658 | | stringLiteral 659 | | booleanLiteral 660 | | NULL 661 | | mapLiteral 662 | | listLiteral 663 | | mapProjection 664 | ; 665 | 666 | stringLiteral : StringLiteral ; 667 | 668 | booleanLiteral : TRUE 669 | | FALSE 670 | ; 671 | 672 | listLiteral : '[' SP? ( expression SP? ( ',' SP? expression SP? )* )? ']' ; 673 | 674 | partialComparisonExpression : ( '=' SP? addOrSubtractExpression ) 675 | | ( '<>' SP? addOrSubtractExpression ) 676 | | ( '!=' SP? addOrSubtractExpression ) 677 | | ( '<' SP? addOrSubtractExpression ) 678 | | ( '>' SP? addOrSubtractExpression ) 679 | | ( '<=' SP? addOrSubtractExpression ) 680 | | ( '>=' SP? addOrSubtractExpression ) 681 | ; 682 | 683 | parenthesizedExpression : '(' SP? expression SP? ')' ; 684 | 685 | relationshipsPattern : nodePattern ( SP? patternElementChain )+ ; 686 | 687 | filterExpression : idInColl ( SP? where )? ; 688 | 689 | idInColl : variable SP IN SP expression ; 690 | 691 | functionInvocation : functionInvocationBody SP? '(' SP? ( DISTINCT SP? )? ( expression SP? ( ',' SP? expression SP? )* )? ')' ; 692 | 693 | functionInvocationBody : namespace functionName ; 694 | 695 | functionName : symbolicName ; 696 | 697 | procedureName : symbolicName ; 698 | 699 | listComprehension : '[' SP? filterExpression ( SP? '|' SP? expression )? SP? ']' ; 700 | 701 | patternComprehension : '[' SP? ( variable SP? '=' SP? )? relationshipsPattern SP? ( WHERE SP? expression SP? )? '|' SP? expression SP? ']' ; 702 | 703 | propertyLookup : '.' SP? ( propertyKeyName ) ; 704 | 705 | caseExpression : ( ( CASE ( SP? caseAlternatives )+ ) | ( CASE SP? expression ( SP? caseAlternatives )+ ) ) ( SP? ELSE SP? expression )? SP? END ; 706 | 707 | caseAlternatives : WHEN SP? expression SP? THEN SP? expression ; 708 | 709 | variable : symbolicName ; 710 | 711 | numberLiteral : doubleLiteral 712 | | integerLiteral 713 | ; 714 | 715 | mapLiteral : '{' SP? ( literalEntry SP? ( ',' SP? literalEntry SP? )* )? '}' ; 716 | 717 | mapProjection : variable SP? '{' SP? mapProjectionVariants? (SP? ',' SP? mapProjectionVariants )* SP? '}' ; 718 | 719 | mapProjectionVariants : ( literalEntry | propertySelector | variableSelector | allPropertiesSelector) ; 720 | 721 | literalEntry : propertyKeyName SP? ':' SP? expression ; 722 | 723 | propertySelector : '.' variable ; 724 | 725 | variableSelector : variable ; 726 | 727 | allPropertiesSelector : '.' '*' ; 728 | 729 | parameter : legacyParameter 730 | | newParameter 731 | ; 732 | 733 | legacyParameter : '{' SP? parameterName SP? '}' ; 734 | 735 | newParameter : '$' parameterName ; 736 | 737 | parameterName : symbolicName 738 | | DecimalInteger 739 | ; 740 | 741 | propertyExpressions : propertyExpression ( SP? ',' SP? propertyExpression )* ; 742 | 743 | propertyExpression : atom ( SP? propertyLookup )+ ; 744 | 745 | propertyKeys : propertyKeyName ( SP? ',' SP? propertyKeyName )* ; 746 | 747 | propertyKeyName: symbolicName; 748 | 749 | integerLiteral: HexInteger | OctalInteger | DecimalInteger; 750 | 751 | doubleLiteral: ExponentDecimalReal | RegularDecimalReal; 752 | 753 | namespace: ( symbolicName '.')*; 754 | 755 | leftArrowHead: '<' | '⟨' | '〈' | '﹤' | '<'; 756 | 757 | rightArrowHead: '>' | '⟩' | '〉' | '﹥' | '>'; 758 | 759 | dash: 760 | '-' 761 | | '­' 762 | | '‐' 763 | | '‑' 764 | | '‒' 765 | | '–' 766 | | '—' 767 | | '―' 768 | | '−' 769 | | '﹘' 770 | | '﹣' 771 | | '-'; 772 | 773 | symbolicName: 774 | UnescapedSymbolicName 775 | | EscapedSymbolicName 776 | | keyword 777 | | HexLetter 778 | ; 779 | 780 | keyword: CYPHER 781 | | EXPLAIN 782 | | PROFILE 783 | | USING 784 | | PERIODIC 785 | | COMMIT 786 | | UNION 787 | | ALL 788 | | CREATE 789 | | DROP 790 | | INDEX 791 | | ON 792 | | CONSTRAINT 793 | | ASSERT 794 | | IS 795 | | UNIQUE 796 | | EXISTS 797 | | LOAD 798 | | CSV 799 | | WITH 800 | | HEADERS 801 | | FROM 802 | | AS 803 | | FIELDTERMINATOR 804 | | OPTIONAL 805 | | MATCH 806 | | UNWIND 807 | | MERGE 808 | | SET 809 | | DETACH 810 | | DELETE 811 | | REMOVE 812 | | FOREACH 813 | | IN 814 | | DISTINCT 815 | | RETURN 816 | | ORDER 817 | | BY 818 | | L_SKIP 819 | | LIMIT 820 | | ASCENDING 821 | | ASC 822 | | DESCENDING 823 | | DESC 824 | | JOIN 825 | | SCAN 826 | | START 827 | | NODE 828 | | RELATIONSHIP 829 | | REL 830 | | WHERE 831 | | SHORTESTPATH 832 | | ALLSHORTESTPATHS 833 | | OR 834 | | XOR 835 | | AND 836 | | NOT 837 | | STARTS 838 | | ENDS 839 | | CONTAINS 840 | | NULL 841 | | COUNT 842 | | FILTER 843 | | EXTRACT 844 | | ANY 845 | | NONE 846 | | SINGLE 847 | | TRUE 848 | | FALSE 849 | | REDUCE 850 | | CASE 851 | | ELSE 852 | | END 853 | | WHEN 854 | | THEN 855 | | CALL 856 | | YIELD 857 | | KEY 858 | | CATALOG 859 | | SHOW 860 | | DEFAULT 861 | | DBMS 862 | | DATABASE 863 | | DATABASES 864 | | GRAPH 865 | | GRAPHS 866 | | REPLACE 867 | | IF 868 | | STOP 869 | | ROLE 870 | | ROLES 871 | | USER 872 | | USERS 873 | | POPULATED 874 | | PASSWORD 875 | | CHANGE 876 | | REQUIRED 877 | | STATUS 878 | | ACTIVE 879 | | SUSPENDED 880 | | ALTER 881 | | CURRENT 882 | | TO 883 | | PRIVILEGES 884 | | GRANT 885 | | DENY 886 | | REVOKE 887 | | RELATIONSHIPS 888 | | NODES 889 | | ELEMENT 890 | | ELEMENTS 891 | | COPY 892 | | OF 893 | | TRAVERSE 894 | | READ 895 | | WRITE 896 | | ACCESS 897 | | INDEXES 898 | | MANAGEMENT 899 | | NEW 900 | | LABEL 901 | | LABELS 902 | | NAME 903 | | NAMES 904 | | TYPE 905 | | TYPES 906 | | PROPERTY 907 | | CONSTRAINTS 908 | | ASSIGN 909 | | BTREE 910 | | EXIST 911 | | FOR 912 | | OPTIONS 913 | | EXECUTE 914 | | DEFINED 915 | | FUNCTION 916 | | FUNCTIONS 917 | | BOOSTED 918 | | PROCEDURE 919 | | PROCEDURES 920 | | ADMIN 921 | | ADMINISTRATOR 922 | | BRIEF 923 | | VERBOSE 924 | | OUTPUT; 925 | 926 | CYPHER: C Y P H E R; 927 | EXPLAIN: E X P L A I N; 928 | PROFILE: P R O F I L E; 929 | USING: U S I N G; 930 | PERIODIC: P E R I O D I C; 931 | COMMIT: C O M M I T; 932 | UNION: U N I O N; 933 | ALL: A L L; 934 | CREATE: C R E A T E; 935 | DROP: D R O P; 936 | INDEX: I N D E X; 937 | ON: O N; 938 | CONSTRAINT: C O N S T R A I N T; 939 | ASSERT: A S S E R T; 940 | IS: I S; 941 | UNIQUE: U N I Q U E; 942 | EXISTS: E X I S T S; 943 | LOAD: L O A D; 944 | CSV: C S V; 945 | WITH: W I T H; 946 | HEADERS: H E A D E R S; 947 | FROM: F R O M; 948 | AS: A S; 949 | FIELDTERMINATOR: F I E L D T E R M I N A T O R; 950 | OPTIONAL: O P T I O N A L; 951 | MATCH: M A T C H; 952 | UNWIND: U N W I N D; 953 | MERGE: M E R G E; 954 | SET: S E T; 955 | DETACH: D E T A C H; 956 | DELETE: D E L E T E; 957 | REMOVE: R E M O V E; 958 | FOREACH: F O R E A C H; 959 | IN: I N; 960 | DISTINCT: D I S T I N C T; 961 | RETURN: R E T U R N; 962 | ORDER: O R D E R; 963 | BY: B Y; 964 | L_SKIP: S K I P; 965 | LIMIT: L I M I T; 966 | ASCENDING: A S C E N D I N G; 967 | ASC: A S C; 968 | DESCENDING: D E S C E N D I N G; 969 | DESC: D E S C; 970 | JOIN: J O I N; 971 | SCAN: S C A N; 972 | START: S T A R T; 973 | NODE: N O D E; 974 | RELATIONSHIP: R E L A T I O N S H I P; 975 | REL: R E L; 976 | WHERE: W H E R E; 977 | SHORTESTPATH: S H O R T E S T P A T H; 978 | ALLSHORTESTPATHS: A L L S H O R T E S T P A T H S; 979 | OR: O R; 980 | XOR: X O R; 981 | AND: A N D; 982 | NOT: N O T; 983 | STARTS: S T A R T S; 984 | ENDS: E N D S; 985 | CONTAINS: C O N T A I N S; 986 | NULL: N U L L; 987 | COUNT: C O U N T; 988 | FILTER: F I L T E R; 989 | EXTRACT: E X T R A C T; 990 | ANY: A N Y; 991 | NONE: N O N E; 992 | SINGLE: S I N G L E; 993 | TRUE: T R U E; 994 | FALSE: F A L S E; 995 | REDUCE: R E D U C E; 996 | CASE: C A S E; 997 | ELSE: E L S E; 998 | END: E N D; 999 | WHEN: W H E N; 1000 | THEN: T H E N; 1001 | CALL: C A L L; 1002 | YIELD: Y I E L D; 1003 | KEY: K E Y; 1004 | CATALOG: C A T A L O G; 1005 | SHOW: S H O W; 1006 | DEFAULT: D E F A U L T; 1007 | DBMS: D B M S; 1008 | DATABASE: D A T A B A S E; 1009 | DATABASES: D A T A B A S E S; 1010 | GRAPH: G R A P H; 1011 | GRAPHS: G R A P H S; 1012 | REPLACE: R E P L A C E; 1013 | IF: I F; 1014 | STOP: S T O P; 1015 | ROLE: R O L E; 1016 | ROLES: R O L E S; 1017 | USER: U S E R; 1018 | USERS: U S E R S; 1019 | POPULATED: P O P U L A T E D; 1020 | PASSWORD: P A S S W O R D; 1021 | CHANGE: C H A N G E; 1022 | REQUIRED: R E Q U I R E D; 1023 | STATUS: S T A T U S; 1024 | ACTIVE: A C T I V E; 1025 | SUSPENDED: S U S P E N D E D; 1026 | ALTER: A L T E R; 1027 | CURRENT: C U R R E N T; 1028 | TO: T O; 1029 | PRIVILEGES: P R I V I L E G E S; 1030 | GRANT: G R A N T; 1031 | DENY: D E N Y; 1032 | REVOKE: R E V O K E; 1033 | RELATIONSHIPS: R E L A T I O N S H I P S; 1034 | NODES: N O D E S; 1035 | ELEMENT: E L E M E N T; 1036 | ELEMENTS: E L E M E N T S; 1037 | COPY: C O P Y; 1038 | OF: O F; 1039 | TRAVERSE: T R A V E R S E; 1040 | READ: R E A D; 1041 | WRITE: W R I T E; 1042 | ACCESS: A C C E S S; 1043 | INDEXES: I N D E X E S; 1044 | MANAGEMENT: M A N A G E M E N T; 1045 | NEW: N E W; 1046 | LABEL: L A B E L; 1047 | LABELS: L A B E L S; 1048 | NAME: N A M E; 1049 | NAMES: N A M E S; 1050 | TYPE: T Y P E; 1051 | TYPES: T Y P E S; 1052 | PROPERTY: P R O P E R T Y; 1053 | CONSTRAINTS: C O N S T R A I N T S; 1054 | ASSIGN: A S S I G N; 1055 | BTREE: B T R E E; 1056 | EXIST: E X I S T; 1057 | FOR: F O R ; 1058 | OPTIONS: O P T I O N S; 1059 | EXECUTE: E X E C U T E; 1060 | DEFINED: D E F I N E D; 1061 | FUNCTION: F U N C T I O N; 1062 | FUNCTIONS: F U N C T I O N S; 1063 | BOOSTED: B O O S T E D; 1064 | PROCEDURE: P R O C E D U R E; 1065 | PROCEDURES: P R O C E D U R E S; 1066 | ADMIN: A D M I N; 1067 | ADMINISTRATOR: A D M I N I S T R A T O R; 1068 | BRIEF: B R I E F; 1069 | VERBOSE: V E R B O S E; 1070 | OUTPUT: O U T P U T; 1071 | 1072 | StringLiteral: ('"' (~["\\] | '\\' (. | EOF))* '"') 1073 | | ( '\'' (~['\\] | '\\' (. | EOF))* '\''); 1074 | 1075 | //UrlString : ([a-zA-Z~0-9] | UrlHex) ([a-zA-Z0-9.-] | UrlHex)* ; 1076 | 1077 | UrlHex: ('%' [a-fA-F0-9] [a-fA-F0-9])+; 1078 | 1079 | //UrlDigits : (Digit)+ ; 1080 | 1081 | EscapedChar: 1082 | '\\' ( 1083 | '\\' 1084 | | '\'' 1085 | | '"' 1086 | | ( 'B' | 'b') 1087 | | ( 'F' | 'f') 1088 | | ( 'N' | 'n') 1089 | | ( 'R' | 'r') 1090 | | ( 'T' | 't') 1091 | | (( 'U' | 'u') ( HexDigit HexDigit HexDigit HexDigit)) 1092 | | ( 1093 | ('U' | 'u') ( 1094 | HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit HexDigit 1095 | ) 1096 | ) 1097 | ); 1098 | 1099 | HexInteger: '0x' ( HexDigit)+; 1100 | 1101 | DecimalInteger: ZeroDigit | ( NonZeroDigit ( Digit)*); 1102 | 1103 | OctalInteger: ZeroDigit ( OctDigit)+; 1104 | 1105 | HexLetter: ('A' | 'a') 1106 | | ( 'B' | 'b') 1107 | | ( 'C' | 'c') 1108 | | ( 'D' | 'd') 1109 | | ( 'E' | 'e') 1110 | | ( 'F' | 'f'); 1111 | 1112 | HexDigit: Digit | HexLetter; 1113 | 1114 | Digit: ZeroDigit | NonZeroDigit; 1115 | 1116 | NonZeroDigit: NonZeroOctDigit | '8' | '9'; 1117 | 1118 | NonZeroOctDigit: '1' | '2' | '3' | '4' | '5' | '6' | '7'; 1119 | 1120 | OctDigit: ZeroDigit | NonZeroOctDigit; 1121 | 1122 | ZeroDigit: '0'; 1123 | 1124 | ExponentDecimalReal: ( 1125 | ( Digit)+ 1126 | | ( ( Digit)+ '.' ( Digit)+) 1127 | | ( '.' ( Digit)+) 1128 | ) (( 'E' | 'e')) '-'? (Digit)+; 1129 | 1130 | RegularDecimalReal: ( Digit)* '.' ( Digit)+; 1131 | 1132 | UnescapedSymbolicName: IdentifierStart ( IdentifierPart)*; 1133 | 1134 | /** 1135 | * Based on the unicode identifier and pattern syntax (http://www.unicode.org/reports/tr31/) And 1136 | * extended with a few characters. 1137 | */ 1138 | IdentifierStart: 1139 | ID_Start 1140 | | '_' 1141 | | '‿' 1142 | | '⁀' 1143 | | '⁔' 1144 | | '︳' 1145 | | '︴' 1146 | | '﹍' 1147 | | '﹎' 1148 | | '﹏' 1149 | | '_'; 1150 | 1151 | /** 1152 | * Based on the unicode identifier and pattern syntax (http://www.unicode.org/reports/tr31/) And 1153 | * extended with a few characters. 1154 | */ 1155 | IdentifierPart: ID_Continue | Sc; 1156 | 1157 | /** 1158 | * Any character except "`", enclosed within `backticks`. Backticks are escaped with double 1159 | * backticks. 1160 | */ 1161 | EscapedSymbolicName: ( '`' (~[`] | '``')* '`'); 1162 | 1163 | SP: (WHITESPACE)+;// -> channel(HIDDEN); 1164 | 1165 | WHITESPACE: 1166 | SPACE 1167 | | TAB 1168 | | LF 1169 | | VT 1170 | | FF 1171 | | CR 1172 | | FS 1173 | | GS 1174 | | RS 1175 | | US 1176 | | ' ' 1177 | | '᠎' 1178 | | ' ' 1179 | | ' ' 1180 | | ' ' 1181 | | ' ' 1182 | | ' ' 1183 | | ' ' 1184 | | ' ' 1185 | | ' ' 1186 | | ' ' 1187 | | ' ' 1188 | | '
' 1189 | | '
' 1190 | | ' ' 1191 | | ' ' 1192 | | ' ' 1193 | | ' ' 1194 | | ' ' 1195 | | Comment; 1196 | 1197 | Comment: ( '/*' .*? '*/') | ( '//' ~[\r\n]*); 1198 | 1199 | ERROR_TOKEN: .; 1200 | 1201 | fragment FF: [\f]; 1202 | 1203 | fragment RS: [\u001E]; 1204 | 1205 | fragment ID_Continue: 1206 | [0-9A-Z_a-z\u00AA\u00B5\u00B7\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376-\u0377\u037A-\u037D\u0386-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0\u08A2-\u08AC\u08E4-\u08FE\u0900-\u0963\u0966-\u096F\u0971-\u0977\u0979-\u097F\u0981-\u0983\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7-\u09C8\u09CB-\u09CE\u09D7\u09DC-\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A3C\u0A3E-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47-\u0B48\u0B4B-\u0B4D\u0B56-\u0B57\u0B5C-\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82-\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C01-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56\u0C58-\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C82-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5-\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1-\u0CF2\u0D02-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82-\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DF2-\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81-\u0E82\u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18-\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1369-\u1371\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772-\u1773\u1780-\u17D3\u17D7\u17DC-\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191C\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1D00-\u1DE6\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F-\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA697\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAA7B\uAA80-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABEA\uABEC-\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE26\uFE33-\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC] 1207 | ; 1208 | 1209 | fragment GS: [\u001D]; 1210 | 1211 | fragment FS: [\u001C]; 1212 | 1213 | fragment CR: [\r]; 1214 | 1215 | fragment Sc: 1216 | [$\u00A2-\u00A5\u058F\u060B\u09F2-\u09F3\u09FB\u0AF1\u0BF9\u0E3F\u17DB\u20A0-\u20BA\uA838\uFDFC\uFE69\uFF04\uFFE0-\uFFE1\uFFE5-\uFFE6] 1217 | ; 1218 | 1219 | fragment SPACE: [ ]; 1220 | 1221 | fragment TAB: [\t]; 1222 | 1223 | fragment LF: [\n]; 1224 | 1225 | fragment VT: [\u000B]; 1226 | 1227 | fragment US: [\u001F]; 1228 | 1229 | fragment ID_Start: 1230 | [A-Za-z\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376-\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E-\u066F\u0671-\u06D3\u06D5\u06E5-\u06E6\u06EE-\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4-\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F-\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC-\u09DD\u09DF-\u09E1\u09F0-\u09F1\u0A05-\u0A0A\u0A0F-\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32-\u0A33\u0A35-\u0A36\u0A38-\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0-\u0AE1\u0B05-\u0B0C\u0B0F-\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B3D\u0B5C-\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99-\u0B9A\u0B9C\u0B9E-\u0B9F\u0BA3-\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58-\u0C59\u0C60-\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0-\u0CE1\u0CF1-\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32-\u0E33\u0E40-\u0E46\u0E81-\u0E82\u0E84\u0E87-\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA-\u0EAB\u0EAD-\u0EB0\u0EB2-\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065-\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F0\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE-\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5-\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2118-\u211D\u2124\u2126\u2128\u212A-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309B-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A-\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5-\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40-\uFB41\uFB43-\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC] 1231 | ; 1232 | 1233 | fragment A: [Aa]; 1234 | fragment B: [Bb]; 1235 | fragment C: [Cc]; 1236 | fragment D: [Dd]; 1237 | fragment E: [Ee]; 1238 | fragment F: [Ff]; 1239 | fragment G: [Gg]; 1240 | fragment H: [Hh]; 1241 | fragment I: [Ii]; 1242 | fragment J: [Jj]; 1243 | fragment K: [Kk]; 1244 | fragment L: [Ll]; 1245 | fragment M: [Mm]; 1246 | fragment N: [Nn]; 1247 | fragment O: [Oo]; 1248 | fragment P: [Pp]; 1249 | fragment Q: [Qq]; 1250 | fragment R: [Rr]; 1251 | fragment S: [Ss]; 1252 | fragment T: [Tt]; 1253 | fragment U: [Uu]; 1254 | fragment V: [Vv]; 1255 | fragment W: [Ww]; 1256 | fragment X: [Xx]; 1257 | fragment Y: [Yy]; 1258 | fragment Z: [Zz]; 1259 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/antlr/Cypher.tokens: -------------------------------------------------------------------------------- 1 | T__0=1 2 | T__1=2 3 | T__2=3 4 | T__3=4 5 | T__4=5 6 | T__5=6 7 | T__6=7 8 | T__7=8 9 | T__8=9 10 | T__9=10 11 | T__10=11 12 | T__11=12 13 | T__12=13 14 | T__13=14 15 | T__14=15 16 | T__15=16 17 | T__16=17 18 | T__17=18 19 | T__18=19 20 | T__19=20 21 | T__20=21 22 | T__21=22 23 | T__22=23 24 | T__23=24 25 | T__24=25 26 | T__25=26 27 | T__26=27 28 | T__27=28 29 | T__28=29 30 | T__29=30 31 | T__30=31 32 | T__31=32 33 | T__32=33 34 | T__33=34 35 | T__34=35 36 | T__35=36 37 | T__36=37 38 | T__37=38 39 | T__38=39 40 | T__39=40 41 | T__40=41 42 | T__41=42 43 | T__42=43 44 | T__43=44 45 | T__44=45 46 | T__45=46 47 | T__46=47 48 | T__47=48 49 | T__48=49 50 | T__49=50 51 | T__50=51 52 | T__51=52 53 | T__52=53 54 | T__53=54 55 | CYPHER=55 56 | EXPLAIN=56 57 | PROFILE=57 58 | USING=58 59 | PERIODIC=59 60 | COMMIT=60 61 | UNION=61 62 | ALL=62 63 | CREATE=63 64 | DROP=64 65 | INDEX=65 66 | ON=66 67 | CONSTRAINT=67 68 | ASSERT=68 69 | IS=69 70 | UNIQUE=70 71 | EXISTS=71 72 | LOAD=72 73 | CSV=73 74 | WITH=74 75 | HEADERS=75 76 | FROM=76 77 | AS=77 78 | FIELDTERMINATOR=78 79 | OPTIONAL=79 80 | MATCH=80 81 | UNWIND=81 82 | MERGE=82 83 | SET=83 84 | DETACH=84 85 | DELETE=85 86 | REMOVE=86 87 | FOREACH=87 88 | IN=88 89 | DISTINCT=89 90 | RETURN=90 91 | ORDER=91 92 | BY=92 93 | L_SKIP=93 94 | LIMIT=94 95 | ASCENDING=95 96 | ASC=96 97 | DESCENDING=97 98 | DESC=98 99 | JOIN=99 100 | SCAN=100 101 | START=101 102 | NODE=102 103 | RELATIONSHIP=103 104 | REL=104 105 | WHERE=105 106 | SHORTESTPATH=106 107 | ALLSHORTESTPATHS=107 108 | OR=108 109 | XOR=109 110 | AND=110 111 | NOT=111 112 | STARTS=112 113 | ENDS=113 114 | CONTAINS=114 115 | NULL=115 116 | COUNT=116 117 | FILTER=117 118 | EXTRACT=118 119 | ANY=119 120 | NONE=120 121 | SINGLE=121 122 | TRUE=122 123 | FALSE=123 124 | REDUCE=124 125 | CASE=125 126 | ELSE=126 127 | END=127 128 | WHEN=128 129 | THEN=129 130 | CALL=130 131 | YIELD=131 132 | KEY=132 133 | CATALOG=133 134 | SHOW=134 135 | DEFAULT=135 136 | DBMS=136 137 | DATABASE=137 138 | DATABASES=138 139 | GRAPH=139 140 | GRAPHS=140 141 | REPLACE=141 142 | IF=142 143 | STOP=143 144 | ROLE=144 145 | ROLES=145 146 | USER=146 147 | USERS=147 148 | POPULATED=148 149 | PASSWORD=149 150 | CHANGE=150 151 | REQUIRED=151 152 | STATUS=152 153 | ACTIVE=153 154 | SUSPENDED=154 155 | ALTER=155 156 | CURRENT=156 157 | TO=157 158 | PRIVILEGES=158 159 | GRANT=159 160 | DENY=160 161 | REVOKE=161 162 | RELATIONSHIPS=162 163 | NODES=163 164 | ELEMENT=164 165 | ELEMENTS=165 166 | COPY=166 167 | OF=167 168 | TRAVERSE=168 169 | READ=169 170 | WRITE=170 171 | ACCESS=171 172 | INDEXES=172 173 | MANAGEMENT=173 174 | NEW=174 175 | LABEL=175 176 | LABELS=176 177 | NAME=177 178 | NAMES=178 179 | TYPE=179 180 | TYPES=180 181 | PROPERTY=181 182 | CONSTRAINTS=182 183 | ASSIGN=183 184 | BTREE=184 185 | EXIST=185 186 | FOR=186 187 | OPTIONS=187 188 | EXECUTE=188 189 | DEFINED=189 190 | FUNCTION=190 191 | FUNCTIONS=191 192 | BOOSTED=192 193 | PROCEDURE=193 194 | PROCEDURES=194 195 | ADMIN=195 196 | ADMINISTRATOR=196 197 | BRIEF=197 198 | VERBOSE=198 199 | OUTPUT=199 200 | StringLiteral=200 201 | UrlHex=201 202 | EscapedChar=202 203 | HexInteger=203 204 | DecimalInteger=204 205 | OctalInteger=205 206 | HexLetter=206 207 | HexDigit=207 208 | Digit=208 209 | NonZeroDigit=209 210 | NonZeroOctDigit=210 211 | OctDigit=211 212 | ZeroDigit=212 213 | ExponentDecimalReal=213 214 | RegularDecimalReal=214 215 | UnescapedSymbolicName=215 216 | IdentifierStart=216 217 | IdentifierPart=217 218 | EscapedSymbolicName=218 219 | SP=219 220 | WHITESPACE=220 221 | Comment=221 222 | ERROR_TOKEN=222 223 | ';'=1 224 | ':'=2 225 | '-'=3 226 | '=>'=4 227 | '://'=5 228 | '/'=6 229 | '.'=7 230 | '@'=8 231 | '#'=9 232 | '?'=10 233 | '&'=11 234 | '='=12 235 | '+'=13 236 | '{'=14 237 | ','=15 238 | '}'=16 239 | '['=17 240 | ']'=18 241 | '*'=19 242 | '('=20 243 | ')'=21 244 | '+='=22 245 | '|'=23 246 | '%'=24 247 | '!'=25 248 | '..'=26 249 | '^'=27 250 | '=~'=28 251 | '<>'=29 252 | '!='=30 253 | '<'=31 254 | '>'=32 255 | '<='=33 256 | '>='=34 257 | '$'=35 258 | '⟨'=36 259 | '〈'=37 260 | '﹤'=38 261 | '<'=39 262 | '⟩'=40 263 | '〉'=41 264 | '﹥'=42 265 | '>'=43 266 | '­'=44 267 | '‐'=45 268 | '‑'=46 269 | '‒'=47 270 | '–'=48 271 | '—'=49 272 | '―'=50 273 | '−'=51 274 | '﹘'=52 275 | '﹣'=53 276 | '-'=54 277 | '0'=212 278 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/antlr/CypherLexer.tokens: -------------------------------------------------------------------------------- 1 | T__0=1 2 | T__1=2 3 | T__2=3 4 | T__3=4 5 | T__4=5 6 | T__5=6 7 | T__6=7 8 | T__7=8 9 | T__8=9 10 | T__9=10 11 | T__10=11 12 | T__11=12 13 | T__12=13 14 | T__13=14 15 | T__14=15 16 | T__15=16 17 | T__16=17 18 | T__17=18 19 | T__18=19 20 | T__19=20 21 | T__20=21 22 | T__21=22 23 | T__22=23 24 | T__23=24 25 | T__24=25 26 | T__25=26 27 | T__26=27 28 | T__27=28 29 | T__28=29 30 | T__29=30 31 | T__30=31 32 | T__31=32 33 | T__32=33 34 | T__33=34 35 | T__34=35 36 | T__35=36 37 | T__36=37 38 | T__37=38 39 | T__38=39 40 | T__39=40 41 | T__40=41 42 | T__41=42 43 | T__42=43 44 | T__43=44 45 | T__44=45 46 | T__45=46 47 | T__46=47 48 | T__47=48 49 | T__48=49 50 | T__49=50 51 | T__50=51 52 | T__51=52 53 | T__52=53 54 | T__53=54 55 | CYPHER=55 56 | EXPLAIN=56 57 | PROFILE=57 58 | USING=58 59 | PERIODIC=59 60 | COMMIT=60 61 | UNION=61 62 | ALL=62 63 | CREATE=63 64 | DROP=64 65 | INDEX=65 66 | ON=66 67 | CONSTRAINT=67 68 | ASSERT=68 69 | IS=69 70 | UNIQUE=70 71 | EXISTS=71 72 | LOAD=72 73 | CSV=73 74 | WITH=74 75 | HEADERS=75 76 | FROM=76 77 | AS=77 78 | FIELDTERMINATOR=78 79 | OPTIONAL=79 80 | MATCH=80 81 | UNWIND=81 82 | MERGE=82 83 | SET=83 84 | DETACH=84 85 | DELETE=85 86 | REMOVE=86 87 | FOREACH=87 88 | IN=88 89 | DISTINCT=89 90 | RETURN=90 91 | ORDER=91 92 | BY=92 93 | L_SKIP=93 94 | LIMIT=94 95 | ASCENDING=95 96 | ASC=96 97 | DESCENDING=97 98 | DESC=98 99 | JOIN=99 100 | SCAN=100 101 | START=101 102 | NODE=102 103 | RELATIONSHIP=103 104 | REL=104 105 | WHERE=105 106 | SHORTESTPATH=106 107 | ALLSHORTESTPATHS=107 108 | OR=108 109 | XOR=109 110 | AND=110 111 | NOT=111 112 | STARTS=112 113 | ENDS=113 114 | CONTAINS=114 115 | NULL=115 116 | COUNT=116 117 | FILTER=117 118 | EXTRACT=118 119 | ANY=119 120 | NONE=120 121 | SINGLE=121 122 | TRUE=122 123 | FALSE=123 124 | REDUCE=124 125 | CASE=125 126 | ELSE=126 127 | END=127 128 | WHEN=128 129 | THEN=129 130 | CALL=130 131 | YIELD=131 132 | KEY=132 133 | CATALOG=133 134 | SHOW=134 135 | DEFAULT=135 136 | DBMS=136 137 | DATABASE=137 138 | DATABASES=138 139 | GRAPH=139 140 | GRAPHS=140 141 | REPLACE=141 142 | IF=142 143 | STOP=143 144 | ROLE=144 145 | ROLES=145 146 | USER=146 147 | USERS=147 148 | POPULATED=148 149 | PASSWORD=149 150 | CHANGE=150 151 | REQUIRED=151 152 | STATUS=152 153 | ACTIVE=153 154 | SUSPENDED=154 155 | ALTER=155 156 | CURRENT=156 157 | TO=157 158 | PRIVILEGES=158 159 | GRANT=159 160 | DENY=160 161 | REVOKE=161 162 | RELATIONSHIPS=162 163 | NODES=163 164 | ELEMENT=164 165 | ELEMENTS=165 166 | COPY=166 167 | OF=167 168 | TRAVERSE=168 169 | READ=169 170 | WRITE=170 171 | ACCESS=171 172 | INDEXES=172 173 | MANAGEMENT=173 174 | NEW=174 175 | LABEL=175 176 | LABELS=176 177 | NAME=177 178 | NAMES=178 179 | TYPE=179 180 | TYPES=180 181 | PROPERTY=181 182 | CONSTRAINTS=182 183 | ASSIGN=183 184 | BTREE=184 185 | EXIST=185 186 | FOR=186 187 | OPTIONS=187 188 | EXECUTE=188 189 | DEFINED=189 190 | FUNCTION=190 191 | FUNCTIONS=191 192 | BOOSTED=192 193 | PROCEDURE=193 194 | PROCEDURES=194 195 | ADMIN=195 196 | ADMINISTRATOR=196 197 | BRIEF=197 198 | VERBOSE=198 199 | OUTPUT=199 200 | StringLiteral=200 201 | UrlHex=201 202 | EscapedChar=202 203 | HexInteger=203 204 | DecimalInteger=204 205 | OctalInteger=205 206 | HexLetter=206 207 | HexDigit=207 208 | Digit=208 209 | NonZeroDigit=209 210 | NonZeroOctDigit=210 211 | OctDigit=211 212 | ZeroDigit=212 213 | ExponentDecimalReal=213 214 | RegularDecimalReal=214 215 | UnescapedSymbolicName=215 216 | IdentifierStart=216 217 | IdentifierPart=217 218 | EscapedSymbolicName=218 219 | SP=219 220 | WHITESPACE=220 221 | Comment=221 222 | ERROR_TOKEN=222 223 | ';'=1 224 | ':'=2 225 | '-'=3 226 | '=>'=4 227 | '://'=5 228 | '/'=6 229 | '.'=7 230 | '@'=8 231 | '#'=9 232 | '?'=10 233 | '&'=11 234 | '='=12 235 | '+'=13 236 | '{'=14 237 | ','=15 238 | '}'=16 239 | '['=17 240 | ']'=18 241 | '*'=19 242 | '('=20 243 | ')'=21 244 | '+='=22 245 | '|'=23 246 | '%'=24 247 | '!'=25 248 | '..'=26 249 | '^'=27 250 | '=~'=28 251 | '<>'=29 252 | '!='=30 253 | '<'=31 254 | '>'=32 255 | '<='=33 256 | '>='=34 257 | '$'=35 258 | '⟨'=36 259 | '〈'=37 260 | '﹤'=38 261 | '<'=39 262 | '⟩'=40 263 | '〉'=41 264 | '﹥'=42 265 | '>'=43 266 | '­'=44 267 | '‐'=45 268 | '‑'=46 269 | '‒'=47 270 | '–'=48 271 | '—'=49 272 | '―'=50 273 | '−'=51 274 | '﹘'=52 275 | '﹣'=53 276 | '-'=54 277 | '0'=212 278 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/index.ts: -------------------------------------------------------------------------------- 1 | export { validate } from "./validate"; 2 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/labelvalidators.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AndLabelTermContext, 3 | AndRelationshipTypeTermContext, 4 | NodeLabelsContext, 5 | NotLabelTermContext, 6 | NotRelationshipTypeTermContext, 7 | OrLabelTermContext, 8 | OrRelationshipTypeTermContext, 9 | ParenthesizedLabelTermContext, 10 | ParenthesizedRelationshipTypeTermContext, 11 | SymbolicNameContext, 12 | WildcardLabelContext, 13 | } from "./antlr/CypherParser"; 14 | import { ParserRuleContext, ParseTreeWalker } from "antlr4"; 15 | import CypherListener from "./antlr/CypherListener"; 16 | import { NameRetriever } from "./NameRetriever"; 17 | 18 | export type Validator = (labelOrType: string) => boolean; 19 | 20 | function labeledValidator(validator: Validator, label: string): Validator { 21 | validator.toString = () => label; 22 | return validator; 23 | } 24 | 25 | export const ANY_LABEL_VALIDATOR: Validator = labeledValidator(() => true, "*"); 26 | 27 | class ValidatorCreatorBase extends CypherListener { 28 | private validatorStack: Validator[][] = [[]]; 29 | 30 | protected pushStack(): Validator[] { 31 | const frame: Validator[] = []; 32 | this.validatorStack.push(frame); 33 | return frame; 34 | } 35 | 36 | protected popStack(): Validator[] { 37 | return this.validatorStack.pop()!; 38 | } 39 | 40 | protected frame(): Validator[] { 41 | return this.validatorStack.at(-1)!; 42 | } 43 | 44 | protected andValidator() { 45 | const oldValidations = this.popStack(); 46 | if (oldValidations.length > 1) { 47 | const andValidator: Validator = (label) => 48 | oldValidations.every((validator) => validator(label)); 49 | const orValidator: Validator = (label) => 50 | oldValidations.some((validator) => validator(label)); 51 | 52 | // If there is an *and* label, we don't have that in the schema, so we accept it if one of them matches... 53 | // we basically treat *and*s as *or*s, which is of course incorrect, but requested by the challenge 54 | // also the validator does not accept arrays of labels for the schema. 55 | // the validator interface could/should be improved to accept arrays of labels/types. 56 | const challengeQuirk = true; 57 | 58 | this.frame().push( 59 | labeledValidator( 60 | challengeQuirk ? orValidator : andValidator, 61 | oldValidations.map((v) => v.toString()).join("&"), 62 | ), 63 | ); 64 | } else { 65 | this.frame().push(...oldValidations); 66 | } 67 | } 68 | 69 | protected orValidator() { 70 | const oldValidations = this.popStack(); 71 | if (oldValidations.length > 1) { 72 | const orValidator: Validator = (label) => 73 | oldValidations.some((validator) => validator(label)); 74 | this.frame().push( 75 | labeledValidator( 76 | orValidator, 77 | oldValidations.map((v) => v.toString()).join("|"), 78 | ), 79 | ); 80 | } else { 81 | this.frame().push(...oldValidations); 82 | } 83 | } 84 | 85 | protected wildCardValidator() { 86 | this.frame().push(ANY_LABEL_VALIDATOR); 87 | } 88 | 89 | protected parenthesizedValidator() { 90 | const currentStack = this.popStack(); 91 | console.assert(currentStack.length === 1); 92 | const currentValidator = currentStack[0]; 93 | 94 | this.frame().push( 95 | labeledValidator( 96 | (s) => currentValidator(s), 97 | `(${currentValidator.toString()})`, 98 | ), 99 | ); 100 | } 101 | 102 | protected inverseValidator(inversions: number | undefined) { 103 | const validator = this.popStack()[0]; 104 | if (typeof inversions !== "undefined" && inversions > 0 && inversions % 2 === 1) { 105 | this.frame().push( 106 | labeledValidator( 107 | (label) => !validator(label), 108 | "!" + validator.toString(), 109 | ), 110 | ); 111 | } else { 112 | this.frame().push(validator); 113 | } 114 | } 115 | 116 | exitSymbolicName: (ctx: SymbolicNameContext) => void = (ctx) => { 117 | const name = NameRetriever.getName(ctx); 118 | if (name) { 119 | this.frame().push( 120 | labeledValidator( 121 | (label) => label.toLowerCase() === name.toLowerCase(), 122 | name, 123 | ), 124 | ); 125 | } 126 | }; 127 | 128 | get validator(): Validator { 129 | return this.frame()[0] ?? ANY_LABEL_VALIDATOR; 130 | } 131 | } 132 | 133 | export class NodeLabelValidator extends ValidatorCreatorBase { 134 | enterOrLabelTerm: (ctx: OrLabelTermContext) => void = () => { 135 | this.pushStack(); 136 | }; 137 | 138 | enterAndLabelTerm: (ctx: AndLabelTermContext) => void = () => { 139 | this.pushStack(); 140 | }; 141 | 142 | enterNodeLabels: (ctx: NodeLabelsContext) => void = () => { 143 | this.pushStack(); 144 | }; 145 | 146 | enterNotLabelTerm: (ctx: NotLabelTermContext) => void = (ctx) => { 147 | this.pushStack(); 148 | }; 149 | 150 | enterParenthesizedLabelTerm: (ctx: ParenthesizedLabelTermContext) => void = 151 | () => { 152 | this.pushStack(); 153 | }; 154 | 155 | exitOrLabelTerm: (ctx: OrLabelTermContext) => void = () => { 156 | this.orValidator(); 157 | }; 158 | 159 | exitAndLabelTerm: (ctx: AndLabelTermContext) => void = () => { 160 | this.andValidator(); 161 | }; 162 | 163 | exitWildcardLabel: (ctx: WildcardLabelContext) => void = () => { 164 | this.wildCardValidator(); 165 | }; 166 | 167 | exitNotLabelTerm: (ctx: NotLabelTermContext) => void = (ctx) => { 168 | this.inverseValidator(ctx.inversionToken_list()?.length); 169 | }; 170 | 171 | exitNodeLabels: (ctx: NodeLabelsContext) => void = () => { 172 | this.andValidator(); 173 | }; 174 | 175 | exitParenthesizedLabelTerm: (ctx: ParenthesizedLabelTermContext) => void = 176 | () => { 177 | this.parenthesizedValidator(); 178 | }; 179 | 180 | static getValidator(root?: ParserRuleContext): Validator { 181 | if (root) { 182 | const validatorWalker = new NodeLabelValidator(); 183 | ParseTreeWalker.DEFAULT.walk(validatorWalker, root); 184 | return validatorWalker.validator; 185 | } else { 186 | return ANY_LABEL_VALIDATOR; 187 | } 188 | } 189 | } 190 | 191 | export class RelationshipTypeValidator extends ValidatorCreatorBase { 192 | enterOrRelationshipTypeTerm: (ctx: OrRelationshipTypeTermContext) => void = 193 | () => { 194 | this.pushStack(); 195 | }; 196 | 197 | enterAndRelationshipTypeTerm: (ctx: AndRelationshipTypeTermContext) => void = 198 | () => { 199 | this.pushStack(); 200 | }; 201 | 202 | enterNotRelationshipTypeTerm: (ctx: NotRelationshipTypeTermContext) => void = 203 | (ctx) => { 204 | this.pushStack(); 205 | }; 206 | 207 | enterParenthesizedRelationshipTypeTerm: ( 208 | ctx: ParenthesizedRelationshipTypeTermContext, 209 | ) => void = () => { 210 | this.pushStack(); 211 | }; 212 | 213 | exitOrRelationshipTypeTerm: (ctx: OrRelationshipTypeTermContext) => void = 214 | () => { 215 | this.orValidator(); 216 | }; 217 | 218 | exitAndRelationshipTypeTerm: (ctx: AndRelationshipTypeTermContext) => void = 219 | () => { 220 | this.andValidator(); 221 | }; 222 | 223 | exitWildcardLabel: (ctx: WildcardLabelContext) => void = () => { 224 | this.wildCardValidator(); 225 | }; 226 | 227 | exitNotRelationshipTypeTerm: (ctx: NotRelationshipTypeTermContext) => void = ( 228 | ctx, 229 | ) => { 230 | this.inverseValidator(ctx.inversionToken_list()?.length); 231 | }; 232 | 233 | exitParenthesizedRelationshipTypeTerm: ( 234 | ctx: ParenthesizedRelationshipTypeTermContext, 235 | ) => void = () => { 236 | this.parenthesizedValidator(); 237 | }; 238 | 239 | static getValidator(root?: ParserRuleContext): Validator { 240 | if (root) { 241 | const validatorWalker = new RelationshipTypeValidator(); 242 | ParseTreeWalker.DEFAULT.walk(validatorWalker, root); 243 | return validatorWalker.validator; 244 | } else { 245 | return ANY_LABEL_VALIDATOR; 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/custom/cypher-query-validator/validate.ts: -------------------------------------------------------------------------------- 1 | import CypherParser, { 2 | CypherContext, 3 | NodePatternContext, 4 | PatternElementContext, 5 | RelationshipPatternContext, 6 | RelationshipsPatternContext, 7 | SubqueryClauseContext, 8 | WithClauseContext, 9 | } from "./antlr/CypherParser"; 10 | import CypherLexer from "./antlr/CypherLexer"; 11 | import { CharStream, CommonTokenStream, ParseTreeWalker } from "antlr4"; 12 | import CypherListener from "./antlr/CypherListener"; 13 | import { NameRetriever } from "./NameRetriever"; 14 | import { 15 | ANY_LABEL_VALIDATOR, 16 | Validator, 17 | NodeLabelValidator, 18 | RelationshipTypeValidator, 19 | } from "./labelvalidators"; 20 | import { Binding, BindingCollector } from "./BindingCollector"; 21 | 22 | type NodeChainElement = { 23 | type: "node"; 24 | nodeLabelValidator: Validator; 25 | }; 26 | 27 | type RelationshipDirection = "LEFT" | "RIGHT" | "UNDIRECTED"; 28 | 29 | type RelChainElement = { 30 | type: "relation"; 31 | relType: RelationshipDirection; 32 | relationshipTypeValidator: Validator; 33 | ctx: RelationshipPatternContext; 34 | }; 35 | 36 | type Scope = { bindings: Binding[] }; 37 | 38 | class PatternFixer extends CypherListener { 39 | private chains: (NodeChainElement | RelChainElement)[][] = []; 40 | public invalid = false; 41 | public diagnostics: string[] = []; 42 | private scopes: Scope[] = []; 43 | 44 | constructor( 45 | private schema: RelationshipTuple[], 46 | public cypher: string, 47 | ) { 48 | super(); 49 | } 50 | 51 | enterCypher: (ctx: CypherContext) => void = () => this.startNewScope(false); 52 | exitCypher: (ctx: CypherContext) => void = () => this.exitScope(); 53 | 54 | enterPatternElement: (ctx: PatternElementContext) => void = () => 55 | this.startNewPattern(); 56 | enterRelationshipsPattern: (ctx: RelationshipsPatternContext) => void = () => 57 | this.startNewPattern(); 58 | 59 | exitRelationshipsPattern: (ctx: RelationshipsPatternContext) => void = () => 60 | this.fixPatterns(); 61 | exitPatternElement: (ctx: PatternElementContext) => void = () => 62 | this.fixPatterns(); 63 | 64 | startNewPattern() { 65 | this.chains.push([]); 66 | } 67 | 68 | startNewScope(inherit: boolean = true) { 69 | this.scopes.push({ 70 | bindings: inherit ? [...this.currentScope().bindings] : [], 71 | }); 72 | } 73 | 74 | private currentScope(): Scope { 75 | if (this.scopes.length === 0) { 76 | throw new Error("Not initialized"); 77 | } 78 | return this.scopes.at(-1)!; 79 | } 80 | 81 | exitScope() { 82 | this.scopes.pop(); 83 | } 84 | 85 | registerVariable(name: string, types: Validator) { 86 | const scope = this.currentScope(); 87 | scope.bindings.push({ name, types }); 88 | } 89 | 90 | resolve(variable: string): Validator { 91 | const scope = this.currentScope(); 92 | const binding = scope.bindings.find((b) => b.name === variable); 93 | if (binding && binding.types) { 94 | return binding.types; 95 | } 96 | // new variable - matches anything 97 | return ANY_LABEL_VALIDATOR; 98 | } 99 | 100 | notFound(message?: string): void { 101 | this.invalid = true; 102 | if (message) { 103 | this.diagnostics.push("Not Found " + message); 104 | } 105 | } 106 | 107 | enterSubqueryClause: (ctx: SubqueryClauseContext) => void = () => 108 | this.startNewScope(); 109 | 110 | exitSubqueryClause: (ctx: SubqueryClauseContext) => void = () => 111 | this.exitScope(); 112 | 113 | fixPatterns() { 114 | const chain = this.chains.pop()!; 115 | 116 | function schemaFits( 117 | schema: RelationshipTuple, 118 | src: NodeChainElement, 119 | relation: RelChainElement, 120 | target: NodeChainElement, 121 | ) { 122 | return ( 123 | src.nodeLabelValidator(schema.sourceLabel) && 124 | target.nodeLabelValidator(schema.targetLabel) && 125 | relation.relationshipTypeValidator(schema.relationshipType) 126 | ); 127 | } 128 | 129 | // we start from 1 130 | for (let i = 1; i < chain.length; i++) { 131 | const chainElement = chain[i]; 132 | if (chainElement.type === "relation") { 133 | const src = chain[i - 1] as NodeChainElement; 134 | const relation = chainElement; 135 | const target = chain[i + 1] as NodeChainElement; 136 | const rightExists = this.schema.find((value) => 137 | schemaFits(value, src, relation, target), 138 | ); 139 | const leftExists = this.schema.find((value) => 140 | schemaFits(value, target, relation, src), 141 | ); 142 | /* 143 | console.log( 144 | `(:${src.nodeLabelValidator})-[:${relation.relationshipTypeValidator}]-(:${target.nodeLabelValidator})`, 145 | ); 146 | */ 147 | if (relation.relType === "RIGHT") { 148 | if (!rightExists) { 149 | if (leftExists) { 150 | const from = relation.ctx.relationshipPatternStart().start.start; 151 | const to = relation.ctx.relationshipPatternEnd().start.start; 152 | this.cypher = 153 | this.cypher.substring(0, from) + 154 | "<" + 155 | this.cypher.substring(from, to + 1) + 156 | "" + 157 | this.cypher.substring(to + 2); 158 | } else { 159 | this.notFound( 160 | `(:${src.nodeLabelValidator})-[:${relation.relationshipTypeValidator}]->(:${target.nodeLabelValidator})`, 161 | ); 162 | } 163 | } 164 | } else if (relation.relType === "LEFT") { 165 | if (!leftExists) { 166 | if (rightExists) { 167 | const from = relation.ctx.relationshipPatternStart().start.start; 168 | const to = relation.ctx.relationshipPatternEnd().start.start; 169 | this.cypher = 170 | this.cypher.substring(0, from) + 171 | "" + 172 | this.cypher.substring(from + 1, to + 1) + 173 | ">" + 174 | this.cypher.substring(to + 1); 175 | } else { 176 | this.notFound( 177 | `(:${src.nodeLabelValidator})<-[:${relation.relationshipTypeValidator}]-(:${target.nodeLabelValidator})`, 178 | ); 179 | } 180 | } 181 | } else if (relation.relType === "UNDIRECTED") { 182 | if (!(rightExists || leftExists)) { 183 | this.notFound( 184 | `(:${src.nodeLabelValidator})-[:${relation.relationshipTypeValidator}]-(:${target.nodeLabelValidator})`, 185 | ); 186 | } 187 | } 188 | } 189 | } 190 | } 191 | 192 | enterWithClause: (ctx: WithClauseContext) => void = (ctx) => { 193 | const bindings = BindingCollector.getBindings( 194 | (variable) => this.resolve(variable), 195 | this.currentScope().bindings, 196 | ctx, 197 | ); 198 | this.startNewScope(false); 199 | if (bindings) { 200 | bindings.forEach((b) => this.registerVariable(b.name, b.types)); 201 | } 202 | }; 203 | exitWithClause: (ctx: WithClauseContext) => void = () => { 204 | // Nothing to do - with creates a chain of contexts 205 | }; 206 | 207 | enterNodePattern: (ctx: NodePatternContext) => void = (ctx) => { 208 | const currentChain = this.currentChain(); 209 | 210 | const nodeLabels = this.getNodeLabels(ctx); 211 | 212 | let name: string | undefined = undefined; 213 | if (ctx.variable()) { 214 | name = NameRetriever.getName(ctx.variable()); 215 | } 216 | if (name && nodeLabels.length > 0) { 217 | this.registerVariable(name, nodeLabels); 218 | } 219 | currentChain.push({ 220 | type: "node", 221 | nodeLabelValidator: nodeLabels, 222 | }); 223 | }; 224 | 225 | private getNodeLabels(ctx: NodePatternContext): Validator { 226 | const name = NameRetriever.getName(ctx.variable()); 227 | if (!ctx.nodeLabels() && name) { 228 | return this.resolve(name); 229 | } 230 | 231 | return NodeLabelValidator.getValidator(ctx.nodeLabels()); 232 | } 233 | 234 | enterRelationshipPattern: (ctx: RelationshipPatternContext) => void = ( 235 | ctx, 236 | ) => { 237 | const relType: RelationshipDirection = ctx 238 | .relationshipPatternStart() 239 | .leftArrowHead() 240 | ? "LEFT" 241 | : ctx.relationshipPatternEnd().rightArrowHead() 242 | ? "RIGHT" 243 | : "UNDIRECTED"; 244 | 245 | const relationshipTypeValidator = this.getRelationshipTypeValidator(ctx); 246 | 247 | if (ctx.relationshipDetail()?.variable()) { 248 | const name = NameRetriever.getName(ctx.relationshipDetail()!.variable()); 249 | if (name) { 250 | this.registerVariable(name, relationshipTypeValidator); 251 | } 252 | } 253 | 254 | const currentChain = this.currentChain(); 255 | currentChain.push({ 256 | type: "relation", 257 | relationshipTypeValidator, 258 | relType, 259 | ctx, 260 | }); 261 | }; 262 | 263 | private currentChain() { 264 | if (this.chains.length === 0) { 265 | throw new Error("Bad state!"); 266 | } 267 | return this.chains.at(-1)!; 268 | } 269 | 270 | private getRelationshipTypeValidator( 271 | ctx: RelationshipPatternContext, 272 | ): Validator { 273 | const name = NameRetriever.getName(ctx.relationshipDetail()?.variable()); 274 | if (name && !ctx.relationshipDetail()?.relationshipTypes()) { 275 | return this.resolve(name); 276 | } 277 | // special contest rule: ignore variable length patterns and stop validation, immediately !? 278 | if (ctx.relationshipDetail()?.rangeLiteral()) { 279 | return ANY_LABEL_VALIDATOR; 280 | } 281 | if (ctx.relationshipDetail()?.relationshipTypes()) { 282 | return RelationshipTypeValidator.getValidator( 283 | ctx.relationshipDetail().relationshipTypes(), 284 | ); 285 | } else { 286 | return ANY_LABEL_VALIDATOR; 287 | } 288 | } 289 | } 290 | 291 | export type RelationshipTuple = { 292 | sourceLabel: string; 293 | relationshipType: string; 294 | targetLabel: string; 295 | }; 296 | 297 | export function validate(cypher: string, schema: RelationshipTuple[]) { 298 | const stream = new CharStream(cypher); 299 | 300 | const lexer = new CypherLexer(stream); 301 | const tokens = new CommonTokenStream(lexer); 302 | const parser = new CypherParser(tokens); 303 | 304 | const tree: CypherContext = parser.cypher(); 305 | if (parser.syntaxErrorsCount > 0) { 306 | return ""; 307 | } 308 | 309 | const patternFixer = new PatternFixer(schema, cypher); 310 | ParseTreeWalker.DEFAULT.walk(patternFixer, tree); 311 | 312 | if (patternFixer.diagnostics.length > 0) { 313 | console.log(patternFixer.diagnostics.join("\n")); 314 | } 315 | 316 | return patternFixer.invalid ? "" : patternFixer.cypher; 317 | } 318 | -------------------------------------------------------------------------------- /src/custom/graph-cypher-qa-chain/chain.ts: -------------------------------------------------------------------------------- 1 | import { ChainValues } from "@langchain/core/utils/types"; 2 | import { BasePromptTemplate } from "@langchain/core/prompts"; 3 | import { CallbackManagerForChainRun } from "@langchain/core/callbacks/manager"; 4 | import { CYPHER_GENERATION_PROMPT, CYPHER_QA_PROMPT } from "./prompts"; 5 | import { BaseChain, ChainInputs, LLMChain } from "langchain/chains"; 6 | 7 | import { Neo4jGraph } from "langchain/graphs/neo4j_graph"; 8 | import { validate } from "../cypher-query-validator"; 9 | 10 | export const INTERMEDIATE_STEPS_KEY = "intermediateSteps"; 11 | 12 | export interface GraphCypherQAChainInput extends ChainInputs { 13 | graph: Neo4jGraph; 14 | cypherGenerationChain: LLMChain; 15 | qaChain: LLMChain; 16 | inputKey?: string; 17 | outputKey?: string; 18 | topK?: number; 19 | returnIntermediateSteps?: boolean; 20 | returnDirect?: boolean; 21 | } 22 | 23 | export interface FromLLMInput { 24 | graph: Neo4jGraph; 25 | llm?: any; 26 | cypherLLM?: any; 27 | qaLLM?: any; 28 | qaPrompt?: BasePromptTemplate; 29 | cypherPrompt?: BasePromptTemplate; 30 | returnIntermediateSteps?: boolean; 31 | returnDirect?: boolean; 32 | } 33 | 34 | /** 35 | * @example 36 | * ```typescript 37 | * const chain = new GraphCypherQAChain({ 38 | * llm: new ChatOpenAI({ temperature: 0 }), 39 | * graph: new Neo4jGraph(), 40 | * }); 41 | * const res = await chain.run("Who played in Pulp Fiction?"); 42 | * ``` 43 | */ 44 | export class GraphCypherQAChain extends BaseChain { 45 | private graph: Neo4jGraph; 46 | 47 | private cypherGenerationChain: LLMChain; 48 | 49 | private qaChain: LLMChain; 50 | 51 | private inputKey = "query"; 52 | 53 | private outputKey = "result"; 54 | 55 | private topK = 10; 56 | 57 | private returnDirect = false; 58 | 59 | private returnIntermediateSteps = false; 60 | 61 | constructor(props: GraphCypherQAChainInput) { 62 | super(props); 63 | const { 64 | graph, 65 | cypherGenerationChain, 66 | qaChain, 67 | inputKey, 68 | outputKey, 69 | topK, 70 | returnIntermediateSteps, 71 | returnDirect, 72 | } = props; 73 | 74 | this.graph = graph; 75 | this.cypherGenerationChain = cypherGenerationChain; 76 | this.qaChain = qaChain; 77 | 78 | if (inputKey) { 79 | this.inputKey = inputKey; 80 | } 81 | if (outputKey) { 82 | this.outputKey = outputKey; 83 | } 84 | if (topK) { 85 | this.topK = topK; 86 | } 87 | if (returnIntermediateSteps) { 88 | this.returnIntermediateSteps = returnIntermediateSteps; 89 | } 90 | if (returnDirect) { 91 | this.returnDirect = returnDirect; 92 | } 93 | } 94 | 95 | _chainType() { 96 | return "graph_cypher_chain" as const; 97 | } 98 | 99 | get inputKeys(): string[] { 100 | return [this.inputKey]; 101 | } 102 | 103 | get outputKeys(): string[] { 104 | return [this.outputKey]; 105 | } 106 | 107 | static fromLLM(props: FromLLMInput): GraphCypherQAChain { 108 | const { 109 | graph, 110 | qaPrompt = CYPHER_QA_PROMPT, 111 | cypherPrompt = CYPHER_GENERATION_PROMPT, 112 | llm, 113 | cypherLLM, 114 | qaLLM, 115 | returnIntermediateSteps = false, 116 | returnDirect = false, 117 | } = props; 118 | 119 | if (!cypherLLM && !llm) { 120 | throw new Error( 121 | "Either 'llm' or 'cypherLLM' parameters must be provided" 122 | ); 123 | } 124 | 125 | if (!qaLLM && !llm) { 126 | throw new Error("Either 'llm' or 'qaLLM' parameters must be provided"); 127 | } 128 | 129 | if (cypherLLM && qaLLM && llm) { 130 | throw new Error( 131 | "You can specify up to two of 'cypherLLM', 'qaLLM', and 'llm', but not all three simultaneously." 132 | ); 133 | } 134 | 135 | const qaChain = new LLMChain({ 136 | llm: (qaLLM || llm) as any, 137 | prompt: qaPrompt, 138 | }); 139 | 140 | const cypherGenerationChain = new LLMChain({ 141 | llm: (cypherLLM || llm) as any, 142 | prompt: cypherPrompt, 143 | }); 144 | 145 | return new GraphCypherQAChain({ 146 | cypherGenerationChain, 147 | qaChain, 148 | graph, 149 | returnIntermediateSteps, 150 | returnDirect, 151 | }); 152 | } 153 | 154 | private extractCypher(text: string): string { 155 | const regex = /```cypher\n([\s\S]*?)```/; 156 | const match = text.match(regex); 157 | return match ? match[1].trim() : null; 158 | } 159 | 160 | async _call( 161 | values: ChainValues, 162 | runManager?: CallbackManagerForChainRun 163 | ): Promise { 164 | const callbacks = runManager?.getChild(); 165 | const question = values[this.inputKey]; 166 | 167 | const intermediateSteps = []; 168 | 169 | const generatedCypher = await this.cypherGenerationChain.call( 170 | { question, schema: this.graph.getSchema() }, 171 | callbacks 172 | ); 173 | 174 | let extractedCypher = this.extractCypher(generatedCypher.text); 175 | 176 | if (extractedCypher !== null) { 177 | const structuredSchema = this.graph 178 | .getStructuredSchema() 179 | .relationships.map(({ start, type, end }) => { 180 | return { 181 | sourceLabel: start, 182 | relationshipType: type, 183 | targetLabel: end, 184 | }; 185 | }); 186 | const validatedCypher = validate(extractedCypher, structuredSchema); 187 | 188 | if (validatedCypher) { 189 | extractedCypher = validatedCypher; 190 | } 191 | } 192 | 193 | await runManager?.handleText(`Generated Cypher:\n`); 194 | await runManager?.handleText(`${extractedCypher} green\n`); 195 | 196 | intermediateSteps.push({ query: extractedCypher }); 197 | 198 | let chainResult: ChainValues; 199 | const context = await this.graph.query(extractedCypher, { 200 | topK: this.topK, 201 | }); 202 | 203 | if (this.returnDirect) { 204 | chainResult = { [this.outputKey]: context }; 205 | } else { 206 | await runManager?.handleText("Full Context:\n"); 207 | await runManager?.handleText(`${context} green\n`); 208 | 209 | intermediateSteps.push({ context }); 210 | 211 | const result = await this.qaChain.call( 212 | { question, context: JSON.stringify(context) }, 213 | callbacks 214 | ); 215 | 216 | chainResult = { 217 | [this.outputKey]: result[this.qaChain.outputKey], 218 | }; 219 | } 220 | 221 | if (this.returnIntermediateSteps) { 222 | chainResult[INTERMEDIATE_STEPS_KEY] = intermediateSteps; 223 | } 224 | 225 | return chainResult; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/custom/graph-cypher-qa-chain/prompts.ts: -------------------------------------------------------------------------------- 1 | import { PromptTemplate } from "@langchain/core/prompts"; 2 | 3 | const CYPHER_GENERATION_TEMPLATE = `Task:Generate Cypher statement to query a graph database. 4 | Instructions: 5 | Use only the provided relationship types and properties in the schema. 6 | Do not use any other relationship types or properties that are not provided. 7 | Schema: 8 | {schema} 9 | Note: Do not include any explanations or apologies in your responses. 10 | Do not respond to any questions that might ask anything else than for you to construct a Cypher statement. 11 | Do not include any text except the generated Cypher statement. 12 | 13 | The question is: 14 | {question}`; 15 | export const CYPHER_GENERATION_PROMPT = /* #__PURE__ */ new PromptTemplate({ 16 | template: CYPHER_GENERATION_TEMPLATE, 17 | inputVariables: ["schema", "question"], 18 | }); 19 | 20 | const CYPHER_QA_TEMPLATE = `You are an assistant that helps to form nice and human understandable answers. 21 | The information part contains the provided information that you must use to construct an answer. 22 | The provided information is authoritative, you must never doubt it or try to use your internal knowledge to correct it. 23 | Make the answer sound as a response to the question. Do not mention that you based the result on the given information. 24 | If the provided information is empty, say that you don't know the answer. 25 | Information: 26 | {context} 27 | 28 | Question: {question} 29 | Helpful Answer:`; 30 | export const CYPHER_QA_PROMPT = /* #__PURE__ */ new PromptTemplate({ 31 | template: CYPHER_QA_TEMPLATE, 32 | inputVariables: ["context", "question"], 33 | }); 34 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easwee/text2cypher/6445fbd9e2359ee6aae93e8070e15f8b32ac6f68/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | ::-webkit-scrollbar { 6 | width: 0.6rem; 7 | } 8 | 9 | ::-webkit-scrollbar-track { 10 | border-radius: 0.5rem; 11 | background-color: rgba(255, 255, 255, 0.3); 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | background-color: rgba(255, 255, 255, 0.3); 16 | border-radius: 0.5rem; 17 | } 18 | 19 | ::-webkit-scrollbar-thumb:hover { 20 | background-color: rgba(255, 255, 255, 0.5); 21 | } 22 | 23 | input:-webkit-autofill, 24 | input:-webkit-autofill:focus { 25 | transition: background-color 600000s 0s, color 600000s 0s; 26 | } 27 | input[data-autocompleted] { 28 | background-color: transparent !important; 29 | } 30 | 31 | html, 32 | body, 33 | #root { 34 | margin: 0; 35 | background: #313342; 36 | } 37 | 38 | html { 39 | font-size: 62.5%; 40 | } 41 | 42 | body { 43 | font-size: 1.6rem; 44 | color: white; 45 | font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif; 46 | padding: 1rem; 47 | } 48 | 49 | a { 50 | color: green; 51 | text-decoration: none; 52 | } 53 | 54 | a:hover { 55 | text-decoration: underline; 56 | } 57 | 58 | button { 59 | overflow: hidden; 60 | position: relative; 61 | display: inline-flex; 62 | align-items: center; 63 | justify-content: center; 64 | cursor: pointer; 65 | background: rgba(0, 0, 0, 0.3); 66 | color: white; 67 | width: 12rem; 68 | height: 4rem; 69 | border: 0; 70 | border-radius: 0.5rem; 71 | font-size: 1.4rem; 72 | line-height: 4rem; 73 | font-weight: 600; 74 | text-transform: uppercase; 75 | padding: 0; 76 | margin: 0; 77 | } 78 | 79 | button span { 80 | height: 100%; 81 | } 82 | 83 | button span, 84 | button svg { 85 | display: inline-flex; 86 | } 87 | 88 | button:hover { 89 | filter: brightness(110%); 90 | } 91 | 92 | button.htmx-request { 93 | opacity: 0.3; 94 | pointer-events: none; 95 | } 96 | 97 | button .text { 98 | opacity: 1; 99 | transition: opacity 200ms ease-in-out; 100 | } 101 | 102 | button .loader { 103 | position: absolute; 104 | top: 0; 105 | left: 0; 106 | right: 0; 107 | bottom: 0; 108 | margin: auto; 109 | } 110 | 111 | button.htmx-request .loader { 112 | opacity: 1; 113 | } 114 | 115 | button.htmx-request .text { 116 | opacity: 0; 117 | } 118 | 119 | input[type="text"], 120 | input[type="password"], 121 | select, 122 | textarea { 123 | height: 4rem; 124 | padding: 0 1rem; 125 | background-color: rgba(0, 0, 0, 0.1); 126 | border: 0.2rem solid rgba(0, 0, 0, 0.2); 127 | color: white; 128 | border-radius: 0.5rem; 129 | font-size: 1.4rem; 130 | } 131 | 132 | textarea { 133 | display: block; 134 | width: 100%; 135 | height: auto; 136 | min-height: 30rem; 137 | padding: 1rem; 138 | resize: none; 139 | } 140 | 141 | button, 142 | input, 143 | select, 144 | textarea { 145 | transition: all 200ms ease-in-out; 146 | } 147 | 148 | select { 149 | padding-left: 1.6rem; 150 | } 151 | 152 | select option { 153 | background-color: #22242d; 154 | } 155 | 156 | button:focus, 157 | input[type="text"]:focus, 158 | input[type="password"]:focus, 159 | select:focus, 160 | textarea:focus { 161 | border-color: rgba(0, 128, 0, 0.5); 162 | outline: 0; 163 | } 164 | 165 | ::placeholder { 166 | font-style: italic; 167 | } 168 | 169 | fieldset { 170 | margin: 2rem 0; 171 | padding: 0; 172 | border: 0; 173 | border-top: 0.2rem solid rgba(0, 0, 0, 0.2); 174 | } 175 | 176 | /* Dialog */ 177 | 178 | dialog { 179 | background: #22242d; 180 | color: white; 181 | border-radius: 0.5rem; 182 | width: 90%; 183 | max-width: 80rem; 184 | border: 0; 185 | } 186 | 187 | dialog header { 188 | display: flex; 189 | align-items: center; 190 | justify-content: space-between; 191 | margin-bottom: 2rem; 192 | } 193 | 194 | dialog header h3 { 195 | margin: 0; 196 | } 197 | 198 | dialog button.close { 199 | display: flex; 200 | align-items: center; 201 | justify-content: center; 202 | width: 4rem; 203 | height: 4rem; 204 | font-size: 1.8rem; 205 | border-radius: 50%; 206 | } 207 | 208 | dialog footer { 209 | display: flex; 210 | align-items: center; 211 | justify-content: flex-end; 212 | margin-top: 2rem; 213 | } 214 | 215 | dialog::backdrop { 216 | background-color: rgba(0, 0, 0, 0.7); 217 | transition: all 5s ease; 218 | backdrop-filter: blur(10px); 219 | } 220 | 221 | .loader { 222 | display: block; 223 | border: 0.3rem solid white; 224 | border-radius: 50%; 225 | border-right-color: transparent; 226 | border-bottom-color: transparent; 227 | width: 2.4rem; 228 | height: 2.4rem; 229 | animation-name: loading; 230 | animation-duration: 700ms; 231 | animation-iteration-count: infinite; 232 | animation-timing-function: linear; 233 | opacity: 0; 234 | transition: opacity 200ms ease-in-out; 235 | pointer-events: none; 236 | } 237 | 238 | @keyframes loading { 239 | from { 240 | transform: rotate(0deg); 241 | } 242 | to { 243 | transform: rotate(360deg); 244 | } 245 | } 246 | 247 | code[class*="language-"], 248 | pre[class*="language-"] { 249 | margin: 0; 250 | padding: 0; 251 | border: 0; 252 | box-shadow: none; 253 | background: transparent; 254 | white-space: pre-wrap; 255 | word-break: break-word; 256 | } 257 | 258 | @keyframes mulShdSpin { 259 | 0%, 260 | 100% { 261 | box-shadow: 0em -2.6em 0em 0em #ffffff, 262 | 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 263 | 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 264 | 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 265 | 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), 266 | -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), 267 | -2.6em 0em 0 0em rgba(255, 255, 255, 0.5), 268 | -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7); 269 | } 270 | 12.5% { 271 | box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.7), 272 | 1.8em -1.8em 0 0em #ffffff, 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 273 | 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 274 | 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), 275 | -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), 276 | -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), 277 | -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5); 278 | } 279 | 25% { 280 | box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.5), 281 | 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.7), 2.5em 0em 0 0em #ffffff, 282 | 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 283 | 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), 284 | -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), 285 | -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), 286 | -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); 287 | } 288 | 37.5% { 289 | box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 290 | 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.5), 291 | 2.5em 0em 0 0em rgba(255, 255, 255, 0.7), 1.75em 1.75em 0 0em #ffffff, 292 | 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), 293 | -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), 294 | -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), 295 | -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); 296 | } 297 | 50% { 298 | box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 299 | 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 300 | 2.5em 0em 0 0em rgba(255, 255, 255, 0.5), 301 | 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.7), 0em 2.5em 0 0em #ffffff, 302 | -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.2), 303 | -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), 304 | -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); 305 | } 306 | 62.5% { 307 | box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 308 | 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 309 | 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 310 | 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.5), 311 | 0em 2.5em 0 0em rgba(255, 255, 255, 0.7), -1.8em 1.8em 0 0em #ffffff, 312 | -2.6em 0em 0 0em rgba(255, 255, 255, 0.2), 313 | -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); 314 | } 315 | 75% { 316 | box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 317 | 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 318 | 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 319 | 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 320 | 0em 2.5em 0 0em rgba(255, 255, 255, 0.5), 321 | -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.7), -2.6em 0em 0 0em #ffffff, 322 | -1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2); 323 | } 324 | 87.5% { 325 | box-shadow: 0em -2.6em 0em 0em rgba(255, 255, 255, 0.2), 326 | 1.8em -1.8em 0 0em rgba(255, 255, 255, 0.2), 327 | 2.5em 0em 0 0em rgba(255, 255, 255, 0.2), 328 | 1.75em 1.75em 0 0em rgba(255, 255, 255, 0.2), 329 | 0em 2.5em 0 0em rgba(255, 255, 255, 0.2), 330 | -1.8em 1.8em 0 0em rgba(255, 255, 255, 0.5), 331 | -2.6em 0em 0 0em rgba(255, 255, 255, 0.7), -1.8em -1.8em 0 0em #ffffff; 332 | } 333 | } 334 | 335 | .notification { 336 | padding: 1rem; 337 | color: white; 338 | } 339 | 340 | .notification.success { 341 | background: green; 342 | } 343 | 344 | .notification.error { 345 | background: green; 346 | } 347 | 348 | /*** Features ***/ 349 | 350 | main { 351 | position: relative; 352 | width: 100%; 353 | max-width: 160rem; 354 | margin: 0 auto; 355 | padding: 2rem; 356 | margin-top: 5rem; 357 | background-color: rgba(0, 0, 0, 0.3); 358 | border-radius: 0.5rem; 359 | overflow: hidden; 360 | } 361 | 362 | main .loading-overlay { 363 | position: absolute; 364 | top: 0; 365 | left: 0; 366 | z-index: 1000; 367 | display: flex; 368 | align-items: center; 369 | justify-content: top; 370 | flex-direction: column; 371 | width: 100%; 372 | height: 100%; 373 | padding-top: 30rem; 374 | transition: opacity 200ms ease-in-out; 375 | opacity: 0; 376 | backdrop-filter: blur(3px); 377 | pointer-events: none; 378 | } 379 | 380 | main .loading-overlay .loader { 381 | opacity: 1; 382 | } 383 | 384 | main.htmx-request .loading-overlay { 385 | opacity: 1; 386 | pointer-events: auto; 387 | } 388 | 389 | main .logo { 390 | position: absolute; 391 | top: 3.2rem; 392 | right: 2rem; 393 | width: 15rem; 394 | opacity: 0.9; 395 | } 396 | 397 | /* Database selector */ 398 | 399 | .database-selector { 400 | position: relative; 401 | margin-bottom: 2rem; 402 | padding-right: 21rem; 403 | } 404 | 405 | .database-selector form { 406 | max-width: calc(100%); 407 | } 408 | 409 | .database-selector select { 410 | width: 100%; 411 | } 412 | 413 | .settings { 414 | position: absolute; 415 | top: 0; 416 | right: 16rem; 417 | } 418 | 419 | .settings-icon { 420 | width: 4rem; 421 | height: 4rem; 422 | display: flex; 423 | align-items: center; 424 | justify-content: center; 425 | fill: rgba(255, 255, 255, 0.5); 426 | } 427 | 428 | .settings-icon:hover { 429 | fill: rgba(255, 255, 255, 0.7); 430 | cursor: pointer; 431 | } 432 | 433 | .settings p { 434 | font-size: 1.3rem; 435 | margin: 2rem 0; 436 | } 437 | 438 | .settings h4 { 439 | display: flex; 440 | justify-content: space-between; 441 | font-size: 1.4rem; 442 | font-weight: bold; 443 | margin: 2rem 0; 444 | color: gray; 445 | } 446 | 447 | .settings fieldset { 448 | padding: 0 2rem; 449 | border: 0.2rem solid rgba(0, 0, 0, 0.2); 450 | border-radius: 0.5rem; 451 | } 452 | 453 | .settings .field { 454 | display: flex; 455 | align-items: center; 456 | margin: 2rem 0; 457 | } 458 | 459 | .settings .field label { 460 | display: flex; 461 | align-items: center; 462 | justify-content: flex-end; 463 | width: 100%; 464 | } 465 | 466 | .settings .field input { 467 | width: 80%; 468 | margin-left: 2rem; 469 | } 470 | 471 | .settings-custom-database-add, 472 | .settings-custom-database-remove { 473 | font-size: 1.2rem; 474 | font-weight: normal; 475 | color: green; 476 | cursor: pointer; 477 | } 478 | 479 | .settings-custom-database-add { 480 | margin: 2rem 0; 481 | } 482 | 483 | 484 | /* Prompt input */ 485 | 486 | .prompt-input { 487 | display: flex; 488 | align-items: center; 489 | padding: 1rem; 490 | margin-top: 2rem; 491 | background-color: rgba(0, 0, 0, 0.1); 492 | border: 0.2rem solid rgba(0, 0, 0, 0.2); 493 | border-radius: 0.5rem; 494 | } 495 | 496 | .prompt-input:focus-within { 497 | border-color: rgba(0, 128, 0, 0.5); 498 | } 499 | 500 | .prompt-input-text { 501 | position: relative; 502 | flex-grow: 1; 503 | padding-right: 10rem; 504 | } 505 | 506 | .prompt-input-text input[type="text"] { 507 | display: block; 508 | width: 100%; 509 | background: transparent; 510 | border: 0; 511 | font-size: 1.6rem; 512 | } 513 | 514 | .prompt-input-submit { 515 | height: 4rem; 516 | flex-grow: 0; 517 | } 518 | 519 | .prompt-input-submit button { 520 | width: 15rem; 521 | background: green; 522 | pointer-events: none; 523 | opacity: 0.3; 524 | } 525 | 526 | .prompt-counter { 527 | display: flex; 528 | position: absolute; 529 | top: 50%; 530 | right: 5rem; 531 | transform: translateY(-50%); 532 | font-size: 1.2rem; 533 | text-align: right; 534 | opacity: 0; 535 | color: gray; 536 | transition: all 200ms ease-in-out; 537 | } 538 | 539 | .prompt-reset { 540 | display: flex; 541 | align-items: center; 542 | justify-content: center; 543 | position: absolute; 544 | bottom: 50%; 545 | transform: translateY(50%); 546 | right: 1rem; 547 | width: 3rem; 548 | height: 3rem; 549 | border: 0; 550 | border-radius: 50%; 551 | color: rgba(255, 255, 255, 0.7); 552 | background-color: rgba(255, 255, 255, 0.1); 553 | text-transform: uppercase; 554 | font-size: 1.6rem; 555 | font-weight: 700; 556 | opacity: 0; 557 | pointer-events: none; 558 | } 559 | 560 | .prompt-input.valid .prompt-counter, 561 | .prompt-input.valid .prompt-reset { 562 | opacity: 1; 563 | pointer-events: auto; 564 | } 565 | 566 | .prompt-input.valid .prompt-input-submit button { 567 | opacity: 1; 568 | pointer-events: auto; 569 | } 570 | 571 | .prompt-input.valid .prompt-input-submit button.htmx-request { 572 | opacity: 0.3; 573 | pointer-events: none; 574 | } 575 | 576 | /* Prompt input */ 577 | 578 | .prompt-output { 579 | display: grid; 580 | grid-template-columns: 1fr 1fr; 581 | grid-gap: 2rem; 582 | } 583 | 584 | .prompt-output-column { 585 | position: relative; 586 | height: 50rem; 587 | background-color: rgba(0, 0, 0, 0.1); 588 | border: 0.2rem solid rgba(0, 0, 0, 0.2); 589 | border-radius: 0.5rem; 590 | } 591 | 592 | .prompt-output-column-scroll { 593 | position: absolute; 594 | top: 0; 595 | left: 0; 596 | width: 100%; 597 | height: 100%; 598 | overflow: auto; 599 | padding: 2rem; 600 | font-size: 1.4rem; 601 | } 602 | 603 | .prompt-output-cypher, 604 | .prompt-output-answer { 605 | font-family: monospace; 606 | } 607 | 608 | .prompt-output-cypher { 609 | padding-bottom: 6rem; 610 | } 611 | 612 | /* Schema preview */ 613 | 614 | .schema { 615 | position: relative; 616 | height: 50rem; 617 | padding: 2rem; 618 | background-color: rgba(0, 0, 0, 0.1); 619 | border: 0.2rem solid rgba(0, 0, 0, 0.2); 620 | border-radius: 0.5rem; 621 | transition: all 200ms ease-in-out; 622 | } 623 | 624 | .schema-description { 625 | height: 100%; 626 | overflow: auto; 627 | white-space: pre-wrap; 628 | font-family: monospace; 629 | font-size: 1.4rem; 630 | line-height: 1.6; 631 | } 632 | 633 | /* Last prompts */ 634 | 635 | .last-prompts { 636 | margin-top: 2rem; 637 | } 638 | 639 | .last-prompts strong { 640 | font-size: 1.2rem; 641 | color: gray; 642 | } 643 | 644 | .last-prompts-clear { 645 | color: green; 646 | text-decoration: underline; 647 | } 648 | 649 | .last-prompts-clear:hover { 650 | text-decoration: none; 651 | cursor: pointer; 652 | } 653 | 654 | .last-prompts-item { 655 | display: flex; 656 | align-items: center; 657 | overflow: hidden; 658 | width: 100%; 659 | margin-top: 1rem; 660 | background-color: rgba(0, 0, 0, 0.1); 661 | border: 0.2rem solid rgba(0, 0, 0, 0.2); 662 | border-radius: 0.5rem; 663 | transition: all 200ms ease-in-out; 664 | cursor: pointer; 665 | } 666 | 667 | .last-prompts-item:hover { 668 | background-color: rgba(255, 255, 255, 0.01); 669 | } 670 | 671 | .last-prompts-item-text { 672 | flex-basis: 50%; 673 | padding: 2rem; 674 | white-space: nowrap; 675 | text-overflow: ellipsis; 676 | overflow: hidden; 677 | } 678 | 679 | .last-prompts-item-cypher { 680 | height: 100%; 681 | padding: 2rem; 682 | flex-basis: 50%; 683 | white-space: nowrap; 684 | text-overflow: ellipsis; 685 | overflow: hidden; 686 | font-family: monospace; 687 | color: gray; 688 | font-size: 1.2rem; 689 | } 690 | 691 | /* Vote for cypher */ 692 | 693 | .vote { 694 | justify-content: center; 695 | display: flex; 696 | align-items: center; 697 | width: 100%; 698 | height: 6rem; 699 | padding: 1rem; 700 | background: rgba(0, 0, 0, 0.1); 701 | position: absolute; 702 | bottom: 0; 703 | } 704 | 705 | .vote-up, 706 | .vote-down { 707 | margin: 0 0.5rem; 708 | } 709 | 710 | .vote-up { 711 | color: green; 712 | } 713 | 714 | .vote-down { 715 | color: red; 716 | } 717 | 718 | .vote-validate-success { 719 | display: flex; 720 | align-items: center; 721 | justify-content: flex-end; 722 | } 723 | 724 | .vote-validate-success button { 725 | margin-left: 2rem; 726 | } 727 | 728 | .validate-cypher-submit { 729 | background: green; 730 | } 731 | -------------------------------------------------------------------------------- /src/public/main.js: -------------------------------------------------------------------------------- 1 | function updatePromptCount(promptMaxlength = 300) { 2 | const input = document.getElementById("promptInput"); 3 | const promptCount = document.getElementById("promptCount"); 4 | const promptInputForm = document.getElementById("promptInputForm"); 5 | 6 | if (input.value.length > 0) { 7 | promptCount.textContent = `${input.value.length}/${promptMaxlength}`; 8 | promptInputForm.classList.add("valid"); 9 | } else { 10 | promptInputForm.classList.remove("valid"); 11 | } 12 | } 13 | 14 | function resetPrompt(element) { 15 | element.form.reset(); 16 | updatePromptCount(); 17 | } 18 | 19 | function updateLastPrompts() { 20 | const prompt = document.querySelector('input[name="prompt"]').value; 21 | const cypher = document.querySelector("#cypher").dataset.cypher; 22 | let newLastPrompts = [{ prompt, cypher }]; 23 | const lastPrompts = JSON.parse(localStorage.getItem("last_prompts")); 24 | 25 | if (lastPrompts) { 26 | newLastPrompts = [...newLastPrompts, ...lastPrompts.slice(0, 2)]; 27 | } 28 | 29 | localStorage.setItem("last_prompts", JSON.stringify(newLastPrompts)); 30 | 31 | renderLastPrompts(newLastPrompts); 32 | } 33 | 34 | function setNewPrompt(event) { 35 | const promptInput = document.querySelector('input[name="prompt"]'); 36 | 37 | promptInput.value = event.target.closest(".last-prompts-item").dataset.prompt; 38 | } 39 | 40 | function clearLastPrompts() { 41 | localStorage.removeItem("last_prompts"); 42 | } 43 | 44 | function renderLastPrompts(prompts) { 45 | const lastPromptsEl = document.getElementById("lastPrompts"); 46 | const lastPromptsListEl = document.getElementById("lastPromptsList"); 47 | 48 | lastPromptsListEl.innerHTML = ""; 49 | 50 | if (prompts) { 51 | lastPromptsEl.style.display = "block"; 52 | prompts.forEach(({ prompt, cypher }) => { 53 | const promptEl = document.createElement("div"); 54 | 55 | promptEl.setAttribute("data-prompt", prompt); 56 | promptEl.classList.add("last-prompts-item"); 57 | promptEl.addEventListener("click", handleLastPromptClick); 58 | 59 | const promptElText = document.createElement("div"); 60 | promptElText.classList.add("last-prompts-item-text"); 61 | promptElText.title = prompt; 62 | promptElText.innerHTML = `${prompt}`; 63 | promptEl.append(promptElText); 64 | 65 | const promptElCypher = document.createElement("div"); 66 | promptElCypher.classList.add("last-prompts-item-cypher"); 67 | promptElCypher.title = cypher; 68 | promptElCypher.innerHTML = cypher; 69 | promptEl.append(promptElCypher); 70 | 71 | lastPromptsListEl.append(promptEl); 72 | }); 73 | } else { 74 | lastPromptsEl.style.display = "none"; 75 | } 76 | } 77 | 78 | function renderClientDatabases(clientDatabases) { 79 | if(clientDatabases) { 80 | const databaseSelect = document.querySelector('select[name="database"]'); 81 | databaseSelect.innerHTML = ''; 82 | 83 | clientDatabases.forEach(({name}) => { 84 | const option = document.createElement('option'); 85 | option.value = name; 86 | option.text = name; 87 | databaseSelect.appendChild(option); 88 | }) 89 | } 90 | } 91 | 92 | function getQueryParam(name) { 93 | const urlParams = new URLSearchParams(window.location.search); 94 | return urlParams.get(name); 95 | } 96 | 97 | function initDownvoteDialog() { 98 | const validateCypher = document.querySelector("#validateCypher"); 99 | const voteDown = document.querySelector("#voteDown"); 100 | 101 | if(voteDown) { 102 | voteDown.addEventListener("click", () => { 103 | validateCypher.showModal(); 104 | }); 105 | } 106 | } 107 | 108 | function initSettingsDialog() { 109 | const settingsTrigger = document.querySelector("#settingsTrigger"); 110 | const settingsDialog = document.querySelector("#settings"); 111 | const databasesContainer = document.getElementById('databasesContainer'); 112 | const databaseSelect = document.querySelector('select[name="database"]'); 113 | 114 | // populate database selection with custom databases from local storage 115 | const databases = localStorage.getItem("databases"); 116 | const databaseFieldsets = databases.length > 0 ? JSON.parse(databases) : [] 117 | databaseFieldsets.forEach((data, index) => { 118 | const option = document.createElement('option'); 119 | option.value = data.name; 120 | option.text = data.name; 121 | databaseSelect.appendChild(option); 122 | }) 123 | 124 | const clientDatabasesInput = document.querySelector("input[name='clientDatabases']") 125 | clientDatabasesInput.value = databases; 126 | 127 | settingsTrigger.addEventListener("click", () => { 128 | // populate settings UI from local storage 129 | const openAIApiKey = localStorage.getItem("openAIApiKey"); 130 | const openAIApiKeyInput = document.querySelector("[name='openai_api_key']") 131 | openAIApiKeyInput.value = openAIApiKey ? openAIApiKey : ""; 132 | 133 | databasesContainer.innerHTML = ""; 134 | const databases = localStorage.getItem("databases"); 135 | const databaseFieldsets = databases.length > 0 ? JSON.parse(databases) : [] 136 | 137 | databaseFieldsets.forEach((data, index) => { 138 | const fieldset = ` 139 |
140 |

Neo4j database connection - remove

141 |
142 | 146 |
147 |
148 | 152 |
153 |
154 | 158 |
159 |
160 | 164 |
165 |
166 | `; 167 | databasesContainer.innerHTML += fieldset; 168 | }) 169 | settingsDialog.showModal(); 170 | }); 171 | 172 | // handle add database connection 173 | const addBtn = document.getElementById('addCustomDatabase'); 174 | addBtn.addEventListener('click', function() { 175 | const lastFieldset = databasesContainer.querySelector('fieldset:last-of-type'); 176 | const index = lastFieldset ? parseInt(lastFieldset.id.split('_')[1]) + 1 : 0; 177 | 178 | const fieldset = ` 179 |
180 |

Neo4j database connection - remove

181 |
182 | 186 |
187 |
188 | 192 |
193 |
194 | 198 |
199 |
200 | 204 |
205 |
206 | `; 207 | 208 | databasesContainer.innerHTML += fieldset; 209 | }); 210 | 211 | databasesContainer.addEventListener('click', function(e) { 212 | if(e.target.classList.contains('settings-custom-database-remove')) { 213 | const fieldset = e.target.closest('fieldset'); 214 | fieldset.remove(); 215 | } 216 | }); 217 | 218 | const settingsSave = document.getElementById("settingsSave"); 219 | settingsSave.addEventListener("click", function(e) { 220 | e.preventDefault(); 221 | 222 | const openAIApiKey = document.querySelector('input[name="openai_api_key"]').value; 223 | localStorage.setItem('openAIApiKey', openAIApiKey); 224 | 225 | const databases = []; 226 | 227 | const fieldsets = document.querySelectorAll('.settings-custom-database'); 228 | const customSelectOptions = databaseSelect.querySelectorAll('option[value]'); 229 | customSelectOptions.forEach(option => { 230 | option.remove(); 231 | }); 232 | 233 | fieldsets.forEach(function(fieldset) { 234 | const dbInfo = { 235 | name: fieldset.querySelector('input[name*="_name"]').value, 236 | uri: fieldset.querySelector('input[name*="_uri"]').value, 237 | username: fieldset.querySelector('input[name*="_username"]').value, 238 | password: fieldset.querySelector('input[name*="_password"]').value 239 | }; 240 | 241 | const option = document.createElement('option'); 242 | option.value = dbInfo.name; 243 | option.text = dbInfo.name; 244 | 245 | databaseSelect.appendChild(option); 246 | databases.push(dbInfo); 247 | }); 248 | 249 | // Save the databases array to localStorage as a JSON string 250 | localStorage.setItem('databases', JSON.stringify(databases)); 251 | 252 | settingsDialog.close(); 253 | }); 254 | } 255 | 256 | function handleLastPromptClick(event) { 257 | setNewPrompt(event); 258 | updatePromptCount(); 259 | } 260 | 261 | function handleAskBeforeRequest(event) { 262 | const openAIApiKey = localStorage.getItem("openAIApiKey") 263 | const databases = localStorage.getItem("databases") 264 | 265 | if(openAIApiKey) { 266 | event.detail.requestConfig.parameters["openAIApiKey"] = openAIApiKey; 267 | } 268 | 269 | if(databases) { 270 | event.detail.requestConfig.parameters["clientDatabases"] = databases; 271 | } 272 | } 273 | 274 | function handleAskAfterRequest() { 275 | const cypher = document.getElementById("cypher"); 276 | const answer = document.getElementById("answer"); 277 | 278 | Prism.highlightAll(cypher); 279 | Prism.highlightAll(answer); 280 | 281 | updateLastPrompts(); 282 | initDownvoteDialog(); 283 | } 284 | 285 | function init() { 286 | 287 | initSettingsDialog(); 288 | 289 | const lastPrompts = JSON.parse(localStorage.getItem("last_prompts")); 290 | renderLastPrompts(lastPrompts); 291 | 292 | // fill prompt from url if present 293 | const promptInput = document.getElementById("promptInput"); 294 | const promptValue = getQueryParam("prompt"); 295 | 296 | if (promptInput && promptValue) { 297 | promptInput.value = decodeURIComponent(promptValue.replace(/\+/g, " ")); 298 | updatePromptCount(); 299 | } 300 | } 301 | 302 | -------------------------------------------------------------------------------- /src/public/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-dark&languages=markup+css+clike+javascript+cypher+json */ 3 | code[class*=language-],pre[class*=language-]{color:#fff;background:0 0;text-shadow:0 -.1em .2em #000;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}:not(pre)>code[class*=language-],pre[class*=language-]{background:#4c3f33}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border:.3em solid #7a6651;border-radius:.5em;box-shadow:1px 1px .5em #000 inset}:not(pre)>code[class*=language-]{padding:.15em .2em .05em;border-radius:.3em;border:.13em solid #7a6651;box-shadow:1px 1px .3em -.1em #000 inset;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#997f66}.token.punctuation{opacity:.7}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.number,.token.property,.token.symbol,.token.tag{color:#d1939e}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#bce051}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f4b73d}.token.atrule,.token.attr-value,.token.keyword{color:#d1939e}.token.important,.token.regex{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.deleted{color:red} 4 | -------------------------------------------------------------------------------- /src/public/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-twilight&languages=markup+css+clike+javascript+cypher+json */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; 5 | !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; 8 | Prism.languages.cypher={comment:/\/\/.*/,string:{pattern:/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/,greedy:!0},"class-name":{pattern:/(:\s*)(?:\w+|`(?:[^`\\\r\n])*`)(?=\s*[{):])/,lookbehind:!0,greedy:!0},relationship:{pattern:/(-\[\s*(?:\w+\s*|`(?:[^`\\\r\n])*`\s*)?:\s*|\|\s*:\s*)(?:\w+|`(?:[^`\\\r\n])*`)/,lookbehind:!0,greedy:!0,alias:"property"},identifier:{pattern:/`(?:[^`\\\r\n])*`/,greedy:!0},variable:/\$\w+/,keyword:/\b(?:ADD|ALL|AND|AS|ASC|ASCENDING|ASSERT|BY|CALL|CASE|COMMIT|CONSTRAINT|CONTAINS|CREATE|CSV|DELETE|DESC|DESCENDING|DETACH|DISTINCT|DO|DROP|ELSE|END|ENDS|EXISTS|FOR|FOREACH|IN|INDEX|IS|JOIN|KEY|LIMIT|LOAD|MANDATORY|MATCH|MERGE|NODE|NOT|OF|ON|OPTIONAL|OR|ORDER(?=\s+BY)|PERIODIC|REMOVE|REQUIRE|RETURN|SCALAR|SCAN|SET|SKIP|START|STARTS|THEN|UNION|UNIQUE|UNWIND|USING|WHEN|WHERE|WITH|XOR|YIELD)\b/i,function:/\b\w+\b(?=\s*\()/,boolean:/\b(?:false|null|true)\b/i,number:/\b(?:0x[\da-fA-F]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/,operator:/:|<--?|--?>?|<>|=~?|[<>]=?|[+*/%^|]|\.\.\.?/,punctuation:/[()[\]{},;.]/}; 9 | Prism.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},Prism.languages.webmanifest=Prism.languages.json; 10 | -------------------------------------------------------------------------------- /src/routes/ask.ts: -------------------------------------------------------------------------------- 1 | import S from "fluent-json-schema"; 2 | import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; 3 | import { Neo4jGraph } from "@langchain/community/graphs/neo4j_graph"; 4 | import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai"; 5 | // this import should be replaced with langchain import when chypher validation is implemented there 6 | import { GraphCypherQAChain } from "../custom/graph-cypher-qa-chain/chain"; 7 | import { Session } from "neo4j-driver"; 8 | import { parseClientDatabases } from "../utils/database"; 9 | 10 | interface AskRequestBody { 11 | database: string; 12 | prompt: string; 13 | openAIApiKey?: string; 14 | clientDatabases?: string; 15 | } 16 | 17 | export default async function ask(fastify: FastifyInstance) { 18 | fastify.route({ 19 | method: "POST", 20 | url: "/ask/", 21 | handler: onAsk, 22 | schema: { 23 | body: S.object() 24 | .prop("database", S.string().required()) 25 | .prop("prompt", S.string().required()) 26 | .prop("openaiApiKey", S.string()) 27 | .valueOf(), 28 | }, 29 | }); 30 | 31 | async function onAsk( 32 | req: FastifyRequest<{ Body: AskRequestBody }>, 33 | reply: FastifyReply 34 | ): Promise { 35 | const { database, prompt, openAIApiKey, clientDatabases = '[]' } = req.body; 36 | const promptMaxLength = fastify.envConfig.PROMPT_MAX_LENGTH; 37 | 38 | fastify.log.info( 39 | `DATABASE: ${database} / PROMPT: ${prompt}` 40 | ); 41 | 42 | if (!prompt.trim()) { 43 | return reply.code(400).send({ 44 | error: "Bad Request", 45 | message: "Payload error: 'prompt' cannot be an empty string", 46 | }); 47 | } 48 | 49 | if (prompt.length > promptMaxLength) { 50 | return reply.code(400).send({ 51 | error: "Bad Request", 52 | message: `Payload error: Max 'prompt' length is ${promptMaxLength} characters.`, 53 | }); 54 | } 55 | 56 | if (!database.trim()) { 57 | return reply.code(400).send({ 58 | error: "Bad Request", 59 | message: "Payload error: 'database' not specified.", 60 | }); 61 | } 62 | 63 | let graph: Neo4jGraph; 64 | let session: Session; 65 | 66 | try { 67 | 68 | // ask LLM 69 | const apiKey = openAIApiKey ? openAIApiKey : fastify.envConfig.OPENAI_API_KEY 70 | const chatOpenAI = new ChatOpenAI({ 71 | openAIApiKey: apiKey, 72 | modelName: "gpt-4-1106-preview", 73 | temperature: 0, 74 | }); 75 | 76 | const allDatabases = [ 77 | ...fastify.envConfig.DATABASES, 78 | ...parseClientDatabases(clientDatabases) 79 | ]; 80 | const dbConnectionData = allDatabases.find( 81 | (db) => db.name == database 82 | ); 83 | 84 | graph = await Neo4jGraph.initialize({ 85 | url: dbConnectionData.uri, 86 | username: dbConnectionData.username, 87 | password: dbConnectionData.password, 88 | database: dbConnectionData.username, 89 | timeoutMs: fastify.envConfig.PROMPT_MAX_DURATION_MS, 90 | }); 91 | 92 | const chain = GraphCypherQAChain.fromLLM({ 93 | llm: chatOpenAI, 94 | graph, 95 | returnDirect: true, 96 | returnIntermediateSteps: true, 97 | }); 98 | 99 | // get data from result 100 | const response = await chain.invoke({ query: prompt }); 101 | const answer = response.result 102 | ? JSON.stringify(response.result, null, 2) 103 | : "No results. Syntax error or db request timed out."; 104 | 105 | // construct the cypher from intermediate steps 106 | let cypher = ""; 107 | if (response.intermediateSteps) { 108 | response.intermediateSteps.forEach((step: any) => { 109 | if (step.query) { 110 | cypher = step.query; 111 | } 112 | }); 113 | } 114 | 115 | // store message data to feedback db 116 | const feedbackEnabled = fastify.envConfig.FEEDBACK_DATABASE !== null; 117 | let messageId = ""; 118 | 119 | if (answer) { 120 | const embeddings = new OpenAIEmbeddings({ openAIApiKey: apiKey }); 121 | const question_embedding = await embeddings.embedQuery(prompt); 122 | 123 | if(feedbackEnabled) { 124 | session = fastify.neo4jDriver.session(); 125 | const storedMessageResult = await session.run( 126 | ` 127 | CREATE (m:Message) 128 | SET m.question = $question, 129 | m.question_embedding = $question_embedding, 130 | m.cypher = $cypher, 131 | m.answer = $answer, 132 | m.database = $database, 133 | m.id = randomUUID(), 134 | m.created_at = datetime() 135 | RETURN m.id AS id 136 | `, 137 | { 138 | database: database, 139 | question: prompt, 140 | question_embedding: question_embedding, 141 | cypher: cypher, 142 | answer: answer, 143 | } 144 | ); 145 | messageId = storedMessageResult.records[0].get("id"); 146 | } 147 | } 148 | 149 | return reply.view("partials/prompt-output.hbs", { 150 | messageId, 151 | cypher, 152 | answer, 153 | promptMaxLength, 154 | feedbackEnabled 155 | }); 156 | } catch (error: any) { 157 | fastify.log.error(`/ask/ Error: ${error}`); 158 | reply.status(500).send({ 159 | error: "Server error", 160 | message: `Server failed processing the request. Error was: ${JSON.stringify(error)}`, 161 | }); 162 | } finally { 163 | if (session) { 164 | session.close(); 165 | } 166 | if (graph) { 167 | graph.close(); 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/routes/home.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 2 | 3 | export default async function home(fastify: FastifyInstance) { 4 | fastify.route({ 5 | method: "GET", 6 | url: "/", 7 | handler: onHomeRequest, 8 | }); 9 | 10 | async function onHomeRequest( 11 | req: FastifyRequest, 12 | reply: FastifyReply 13 | ): Promise { 14 | try { 15 | return reply.view("index.hbs", { 16 | layout: true, 17 | databases: fastify.envConfig.DATABASES.map((database) => database.name), 18 | promptMaxlength: fastify.envConfig.PROMPT_MAX_LENGTH, 19 | }); 20 | } catch (error) { 21 | fastify.log.error(`/home/ Error: ${error}`); 22 | reply.status(500).send({ 23 | error: "Server error", 24 | message: "Server failed processing the request.", 25 | }); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/routes/schema.ts: -------------------------------------------------------------------------------- 1 | import S from "fluent-json-schema"; 2 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 3 | import { Neo4jGraph } from "@langchain/community/graphs/neo4j_graph"; 4 | import { parseClientDatabases } from "../utils/database"; 5 | 6 | interface StatusRequestBody { 7 | database: string; 8 | clientDatabases?: string; 9 | } 10 | 11 | export default async function schema(fastify: FastifyInstance) { 12 | fastify.route({ 13 | method: "POST", 14 | url: "/schema/", 15 | handler: onStatus, 16 | schema: { 17 | body: S.object() 18 | .prop("database", S.string().required()) 19 | .valueOf(), 20 | }, 21 | }); 22 | 23 | async function onStatus( 24 | req: FastifyRequest<{ Body: StatusRequestBody }>, 25 | reply: FastifyReply 26 | ): Promise { 27 | let graph: Neo4jGraph; 28 | 29 | try { 30 | const { database, clientDatabases } = req.body; 31 | 32 | if (!database) { 33 | throw new Error("Payload error: Missing /schema/ parameter 'database'"); 34 | } 35 | 36 | const allDatabases = [ 37 | ...fastify.envConfig.DATABASES, 38 | ...parseClientDatabases(clientDatabases) 39 | ]; 40 | const dbConnectionData = allDatabases.find( 41 | (db) => db.name == database 42 | ); 43 | 44 | graph = await Neo4jGraph.initialize({ 45 | url: dbConnectionData.uri, 46 | username: dbConnectionData.username, 47 | password: dbConnectionData.password, 48 | database: dbConnectionData.username, 49 | }); 50 | 51 | const schema = graph.getSchema(); 52 | 53 | return reply.view("partials/schema.hbs", { 54 | schema, 55 | }); 56 | } catch (error) { 57 | fastify.log.error(`/schema/ Error: ${error}`); 58 | reply.status(500).send({ 59 | error: "Server error", 60 | message: "Server failed processing the request.", 61 | }); 62 | } finally { 63 | if (graph) { 64 | graph.close(); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/routes/validate.ts: -------------------------------------------------------------------------------- 1 | import S from "fluent-json-schema"; 2 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 3 | 4 | interface ValidateRequestBody { 5 | messageId: string; 6 | validatedCypher?: string; 7 | } 8 | 9 | export default async function validate(fastify: FastifyInstance) { 10 | fastify.route({ 11 | method: "POST", 12 | url: "/validate/", 13 | handler: onValidate, 14 | schema: { 15 | body: S.object() 16 | .prop("messageId", S.string().required()) 17 | .prop("validatedCypher", S.string()) 18 | .valueOf(), 19 | }, 20 | }); 21 | 22 | async function onValidate( 23 | req: FastifyRequest<{ Body: ValidateRequestBody }>, 24 | reply: FastifyReply 25 | ): Promise { 26 | const { messageId, validatedCypher } = req.body; 27 | 28 | if (!messageId.trim()) { 29 | return reply.code(400).send({ 30 | error: "Bad Request", 31 | message: "Payload error: 'messageId' cannot be an empty string", 32 | }); 33 | } 34 | 35 | if (!validatedCypher.trim()) { 36 | return reply.code(400).send({ 37 | error: "Bad Request", 38 | message: "Payload error: 'validatedCypher' cannot be an empty string", 39 | }); 40 | } 41 | 42 | const session = fastify.neo4jDriver.session(); 43 | 44 | try { 45 | // store feedback cypher 46 | await session.run( 47 | ` 48 | MATCH (m:Message {id:$id}) 49 | SET m.validated_cypher = $validated_cypher, 50 | m.updated_at = datetime() 51 | `, 52 | { 53 | id: messageId, 54 | validated_cypher: validatedCypher, 55 | } 56 | ); 57 | 58 | return reply.view("partials/vote-validate-success.hbs"); 59 | } catch (error) { 60 | fastify.log.error(`/validate/ Error: ${error}`); 61 | reply.status(500).send({ 62 | error: "Server error", 63 | message: "Server failed processing the request.", 64 | }); 65 | } finally { 66 | if (session) { 67 | session.close(); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/routes/vote-down.ts: -------------------------------------------------------------------------------- 1 | import S from "fluent-json-schema"; 2 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 3 | 4 | interface VoteDownRequestBody { 5 | messageId: string; 6 | } 7 | 8 | export default async function voteDown(fastify: FastifyInstance) { 9 | fastify.route({ 10 | method: "POST", 11 | url: "/vote/down/", 12 | handler: onVoteDown, 13 | schema: { 14 | body: S.object() 15 | .prop("messageId", S.string().required()) 16 | .valueOf(), 17 | }, 18 | }); 19 | 20 | async function onVoteDown( 21 | req: FastifyRequest<{ Body: VoteDownRequestBody }>, 22 | reply: FastifyReply 23 | ): Promise { 24 | const { messageId } = req.body; 25 | 26 | if (!messageId) { 27 | return reply.code(400).send({ 28 | error: "Bad Request", 29 | message: "Payload error: 'messageId' cannot be an empty string", 30 | }); 31 | } 32 | 33 | const session = fastify.neo4jDriver.session(); 34 | 35 | try { 36 | // store vote 37 | await session.run( 38 | ` 39 | MATCH (m:Message {id:$id}) 40 | SET m.vote = $vote, 41 | m.updated_at = datetime() 42 | `, 43 | { 44 | id: messageId, 45 | vote: -1, 46 | } 47 | ); 48 | 49 | return reply.view("partials/vote-success.hbs"); 50 | } catch (error) { 51 | fastify.log.error(`/vote/down/ Error: ${error}`); 52 | reply.status(500).send({ 53 | error: "Server error", 54 | message: "Server failed processing the request.", 55 | }); 56 | } finally { 57 | if (session) { 58 | session.close(); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/routes/vote-up.ts: -------------------------------------------------------------------------------- 1 | import S from "fluent-json-schema"; 2 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; 3 | 4 | interface VoteUpRequestBody { 5 | messageId: string; 6 | } 7 | 8 | export default async function voteUp(fastify: FastifyInstance) { 9 | fastify.route({ 10 | method: "POST", 11 | url: "/vote/up/", 12 | handler: onVoteUp, 13 | schema: { 14 | body: S.object() 15 | .prop("messageId", S.string().required()) 16 | .valueOf(), 17 | }, 18 | }); 19 | 20 | async function onVoteUp( 21 | req: FastifyRequest<{ Body: VoteUpRequestBody }>, 22 | reply: FastifyReply 23 | ): Promise { 24 | const { messageId } = req.body; 25 | 26 | if (!messageId) { 27 | return reply.code(400).send({ 28 | error: "Bad Request", 29 | message: "Payload error: 'messageId' cannot be an empty string", 30 | }); 31 | } 32 | 33 | const session = fastify.neo4jDriver.session(); 34 | 35 | try { 36 | // store vote 37 | await session.run( 38 | ` 39 | MATCH (m:Message {id:$id}) 40 | SET m.vote = $vote, 41 | m.updated_at = datetime() 42 | `, 43 | { 44 | id: messageId, 45 | vote: 1, 46 | } 47 | ); 48 | 49 | return reply.view("partials/vote-success.hbs"); 50 | } catch (error) { 51 | fastify.log.error(`/vote/up/ Error: ${error}`); 52 | reply.status(500).send({ 53 | error: "Server error", 54 | message: "Server failed processing the request.", 55 | }); 56 | } finally { 57 | if (session) { 58 | session.close(); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import fastify, { FastifyInstance } from "fastify"; 2 | import Env from "@fastify/env"; 3 | import FastifyView from "@fastify/view"; 4 | import FastifyStatic from "@fastify/static"; 5 | import FastifyFormBody from "@fastify/formbody"; 6 | import AutoLoad from "@fastify/autoload"; 7 | import Sensible from "@fastify/sensible"; 8 | import S from "fluent-json-schema"; 9 | import { join } from "path"; 10 | import { loadPartials } from "./utils/partials"; 11 | import { initConfig, EnvConfig } from "./config"; 12 | import { initFeedbackDatabase } from "./services/database"; 13 | import { Driver } from "neo4j-driver"; 14 | 15 | declare module "fastify" { 16 | interface FastifyInstance { 17 | envConfig: EnvConfig; 18 | neo4jDriver: Driver; 19 | } 20 | } 21 | 22 | interface CustomOptions {} 23 | 24 | async function initServer() { 25 | const server = fastify({ 26 | logger: true, 27 | }); 28 | 29 | await server.register(Env, { 30 | dotenv: true, 31 | data: process.env, 32 | schema: S.object() 33 | .prop("OPENAI_API_KEY", S.string()) 34 | .prop("DATABASES", S.string()) 35 | .prop("FEEDBACK_DATABASE", S.string()) 36 | .prop("PROMPT_MAX_LENGTH", S.string()) 37 | .prop("PROMPT_MAX_DURATION_MS", S.string()) 38 | .prop("PORT", S.string()) 39 | .prop("HOST", S.string()) 40 | .valueOf(), 41 | }); 42 | 43 | server.decorate("envConfig", initConfig(process.env)); 44 | if(server.envConfig.FEEDBACK_DATABASE) { 45 | server.decorate("neo4jDriver", initFeedbackDatabase(server.envConfig)); 46 | } 47 | 48 | return server; 49 | } 50 | 51 | async function startServer(fastify: FastifyInstance, opts: CustomOptions) { 52 | try { 53 | // add static files for FE 54 | await fastify.register(FastifyStatic, { 55 | root: join(__dirname, "public"), 56 | prefix: "/public", 57 | }); 58 | 59 | // utility 60 | await fastify.register(Sensible); 61 | 62 | // required for htmx requests 63 | await fastify.register(FastifyFormBody); 64 | 65 | // enable templating engine 66 | await fastify.register(FastifyView, { 67 | engine: { 68 | handlebars: require("handlebars"), 69 | }, 70 | root: join(__dirname, "views"), 71 | includeViewExtension: true, 72 | layout: "./layouts/main", 73 | options: { 74 | partials: loadPartials(join(__dirname, "views/partials")), 75 | }, 76 | }); 77 | 78 | // autoload routes 79 | await fastify.register(AutoLoad, { 80 | dir: join(__dirname, "routes"), 81 | options: Object.assign({}, opts), 82 | }); 83 | 84 | await fastify.listen({ 85 | host: fastify.envConfig.HOST, 86 | port: fastify.envConfig.PORT, 87 | }); 88 | } catch (err) { 89 | fastify.log.error(err); 90 | process.exit(1); 91 | } 92 | } 93 | 94 | (async function () { 95 | const server = await initServer(); 96 | startServer(server, {}); 97 | })(); 98 | -------------------------------------------------------------------------------- /src/services/database.ts: -------------------------------------------------------------------------------- 1 | import neo4j from "neo4j-driver"; 2 | import { EnvConfig } from "../config"; 3 | 4 | export function initFeedbackDatabase(env: EnvConfig) { 5 | const feedbackDatabase = env.FEEDBACK_DATABASE; 6 | 7 | return neo4j.driver( 8 | feedbackDatabase.uri, 9 | neo4j.auth.basic(feedbackDatabase.username, feedbackDatabase.password) 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/database.ts: -------------------------------------------------------------------------------- 1 | import { DbConnectionData } from "../config"; 2 | 3 | export function parseClientDatabases(clientDatabases: string): DbConnectionData[] { 4 | let databases: any[]; 5 | 6 | try { 7 | databases = JSON.parse(clientDatabases); 8 | } catch { 9 | return []; 10 | } 11 | 12 | return databases.filter((database) => 13 | isValidDatabase(database) 14 | ); 15 | } 16 | 17 | function isValidDatabase(database: any) { 18 | return ( 19 | database && 20 | typeof database === "object" && 21 | typeof database.name === "string" && 22 | database.name.trim() !== "" && 23 | typeof database.uri === "string" && 24 | database.uri.trim() !== "" && 25 | typeof database.username === "string" && 26 | database.username.trim() !== "" && 27 | typeof database.password === "string" && 28 | database.password.trim() !== "" 29 | ); 30 | } -------------------------------------------------------------------------------- /src/utils/partials.ts: -------------------------------------------------------------------------------- 1 | import { join, parse } from "path"; 2 | import { readFileSync, readdirSync } from "fs"; 3 | 4 | interface PartialConfig { 5 | [partialName: string]: string; 6 | } 7 | 8 | export function loadPartials(folderPath: string): PartialConfig { 9 | const partialsFiles = readdirSync(folderPath); 10 | const partials: PartialConfig = {}; 11 | 12 | partialsFiles.forEach((file) => { 13 | const partialName = parse(file).name; 14 | partials[partialName] = "partials/" + file; 15 | }); 16 | 17 | return partials; 18 | } 19 | -------------------------------------------------------------------------------- /src/views/index.hbs: -------------------------------------------------------------------------------- 1 | {{>database-selector databases=databases}} 2 | {{>schema}} 3 | {{>prompt-input}} 4 | {{>last-prompts}} 5 | -------------------------------------------------------------------------------- /src/views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | {{#if layout}} 2 | 3 | 4 | 5 | Text2Cypher 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 |

Loading database schema. Please wait ...

19 |
20 | {{/if}} 21 | 22 | {{{body}}} 23 | 24 | {{#if layout}} 25 |
26 | 31 | 35 | 36 | 37 | 38 | {{/if}} -------------------------------------------------------------------------------- /src/views/partials/database-selector.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 17 | 18 |
19 | {{>settings}} 20 |
-------------------------------------------------------------------------------- /src/views/partials/last-prompts.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/partials/notification.hbs: -------------------------------------------------------------------------------- 1 |

{{text}}

-------------------------------------------------------------------------------- /src/views/partials/prompt-input.hbs: -------------------------------------------------------------------------------- 1 |
13 |
14 | 22 |
23 | 0/{{promptMaxlength}} 24 |
25 | 33 |
34 |
35 | 41 |
42 |
-------------------------------------------------------------------------------- /src/views/partials/prompt-output.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 |
{{cypher}}
6 |
7 | {{#if feedbackEnabled}} 8 | {{> vote cypher=cypher }} 9 | {{/if}} 10 |
11 |
12 |
13 |
{{answer}}
14 |
15 |
16 |
-------------------------------------------------------------------------------- /src/views/partials/schema.hbs: -------------------------------------------------------------------------------- 1 |
2 |
{{schema}}
3 |
-------------------------------------------------------------------------------- /src/views/partials/settings.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
7 | 8 |
9 |

Settings

10 |
11 | 12 |
13 |
14 |

15 | This settings override server config. Settings are saved to your local storage and are not saved on server. 16 |
17 | Use this for demo porpuses only. Delete data afterwards. 18 |

19 |
20 |
21 |
22 | 25 |
26 |
27 |
28 |
+ add database connection
29 |
30 | 38 |
39 |
40 |
41 |
-------------------------------------------------------------------------------- /src/views/partials/vote-success.hbs: -------------------------------------------------------------------------------- 1 |
2 | Thank you for your feedback! Try another prompt. 3 |
-------------------------------------------------------------------------------- /src/views/partials/vote-validate-dialog.hbs: -------------------------------------------------------------------------------- 1 | 2 |
3 |

Please correct the Cypher query

4 |
5 | 6 |
7 |
8 |
9 | 13 |
14 | 27 |
28 |
29 |
-------------------------------------------------------------------------------- /src/views/partials/vote-validate-success.hbs: -------------------------------------------------------------------------------- 1 |
2 | Your changes are sent! 3 |
4 | 5 |
6 |
-------------------------------------------------------------------------------- /src/views/partials/vote.hbs: -------------------------------------------------------------------------------- 1 |
2 | 16 | 30 |
31 | {{> vote-validate-dialog cypher=cypher }} 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "lib": ["es2018", "es2015", "es2017", "esnext", "DOM"], 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "removeComments": false, 9 | "downlevelIteration": true, 10 | "strict": true, 11 | "noImplicitAny": false, 12 | "strictNullChecks": false, 13 | "noImplicitThis": false, 14 | "alwaysStrict": true, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "moduleResolution": "node", 19 | "experimentalDecorators": true, 20 | "sourceMap": true, 21 | "types": ["web-streams-polyfill"], 22 | "skipLibCheck": true, 23 | "declaration": true 24 | }, 25 | "include": ["src/**/*.ts", "src/public/main.js"], 26 | "exclude": ["node_modules"], 27 | "outDir": "./dist", 28 | "rootDir": "./src" 29 | } 30 | --------------------------------------------------------------------------------