diff --git a/.gitignore b/.gitignore index 07d2252..f1cd5bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -out \ No newline at end of file +out +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f2ced7..d9c9a32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,6 @@ All notable changes to the "Code Assistant" extension will be documented in this file. -Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. - ## [0.0.2] - 2023-10-29 - code completion with `` separator diff --git a/README.md b/README.md index 178c3d8..14bcd9d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Code Assistant -A small personnal project to play with AI and VS Code. +A small personal project to play with AI and VS Code. Code assistant provides a bridge between coding LLM and your development environment. @@ -10,7 +10,7 @@ Code assistant provides a bridge between coding LLM and your development environ Chat with your source code through LLM. ## Requirements -A gRPC server following the instruction protocol has to be up. +LLM server implementing OpenAI stream API. ## Extension Settings This extension contributes the following settings: @@ -23,7 +23,7 @@ This extension contributes the following settings: ## Extension commands * `codeAssistant.clearChat`: clear current chat. * `codeAssistant.openChat`: sends prompt for current chat. -* `codeAssistant.infill`: use `` separator in selection for code completion. + ## Known Issues - general robustness regarding server connection diff --git a/package-lock.json b/package-lock.json index bd2f3c3..e77fab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { - "name": "hello-world", - "version": "0.0.1", + "name": "code-assistant", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "hello-world", - "version": "0.0.1", + "name": "code-assistant", + "version": "0.0.2", "dependencies": { "@grpc/grpc-js": "^1.9.5", "@grpc/proto-loader": "^0.7.10", "@vscode/webview-ui-toolkit": "^1.2.2", - "markdown-it": "^13.0.2" + "markdown-it": "^13.0.2", + "openai": "^4.22.1" }, "devDependencies": { "@types/mocha": "^10.0.2", @@ -787,6 +788,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz", "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==" }, + "node_modules/@types/node-fetch": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.9.tgz", + "integrity": "sha512-bQVlnMLFJ2d35DkPNjEPmd9ueO/rh5EiaZt2bhqiSarPjZIuIV6bPQVqcrEyvNo+AfTrRGVazle1tl597w3gfA==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/semver": { "version": "7.5.3", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", @@ -1016,6 +1026,17 @@ "react": ">=16.9.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1049,6 +1070,17 @@ "node": ">= 6.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1123,12 +1155,22 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1203,6 +1245,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1306,6 +1356,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1332,6 +1393,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1367,6 +1436,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/diff": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", @@ -1376,6 +1453,15 @@ "node": ">=0.3.1" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1636,6 +1722,14 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/exenv-es6": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/exenv-es6/-/exenv-es6-1.1.1.tgz", @@ -1781,6 +1875,44 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", @@ -1973,6 +2105,14 @@ "node": ">= 6" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2041,6 +2181,11 @@ "node": ">=8" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2321,6 +2466,16 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", @@ -2348,6 +2503,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2486,8 +2660,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/nanoid": { "version": "3.3.3", @@ -2507,6 +2680,43 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2525,6 +2735,25 @@ "wrappy": "1" } }, + "node_modules/openai": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.22.1.tgz", + "integrity": "sha512-Igk2FixXiEDQKkS3VJR0tTpO27O48mJqH4ztayATHTvcAmKqrIrYOjUBc7DrJcq+cKcQS5lTQalGZD05ySydHA==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "bin": { + "openai": "bin/cli" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -3089,6 +3318,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -3172,6 +3406,28 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 5641e68..243a1dd 100644 --- a/package.json +++ b/package.json @@ -33,57 +33,63 @@ "command": "codeAssistant.openChat", "title": "Open chat", "category": "Code Assistant" - }, - { - "command": "codeAssistant.infill", - "title": "Infill code", - "category": "Code Assistant" } ], "menus": { - "view/title": [ - { - "command": "codeAssistant.clearChat", - "group": "navigation", - "when": "view == codeAssistant.chatView" - } - ] - }, + "view/title": [ + { + "command": "codeAssistant.clearChat", + "group": "navigation", + "when": "view == codeAssistant.chatView" + } + ] + }, "configuration": { - "id": "codeAssistant", - "title": "Code Assistant", - "properties": { - "codeAssistant.server.address": { - "default": "localhost:50051", - "description": "LLM gRPC server address, :", - "type": "string", - "scope": "resource" - }, - "codeAssistant.prompt.selection": { - "default": "\nPlease answer knowing that the code selected is:\n{selection}", - "description": "Selection template for user prompt", - "type": "string", - "scope": "resource" - }, - "codeAssistant.prompt.system": { - "default": "As an expert in software engineering, your task is to provide concise and relevant answers.", - "description": "System for first prompt", - "type": "string", - "scope": "resource" - }, - "codeAssistant.prompt.user": { - "default": "The next answers are considered using the following code, delimited by <<>> tag. The user can ask any question regarding that code, and you shall answer any question about it:\n <<>>{sourceCode}<<>>", - "description": "User prompt to introduce source code, pasted server side with {sourceCode}.", - "type": "string", - "scope": "resource" - }, - "codeAssistant.prompt.assistant": { - "default": "Very well, I shall answer every question in the most accurate and concise way", - "description": "Simulated prompt answer for first user prompt. Plays as introduction to the chat with LLM.", - "type": "string", - "scope": "resource" - } + "id": "codeAssistant", + "title": "Code Assistant", + "properties": { + "codeAssistant.server.address": { + "default": "http://localhost:8000/v1", + "description": "LLM API address", + "type": "string", + "scope": "resource" + }, + "codeAssistant.server.key": { + "default": "EMPTY", + "description": "LLM API key", + "type": "string", + "scope": "resource" + }, + "codeAssistant.prompt.selection": { + "default": "\nPlease answer knowing that the code selected is:\n{selection}", + "description": "Selection template for user prompt", + "type": "string", + "scope": "resource" + }, + "codeAssistant.prompt.useSystem": { + "description": "Use system role to use", + "type": "boolean", + "scope": "resource" + }, + "codeAssistant.prompt.system": { + "default": "As an expert in software engineering, your task is to provide concise and relevant answers.", + "description": "System for first prompt", + "type": "string", + "scope": "resource" + }, + "codeAssistant.prompt.user": { + "default": "The next answers are considered using the following code, delimited by <<>> tag. The user can ask any question regarding that code, and you shall answer any question about it:\n <<>>{sourceCode}<<>>", + "description": "User prompt to introduce source code, pasted server side with {sourceCode}.", + "type": "string", + "scope": "resource" + }, + "codeAssistant.prompt.assistant": { + "default": "Very well, I shall answer every question in the most accurate and concise way", + "description": "Simulated prompt answer for first user prompt. Plays as introduction to the chat with LLM.", + "type": "string", + "scope": "resource" } + } } }, "scripts": { @@ -113,6 +119,7 @@ "@grpc/grpc-js": "^1.9.5", "@grpc/proto-loader": "^0.7.10", "@vscode/webview-ui-toolkit": "^1.2.2", - "markdown-it": "^13.0.2" + "markdown-it": "^13.0.2", + "openai": "^4.22.1" } } diff --git a/src/assistant/Assistant.ts b/src/assistant/Assistant.ts new file mode 100644 index 0000000..3d25f1a --- /dev/null +++ b/src/assistant/Assistant.ts @@ -0,0 +1,33 @@ +import { Conversation } from "./Conversation"; +import OpenAI from "openai"; + +/** + * \brief class defining a link to the assistant and therefore + * establishes communication to the llm for requests + * unique interface, several chunks and only one if not streaming + */ +export class Assistant { + + private _communication : OpenAI; + + constructor(apiBase: string, apiKey: string) { + this._communication = new OpenAI({apiKey: apiKey, baseURL: apiBase}); + } + + /* + * sends a request to the communication layout + * returns an asynchronous promise for batch and stream. + * no difference is made. + */ + async request(conversation: Conversation) { + const models = await this._communication.models.list(); + const model = models.data[0].id; + const answer = this._communication.chat.completions.create({ + messages: conversation.unfold() as Array, + model: model, + stream: true + }); + + return answer; + } +} diff --git a/src/assistant/Conversation.ts b/src/assistant/Conversation.ts new file mode 100644 index 0000000..66e851a --- /dev/null +++ b/src/assistant/Conversation.ts @@ -0,0 +1,46 @@ +import { StringFormatter } from "../utilities/StringFormatter"; + +export class Conversation { + private _chat : {role: string, content: string, keys: any}[]; + + constructor(){ + this._chat = []; + } + + add(entry: {role: string, content: string, keys: any}) : void { + this._chat.push({role: entry.role, content: entry.content, keys: entry.keys}); + } + + takeLast() : {role: string, content: string, keys: any} | undefined { + return this._chat.pop(); + } + + getAll(): {role: string, content: string, keys: any}[] { + return this._chat; + } + + unfold(): {role: string, content: string}[] { + /* formats given the different keys, transforming from content keys to content only */ + const isEmpty = (value: any) => {return !value || Object.keys(value).length === 0;}; + + const format = (value: {role: string, content: string, keys: any}) : {role: string, content: string} => { + if (!isEmpty(value.keys)) { + const args = value.keys; + const formatter = new StringFormatter(value.content, value.keys); + return {role : value.role, content : formatter.format()}; + } else { + return {role : value.role, content : value.content}; + } + }; + + return this._chat.map(format); + } + + clear() { + this._chat = []; + } + + append(other: Conversation) { + this._chat = this._chat.concat(other._chat); + } +} diff --git a/src/extension.ts b/src/extension.ts index 5153e4c..082f56f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,13 +8,14 @@ import { ChatViewProvider } from './panels/ChatView'; export function activate(context: vscode.ExtensionContext) { const configuration = vscode.workspace.getConfiguration(); const target = configuration.get('codeAssistant.server.address'); + const key = configuration.get('codeAssistant.server.key'); // no server configuration, no extension. if (!target) { return; } - const provider = new ChatViewProvider(context.extensionUri, target); + const provider = new ChatViewProvider(context.extensionUri, target, key ? key : ""); context.subscriptions.push( vscode.window.registerWebviewViewProvider(ChatViewProvider.viewType, provider)); @@ -26,9 +27,6 @@ export function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand("codeAssistant.openChat", () => { provider.openChat(); }), - vscode.commands.registerCommand("codeAssistant.infill", () => { - provider.infill(); - }), ); } diff --git a/src/panels/ChatView.ts b/src/panels/ChatView.ts index cc8a841..d28b0c2 100644 --- a/src/panels/ChatView.ts +++ b/src/panels/ChatView.ts @@ -1,43 +1,31 @@ import * as vscode from "vscode"; import { getUri } from "../utilities/getUri"; import { getNonce } from "../utilities/getNonce"; -import { StringFormatter} from "../utilities/StringFormatter"; +import { Assistant } from "../assistant/Assistant"; +import { Conversation } from "../assistant/Conversation"; -var grpc = require('@grpc/grpc-js'); -var protoLoader = require('@grpc/proto-loader'); export class ChatViewProvider implements vscode.WebviewViewProvider { + public static readonly viewType = 'codeAssistant.chatView'; + private _view?: vscode.WebviewView; - private _conversation: any; - private _lastResponse: string; - private _client; + private _conversation = new Conversation(); + private _lastResponse = ""; + private _assistant: Assistant; + constructor( private readonly _extensionUri: vscode.Uri, - target: string + target: string, + key: string ) { - this._conversation = []; - this._lastResponse = ""; - - const PROTO_PATH = vscode.Uri.joinPath(_extensionUri, "out", "instruct.proto").fsPath; - - const packageDefinition = protoLoader.loadSync( - PROTO_PATH, - { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true - }); - - const instProto = grpc.loadPackageDefinition(packageDefinition).instruct; - this._client = new instProto.Instruction(target, grpc.credentials.createInsecure()); + this._assistant = new Assistant(target, key); } + public resolveWebviewView( webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, @@ -63,7 +51,19 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { { this._sendToChat(data.value); break; - } + } + case 'insertText': + { + const editor = vscode.window.activeTextEditor; + if (editor) { + // Insérer du texte à la position du curseur actuel + editor.edit(editBuilder => { + if (editor) { + editBuilder.insert(editor.selection.active, data.text); + } + }); + } + } } }); } @@ -71,7 +71,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { public clearChat() { // reset conversation - this._conversation = []; + this._conversation.clear(); // and send refresh order this._view?.webview.postMessage({ type: 'chatHistory', history: this._conversation }); @@ -89,121 +89,85 @@ export class ChatViewProvider implements vscode.WebviewViewProvider { }); } - public infill() { - // gets the selection, splits around , sends out request and updates selection range - const activeTextEditor = vscode.window.activeTextEditor; - if (!activeTextEditor) { - return; - } - let selected = activeTextEditor.selection; - if (selected.isEmpty) { - return; - } - - const selectedText = activeTextEditor.document.getText(selected); - const splitText = selectedText?.split(""); - - let call = this._client.fill({ prefix: splitText[0], suffix: splitText[0]}); - let dataCallBack = (response: any) => { - this._lastResponse = response.whole_text; - activeTextEditor.edit(editBuilder => { - // update current selection - selected = activeTextEditor.selection; - editBuilder.replace(selected, this._lastResponse); - }); - }; - - let validateCallBack = () => { - activeTextEditor.edit(editBuilder => { - // update current selection - selected = activeTextEditor.selection; - - editBuilder.replace(selected, this._lastResponse); - - this._lastResponse = ""; - }); - - call.end(); - }; - - call.on('data', dataCallBack); - call.on('end', validateCallBack); - } - - private _sendToChat(prompt: string) { + private async _sendToChat(prompt: string) { // retrieve selection const selected = vscode.window.activeTextEditor?.selection; const fullCode = vscode.window.activeTextEditor?.document; const configuration = vscode.workspace.getConfiguration(); + const useSystem = configuration.get('codeAssistant.prompt.useSystem'); const systemPrompt = configuration.get('codeAssistant.prompt.system'); const userPrompt = configuration.get('codeAssistant.prompt.user'); - const assistantPrompt = configuration.get('codeAssistant.prompt.user'); + const assistantPrompt = configuration.get('codeAssistant.prompt.assistant'); const selectionTemplate = configuration.get('codeAssistant.prompt.selection'); if (selected && !selected.isEmpty) { prompt += selectionTemplate; } - this._conversation.push({ + this._conversation.add({ role: "user", content: prompt, keys: { selection: fullCode?.getText(selected) } - }); - - const firstPromps = [{ - role: 'system', - content: systemPrompt, - }, - { - role: 'user', - content: userPrompt, - keys: { - sourceCode: fullCode?.getText() - } - }, - { - role: 'assistant', - content: assistantPrompt, - keys:{} - } - ]; + } + ); - const conversationToSend = firstPromps.concat(this._conversation); + let conversationToSend = new Conversation(); - let call = this._client.generate({ instruction: conversationToSend }); - let dataCallBack = (response: any) => { - this._lastResponse = response.generation.content; - this._view?.webview.postMessage({ type: 'chatUpdate', content: this._lastResponse }); + if (systemPrompt) { + conversationToSend.add({ + role: useSystem ? 'system' : 'user', + content: systemPrompt, + keys: {} + }); + } - }; + if (!useSystem && assistantPrompt) { + conversationToSend.add({ + role: 'assistant', + content: "", + keys: {} + }); + } - let registerResponseCallBack = () => { - this._conversation.pop(); - this._conversation.push({ role: 'assistant', content: this._lastResponse }); - call.end(); + if (userPrompt && assistantPrompt) { + conversationToSend.add({ + role: 'user', + content: userPrompt, + keys: { + sourceCode: fullCode?.getText() + } + }); - this._lastResponse = ""; - }; + conversationToSend.add({ + role: 'assistant', + content: "", + keys: {} + }); + } + + conversationToSend.append(this._conversation); + + const answer = await this._assistant.request(conversationToSend); - call.on('data', dataCallBack); - call.on('end', registerResponseCallBack); - // preparing the history - this._conversation.push({ role: 'assistant', content: "" }); - - // converting selections in the display - const developpedConversiontion = this._conversation.map((row: any) => { - let developpedContent = row.content; - if (row.keys && row.keys.selection) { - developpedContent = new StringFormatter(row.content, {selection: `\`\`\`${row.keys.selection}\`\`\``}).format(); + this._conversation.add({ role: 'assistant', content: "", keys: {} }); + + for await (const chunk of answer) { + if (chunk.choices[0].delta.content) { + this._lastResponse += chunk.choices[0].delta.content; } - return {role: row.role, content: developpedContent}; - }); - this._view?.webview.postMessage({ type: 'chatHistory', history: developpedConversiontion }); + this._view?.webview.postMessage({ type: 'chatUpdate', content: this._lastResponse }); + } + + this._conversation.takeLast(); + this._conversation.add({ role: 'assistant', content: this._lastResponse, keys: {} }); + this._lastResponse = ""; + + this._view?.webview.postMessage({ type: 'chatHistory', history: this._conversation.unfold() }); } diff --git a/src/protos/instruct.proto b/src/protos/instruct.proto deleted file mode 100644 index d2e37a4..0000000 --- a/src/protos/instruct.proto +++ /dev/null @@ -1,39 +0,0 @@ -syntax = "proto3"; - -package instruct; - -// The instruction service definition. -service Instruction { - // instruct LLM - rpc generate (InstructionRequest) returns (stream InstructionReply); - rpc fill (FillRequest) returns (stream FillReply); -} - -// dictionnary KV - -// The instruction containing the context, role and instruciton -message Message { - string role = 1; - string content = 2; - map keys = 3; -} - -// requelst for instruction (several instruction messages) -message InstructionRequest { - repeated Message instruction = 1; -} - -// The response message containing the reply -message InstructionReply { - Message generation = 1; -} - -// To fill request -message FillRequest { - string prefix = 1; - string suffix = 2; -} - -message FillReply { - string whole_text = 1; -} diff --git a/src/utilities/StringFormatter.ts b/src/utilities/StringFormatter.ts index 393c0d2..cf16d1a 100644 --- a/src/utilities/StringFormatter.ts +++ b/src/utilities/StringFormatter.ts @@ -4,7 +4,7 @@ * A class for formatting strings with placeholders. * * @author DScudeler -* @version 0.0.1 +* @version 0.0.2 */ export class StringFormatter { /** @@ -22,14 +22,9 @@ export class StringFormatter { * @param format The format string. * @param args The arguments for the format string. */ - constructor(format: string, ...args: any[]) { + constructor(format: string, args: any) { this._format = format; - this._args = {}; - for (let i = 0; i < args.length; i++) { - const keys = Object.keys(args[i]); - const values = Object.values(args[i]); - this._args[keys[0]] = values[0]; - } + this._args = args; } /** diff --git a/src/webview/main.css b/src/webview/main.css index 5ecb100..27fafdd 100644 --- a/src/webview/main.css +++ b/src/webview/main.css @@ -21,5 +21,39 @@ div.assistant-text { position: relative; left: 10%; width: 90%; - background-color: rgb(65, 65, 65);; + background-color: rgb(65, 65, 65); +} + +code { + color: rgb(180, 117, 0); + background-color: rgb(65, 65, 65); +} + +div.code-container { + position: relative; + display: inline-block; +} + +.copy-button { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: none; + padding: 5px 10px; + background-color: #007bff; + color: #fff; + border: none; + cursor: pointer; + border-radius: 5px; +} + +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); /* Adjust transparency here */ + display: none; } diff --git a/src/webview/main.ts b/src/webview/main.ts index b502901..f89b385 100644 --- a/src/webview/main.ts +++ b/src/webview/main.ts @@ -42,12 +42,73 @@ function updateChatHistory(chatHistory: Array<{ role: string, content: string }> content.className = "assistant-text"; } content.innerHTML = md.render(content.textContent); + + insertCopyButton(content); conversationList.appendChild(content); } } } +function insertCopyButton(chatElement: HTMLDivElement) { + // Get all the code sections inside the provided element + const codeSections = chatElement.querySelectorAll('code'); + + // Iterate through each code section + codeSections.forEach(code => { + // Create a copy button + const copyButton = document.createElement('button'); + copyButton.textContent = 'Copy'; + copyButton.classList.add('copy-button'); + + // Create an overlay + const overlay = document.createElement('div'); + overlay.classList.add('overlay'); + + // Create a container div for the code section and the copy button + const containerDiv = document.createElement('div'); + containerDiv.classList.add('code-container'); + + // Append the code section, the copy button, and the overlay to the container + const parent = code.parentNode; + + containerDiv.appendChild(code.cloneNode(true)); + containerDiv.appendChild(overlay); + containerDiv.appendChild(copyButton); + + // Append the container to the parent of the code section + parent?.replaceChild(containerDiv, code); + + // Add event listener for mouseover to show the copy button and overlay + containerDiv.addEventListener('mouseover', () => { + copyButton.style.display = 'block'; + overlay.style.display = 'block'; + }); + + // Add event listener for mouseout to hide the copy button and overlay + containerDiv.addEventListener('mouseout', () => { + copyButton.style.display = 'none'; + overlay.style.display = 'none'; + }); + + // Add event listener for click to copy the code + copyButton.addEventListener('click', () => { + const textToCopy = code.textContent; + navigator.clipboard.writeText(textToCopy ? textToCopy : "").then(() => { + alert('Code copied successfully!'); + }).catch(err => { + console.error('Failed to copy code: ', err); + }); + }); + + // dbl click sends to current editor + copyButton.addEventListener('dblclick', () => { + const textToCopy = code.textContent; + vscode.postMessage({type: 'insertText', text: textToCopy}); + }); + }); +} + // sets chat result function updateChatResponse(text: string) { const div = document.querySelector('.conversation-list');