diff --git a/.gitignore b/.gitignore index b2acde3e23..6be08bc00d 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,4 @@ hazel.opam.locked.old # Code coverage _coverage/ node_modules/** +src/llm-tests/node_modules/** \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4e6b2e5b52..fd3d898aef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,32 @@ "ninja-keys": "^1.2.2" }, "devDependencies": { - "@types/node": "^22.14.0", + "@types/js-yaml": "^4.0.9", + "@types/minimist": "^1.2.5", + "@types/node": "^24.3.0", "esbuild": "^0.25.1", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "playwright": "^1.55.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.2", "vite": "^6.4.1", "vite-plugin-static-copy": "^2.3.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild-plugins/node-resolve": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.2.2.tgz", @@ -435,6 +455,34 @@ "node": ">=18" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@lit-labs/ssr-dom-shim": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz", @@ -779,6 +827,34 @@ "win32" ] }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -786,14 +862,28 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { - "version": "22.14.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", - "integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.10.0" } }, "node_modules/@types/resolve": { @@ -808,6 +898,32 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/algebrite": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/algebrite/-/algebrite-1.4.0.tgz", @@ -831,6 +947,20 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", @@ -891,6 +1021,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -908,6 +1045,16 @@ } } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/esbuild": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", @@ -1138,6 +1285,19 @@ "node": ">=0.12.0" } }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -1182,6 +1342,13 @@ "@types/trusted-types": "^2.0.2" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1206,6 +1373,16 @@ "node": ">=8.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1291,6 +1468,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -1489,16 +1713,74 @@ "node": ">=8.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", "dev": true, "license": "MIT" }, @@ -1512,6 +1794,13 @@ "node": ">= 10.0.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", @@ -1674,6 +1963,16 @@ "@rollup/rollup-win32-x64-msvc": "4.39.0", "fsevents": "~2.3.2" } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } } } } diff --git a/package.json b/package.json index 40c7b55657..fbb398e05d 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,15 @@ "algebrite": "^1.4.0" }, "devDependencies": { - "@types/node": "^22.14.0", + "@types/node": "^24.3.0", "esbuild": "^0.25.1", + "@types/js-yaml": "^4.0.9", + "@types/minimist": "^1.2.5", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "playwright": "^1.55.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.2", "vite": "^6.4.1", "vite-plugin-static-copy": "^2.3.0" } diff --git a/src/haz3lcore/TyDi/ErrorPrint.re b/src/haz3lcore/TyDi/ErrorPrint.re index ab40c9e4b9..c54046e5e4 100644 --- a/src/haz3lcore/TyDi/ErrorPrint.re +++ b/src/haz3lcore/TyDi/ErrorPrint.re @@ -216,3 +216,8 @@ let all = (info_map: Statics.Map.t): list(string) => { } ); }; + +// let subtree = (info: Info.t): list(string) => { +// let subtree = HighLevelNodeMap.subtree_of(info); +// all(subtree); +// }; diff --git a/src/haz3lcore/ai_assistant_core/AssistantModel.re b/src/haz3lcore/ai_assistant_core/AssistantModel.re new file mode 100755 index 0000000000..569d343e88 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/AssistantModel.re @@ -0,0 +1,315 @@ +open Util; + +/* + The main model for the AI assistant. + */ + +[@deriving (show({with_path: false}), sexp, yojson)] +type system = + // Issue that we catch and inform the user about. + // Do not send this to the model and display to user as error message + | InternalError + // The system prompt for the model. + // Send this to the model. + // Display to user as expandable/collapsable system message. + | AssistantPrompt + // Agent view of the codebase. + // Only one should ever exist in a chat at a time. + | AgentView + // The agent's task/todo list. + // Only one should ever exist in a chat at a time. + | AgentWorkbench; +// // Tool response from our backend. +// | ToolResponse; + +// Role of the entity sending the message. +// This is kept separate from the OpenRouter.role type, +// as we need to keep track of the system role for display purposes. +// (AssistantModel.System(InternalError) != OpenRouter.System) +// (AssistantModel.System(AssistantPrompt) != OpenRouter.System, an example +// of this is sketch displays. The only thing that is an OpenRouter.System type is +// the initial prompt itself.) +[@deriving (show({with_path: false}), sexp, yojson)] +type role = + | System(system) + | User + | Assistant + | Tool; + +let string_of_role = + fun + | System(AssistantPrompt) => "System" + | System(InternalError) => "Error" + | System(AgentView) => "Agent View" + | System(AgentWorkbench) => "Todo List" + | User => "User" + | Assistant => "Assistant" + | Tool => "Tool"; + +// We currently parse code blocks out here +// In the future we could move this to have the Omd module handle this +[@deriving (show({with_path: false}), sexp, yojson)] +type block_kind = + | Text(string) + | Code(Segment.t); + +// The displayable content of a message. This is here mainly to cache it +// in storage, avoiding runtime hindrances from parsing the content on the fly. +[@deriving (show({with_path: false}), sexp, yojson)] +type display = { + displayable_content: list(block_kind), + raw_content: string, + collapsed: bool, +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type sketch_snapshot = option(Zipper.t); + +// A coupling of a message sent to the LLM and the displayable content of the message. +[@deriving (show({with_path: false}), sexp, yojson)] +type message = { + // It may be the case we don't want to send a message to the LLM + // E.g. Tool descriptions (user-facing, describing what the agent did) + content: option(OpenRouter.message), + // It may be the case we don't want to display a message to the user + // E.g. Empty LLM responses/responses that only contain tool calls (who's text content is empty) + display: option(display), + role, + sketch_snapshot, + tool_calls: list(unit), +}; + +// A chat is a collection of messages, attached to an ID +// We also include a timestamp, a descriptor, and a loading dots flag for stylistic purposes. +[@deriving (show({with_path: false}), sexp, yojson)] +type chat = { + messages: list(message), + id: Id.t, + descriptor: string, + timestamp: float, + context_usage: int, + awaiting_response: bool, + composition_model: CompositionAgentWorkbench.Model.t, +}; + +// We save the history of past chats as a hash map with chat IDs as keys. +[@deriving (show({with_path: false}), sexp, yojson)] +type chat_history = { + // History logs of past chats stored as hash maps with chat IDs as keys + past_tutor_chats: Id.Map.t(chat), + past_suggestion_chats: Id.Map.t(chat), + past_composition_chats: Id.Map.t(chat), +}; + +// We need to keep track of the chats which the user currently has active in each mode. +[@deriving (show({with_path: false}), sexp, yojson)] +type current_chats = { + // Current active chat IDs for each mode + curr_tutor_chat: Id.t, + curr_suggestion_chat: Id.t, + curr_composition_chat: Id.t, +}; + +// A record of the external API information, typically set in the settings menu. +[@deriving (show({with_path: false}), sexp, yojson)] +type external_api_info = { + available_models: list(OpenRouter.model_info), + set_model_info: OpenRouter.model_info, + api_key: string, +}; + +// We cache these to avoid runtime hindrances from parsing the content on the fly. +// Pitfall: If prompt(s) ever change, must do hard reset of Hazel/clear local storage. +[@deriving (show({with_path: false}), sexp, yojson)] +type init_prompt_data = { + init_tutor_chat: chat, + init_composition_chat: chat, + init_suggestion_chat_basic: chat, + init_suggestion_chat_cot: chat, +}; + +// The AssistantModel type houses the current active chats, the history of past chats, +// the external API information, and the initial prompt data. +// The loop parameter is used exclusively for the task completion mode... +// there is likely a much better way to do this. +[@deriving (show({with_path: false}), sexp, yojson)] +type t = { + // Uuids of the currently active chats in each mode + current_chats, + // Collection of past chats of each mode, stored as a hash map with chat IDs as keys + chat_history, + // Information for OpenRouter API + external_api_info, + // Loading in and parsing the prompts is an expensive operation, so we perform this eagerly, + // before the user sends their first request. This is currently done when the user sets an API key. + init_prompt_data, +}; + +// Allow for the displaying of chats in chronological order. +let sorted_chats = (chat_map: Id.Map.t(chat)): list(chat) => { + chat_map + |> Id.Map.bindings + |> List.map(((_, chat)) => chat) + |> List.sort((a, b) => int_of_float(b.timestamp -. a.timestamp)); +}; + +// --- Helper Functions --- + +let get_messages_content = + (messages: list(message)): list(OpenRouter.message) => { + List.filter_map(message => message.content, messages); +}; + +let get_messages_display = (messages: list(message)): list(display) => { + List.filter_map(message => message.display, messages); +}; + +let mk_mode_prompt = (~mode: AssistantSettings.mode): OpenRouter.message => { + let prompt = + switch (mode) { + | HazelTutor => InitPrompts.mk_tutor() + | CodeSuggestion => InitPrompts.mk_suggestion("code_suggestion", false) + | Composition => InitPrompts.mk_composition() + }; + prompt; +}; + +let mk_message_display = (~content: string): display => { + { + displayable_content: [Text(content)], + raw_content: content, + collapsed: true, + }; +}; + +let init_chat = (mode: AssistantSettings.mode): chat => { + let init_message = mk_mode_prompt(~mode); + { + messages: [ + { + content: Some(init_message), + display: Some(mk_message_display(~content=init_message.content)), + role: System(AssistantPrompt), + sketch_snapshot: None, + tool_calls: [], + }, + ], + id: Id.mk(), + descriptor: "", + timestamp: JsUtil.timestamp(), + context_usage: 0, + awaiting_response: false, + composition_model: CompositionAgentWorkbench.Utils.MainUtils.init(), + }; +}; + +let new_chat = (model: t, mode: AssistantSettings.mode): chat => { + let init_message = + switch (mode) { + | HazelTutor => model.init_prompt_data.init_tutor_chat.messages + | CodeSuggestion => + model.init_prompt_data.init_suggestion_chat_basic.messages + | Composition => model.init_prompt_data.init_composition_chat.messages + }; + { + messages: init_message, + id: Id.mk(), + descriptor: "", + timestamp: JsUtil.timestamp(), + context_usage: 0, + awaiting_response: false, + composition_model: CompositionAgentWorkbench.Utils.MainUtils.init(), + }; +}; + +let add_chat_to_history = + (chat: chat, history: Id.Map.t(chat)): Id.Map.t(chat) => + Id.Map.add(chat.id, chat, history); + +let init = (): t => { + let (init_tutor_chat, init_suggestion_chat, init_composition_chat) = ( + init_chat(HazelTutor), + init_chat(CodeSuggestion), + init_chat(Composition), + ); + { + init_prompt_data: { + init_tutor_chat, + init_composition_chat, + init_suggestion_chat_basic: init_suggestion_chat, + init_suggestion_chat_cot: init_suggestion_chat, + }, + current_chats: { + curr_tutor_chat: init_tutor_chat.id, + curr_suggestion_chat: init_suggestion_chat.id, + curr_composition_chat: init_composition_chat.id, + }, + chat_history: { + past_tutor_chats: add_chat_to_history(init_tutor_chat, Id.Map.empty), + past_suggestion_chats: + add_chat_to_history(init_suggestion_chat, Id.Map.empty), + past_composition_chats: + add_chat_to_history(init_composition_chat, Id.Map.empty), + }, + external_api_info: { + available_models: [], + set_model_info: { + id: "n/a", + name: "n/a", + context_length: 0, + pricing: { + prompt: "n/a", + completion: "n/a", + }, + }, + api_key: "", + }, + }; +}; + +// We defer true initialization of the assistant model until the user opens the chat interface. +let null_model = (): t => { + let null_chat = { + messages: [], + id: Id.invalid, + descriptor: "Please set an API key", + timestamp: JsUtil.timestamp(), + context_usage: 0, + awaiting_response: false, + composition_model: CompositionAgentWorkbench.Utils.MainUtils.init(), + }; + { + init_prompt_data: { + init_tutor_chat: null_chat, + init_composition_chat: null_chat, + init_suggestion_chat_basic: null_chat, + init_suggestion_chat_cot: null_chat, + }, + current_chats: { + curr_tutor_chat: null_chat.id, + curr_suggestion_chat: null_chat.id, + curr_composition_chat: null_chat.id, + }, + chat_history: { + past_tutor_chats: add_chat_to_history(null_chat, Id.Map.empty), + past_suggestion_chats: add_chat_to_history(null_chat, Id.Map.empty), + past_composition_chats: add_chat_to_history(null_chat, Id.Map.empty), + }, + external_api_info: { + available_models: [], + set_model_info: { + id: "n/a", + name: "n/a", + context_length: 0, + pricing: { + prompt: "n/a", + completion: "n/a", + }, + }, + api_key: "", + }, + }; +}; + +[@deriving (show({with_path: false}), yojson, sexp)] +type model = t; diff --git a/src/haz3lcore/ai_assistant_core/AssistantModes.re b/src/haz3lcore/ai_assistant_core/AssistantModes.re new file mode 100644 index 0000000000..086d592418 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/AssistantModes.re @@ -0,0 +1,325 @@ +open Util; +open Language; +open Language.Statics; + +/* + Modules organizing out functionalities for each mode the AI assistant can operate in. + */ + +module Completion = { + /* + --- Completion Mode --- + LLM-suggested hole completions, based strongly on https://hazel.org/papers/chatlsp-oopsla2024.pdf + */ + let get_static_context = + ( + expected_type: bool, + relevant_ctx: bool, + ci: Info.t, + hole_label: string, + ) + : list(string) => + switch (ci) { + | InfoExp({ana, ctx, _}) + | InfoPat({ana, ctx, _}) => + let expected = RelevantTypes.get(ctx, ana, hole_label); + let relevant = RelevantValues.get(ctx, ana); + (expected_type ? ["expected_ty: " ++ expected] : []) + @ (relevant_ctx ? ["relevant_ctx:\n " ++ relevant] : []); + | InfoTyp(_) + | InfoTPat(_) + | Secondary(_) => [] + }; + + let mk_ctx_prompt = + ( + options: InitPrompts.Options.t, + ci: Info.t, + sketch: Segment.t, + hole_label: string, + ) + : OpenRouter.message => + OpenRouter.mk_user_msg( + String.concat( + "\n", + ["sketch: ```" ++ ErrorPrint.Print.seg(~holes="?", sketch) ++ "```"] + @ get_static_context( + options.expected_type, + options.relevant_ctx, + ci, + hole_label, + ), + ), + ); + + let add_suggestion = + ( + ~response: string, + ~tile: Id.t, + ~schedule_action: Editor.Update.t => unit, + ) => { + let actions = [ + Action.Select(Tile(Id(tile, Direction.Left))), + Action.Buffer(Set(LLM(response))), + ]; + // Apply each action in sequence + List.iter(action => {schedule_action(action)}, actions); + }; + + module ErrorRound = { + open OptUtil.Syntax; + module StringSet = Set.Make(String); + + let get_parse_errs = + (sketch_z: Zipper.t, completion: string): Result.t(Zipper.t, string) => + //NOTE: This function is pretty basic; reporting approach could be improved + /* For now we required that the completion be complete in-itself: */ + switch (Parser.to_zipper(~zipper_init=Zipper.init(), completion)) { + | None => Error("Undocumented parse error, no feedback available") + | Some(completion_z) => + switch (Zipper.local_backpack(completion_z)) { + | [_, ..._] as orphans => + let orphans = + List.map( + (tile: Tile.t) => + String.concat("", Tile.effective_label(tile)), + orphans, + ); + Error( + "The parser has detected unmatched delimiters. (The presence of a '=>' in the list likely indicates that a '->' was mistakingly used in a case expression). Unmatched delimiters: " + ++ String.concat(", ", orphans), + ); + | [] => + let segment = Zipper.zip(completion_z); + switch ( + { + let* sketch_z = Destruct.go(Left, sketch_z); + let+ sketch_z = Destruct.go(Left, sketch_z); + Zipper.insert_segment(sketch_z, segment); + } + ) { + | None => Error("Undocumented parse error, no feedback available") + | Some(completion_z) => Ok(completion_z) + }; + } + }; + + let statics_of_exp_zipper = + (init_ctx: Ctx.t, z: Zipper.t): (Info.exp, Statics.Map.t) => + Statics.uexp_to_info_map( + ~ctx=init_ctx, + ~ancestors=[], + MakeTerm.from_zip_for_sem(z).term, + Id.Map.empty, + ~duplicates=[], + ~expected_labels=None, + ~label_sort=false, + ); + + let mk_report = (ctx: Ctx.t, z: Zipper.t, reply: string): ErrorPrint.t => + // TODO: Currently this only works in expression position + switch (get_parse_errs(z, reply)) { + | Error(err) => ParseError(err) + | Ok(full_z) => + let (_, info_map) = statics_of_exp_zipper(ctx, z); + let static_errs_sketch = ErrorPrint.all(info_map); + let (_, info_map) = statics_of_exp_zipper(ctx, full_z); + let static_errs_full = ErrorPrint.all(info_map); + if (List.length(static_errs_full) == 0) { + NoErrors; + } else { + let sketch_errs = StringSet.of_list(static_errs_sketch); + let new_errs = + List.filter( + err => !StringSet.mem(err, sketch_errs), + static_errs_full, + ); + if (List.length(new_errs) == 0) { + NoErrors; + } else { + StaticErrors(new_errs); + }; + }; + }; + + let mk_reply = + (ci: Info.t, sketch_z: Zipper.t, reply: string): option(string) => { + // TODO: Currently this only works in expression position + let wrap = (intro, errs) => + [intro] + @ errs + @ [ + "Please try to address the error(s) by updating your previous code suggestion", + "Please respond ONLY with the update suggestion", + ] + |> String.concat("\n"); + let error_report = mk_report(Info.ctx_of(ci), sketch_z, reply); + switch (error_report) { + | NoErrors => None + | ParseError(err) => + Some(wrap("The following parse error occured:", [err])) + | StaticErrors(errs) => + Some(wrap("The following static errors were discovered:", errs)) + }; + }; + }; + + let check_req = + ( + ~schedule_action: AssistantUpdateAction.t => unit, + ~schedule_setting: AssistantSettings.action => unit, + ~z: Zipper.t, + ~chat_id: Id.t, + ) + : unit => { + let caret = z.caret; + let send_message = (tile_id, advanced_reasoning) => { + schedule_setting(AssistantSettings.SwitchMode(CodeSuggestion)); + schedule_action( + AssistantUpdateAction.SendMessage( + Completion(Request(tile_id, advanced_reasoning)), + None, + chat_id, + ), + ); + }; + + // Check if user just typed ?? + switch (caret, Zipper.neighbor_tokens(z)) { + | (Outer, (_, Some("??"))) + | (Outer, (Some("??"), _)) => + let tileId = Option.get(Indicated.index(z)); + let advanced_reasoning = false; + send_message(tileId, advanced_reasoning); + | (Outer, (_, Some("?a"))) + | (Outer, (Some("?a"), _)) => + let tileId = Option.get(Indicated.index(z)); + let advanced_reasoning = true; + send_message(tileId, advanced_reasoning); + | _ => () + }; + }; +}; + +module Composition = { + /* + --- Composition Mode --- + LLM-based agentic code sysnthesis. Differs from code completion in that it can + navigate the program structure, and perform more complex, multi-step edits. + */ + let max_tool_calls = 40; + + // Prompt with appropriate AST context for each message. + // The default information as of now is as follows: + // + // Current node: + // Parent node: + // Children nodes: [, , ...] + // Static errors: + let mk_structured_code_map_prompt = + (z: Zipper.t, info_map: Id.Map.t(Info.t)) + : (OpenRouter.message, AssistantModel.display) => { + let sketch_snapshot = + "\n```" + ++ CompositionView.Public.print(~z, ~info_map) + ++ "```\n\n"; + let static_errors = ErrorPrint.all(info_map); + let static_errors_str = + "\n\n" + ++ ( + switch (static_errors) { + | [] => "No static errors found in the program." + | _ => String.concat(", ", static_errors) + } + ) + ++ "\n\n"; + let res = sketch_snapshot ++ static_errors_str; + ( + OpenRouter.mk_user_msg(res), + { + displayable_content: [Text(res)], + raw_content: res, + collapsed: true, + }, + ); + }; + + let mk_structure_edit_msg = (~tool_call: OpenRouter.tool_call): string => + // AddToolLabel_3.0: what should the text content of this tool call to the user be? + // (not to the llm, that is the string returned in AssistantModes.Composition.apply_action) + try({ + let tool_name = tool_call.tool_name; + let args = tool_call.args; + let action = CompositionUtils.Public.action_of(~tool_name, ~args); + let _enclose_in_backticks = (str: string) => "```" ++ str ++ "```"; + switch (action) { + | Action(action) => + "Agent called tool: " ++ CompositionUtils.Public.string_of(action) + | Failure(s) => + "Agent called tool: " + ++ tool_call.tool_name + ++ "\nBut a failure occured: " + ++ s + }; + }) { + | Failure(err) => + "The agent may have called tools with invalid arguments: " ++ err + | Invalid_argument(e) => + "The argument map creation may have failed, or some other fatal issue occurred: " + ++ e + }; + + type result = string; + + let get_static_context = (relevant_ctx: bool, ci: Info.t): list(string) => + switch (ci) { + | InfoExp({ana, ctx, _}) + | InfoPat({ana, ctx, _}) => + let relevant = RelevantValues.get(ctx, ana); + relevant_ctx ? ["relevant_ctx:\n " ++ relevant] : []; + | InfoTyp(_) + | InfoTPat(_) + | Secondary(_) => [] + }; + + // AddToolLabel_2.0: handle the effects of the action on the editor itself + let apply_editor_action = + ( + ~z: Zipper.t, + ~info_map: Id.Map.t(Info.t), + ~action: CompositionActions.composition_action, + ~schedule_editor_action: Editor.Update.t => unit, + ~schedule_assistant_action: AssistantUpdateAction.t => unit, + ~schedule_tool_response: AssistantUpdateAction.status => unit, + ~chat_id: Id.t, + ) + : unit => { + let _ = z; + let _ = info_map; + switch (action) { + | Editor(editor_action) => + switch (editor_action) { + | Read(_r) => schedule_tool_response(Success("todo")) + | _ => + let payload = (editor_action, schedule_tool_response); + schedule_editor_action(Action.Composition(payload)); + } + | Assistant(composition_model_action) => + schedule_assistant_action( + CompositionAgentWorkbenchAction( + BackendAction(composition_model_action), + Agent(schedule_tool_response), + chat_id, + ), + ) + }; + }; +}; + +module Tutor = { + /* + --- Tutor Mode --- + Empty. + Tutor mode is just an LLM chat prompted with hazel-specific information. + */ +}; diff --git a/src/web/app/helpful-assistant/AssistantSettings.re b/src/haz3lcore/ai_assistant_core/AssistantSettings.re old mode 100644 new mode 100755 similarity index 84% rename from src/web/app/helpful-assistant/AssistantSettings.re rename to src/haz3lcore/ai_assistant_core/AssistantSettings.re index 503470130d..4de47c3251 --- a/src/web/app/helpful-assistant/AssistantSettings.re +++ b/src/haz3lcore/ai_assistant_core/AssistantSettings.re @@ -3,7 +3,7 @@ open Util; [@deriving (show({with_path: false}), sexp, yojson)] type mode = | CodeSuggestion - | TaskCompletion + | Composition | HazelTutor; [@deriving (show({with_path: false}), sexp, yojson)] @@ -27,4 +27,10 @@ type action = // Toggles the visibility of the API key in the settings menu | ToggleAPIKeyVisibility; +// --- Constant Magic Ints --- let make_descriptor_max = 3; + +let max_collapsed_length: int = 500; + +let context_threshold_ratio: float = 0.3; +// --- End Constant Magic Ints --- diff --git a/src/haz3lcore/ai_assistant_core/AssistantUpdate/AssistantUpdate.re b/src/haz3lcore/ai_assistant_core/AssistantUpdate/AssistantUpdate.re new file mode 100644 index 0000000000..d8e4217743 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/AssistantUpdate/AssistantUpdate.re @@ -0,0 +1,1572 @@ +open Util; +open Language; +open OptUtil.Syntax; + +module Model = AssistantModel; + +open AssistantUpdateAction; +type t = AssistantUpdateAction.t; + +let get_mode_info = (mode: AssistantSettings.mode, model: Model.t) => { + switch (mode) { + | HazelTutor => ( + model.chat_history.past_tutor_chats, + Id.Map.find( + model.current_chats.curr_tutor_chat, + model.chat_history.past_tutor_chats, + ), + ) + | CodeSuggestion => ( + model.chat_history.past_suggestion_chats, + Id.Map.find( + model.current_chats.curr_suggestion_chat, + model.chat_history.past_suggestion_chats, + ), + ) + | Composition => ( + model.chat_history.past_composition_chats, + Id.Map.find( + model.current_chats.curr_composition_chat, + model.chat_history.past_composition_chats, + ), + ) + }; +}; + +let filter_chat_messages = + (messages: list(Model.message)): list(Model.message) => { + List.filter((_: Model.message) => {true}, messages); +}; + +let resculpt_model = + ( + ~model: Model.t, + ~mode: AssistantSettings.mode, + ~updated_past_chats: Id.Map.t(Model.chat), + ~chat_id: Id.t, + ) => { + Model.{ + ...model, + chat_history: { + past_tutor_chats: + mode == HazelTutor + ? updated_past_chats : model.chat_history.past_tutor_chats, + past_suggestion_chats: + mode == CodeSuggestion + ? updated_past_chats : model.chat_history.past_suggestion_chats, + past_composition_chats: + mode == Composition + ? updated_past_chats : model.chat_history.past_composition_chats, + }, + // This is tentative. Keep this if we want the user to be shown the most recent chat. + // Remove this if we want the user to be shown the chat they last/currently interact with. + // This is honestly such an edge case that it probably doesn't matter. + current_chats: { + curr_tutor_chat: + mode == HazelTutor ? chat_id : model.current_chats.curr_tutor_chat, + curr_suggestion_chat: + mode == CodeSuggestion + ? chat_id : model.current_chats.curr_suggestion_chat, + curr_composition_chat: + mode == Composition + ? chat_id : model.current_chats.curr_composition_chat, + }, + }; +}; + +let update_model_chat_history = + ( + ~model: Model.t, + ~mode: AssistantSettings.mode, + ~updated_chat: Model.chat, + ~awaiting_response: bool, + ) + : Model.t => { + let updated_chat = { + ...updated_chat, + awaiting_response, + }; + let new_chat = + switch (mode) { + | HazelTutor => + Id.Map.update( + updated_chat.id, + maybe_chat => + switch (maybe_chat) { + | Some(_) => Some(updated_chat) + | None => None + }, + model.chat_history.past_tutor_chats, + ) + | CodeSuggestion => + Id.Map.update( + updated_chat.id, + maybe_chat => + switch (maybe_chat) { + | Some(_) => Some(updated_chat) + | None => None + }, + model.chat_history.past_suggestion_chats, + ) + | Composition => + Id.Map.update( + updated_chat.id, + maybe_chat => + switch (maybe_chat) { + | Some(_) => Some(updated_chat) + | None => None + }, + model.chat_history.past_composition_chats, + ) + }; + let updated_chat_history = + switch (mode) { + | HazelTutor => { + ...model.chat_history, + past_tutor_chats: new_chat, + } + | CodeSuggestion => { + ...model.chat_history, + past_suggestion_chats: new_chat, + } + | Composition => { + ...model.chat_history, + past_composition_chats: new_chat, + } + }; + { + ...model, + chat_history: updated_chat_history, + }; +}; + +let create_chat_descriptor = + ( + ~model: Model.t, + ~schedule_action, + ~mode: AssistantSettings.mode, + ~chat_id: Id.t, + ) + : unit => { + let (past_chats, _) = get_mode_info(mode, model); + let curr_chat = Id.Map.find(chat_id, past_chats); + + let this_prompt = + String.concat( + "\n", + [ + "You are a helpful assistant that *summarizes* conversations between other assistants and users. ", + "Your summaries should be less than or equal to 7 words, and may include 1 or 2 emojis, if appropriate. ", + "NEVER exceed 7 words. ", + "ONLY provide the summarizing title in your response, do NOT include any other text. ", + "You will be given a conversation between an assistant and a user. ", + "Focus on the giving a summarizing topic title to the conversation between the assistant and the user. ", + "NEVER use first person pronouns in your response. ", + "EVERY response will be displayed as a summarizaing title, so do NOT respond with anything other than a summarizing title. ", + switch (mode) { + | HazelTutor => "This is known to be a chat between a hazel user and an LLM acting as a tutor." + | CodeSuggestion => "This is known to be a chat between a hazel user and an LLM acting as a code suggestion assistant. This means there won't be much dialogue, rather just a prompt, code contexts, and a code suggestion (potentially with a chain of thought), so please do your best to summarize based on the code context and the code suggestion." + | Composition => "This is known to be a chat between a student and an LLM acting as a task completion assistant." + }, + "With this said, please now provide a summary for the conversation: ", + ], + ); + + let filtered_messages = + List.filter( + (message: Model.message) => { + message.role == User || message.role == Assistant + }, + curr_chat.messages, + ); + + let combined_messages = + String.concat( + "\n", + List.filter_map( + (message: Model.message) => { + switch (message.content) { + | Some(content) => + Some( + "<" + ++ Model.string_of_role(message.role) + ++ ">" + ++ content.content + ++ "", + ) + | None => None + } + }, + filtered_messages, + ), + ); + + let outgoing_messages_for_descriptor = [ + OpenRouter.mk_system_msg(this_prompt), + OpenRouter.mk_user_msg(combined_messages), + ]; + + // Only make descriptor after first few exchanges + List.length(filtered_messages) <= AssistantSettings.make_descriptor_max + ? try({ + let model_id = model.external_api_info.set_model_info.id; + let key = model.external_api_info.api_key; + let params: OpenRouter.params = { + ...OpenRouter.default_params, + model_id, + stream: false // No streaming for descriptor + }; + OpenRouter.start_chat( + ~params, + ~key, + ~outgoing_messages=outgoing_messages_for_descriptor, + req => + switch (OpenRouter.handle_chat(req)) { + | Some(Reply({content, _})) => + schedule_action( + EmployLLMAction(Describe(content, mode, chat_id)), + ) + | Some(Error(_)) => + raise( + Invalid_argument( + "Error in receiving response from OpenRouter when creating descriptor", + ), + ) + | None => () + } + ); + }) { + | Invalid_argument(e) => + print_endline("Invalid_argument when creating descriptor: " ++ e); + (); + } + : (); +}; + +module TodoListUtils = { + type todo_action_result = + | Success(Model.t, string) + | Failure(string); +}; + +let filter_out_agent_view = + (messages: list(Model.message)): list(Model.message) => { + List.filter( + (message: Model.message) => {message.role != System(AgentView)}, + messages, + ); +}; + +let has_new_agent_view = (messages: list(Model.message)): bool => { + List.exists( + (message: Model.message) => {message.role == System(AgentView)}, + messages, + ); +}; + +let mk_active_task_message = + (composition_model: CompositionAgentWorkbench.Model.t): Model.message => { + let content = + CompositionAgentWorkbench.Utils.MainUtils.active_task_to_pretty_string( + composition_model, + ); + { + content: Some(OpenRouter.mk_system_msg(content)), + display: Some(Model.mk_message_display(~content)), + role: System(AgentWorkbench), + sketch_snapshot: None, + tool_calls: [], + }; +}; + +let filter_out_task_message = + (messages: list(Model.message)): list(Model.message) => { + List.filter( + (message: Model.message) => {message.role != System(AgentWorkbench)}, + messages, + ); +}; + +let update_chat = + ( + ~context_usage: option(int)=?, + chat: Model.chat, + new_messages: list(Model.message), + ) => { + let updated_messages = + if (has_new_agent_view(new_messages)) { + // We hinge off the precondition that either zero or one AgentView message is present in the new messages list. + filter_out_agent_view(chat.messages) @ new_messages; + } else { + chat.messages @ new_messages; + }; + let updated_messages = { + let todo_list_message = mk_active_task_message(chat.composition_model); + filter_out_task_message(updated_messages) @ [todo_list_message]; + }; + { + ...chat, + messages: updated_messages, + context_usage: + switch (context_usage) { + | Some(context_usage) => context_usage + | None => chat.context_usage + }, + }; +}; + +// Sends a request to OpenRouter given outgoing messages. +// Handles the response from OpenRouter. +// Emits internal error if API key or model ID is not set. +let mk_llm_call = + ( + ~mode: AssistantSettings.mode, + ~model: Model.t, + ~schedule_action: t => unit, + ~updated_chat: Model.chat, + ~response_handler: OpenRouter.reply => t, + ) + : Model.t => { + switch ( + model.external_api_info.api_key, + model.external_api_info.set_model_info.id, + ) { + | ("", _) => + let content = "No API key found. Please set an API key in the assistant settings."; + schedule_action(InternalError(content, mode, updated_chat.id)); + model; + | (_, "") => + let content = "No model ID found. Please set a model ID in the assistant settings."; + schedule_action(InternalError(content, mode, updated_chat.id)); + model; + | (key, model_id) => + let tools = + if (mode == Composition) { + CompositionUtils.Public.tools; + } else { + []; + }; + let params: OpenRouter.params = { + ...OpenRouter.default_params, + model_id, + tools, + }; + try( + OpenRouter.start_chat( + ~params, + ~key, + ~outgoing_messages=Model.get_messages_content(updated_chat.messages), + req => + switch (OpenRouter.handle_chat(req)) { + | Some(Reply(response)) => + schedule_action(response_handler(response)) + | Some(Error({message, code})) => + schedule_action( + InternalError( + "Error: " ++ message ++ " (code: " ++ string_of_int(code) ++ ")", + mode, + updated_chat.id, + ), + ) + | None => + let str_of_mode = + switch (mode) { + | HazelTutor => "HazelTutor" + | CodeSuggestion => "CodeSuggestion" + | Composition => "Composition" + }; + (); + print_endline( + "Assistant: response still generating: " ++ str_of_mode, + ); + (); + } + ) + ) { + | Invalid_argument(e) => + print_endline( + "Issue when making LLM call. (This is likely from an Option.get during sketch sending): " + ++ e, + ) + | _ => () + }; + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=true, + ); + }; +}; + +let mk_user_content_message = + (~content: string, ~role: Model.role, ~zipper: Zipper.t): Model.message => { + let _ = zipper; + { + content: Some(OpenRouter.mk_user_msg(content)), + display: Some(Model.mk_message_display(~content)), + role, + sketch_snapshot: None, // Some(editor), todo: figure out how to serialize editor + tool_calls: [], + }; +}; + +let summarize_chat = + ( + model: Model.t, + chat: Model.chat, + mode: AssistantSettings.mode, + schedule_action: t => unit, + ) + : unit => { + // Filter our initial prompt + let outgoing_messages: list(OpenRouter.message) = + List.filter_map( + (message: Model.message) => + switch (message.content) { + | Some(content) => + switch (content.role) { + | System => None + | _ => Some(content) + } + | None => None + }, + chat.messages, + ); + let summarize_message: OpenRouter.message = + OpenRouter.mk_user_msg(SummarizePrompt.prelude); + let outgoing_messages = outgoing_messages @ [summarize_message]; + try({ + let model_id = model.external_api_info.set_model_info.id; + let key = model.external_api_info.api_key; + let params: OpenRouter.params = { + ...OpenRouter.default_params, + model_id, + stream: false // No streaming for summarization + }; + OpenRouter.start_chat(~params, ~key, ~outgoing_messages, req => + switch (OpenRouter.handle_chat(req)) { + | Some(Reply({content, _})) => + schedule_action(EmployLLMAction(Summarize(content, mode, chat.id))) + | Some(Error(_)) => + raise( + Invalid_argument( + "Error in receiving response from OpenRouter when summarizing chat", + ), + ) + | None => () + } + ); + }) { + | Invalid_argument(e) => + print_endline("Invalid_argument when summarizing chat: " ++ e); + (); + }; +}; + +let can_undo = (action: t) => { + // TODO: Implement the handling of actions that should be undoable + // Thinking none of these actions should be undoable... + // Maybe set API key? + // That could be a good starter project to navigate this assistant part of the codebase. + switch (action) { + | SendMessage(_) => false + | HandleResponse(_) => true /* Necessary to make completion instantiation undoable */ + | EmployLLMAction(_) => false + | ChatAction(_) => false + | InternalError(_) => false + | ExternalAPIAction(_) => false + | InitializeAssistant => false + | CompositionAgentWorkbenchAction(_) => false + }; +}; + +let update = + ( + ~settings: AssistantSettings.t, + ~action: AssistantUpdateAction.t, + ~model: Model.t, + // todo: Find a way to track unqique editor between concurrent actions + ~zipper: Zipper.t, + ~info_map: Statics.Map.t, + ~schedule_action: t => unit, + ~schedule_editor_action: Editor.Update.t => unit, + ) + : Model.t => { + switch (action) { + | SendMessage(kind, zipper_opt, chat_id) => + let zipper = + switch (zipper_opt) { + | Some(zipper) => zipper + | None => zipper + }; + if (model.current_chats.curr_tutor_chat == Id.invalid) { + model; + } else { + switch (kind) { + | Tutor(content) => + let mode = AssistantSettings.HazelTutor; + let curr_chat = + Id.Map.find(chat_id, model.chat_history.past_tutor_chats); + let content_message = + mk_user_content_message(~content, ~role=User, ~zipper); + let ctx_message: Model.message = { + content: + Some( + OpenRouter.mk_user_msg( + String.concat( + "\n", + ChatLSP.get_sketch_and_error_ctx(zipper, info_map), + ), + ), + ), + display: + Some( + Model.mk_message_display( + ~content= + String.concat( + "\n", + ChatLSP.get_sketch_and_error_ctx(zipper, info_map), + ), + ), + ), + role: System(AssistantPrompt), + sketch_snapshot: None, + tool_calls: [], + }; + + let updated_chat = + update_chat(curr_chat, [content_message, ctx_message]); + + mk_llm_call( + ~mode, + ~model, + ~schedule_action, + ~updated_chat, + ~response_handler=response => + HandleResponse(Tutor, response, chat_id) + ); + + | Composition(kind, eval_mode) => + print_endline( + "Here #6 : Composition Eval mode is set to " + ++ string_of_bool(eval_mode), + ); + let mode = AssistantSettings.Composition; + let curr_chat = + Id.Map.find(chat_id, model.chat_history.past_composition_chats); + switch (kind) { + // The initial message sent to the LLM via the User -- + // We can think of the agentic looping as a directed graph: + // 1. The user sends a message to the LLM, appending with info from (2) + // 2. We gather context of the program relative to the cursor + // and send it as a user message. + // 3. The LLM responds with either a tool call or no tool call. + // if tool call, parse and handle it and then loop back to (2) + // (this cyclic edge is what makes enables the "agentic" nature) + // if no tool call, output final result to the user + | Request(content) => + print_endline("Here #5 : Sending Composition Request"); + // This is step (1) of the directed graph example above. + // The user sends a message to the LLM, appending with info from (2) + // Note: (2) is done here, jointly with (1) and done in Loop(_, _) below, + // after a tool call has been handled. + print_endline("handling composition request"); + let content_message: Model.message = + mk_user_content_message(~content, ~role=User, ~zipper); + let (local_code_map_str, display) = + AssistantModes.Composition.mk_structured_code_map_prompt( + zipper, + info_map, + ); + let agent_view: Model.message = { + content: Some(local_code_map_str), + display: Some(display), + role: System(AgentView), + sketch_snapshot: None, + tool_calls: [], + }; + + let updated_chat = + update_chat(curr_chat, [content_message, agent_view]); + + mk_llm_call( + ~mode, + ~model, + ~schedule_action, + ~updated_chat, + ~response_handler=response => + HandleResponse( + CompositionLoopRound( + zipper, + AssistantModes.Composition.max_tool_calls, + eval_mode, + ), + response, + chat_id, + ) + ); + + | Loop(fuel, tool_contents, status) => + // This is step (2) from the directed graph above -- + // The agent just made a tool call. After + // (assumably) handling the tool call previously, we gather the new context + // from the program and cursor location, and then append these to our list + // of messages. This message is an OpenRouter tool message (as opposed to a user message), + // which takes the tool call and the tool call results (which we send as the context). + // We then send off this message to the LLM and await a response, either + // an end output to the user (implying no more looping) or a new tool call. + let (local_code_map_str, display) = + AssistantModes.Composition.mk_structured_code_map_prompt( + zipper, + info_map, + ); + + let local_code_map_str = + "\n\nThe new AST context is:\n" ++ local_code_map_str.content; + + let updated_chat = + switch (status) { + | Success(response) => + print_endline("Here #8 : Success status: " ++ response); + let tool_response_message: Model.message = { + content: + Some(OpenRouter.mk_tool_msg(response, tool_contents)), + display: Some(Model.mk_message_display(~content=response)), + role: System(AssistantPrompt), + sketch_snapshot: None, + tool_calls: [], + }; + let agent_view: Model.message = { + content: Some(OpenRouter.mk_user_msg(local_code_map_str)), + display: Some(display), + role: System(AgentView), + sketch_snapshot: None, + tool_calls: [], + }; + update_chat(curr_chat, [tool_response_message, agent_view]); + | Failure(err) => + let err_message: Model.message = { + content: Some(OpenRouter.mk_tool_msg(err, tool_contents)), + display: Some(Model.mk_message_display(~content=err)), + role: System(InternalError), + sketch_snapshot: None, + tool_calls: [], + }; + update_chat(curr_chat, [err_message]); + }; + + mk_llm_call( + ~mode, + ~model, + ~schedule_action, + ~updated_chat, + ~response_handler=response => + HandleResponse( + CompositionLoopRound(zipper, fuel, eval_mode), + response, + chat_id, + ) + ); + }; + + | Completion(kind) => + let mode = AssistantSettings.CodeSuggestion; + switch (kind) { + | Request(tile_id, advanced_reasoning) => + let new_chat = Model.new_chat(model, mode); + print_endline("new_chat: " ++ Id.to_string(new_chat.id)); + let updated_past_chats = + Model.add_chat_to_history( + new_chat, + model.chat_history.past_suggestion_chats, + ); + let model_with_new_chat = + resculpt_model( + ~model, + ~mode, + ~updated_past_chats, + ~chat_id=new_chat.id, + ); + let tag = String.sub(Id.to_string(tile_id), 0, 3); + switch ( + { + let* sketch_z_with_tag = + Parser.to_zipper(~zipper_init=zipper, tag); + let sketch_seg = Dump.to_segment(sketch_z_with_tag); + let* index = Indicated.index(zipper); + let+ ci = Id.Map.find_opt(index, info_map); + AssistantModes.Completion.mk_ctx_prompt( + InitPrompts.Options.init, + ci, + sketch_seg, + (advanced_reasoning ? "?a" : "??") ++ tag, + ); + } + ) { + | None => + print_endline("Suggestion prompt generation failed"); + model_with_new_chat; + | Some(ctx_prompt) => + let ctx_message: Model.message = { + content: Some(ctx_prompt), + display: + Some(Model.mk_message_display(~content=ctx_prompt.content)), + role: System(AssistantPrompt), + sketch_snapshot: None, + tool_calls: [], + }; + let updated_chat = update_chat(new_chat, [ctx_message]); + + mk_llm_call( + ~mode, + ~model=model_with_new_chat, + ~schedule_action, + ~updated_chat, + ~response_handler=response => + HandleResponse( + CompletionErrorRound( + zipper, + InitPrompts.Options.init.error_rounds_max, + tile_id, + ), + response, + new_chat.id, + ) + ); + }; + | Query(content) => + let curr_chat = + Id.Map.find(chat_id, model.chat_history.past_suggestion_chats); + let ctx = + OpenRouter.mk_user_msg( + String.concat( + "\n", + ChatLSP.get_sketch_and_error_ctx(zipper, info_map), + ), + ); + let ctx_message: Model.message = { + content: Some(ctx), + display: Some(Model.mk_message_display(~content=ctx.content)), + role: System(AssistantPrompt), + sketch_snapshot: None, + tool_calls: [], + }; + let content_message = + mk_user_content_message(~content, ~role=User, ~zipper); + let updated_chat = + update_chat(curr_chat, [ctx_message, content_message]); + + mk_llm_call( + ~mode, + ~model, + ~schedule_action, + ~updated_chat, + ~response_handler=response => + HandleResponse(CompletionQueryResponse, response, chat_id) + ); + + | Loop(error, tile_id, fuel) => + let curr_chat = + Id.Map.find(chat_id, model.chat_history.past_suggestion_chats); + let error_message = + OpenRouter.mk_user_msg( + "Your previous response caused the following error. Please fix it in your response: " + ++ error, + ); + let error_message: Model.message = { + content: Some(error_message), + display: + Some(Model.mk_message_display(~content=error_message.content)), + role: System(AssistantPrompt), + sketch_snapshot: None, + tool_calls: [], + }; + let updated_chat = update_chat(curr_chat, [error_message]); + + // check that fuel is not 0 + if (fuel < 0) { + let content = + "By default we stop the assistant after " + ++ string_of_int(InitPrompts.Options.init.error_rounds_max) + ++ " error rounds."; + schedule_action(InternalError(content, mode, updated_chat.id)); + model; + } else { + mk_llm_call( + ~mode, + ~model, + ~schedule_action, + ~updated_chat, + ~response_handler=response => + HandleResponse( + CompletionErrorRound(zipper, fuel, tile_id), + response, + chat_id, + ) + ); + }; + }; + }; + }; + | InternalError(content, mode, chat_id) => + let curr_chat = + switch ( + Id.Map.find_opt(chat_id, model.chat_history.past_composition_chats) + ) { + | Some(chat) => chat + | None => + print_endline("Error: Chat not found"); + get_mode_info(mode, model) |> snd; + }; + + if (!curr_chat.awaiting_response) { + model; + } else { + // todo: Should this be a user, assistant, or system message? + // We could make it assistant and put it in the first-person. + let system_message: Model.message = { + content: Some(OpenRouter.mk_user_msg(content)), + display: Some(Model.mk_message_display(~content)), + role: System(InternalError), + sketch_snapshot: None, + tool_calls: [], + }; + + // Note: We aren't sending a message here, but we do add it to the chat history. + // for future reference for the LLM so it isn't confused. + // (Eg: Max tool call limit reached, agent should know from history that this + // is why their prior task completion was not successful.) + let updated_chat = { + ...curr_chat, + messages: curr_chat.messages @ [system_message], + }; + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false, + ); + }; + + | HandleResponse(response_kind, reply, chat_id) => + // Check if we're still awaiting a promise - if not, ignore the response + + let (curr_chat, mode) = + switch (response_kind) { + | Tutor => ( + Id.Map.find(chat_id, model.chat_history.past_tutor_chats), + AssistantSettings.HazelTutor, + ) + | CompositionLoopRound(_) => ( + Id.Map.find(chat_id, model.chat_history.past_composition_chats), + AssistantSettings.Composition, + ) + | CompletionErrorRound(_) => ( + Id.Map.find(chat_id, model.chat_history.past_suggestion_chats), + AssistantSettings.CodeSuggestion, + ) + | CompletionQueryResponse => ( + Id.Map.find(chat_id, model.chat_history.past_suggestion_chats), + AssistantSettings.CodeSuggestion, + ) + }; + + if (!curr_chat.awaiting_response) { + // There was an early exit, so throw away/ignore the response + model; + } else { + // todo: turning off for now to save credits + // fixme: uncomment to enable chat descriptors again + //create_chat_descriptor(~model, ~schedule_action, ~mode, ~chat_id); + let threshold = + int_of_float( + float_of_int(model.external_api_info.set_model_info.context_length) + *. AssistantSettings.context_threshold_ratio, + ); + // Thin wrapper to avoid need of passing response.usage.total_tokens + let summarize_chat = () => + if (reply.usage.total_tokens > threshold) { + summarize_chat(model, curr_chat, mode, schedule_action); + }; + + let content = reply.content; + print_endline("content: " ++ content); + // Todo: Allow for multiple tool calls + let tool_calls_json = reply.tool_calls_json; + let assistant_message: Model.message = { + content: Some(OpenRouter.mk_assistant_msg(content, tool_calls_json)), + display: + switch (content) { + | "" => None + | _ => Some(Model.mk_message_display(~content)) + }, + role: Assistant, + sketch_snapshot: None, + tool_calls: [], + }; + + // This commented out code below is for streaming, if we ever choose to add + // If streaming, update the last message display + let updated_chat = + update_chat( + ~context_usage=reply.usage.total_tokens, + curr_chat, + [assistant_message], + ); + /* let last_display = ListUtil.last(curr_chat.message_displays); + if (last_display.role == Assistant) { + let updated_content = last_display.original_content ++ content; + ( + ListUtil.leading(curr_chat.messages) + @ [OpenRouter.mk_assistant_msg(updated_content)], + ListUtil.leading(curr_chat.messages) + @ [ + Model.mk_message_display( + ~content=updated_content, + ~role=Assistant, + ), + ], + ); + } else */ + + switch (response_kind) { + | Tutor => + summarize_chat(); + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false, + ); + + | CompositionLoopRound(_, fuel, eval_mode) => + print_endline( + "Here #7 : Composition Eval mode is set to " + ++ string_of_bool(eval_mode), + ); + // This is step (3) from the directed graph above -- + switch (tool_calls_json, fuel) { + | ([], _) => + // The agent did not make a tool call, thus there is nothing to handle on the backend, + // we can proceed as if there were a normal LLM chat interaction. + summarize_chat(); + // if (eval_mode) { + // schedule_eval_action(CollectResults); + // }; + + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false // false because no tool call + ); + + | (_, 0) => + // The agent ran out of fuel. We should experiment with this in the future. + schedule_action( + InternalError( + "By default, we stop the agent after " + ++ string_of_int(AssistantModes.Composition.max_tool_calls) + ++ " tool calls.", + mode, + chat_id, + ), + ); + summarize_chat(); + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false // false because out of fuel + ); + + | (tool_calls_json, _) => + let parse_tool_args = (args: API.Json.t): API.Json.t => { + switch (args) { + | `String(str) => + try(Yojson.Safe.from_string(str)) { + | _ => args + } + | json => json + }; + }; + let tool_calls: list(OpenRouter.tool_call) = + List.filter_map( + (tool_call: API.Json.t) => { + let* id = API.Json.dot("id", tool_call); + let* id = API.Json.str(id); + let* tool_call = API.Json.dot("function", tool_call); + let* name = API.Json.dot("name", tool_call); + let* name = API.Json.str(name); + let* args = API.Json.dot("arguments", tool_call); + let parsed_args = parse_tool_args(args); + let tool_call: OpenRouter.tool_call = { + id, + tool_name: name, + args: parsed_args, + }; + Some(tool_call); + }, + tool_calls_json, + ); + let _actions = + List.map( + (tc: OpenRouter.tool_call) => + CompositionUtils.Public.action_of( + ~tool_name=tc.tool_name, + ~args=tc.args, + ), + tool_calls, + ); + let updated_chat = { + let structure_edit_message: Model.message = { + content: None, + display: + Some( + Model.mk_message_display( + ~content= + AssistantModes.Composition.mk_structure_edit_msg( + ~tool_call=List.hd(tool_calls), + ), + ), + ), + role: Tool, + sketch_snapshot: None, + tool_calls: [()] // TODO: fill in with converted action + }; + update_chat( + curr_chat, + [assistant_message, structure_edit_message], + ); + }; + // We don't summarize while the agent loops on tool calls. + + // The agent made a tool call, we need to handle it and then perform a loop + // round (the loop round itself will later handle it) + let loop_message = (status: status) => + SendMessage( + Composition( + Loop( + fuel - 1, + { + tool_call_id: List.hd(tool_calls).id, + name: List.hd(tool_calls).tool_name, + }, + status, + ), + eval_mode, + ), + None, + chat_id, + ); + let action = + CompositionUtils.Public.action_of( + ~tool_name=List.hd(tool_calls).tool_name, + ~args=List.hd(tool_calls).args, + ); + switch (action) { + | Failure(s) => + print_endline(s); + schedule_action(loop_message(Failure(s))); + | Action(action) => + AssistantModes.Composition.apply_editor_action( + ~z=zipper, + ~info_map, + ~chat_id, + ~action, + ~schedule_editor_action, + ~schedule_assistant_action=schedule_action, + ~schedule_tool_response=(res: AssistantUpdateAction.status) => { + schedule_action(loop_message(res)) + }) + }; + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false, + ); + }; + | CompletionErrorRound(zipper, fuel, tileId) => + /* --- todo: test if this works --- */ + let code_pattern: StringUtil.regexp = + StringUtil.regexp( + "([\\s\\S]*)```[ \\n]*([^`]+)[ \\n]*```([\\s\\S]*)", + ); + + let index = Option.get(Indicated.index(zipper)); + let ci = Option.get(Id.Map.find_opt(index, info_map)); + let sketch_z = zipper; + + /* small helper to grab a capture group using replace */ + let capture = (n: int, s: string): string => + StringUtil.replace(code_pattern, s, "$" ++ string_of_int(n)); + + let (_, completion) = + if (StringUtil.match(code_pattern, content)) { + let before = capture(1, content) |> String.trim; + let code = capture(2, content) |> String.trim; + (before, code |> StringUtil.trim_leading); + } else { + print_endline("Regex match failed for: " ++ content); + ("", content |> StringUtil.trim_leading); /* Fallback if no code block found */ + }; + /* --- End todo -- */ + + switch ( + AssistantModes.Completion.ErrorRound.mk_reply( + ci, + sketch_z, + completion, + ) + ) { + | None => + print_endline("ERROR ROUNDS (Non-error Response): " ++ completion); + schedule_action( + EmployLLMAction(RemoveAndSuggest(completion, tileId)), + ); + | Some(error) => + print_endline("ERROR ROUNDS (Error): " ++ error); + print_endline( + "ERROR ROUNDS (Error-causing Response): " ++ completion, + ); + schedule_action( + SendMessage( + Completion(Loop(error, tileId, fuel - 1)), + None, + chat_id, + ), + ); + }; + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false, + ); + + | CompletionQueryResponse => + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false, + ) + }; + }; + | EmployLLMAction(action) => + let add_suggestion = + AssistantModes.Completion.add_suggestion( + ~schedule_action=schedule_editor_action, + ); + switch (action) { + | Summarize(content, mode, chat_id) => + let (past_chats, _) = get_mode_info(mode, model); + let curr_chat = Id.Map.find(chat_id, past_chats); + // Only keep the prompt + // Decide what else to keep here (last few code contexts?) + let truncated_messages: list(Model.message) = + List.filter_map( + (message: Model.message) => { + switch (message.content) { + | Some(content) => + switch (content.role) { + | Assistant => + Some({ + ...message, + content: None, + }) + | User => + Some({ + ...message, + content: None, + }) + | _ => Some(message) + } + | None => None + } + }, + curr_chat.messages, + ); + let truncated_chat = { + ...curr_chat, + messages: truncated_messages, + }; + let summarization_message_content = "Approaching Context Limit: A summary of the chat has been generated..."; + let summarization_message: Model.message = { + content: Some(OpenRouter.mk_user_msg(summarization_message_content)), + display: + Some( + Model.mk_message_display(~content=summarization_message_content), + ), + role: Tool, + sketch_snapshot: None, + tool_calls: [], + }; + let summarized_chat_message: Model.message = { + // note: making this an outgoing assistant message, but displaying as system message, + // as it might make more sense for assistant to see that it or some other LLM made a summary, + // and it might be more intuitive for user to see the summary as a collapsable system prompt, + // (akin to init prompt/sketch contexts) + content: Some(OpenRouter.mk_assistant_msg(content, [])), + display: Some(Model.mk_message_display(~content)), + role: System(AssistantPrompt), + sketch_snapshot: None, + tool_calls: [], + }; + let updated_chat = + update_chat( + truncated_chat, + [summarization_message, summarized_chat_message], + ); + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false, + ); + + | RemoveAndSuggest(response, tileId) => + // Only side effects in the editor are performed here + add_suggestion(~response, ~tile=tileId); + model; + | Describe(content, mode, chat_id) => + let (past_chats, _) = get_mode_info(mode, model); + let updated_past_chats = + Id.Map.update( + chat_id, + opt_chat => + switch (opt_chat) { + | Some(chat: Model.chat) => + Some({ + ...chat, + descriptor: content, + }) + | None => None + }, + past_chats, + ); + let curr_chat_id = + switch (mode) { + | HazelTutor => model.current_chats.curr_tutor_chat + | CodeSuggestion => model.current_chats.curr_suggestion_chat + | Composition => model.current_chats.curr_composition_chat + }; + resculpt_model( + ~model, + ~mode, + ~updated_past_chats, + ~chat_id=curr_chat_id, + ); + | Quit => + // Set awaiting_promise to false and add a system message + let quit_message: Model.message = { + content: Some(OpenRouter.mk_user_msg("User quit early")), + display: Some(Model.mk_message_display(~content="User quit early")), + role: System(InternalError), + sketch_snapshot: None, + tool_calls: [], + }; + let (_, curr_chat) = get_mode_info(settings.mode, model); + let updated_chat = update_chat(curr_chat, [quit_message]); + update_model_chat_history( + ~model, + ~mode=settings.mode, + ~updated_chat, + ~awaiting_response=false, + ); + }; + + | ChatAction(action) => + switch (action) { + | NewChat => + print_endline("Here #2 : Adding Chat"); + let mode = settings.mode; + let (past_chats, _) = get_mode_info(mode, model); + let new_chat: Model.chat = Model.new_chat(model, mode); + let updated_history = Model.add_chat_to_history(new_chat, past_chats); + resculpt_model( + ~model, + ~mode, + ~updated_past_chats=updated_history, + ~chat_id=new_chat.id, + ); + + | DeleteChat(chat_to_be_gone_id) => + let mode = settings.mode; + // Filter out the chat we're deleting + let (past_chats, curr_chat) = get_mode_info(mode, model); + let filtered_past_chats = + Id.Map.filter((id, _) => id != chat_to_be_gone_id, past_chats); + let chrono_history = Model.sorted_chats(filtered_past_chats); + let updated_model = + curr_chat.id == chat_to_be_gone_id + ? switch (ListUtil.hd_opt(chrono_history)) { + | Some(chat) => + resculpt_model( + ~model, + ~mode, + ~updated_past_chats=filtered_past_chats, + ~chat_id=chat.id, + ) + | None => + resculpt_model( + ~model, + ~mode, + ~updated_past_chats=past_chats, + ~chat_id=curr_chat.id, + ) + } + : resculpt_model( + ~model, + ~mode, + ~updated_past_chats=filtered_past_chats, + ~chat_id=curr_chat.id, + ); + updated_model; + + // Concat LS' error message and await_llm_response (... animation) + // This works even if out of fuel, as both Respond and ErrorRespond + // remove await_llm_response + | CollapseMessage(index) => + let mode = settings.mode; + let (past_chats, curr_chat) = get_mode_info(mode, model); + let is_prompt_display = + try( + List.nth(curr_chat.messages, index).role == System(AssistantPrompt) + ) { + | Invalid_argument(_) => true + }; + let updated_messages = + List.mapi( + (i: int, msg: Model.message) => + if (i == index) { + { + ...msg, + display: + switch (msg.display) { + | Some(display) => + Some({ + ...display, + collapsed: !display.collapsed, + }) + | None => None + }, + }; + } else if (( + msg.role == System(AssistantPrompt) + || msg.role == System(AgentView) + ) + && is_prompt_display) { + { + ...msg, + display: + switch (msg.display) { + | Some(display) => + Some({ + ...display, + collapsed: true, + }) + | None => None + }, + }; + } else { + msg; + }, + curr_chat.messages, + ); + let updated_past_chats = + Id.Map.update( + curr_chat.id, + opt_chat => + switch (opt_chat) { + | Some(chat: Model.chat) => + Some({ + ...chat, + messages: updated_messages, + }) + | None => None + }, + past_chats, + ); + resculpt_model( + ~model, + ~mode, + ~updated_past_chats, + ~chat_id=curr_chat.id, + ); + + | SwitchChat(chat_id) => + let mode = settings.mode; + let (past_chats, _) = get_mode_info(mode, model); + resculpt_model(~model, ~mode, ~updated_past_chats=past_chats, ~chat_id); + + | FilterLoadingMessages => + Model.{ + ...model, + chat_history: { + past_tutor_chats: + Id.Map.map( + (chat: Model.chat) => { + { + ...chat, + messages: filter_chat_messages(chat.messages), + } + }, + model.chat_history.past_tutor_chats, + ), + past_suggestion_chats: + Id.Map.map( + (chat: Model.chat) => { + { + ...chat, + messages: filter_chat_messages(chat.messages), + } + }, + model.chat_history.past_suggestion_chats, + ), + past_composition_chats: + Id.Map.map( + (chat: Model.chat) => { + { + ...chat, + messages: filter_chat_messages(chat.messages), + } + }, + model.chat_history.past_composition_chats, + ), + }, + } + | Lop(index) => + // Lop off the messages after the index + let mode = settings.mode; + let (_, curr_chat) = get_mode_info(mode, model); + let _sketch_snapshot = + List.nth(curr_chat.messages, index).sketch_snapshot; + // switch (sketch_snapshot) { + // | Some(sketch) => + // let perform_action = + // CodeEditable.Update.Perform(Restore(sketch.editor.state.zipper)); + // let cell_action = CellEditor.Update.MainEditor(perform_action); + // let scratch_action = Editors.Update.Scratch(CellAction(cell_action)); + // schedule_editor_action(scratch_action); + // | None => () + // }; + let updated_messages = + curr_chat.messages |> ListUtil.take_up_to_n(index); + let updated_chat = { + ...curr_chat, + messages: updated_messages, + }; + update_model_chat_history( + ~model, + ~mode, + ~updated_chat, + ~awaiting_response=false, + ); + } + | ExternalAPIAction(external_api_action) => + switch (external_api_action) { + | SetLLM(model_info) => { + ...model, + external_api_info: { + ...model.external_api_info, + set_model_info: model_info, + }, + } + | SetAPIKey(api_key) => + // Set the available models using the provided API key + OpenRouter.get_models(~key=api_key, ~handler=response => { + switch (response) { + | Some(json) => + switch (OpenRouter.parse_models_response(json)) { + | Some(models_response) => + schedule_action( + ExternalAPIAction(SetListOfLLMs(models_response.data)), + ) + | None => + print_endline("Assistant: failed to parse models response") + } + | None => + print_endline("Assistant: no response received from OpenRouter API") + } + }); + { + ...model, + external_api_info: { + ...model.external_api_info, + api_key, + }, + }; + + | SetListOfLLMs(llms) => { + ...model, + external_api_info: { + ...model.external_api_info, + available_models: llms, + // set llm as the first model to prevent mismatch between dropdown display and set model + set_model_info: List.hd(llms), + }, + } + } + | InitializeAssistant => AssistantModel.init() + | CompositionAgentWorkbenchAction(action, caller, chat_id) => + let mode = AssistantSettings.Composition; + let curr_chat = + OptUtil.get_or_fail( + "Failed to find the current chat", + Id.Map.find_opt(chat_id, model.chat_history.past_composition_chats), + ); + let composition_model_res = + CompositionAgentWorkbench.Update.Action.update( + ~model=curr_chat.composition_model, + ~action, + ); + switch (caller) { + | User => + switch (composition_model_res) { + | Success(updated_composition_model) => + update_model_chat_history( + ~model, + ~mode, + ~updated_chat={ + ...curr_chat, + composition_model: updated_composition_model, + }, + ~awaiting_response=curr_chat.awaiting_response, + ) + | Failure(err) => + print_endline("Composition Agent Workbench Action Error: " ++ err); + model; + } + | Agent(schedule_tool_response) => + switch (composition_model_res) { + | Success(updated_composition_model) => + schedule_tool_response( + Success( + "Composition workbench action performed successfully. Changes applied.", + ), + ); + update_model_chat_history( + ~model, + ~mode, + ~updated_chat={ + ...curr_chat, + composition_model: updated_composition_model, + }, + ~awaiting_response=curr_chat.awaiting_response, + ); + | Failure(err) => + schedule_tool_response( + Failure( + "Failed to perform the composition workbench action:\n" ++ err, + ), + ); + model; + } + }; + }; +}; diff --git a/src/haz3lcore/ai_assistant_core/AssistantUpdate/AssistantUpdateAction.re b/src/haz3lcore/ai_assistant_core/AssistantUpdate/AssistantUpdateAction.re new file mode 100644 index 0000000000..60e0a3fe67 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/AssistantUpdate/AssistantUpdateAction.re @@ -0,0 +1,87 @@ +open Util; + +/* Update Action Types are defined here to avoid circular dependencies */ + +[@deriving (show({with_path: false}), sexp, yojson)] +type completion = + | Request(Id.t, bool) // When user presses ?? or ?a + | Query(string) // User may followup with a query + | Loop(string, Id.t, int); // Error rounds + +[@deriving (show({with_path: false}), sexp, yojson)] +type status = + | Success(string) + | Failure(string); + +[@deriving (show({with_path: false}), sexp, yojson)] +type composition = + | Request(string) // User-submitted task, question, etc + // TODO: maybe remove in the future, this is a hack for selecting the current code the + // agent is at, a useful UI feature + | Loop(int, OpenRouter.tool_contents, status); // Iterative tool completion loop + +// Actions to send various kinds of messages to the LLM +[@deriving (show({with_path: false}), sexp, yojson)] +type send_message = + | Tutor(string) + | Completion(completion) + | Composition(composition, bool); + +// Actions to handle certain kinds of LLM responses +[@deriving (show({with_path: false}), sexp, yojson)] +type handle_response = + | Tutor + | CompletionErrorRound(Zipper.t, int, Id.t) + | CompletionQueryResponse + | CompositionLoopRound(Zipper.t, int, bool); + +// Actions which actualize actions via LLM responses +[@deriving (show({with_path: false}), sexp, yojson)] +type employ_llm_action = + | RemoveAndSuggest(string, Id.t) + | Describe(string, AssistantSettings.mode, Id.t) + | Summarize(string, AssistantSettings.mode, Id.t) + | Quit; + +// Future Todo: (Check whether) These might be able to be relocated to AssistantSettings +// Although, arguably, the chat is inherently part of the assistant model, +// serving as a sort of memory. +// Actions that are related to the chat history and/or display of chat messages +[@deriving (show({with_path: false}), sexp, yojson)] +type chat_action = + | NewChat + | DeleteChat(Id.t) + | SwitchChat(Id.t) + | CollapseMessage(int) + | FilterLoadingMessages + | Lop(int); + +[@deriving (show({with_path: false}), sexp, yojson)] +type external_api_action = + // Sets the LLM model + | SetLLM(OpenRouter.model_info) + // Sets the API key. + // This will implicitely make a call to OpenRouter to get and set the list of available LLMs. + | SetAPIKey(string) + // Sets the list of available LLMs from OpenRouter + | SetListOfLLMs(list(OpenRouter.model_info)); + +[@deriving (show({with_path: false}), sexp, yojson)] +type caller = + | Agent(status => unit) + | User; + +[@deriving (show({with_path: false}), sexp, yojson)] +type t = + | SendMessage(send_message, option(Zipper.t), Id.t) + | HandleResponse(handle_response, OpenRouter.reply, Id.t) + | EmployLLMAction(employ_llm_action) + | ChatAction(chat_action) + | InternalError(string, AssistantSettings.mode, Id.t) + | ExternalAPIAction(external_api_action) + | InitializeAssistant + | CompositionAgentWorkbenchAction( + CompositionAgentWorkbench.Update.Action.action, + caller, + Id.t, + ); diff --git a/src/haz3lcore/ai_assistant_core/Composition/AgentState.re b/src/haz3lcore/ai_assistant_core/Composition/AgentState.re new file mode 100644 index 0000000000..8a85015cf6 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/AgentState.re @@ -0,0 +1,19 @@ +open Util; + +[@deriving (show({with_path: false}), sexp, yojson, eq)] +type t = { + // Store paths, not ids, as ids may change with binding edits + // Definitely a thought experiment, can revisit in future + expanded_paths: list(string), +}; + +let init = {expanded_paths: []}; + +let add_paths = (paths: list(string), agent_view: t): t => { + expanded_paths: List.append(paths, agent_view.expanded_paths), +}; + +let remove_paths = (paths: list(string), agent_view: t): t => { + expanded_paths: + List.filter(p => !List.mem(p, paths), agent_view.expanded_paths), +}; diff --git a/src/haz3lcore/ai_assistant_core/Composition/CompositionActions.re b/src/haz3lcore/ai_assistant_core/Composition/CompositionActions.re new file mode 100644 index 0000000000..74632ace19 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/CompositionActions.re @@ -0,0 +1,83 @@ +open Util; + +/* + * ------------------------------ + * Structure-Based Action Language + * ------------------------------ + */ + +[@deriving (show({with_path: false}), sexp, yojson, eq)] +// The path string should be formatted as "name/name/name/..." +type path = string; + +[@deriving (show({with_path: false}), sexp, yojson, eq)] +type code = string; + +// --- File-Read Actions --- +// These actions are used purely to read information from the program, +// and do not modify the program or the cursor location in the AST. +// Note: We supply several "things to read" to the agent by default +// on each iteration: +// - AST Info +// - structure-based code map +// - variables referenced +// - static error info +// We could, technically, move any of these to here, but need to consider implications +// of doing so + +[@deriving (show({with_path: false}), sexp, yojson, eq)] +type read_action = + // Lists all the use sites of the indicated variable + | ShowUseSites(path) + // Displays the typing context/scope at the current let expression in the AST + // | ShowContext //todo: technically this is accomplished via showing sibs/parent + // Displays the entire definition of the current node, with no child/sub definitions abstracted away + | ShowReferences(path); + +// --- Edit Actions --- + +[@deriving (show({with_path: false}), sexp, yojson)] +type edit_action = + | Initialize(code) + | UpdateDefinition(path, code) + | UpdateBody(path, code) + | UpdatePattern(path, code) + | UpdateBindingClause(path, code) + | DeleteBindingClause(path) + | DeleteBody(path) + | InsertAfter(path, code) + | InsertBefore(path, code); + +[@deriving (show({with_path: false}), sexp, yojson, eq)] +type view_action = + // Leave out option(int) index param for now. + // TODO: add later. resort to prompting to avoid shadowing ambiguity for now. + | Expand(list(path)) + | Collapse(list(path)); +// TODO: Uncomment once we add functionality for file systems. +// This would require a separate file_path and variable_path in Expand and Collapse actions. +/* + | Open(path) + | Close(path); + */ + +// AddToolLabel_1.0: Make the action types (above) and add their cases to the funs (below) +[@deriving (show({with_path: false}), sexp, yojson)] +type editor_action = + | View(view_action) // Main source of ingesting the codebase + | Read(read_action) // Language server helpers + | Edit(edit_action); // Main source of editing the codebase + +[@deriving (show({with_path: false}), sexp, yojson)] +type editor_payload = (editor_action, AssistantUpdateAction.status => unit); + +[@deriving (show({with_path: false}), sexp, yojson)] +type composition_action = + | Editor(editor_action) + | Assistant(CompositionAgentWorkbench.Update.Action.BackendAction.action); + +[@deriving (show({with_path: false}), sexp, yojson)] +type composition_payload = ( + composition_action, + AssistantUpdateAction.status => unit, +); diff --git a/src/haz3lcore/ai_assistant_core/Composition/CompositionAgentWorkbench.re b/src/haz3lcore/ai_assistant_core/Composition/CompositionAgentWorkbench.re new file mode 100644 index 0000000000..e9e9baf14b --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/CompositionAgentWorkbench.re @@ -0,0 +1,801 @@ +open Util; +open OptUtil.Syntax; + +module Model = { + module UI = { + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type active_view = + | Chat + | Todos; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type subtask_ui = {expanded: bool}; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type task_ui = {expanded: bool}; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type t = { + active_view, + display_task: option(string), + show_archive: bool, + }; + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type tool_usage_info = { + tool: string, + sketch_before: string, + sketch_after: string, + timestamp: float, + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type completion_info = { + summary: string, + elapsed_time: float, + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type metadata = { + began_at: float, + completed_at: option(float), + last_updated_at: float, + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type subtask = { + title: string, // Will also serve as a unique identifier for the subtask item + description: string, // Description of the subtask item + tools_used: list(tool_usage_info), // TODO: List of tools used for this subtask item + completion_info: option(completion_info), + subtask_ui: UI.subtask_ui, + metadata, + // TODO: Add fields to tie it to code? + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type task = { + title: string, + description: string, + subtasks: Maps.StringMap.t(subtask), // Map of title -> subtask, (titles are keys) + subtask_ordering: list(string), // List of subtask titles in the order they should be completed + tools_used: list(tool_usage_info), + completion_info: option(completion_info), + active_subtask: option(string), + task_ui: UI.task_ui, + metadata, + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type task_dict = Maps.StringMap.t(task); // Map of title -> todo_list, (titles are keys) + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type t = { + active_task: option(string), // Title of the active task + task_dict, + t_ui: UI.t, + }; +}; + +module Utils = { + module SubtaskUtils = { + // == IMPORTS == + open Model; + // ============= + + let is_completed = (subtask: subtask): bool => { + switch (subtask.completion_info) { + | Some(_) => true + | None => false + }; + }; + + let find_subtask = (task: task, subtask_name: string): option(subtask) => { + // Finds the todo item with the given name + Maps.StringMap.find_opt( + subtask_name, + task.subtasks, + ); + }; + + let is_active = (task: task, subtask_name: string): bool => { + switch (task.active_subtask) { + | Some(active_subtask_name) => active_subtask_name == subtask_name + | None => false + }; + }; + + let subtask_to_json_string = (subtask: subtask): string => + yojson_of_subtask(subtask) |> Yojson.Safe.pretty_to_string; + + let subtask_to_pretty_string = (task: task, subtask: subtask): string => { + "=== Subtask ===\n" + ++ "Title: " + ++ subtask.title + ++ "\n" + ++ "Description: " + ++ subtask.description + ++ "\n" + ++ "Is Active: " + ++ string_of_bool(is_active(task, subtask.title)) + ++ "\n" + ++ "Completion Info: " + ++ ( + switch (subtask.completion_info) { + | Some(info) => + "Summary: " + ++ info.summary + ++ ", Elapsed Time (s): " + ++ string_of_float(info.elapsed_time) + | None => + is_active(task, subtask.title) + ? "Not Completed but In Progress." + : "Not Completed and Not in Progress." + } + ) + ++ "\n"; + }; + + let mk = (~title: string, ~description: string): subtask => { + { + title, + description, + tools_used: [], + completion_info: None, + subtask_ui: { + expanded: false, + }, + metadata: { + began_at: JsUtil.timestamp(), + completed_at: None, + last_updated_at: JsUtil.timestamp(), + }, + }; + }; + }; + + module TaskUtils = { + // == IMPORTS == + open Model; + open SubtaskUtils; + // ============= + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type task = Model.task; + + let ordered_subtasks_of = (task: task): list(subtask) => { + // Returns the subtasks in the order specified by subtask_ordering + task.subtask_ordering + |> List.filter_map((title: string) => + Maps.StringMap.find_opt(title, task.subtasks) + ); + }; + + let get_incompleted_subtasks = (task: task): list(subtask) => { + // Returns a list of all incompleted subtasks in the given task + ordered_subtasks_of(task) + |> List.filter((subtask: subtask) => !is_completed(subtask)); + }; + + let get_next_incomplete_subtask_title = (task: task): option(string) => { + // Returns the next incompleted subtask in the given task, if any + let* subtask = get_incompleted_subtasks(task) |> ListUtil.hd_opt; + Some(subtask.title); + }; + + let find_task = (model: t, task_name: string): option(task) => { + Maps.StringMap.find_opt(task_name, model.task_dict); + }; + + let is_active = (model: t, task_name: string): bool => { + switch (model.active_task) { + | Some(active_task_name) => active_task_name == task_name + | None => false + }; + }; + + let task_to_json_string = (task: task): string => + yojson_of_task(task) |> Yojson.Safe.pretty_to_string; + + let task_to_pretty_string = (model: t, task: task): string => { + "=== Task ===\n" + ++ "Title: " + ++ task.title + ++ "\n" + ++ "Description: " + ++ task.description + ++ "\n" + ++ "Is Active: " + ++ string_of_bool(is_active(model, task.title)) + ++ "\n" + ++ "Last Updated: " + ++ string_of_float(task.metadata.last_updated_at) + ++ "\n" + ++ "Completion Info: " + ++ ( + switch (task.completion_info) { + | Some(info) => + "Summary: " + ++ info.summary + ++ ", Elapsed Time (s): " + ++ string_of_float(info.elapsed_time) + | None => + is_active(model, task.title) + ? "Not Completed but In Progress." + : "Not Completed and Not in Progress." + } + ) + ++ "\nSubtasks:\n" + ++ List.fold_left( + (acc: string, subtask: subtask) => + acc ++ subtask_to_pretty_string(task, subtask), + "", + ordered_subtasks_of(task), + ); + }; + + let active_subtask = (task: task): option(subtask) => { + switch (task.active_subtask) { + | Some(active_subtask_name) => find_subtask(task, active_subtask_name) + | None => None + }; + }; + + let write_subtask = (~task: task, ~subtask: subtask): task => { + ...task, + subtasks: Maps.StringMap.add(subtask.title, subtask, task.subtasks), + }; + + let mk = + (~title: string, ~description: string, ~subtasks: list(subtask)) + : task => { + let subtask_map = + List.fold_left( + (acc: Maps.StringMap.t(subtask), subtask: subtask) => + Maps.StringMap.add(subtask.title, subtask, acc), + Maps.StringMap.empty, + subtasks, + ); + let subtask_ordering = + List.map((subtask: subtask) => subtask.title, subtasks); + { + title, + description, + subtasks: subtask_map, + subtask_ordering, + tools_used: [], + completion_info: None, + active_subtask: None, + task_ui: { + expanded: false, + }, + metadata: { + began_at: JsUtil.timestamp(), + completed_at: None, + last_updated_at: JsUtil.timestamp(), + }, + }; + }; + }; + + module TaskDictUtils = { + // == Description == + // All model updates should occur at this level + // ================= + // == IMPORTS == + open Model; + open TaskUtils; + // ============= + + let sorted_task_dict = (task_dict: task_dict): list(task) => { + // Sorts the given todo archive by last updated time, most recent first + task_dict + |> Maps.StringMap.bindings + |> List.map(((_, task: task)) => task) + |> List.sort((a: task, b: task) => + int_of_float( + b.metadata.last_updated_at -. a.metadata.last_updated_at, + ) + ); + }; + + let task_dict_to_string = (task_dict: task_dict): string => { + let header = "=== Task Archive (Sorted by Last Updated, Most Recent First) ==="; + header + ++ List.fold_left( + (acc: string, task: task) => acc ++ task_to_json_string(task), + header, + sorted_task_dict(task_dict), + ); + }; + }; + + module MainUtils = { + // == IMPORTS == + open Model; + open TaskUtils; + // ============= + + let active_task = (model: t): option(task) => { + // Returns: + // The active task if model.active_task is not None, + // o.w. None + switch (model.active_task) { + | Some(active_task) => + Some( + TaskUtils.find_task(model, active_task) + |> OptUtil.get_or_fail( + "Active task not found: " + ++ active_task + ++ "\nThis implies a bug. The string title in active_task should never point to a non-existent task. It either became stale or was ill-set.", + ), + ) + | None => None // No active task set + }; + }; + + let displayed_task = (model: t): option(task) => { + // Returns: + // The displayed task if model.t_ui.display_task is not None, + // o.w. None + switch (model.t_ui.display_task) { + | Some(displayed_task) => + Some( + TaskUtils.find_task(model, displayed_task) + |> OptUtil.get_or_fail( + "Displayed task not found: " + ++ displayed_task + ++ "\nThis implies a bug. The string title in t_ui.display_task should never point to a non-existent task. It either became stale or was ill-set.", + ), + ) + | None => None // No displayed task set + }; + }; + + let active_task_to_pretty_string = (model: t): string => { + let active_task_str = + switch (active_task(model)) { + | Some(active_task) => + "Active Task:\n" ++ task_to_pretty_string(model, active_task) + | None => "No Active Task Set, set one using the set_active_task tool." + }; + "=== Composition Active Task Information ===\n" + ++ active_task_str + ++ "\n========================"; + }; + + let init = (): t => { + { + active_task: None, + task_dict: Maps.StringMap.empty, + t_ui: { + active_view: UI.Chat, + display_task: None, + show_archive: false, + }, + }; + }; + + module SafeTaskAssertion = { + module StringSet = Set.Make(String); + let assert_no_duplicates_in_ordering = (~task: task): unit => { + // Assert no duplicates in subtask_ordering + let unique_subtask_titles = + task.subtask_ordering + |> List.fold_left( + (acc: StringSet.t, title: string) => { + StringSet.add(title, acc) + }, + StringSet.empty, + ); + StringSet.cardinal(unique_subtask_titles) + == List.length(task.subtask_ordering) + ? () + : raise( + Failure("Duplicate subtask titles found in subtask_ordering"), + ); + }; + + let assert_all_subtasks_from_ordering_exist_in_map = (~task: task): unit => + // Assert all subtasks in ordering exist in the subtasks map + List.for_all( + (subtask_title: string) => { + Maps.StringMap.mem(subtask_title, task.subtasks) + }, + task.subtask_ordering, + ) + ? () + : raise( + Failure("Subtask titles in ordering not found in subtasks map"), + ); + + let assert_all_subtasks_from_map_referenced_in_ordering = + (~task: task): unit => + // Assert all subtasks in the subtasks map are referenced in the ordering + Maps.StringMap.for_all( + (_, subtask: subtask) => { + List.mem(subtask.title, task.subtask_ordering) + }, + task.subtasks, + ) + ? () + : raise( + Failure( + "Subtask titles in subtasks map not referenced in ordering", + ), + ); + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type task_write_result = + | Success(t) + | Failure(string); + + let write_task = (~model: t, ~task: task): task_write_result => + try( + { + SafeTaskAssertion.assert_no_duplicates_in_ordering(~task); + SafeTaskAssertion.assert_all_subtasks_from_ordering_exist_in_map( + ~task, + ); + SafeTaskAssertion.assert_all_subtasks_from_map_referenced_in_ordering( + ~task, + ); + Success({ + ...model, + task_dict: Maps.StringMap.add(task.title, task, model.task_dict), + }); + } + ) { + | Failure(msg) => Failure(msg) + }; + }; +}; + +module Update = { + open Utils; + open Model; + + module Action = { + module UIAction = { + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type action = + // Switches the view between Chat and Todos + | SwitchView(UI.active_view) + // Toggles whether the task dictionary archive is shown + | ToggleShowTaskDictionary + | SetDisplayTask(string) + | ExpandSubtask(string); + }; + + module BackendAction = { + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type action = + | CreateNewTask(task) + | UnsetActiveTask + | SetActiveTask(string) // task title + | UnsetActiveSubtask + | SetActiveSubtask(string) // subtask title + | MarkActiveTaskComplete(string) // summary + | MarkActiveTaskIncomplete + | MarkActiveSubtaskComplete(string) // summary + | MarkActiveSubtaskIncomplete + | AddNewSubtaskToActiveTask(subtask) // title, description; + | ReorderSubtasksInActiveTask(list(string)); // list of subtask titles in new order + }; + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type action = + | UIAction(UIAction.action) + | BackendAction(BackendAction.action); + + [@deriving (show({with_path: false}), sexp, yojson, eq)] + type result = + | Success(Model.t) + | Failure(string); + + module UpdateUtils = { + let write_task = (~model: t, ~task: task): result => { + // Attempts to safely write the task to the model's task dictionary + // If it fails, an error is bubbled up and caught, failing this update action + // with the respective error message + switch (MainUtils.write_task(~model, ~task)) { + | Success(updated_model) => Success(updated_model) + | Failure(msg) => Failure(msg) + }; + }; + + let set_active_subtask_dispatch = + (~model: t, ~subtask_name: option(string)): result => { + switch (MainUtils.active_task(model)) { + | Some(active_task) => + let updated_task = { + ...active_task, + active_subtask: subtask_name, + }; + let updated_task = + switch (TaskUtils.active_subtask(updated_task)) { + | None => updated_task + | Some(active_subtask) => + let updated_subtask = { + ...active_subtask, + metadata: { + ...active_subtask.metadata, + began_at: JsUtil.timestamp(), + }, + }; + TaskUtils.write_subtask( + ~task=updated_task, + ~subtask=updated_subtask, + ); + }; + write_task(~model, ~task=updated_task); + | None => Failure("No active task to unset active subtask from") + }; + }; + }; + + let update = (~model: t, ~action: action): result => { + switch (action) { + | UIAction(a) => + switch (a) { + | SwitchView(new_view) => + Success({ + ...model, + t_ui: { + ...model.t_ui, + active_view: new_view, + }, + }) + | ToggleShowTaskDictionary => + Success({ + ...model, + t_ui: { + ...model.t_ui, + show_archive: !model.t_ui.show_archive, + }, + }) + | SetDisplayTask(task_title) => + Success({ + ...model, + t_ui: { + ...model.t_ui, + display_task: Some(task_title), + }, + }) + | ExpandSubtask(subtask_title) => + switch (MainUtils.displayed_task(model)) { + | None => Failure("No displayed task to expand subtask in") + | Some(displayed_task) => + switch (SubtaskUtils.find_subtask(displayed_task, subtask_title)) { + | None => + Failure( + "Subtask not found in displayed task: " ++ subtask_title, + ) + | Some(subtask) => + let updated_subtask = { + ...subtask, + subtask_ui: { + expanded: !subtask.subtask_ui.expanded, + }, + }; + let updated_task = + TaskUtils.write_subtask( + ~task=displayed_task, + ~subtask=updated_subtask, + ); + UpdateUtils.write_task(~model, ~task=updated_task); + } + } + } + | BackendAction(a) => + switch (a) { + | CreateNewTask(new_task) => + /* + - Writes the new task to the model's task dictionary + - Sets it as the active task + */ + let model = { + ...model, + active_task: Some(new_task.title), + t_ui: { + ...model.t_ui, + display_task: + model.t_ui.display_task == None + ? Some(new_task.title) : model.t_ui.display_task, + }, + }; + UpdateUtils.write_task(~model, ~task=new_task); + | UnsetActiveTask => + /* + - Unsets the active task in the model + */ + Success({ + ...model, + active_task: None, + }) + | SetActiveTask(task_title) => + /* + - Sets the active task in the model to the given task title + */ + Success({ + ...model, + active_task: Some(task_title), + }) + | UnsetActiveSubtask => + /* + - If an active task exists, unsets the active subtask in that task + */ + UpdateUtils.set_active_subtask_dispatch(~model, ~subtask_name=None) + | SetActiveSubtask(subtask_title) => + /* + - If an active task exists, sets the active subtask in that task to the given subtask title + */ + UpdateUtils.set_active_subtask_dispatch( + ~model, + ~subtask_name=Some(subtask_title), + ) + | MarkActiveTaskComplete(summary) => + /* + - Sets/records the completion info of the active task + */ + switch (MainUtils.active_task(model)) { + | None => Failure("No active task to mark complete") + | Some(active_task) => + switch (TaskUtils.get_incompleted_subtasks(active_task)) { + | [] => + let clock_it = JsUtil.timestamp(); + let task = { + ...active_task, + completion_info: + Some({ + summary, + elapsed_time: clock_it -. active_task.metadata.began_at, + }), + metadata: { + ...active_task.metadata, + completed_at: Some(clock_it), + last_updated_at: clock_it, + }, + }; + UpdateUtils.write_task(~model, ~task); + | incompleted_subtasks => + let incompleted_titles = + incompleted_subtasks + |> List.map((subtask: subtask) => subtask.title); + Failure( + "Cannot mark active task complete. Please complete the following subtasks first before marking the task complete: " + ++ String.concat(", ", incompleted_titles), + ); + } + } + | MarkActiveTaskIncomplete => + /* + - Clears the completion info of the active task + - Keeps the active task set in the model + */ + switch (MainUtils.active_task(model)) { + | None => Failure("No active task to mark complete") + | Some(active_task) => + let clock_it = JsUtil.timestamp(); + let task = { + ...active_task, + completion_info: None, + metadata: { + ...active_task.metadata, + completed_at: None, + last_updated_at: clock_it, + }, + }; + UpdateUtils.write_task(~model, ~task); + } + | MarkActiveSubtaskComplete(summary) => + /* + - Sets/records the completion info of the active subtask in the active task + */ + switch (MainUtils.active_task(model)) { + | None => Failure("No active task to mark subtask complete") + | Some(active_task) => + switch (TaskUtils.active_subtask(active_task)) { + | None => Failure("No active subtask to mark complete") + | Some(active_subtask) => + let clock_it = JsUtil.timestamp(); + let updated_subtask = { + ...active_subtask, + completion_info: + Some({ + summary, + elapsed_time: clock_it -. active_subtask.metadata.began_at, + }), + metadata: { + ...active_subtask.metadata, + completed_at: Some(clock_it), + last_updated_at: clock_it, + }, + }; + let updated_task = + TaskUtils.write_subtask( + ~task={ + ...active_task, + active_subtask: + Utils.TaskUtils.get_next_incomplete_subtask_title( + active_task, + ), + }, + ~subtask=updated_subtask, + ); + UpdateUtils.write_task(~model, ~task=updated_task); + } + } + | MarkActiveSubtaskIncomplete => + /* + - Clears the completion info of the active subtask in the active task + */ + switch (MainUtils.active_task(model)) { + | None => Failure("No active task to mark subtask incomplete") + | Some(active_task) => + switch (TaskUtils.active_subtask(active_task)) { + | None => Failure("No active subtask to mark incomplete") + | Some(active_subtask) => + let clock_it = JsUtil.timestamp(); + let updated_subtask = { + ...active_subtask, + completion_info: None, + metadata: { + ...active_subtask.metadata, + completed_at: None, + last_updated_at: clock_it, + }, + }; + let updated_task = + TaskUtils.write_subtask( + ~task=active_task, + ~subtask=updated_subtask, + ); + UpdateUtils.write_task(~model, ~task=updated_task); + } + } + // | _ => Failure("unimplemented") + | AddNewSubtaskToActiveTask(new_subtask) => + /* + - Adds the given new subtask to the active task's subtasks + - Appends the new subtask's title to the active task's subtask ordering + */ + switch (MainUtils.active_task(model)) { + | None => Failure("No active task to add new subtask to") + | Some(active_task) => + let updated_task = + TaskUtils.write_subtask( + ~task=active_task, + ~subtask=new_subtask, + ); + let updated_task = { + ...updated_task, + subtask_ordering: + List.append( + updated_task.subtask_ordering, + [new_subtask.title], + ), + }; + UpdateUtils.write_task(~model, ~task=updated_task); + } + | ReorderSubtasksInActiveTask(new_ordering) => + /* + - Reorders the subtasks in the active task according to the given new ordering + - The new ordering is a list of subtask titles + */ + switch (MainUtils.active_task(model)) { + | None => Failure("No active task to reorder subtasks in") + | Some(active_task) => + let updated_task = { + ...active_task, + subtask_ordering: new_ordering, + }; + UpdateUtils.write_task(~model, ~task=updated_task); + } + } + }; + }; + }; +}; diff --git a/src/haz3lcore/ai_assistant_core/Composition/CompositionGo.re b/src/haz3lcore/ai_assistant_core/Composition/CompositionGo.re new file mode 100644 index 0000000000..84361c725b --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/CompositionGo.re @@ -0,0 +1,690 @@ +open Util; +open HighLevelNodeMap.Public; +open Language; + +type node_map = HighLevelNodeMap.t; +type node = HighLevelNodeMap.node; + +module Local = { + type inner_term = + | Pat + | Def + | Body; + + module Utils = { + let get_inner_term_id = (inner_term: inner_term, node: node): Id.t => { + /* + Returns the specified "inner_term" from the "curr_node_info" + + E.g. If current node is "x" in a program "let x : Int = 2 + 3 in 100 + 200", + calling get_inner_term_id(curr_node_info, Pat) will return the id of the pattern "x : Int", + calling get_inner_term_id(curr_node_info, Def) will return the id of the definition "2 + 3", + calling get_inner_term_id(curr_node_info, Body) will return the id of the body "100 + 200". + */ + switch (node.info) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(pat, def, body) => + switch (inner_term) { + | Pat => Pat.rep_id(pat) + | Def => Exp.rep_id(def) + | Body => Exp.rep_id(body) + } + | TyAlias(tpat, tdef, body) => + switch (inner_term) { + | Pat => TPat.rep_id(tpat) + | Def => Typ.rep_id(tdef) + | Body => Exp.rep_id(body) + } + | _ => + raise( + Failure( + "UNIMPLEMENTED_NODE_TYPE: Only let and type alias expressions are currently supported as nodes", + ), + ) + } + | _ => + raise( + Failure( + "Current node is not a let or type alias expression, so no pattern to update", + ), + ) + }; + }; + }; + + module PerformUtils = { + let edit_action_to_static_error_scrutiny = + (~edit_action: CompositionActions.edit_action): (bool, bool, bool) => { + // Returns (of_pat, of_def, of_body), i.e. which parts of the program to check for static errors. + switch (edit_action) { + | Initialize(_) => + raise( + Failure( + "Initialize action handles static error checking on its own.", + ), + ) + | UpdateDefinition(_) => (true, true, false) + | UpdateBody(_) => (true, true, true) + | UpdatePattern(_) => (true, false, false) + | UpdateBindingClause(_) => (false, true, false) + | InsertBefore(_) => (false, false, false) + | InsertAfter(_) => (false, false, false) + | DeleteBindingClause(_) => (false, true, false) + | DeleteBody(_) => (false, false, true) + }; + }; + + let static_error_check = + ( + ~edit_action: CompositionActions.edit_action, + ~initial_node: option(node), + ~initial_info_map: Id.Map.t(Info.t), + ~new_node: node, + ~new_info_map: Id.Map.t(Info.t), + ) + : option(string) => { + /* + A localized static error check to ensure that newly inserted segments do not introduce any errors. + + This is a localized check, as obligations occuring elsewhere in the program are inevitable for + many types of edits. + + of_pat, of_def, and of_body are used to specify which parts of the program to check for errors. + */ + let (of_pat, of_def, of_body) = + edit_action_to_static_error_scrutiny(~edit_action); + let initial_errors = + switch (initial_node) { + | None => [] + | Some(initial_node) => + let initial_subtree = + GeneralTreeUtils.subtree_of( + ~info=initial_node.info, + ~orig_info_map=initial_info_map, + ~of_pat, + ~of_def, + ~of_body, + ); + ErrorPrint.all(initial_subtree); + }; + let new_subtree = + GeneralTreeUtils.subtree_of( + ~info=new_node.info, + ~orig_info_map=new_info_map, + ~of_pat, + ~of_def, + ~of_body, + ); + let new_errors = ErrorPrint.all(new_subtree); + if (List.length(new_errors) > List.length(initial_errors)) { + Some( + "Not applying the action you requested as it would have the following static error(s): " + ++ String.concat(", ", new_errors), + ); + } else { + None; + }; + }; + + let statics_map_new_ids = + (old_statics: StaticsBase.Map.t, new_statics: StaticsBase.Map.t) => { + // Returns only the IDs of the new statics map that are not in the old statics map + // This is useful to identify which new static information was added + Id.Map.fold( + (id, _info, acc) => + // Check if the ID exists in the old statics map + switch (StaticsBase.Map.lookup(id, old_statics)) { + | Some(_) => acc // ID exists in old map, don't include it + | None => [id, ...acc] // ID doesn't exist in old map, include it + }, + new_statics, + [], + ); + }; + + let introduce = + ( + z: Zipper.t, + code: string, + return: + (Action.Failure.t, option(Zipper.t)) => + result(Zipper.t, Action.Failure.t), + ) => { + // A wrapper function for trying to paste code into the zipper + // Note that we paste a segment; so, we convert the string to a segment + // first, and then insert the segment into the zipper. This helps to + // avoid potential current buggy parsing issues. + Parser.to_segment(code) + |> OptUtil.and_then((segment: Segment.t) => + Some(Zipper.insert_segment(z, segment)) + ) + |> return(CantPaste); + }; + + let destruct = + ( + ~defs_exclude_bodies: bool, + z: Zipper.t, + target_id: Id.t, + syntax: CachedSyntax.t, + ) => { + switch ( + Select.term( + ~defs_exclude_bodies, + ~case_rules=false, + syntax.term_data, + target_id, + z, + ) + ) { + | Some(z') => + switch (Destruct.go(Left, z')) { + | None => Error(Action.Failure.Cant_destruct) + | Some(z'') => Ok(z'') + } + | None => Error(Action.Failure.Cant_select) + }; + }; + + let overwrite_term = + ( + z: Zipper.t, + target_id: Id.t, + code: string, + defs_exclude_bodies: bool, + syntax: CachedSyntax.t, + return: + (Action.Failure.t, option(Zipper.t)) => + result(Zipper.t, Action.Failure.t), + ) => { + // Select the respective term (in this case the definition term) + switch ( + Select.term( + ~defs_exclude_bodies, + ~case_rules=false, + syntax.term_data, // todo: not sure about this arg + target_id, + z, + ) + ) { + | Some(z') => + // Paste the code over the selected tile + introduce(z', code, return) + | None => Error(Action.Failure.Cant_select) + }; + }; + let insert_term = + ( + z: Zipper.t, + target_id: Id.t, + code: string, + d: Direction.t, + syntax: CachedSyntax.t, + return: + (Action.Failure.t, option(Zipper.t)) => + result(Zipper.t, Action.Failure.t), + ) => { + switch ( + // ' let a = 0 in' + Select.term( + ~defs_exclude_bodies=true, + ~case_rules=false, + syntax.term_data, // todo: not sure about this arg, is it right? + target_id, + z, + ) + ) { + | Some(z') => + switch (Move.by_token(d, z')) { + | Some(z'') => introduce(z'', code, return) + | None => Error(Action.Failure.Cant_move) + } + | None => Error(Action.Failure.Cant_select) + }; + }; + }; + + let initialize_dispatch = + ( + z: Zipper.t, + mk_statics: Zipper.t => StaticsBase.Map.t, + return: + (Action.Failure.t, option(Zipper.t)) => + result(Zipper.t, Action.Failure.t), + code: string, + ) => { + switch (PerformUtils.introduce(Select.all(z), code, return)) { + | Ok(new_z) => + let new_statics = mk_statics(new_z); + // For initialization, check the entire program for errors + let new_errors = ErrorPrint.all(new_statics); + if (List.length(new_errors) > 0) { + Error( + Action.Failure.Composition_action_failure( + "Not applying the action you requested as it would have the following static error(s): " + ++ String.concat(", ", new_errors), + ), + ); + } else { + Ok(new_z); + }; + | Error(e) => Error(e) + }; + }; + + let view_dispatch = (a: CompositionActions.view_action, z: Zipper.t) => { + switch (a) { + | Expand(paths) => + let z' = { + ...z, + agent_view: AgentState.add_paths(paths, z.agent_view), + }; + Ok(z'); + | Collapse(paths) => + let z' = { + ...z, + agent_view: AgentState.remove_paths(paths, z.agent_view), + }; + Ok(z'); + }; + }; + + let edit_dispatch = + ( + ~e: CompositionActions.edit_action, + ~initial_z: Zipper.t, + ~initial_node_map: node_map, + ~initial_info_map: Id.Map.t(Info.t), + ~syntax: CachedSyntax.t, + ~return: + (Action.Failure.t, option(Zipper.t)) => + result(Zipper.t, Action.Failure.t), + ~mk_statics: Zipper.t => StaticsBase.Map.t, + ) => { + switch (e) { + | UpdateDefinition(path, code) => + let initial_node = path_to_node(initial_node_map, path); + let target_id = Utils.get_inner_term_id(Def, initial_node); + switch ( + PerformUtils.overwrite_term( + initial_z, + target_id, + code, + false, + syntax, + return, + ) + ) { + | Error(e) => Error(e) + | Ok(new_z) => + let new_info_map = mk_statics(new_z); + switch (build(new_z, new_info_map)) { + | None => Error(Action.Failure.Cant_derive_local_AST_information) + | Some(new_node_map) => + switch ( + PerformUtils.static_error_check( + ~edit_action=e, + ~initial_node=Some(initial_node), + ~initial_info_map, + ~new_node=node_of_cursor(new_node_map, new_z, new_info_map), + ~new_info_map, + ) + ) { + | Some(e) => Error(Action.Failure.Composition_action_failure(e)) + | None => Ok(new_z) + } + }; + }; + | UpdateBody(path, code) => + let initial_node = path_to_node(initial_node_map, path); + let target_id = Utils.get_inner_term_id(Body, initial_node); + switch ( + PerformUtils.overwrite_term( + initial_z, + target_id, + code, + false, + syntax, + return, + ) + ) { + | Error(e) => Error(e) + | Ok(new_z) => + let new_info_map = mk_statics(new_z); + switch (build(new_z, new_info_map)) { + | None => Error(Action.Failure.Cant_derive_local_AST_information) + | Some(new_node_map) => + switch ( + PerformUtils.static_error_check( + ~edit_action=e, + ~initial_node=Some(initial_node), + ~initial_info_map, + ~new_node=node_of_cursor(new_node_map, new_z, new_info_map), + ~new_info_map, + ) + ) { + | Some(e) => Error(Action.Failure.Composition_action_failure(e)) + | None => Ok(new_z) + } + }; + }; + | UpdatePattern(path, code) => + let initial_node = path_to_node(initial_node_map, path); + let target_id = Utils.get_inner_term_id(Pat, initial_node); + let old_pat = + StaticsBase.Map.lookup(target_id, initial_info_map) + |> OptUtil.get_or_fail( + "Failed trying to rename all occurences of the pattern. Could not find the old pattern in the statics map.", + ); + switch ( + PerformUtils.overwrite_term( + initial_z, + target_id, + code, + false, + syntax, + return, + ) + ) { + | Error(e) => Error(e) + | Ok(new_z) => + let new_info_map = mk_statics(new_z); + switch (build(new_z, new_info_map)) { + | None => Error(Action.Failure.Cant_derive_local_AST_information) + | Some(new_node_map) => + switch ( + PerformUtils.static_error_check( + ~edit_action=e, + ~initial_info_map, + ~initial_node=Some(initial_node), + ~new_info_map, + ~new_node=node_of_cursor(new_node_map, new_z, new_info_map), + ) + ) { + | Some(e) => Error(Action.Failure.Composition_action_failure(e)) + | None => + let new_node = node_of_cursor(new_node_map, new_z, new_info_map); + let new_target_id = Utils.get_inner_term_id(Pat, new_node); + let new_pat = + StaticsBase.Map.lookup(new_target_id, new_info_map) + |> OptUtil.get_or_fail( + "Failed trying to rename all occurences of the pattern. Could not find the new pattern in the statics map.", + ); + Ok( + GeneralTreeUtils.update_use_sites_of_pat( + ~z=new_z, + ~co_ctx= + GeneralTreeUtils.get_refs_to( + initial_node.info, + new_info_map, + ), + ~old_names=GeneralTreeUtils.get_var_names_from_pat(old_pat), + ~new_names=GeneralTreeUtils.get_var_names_from_pat(new_pat), + ), + ); + } + }; + }; + | UpdateBindingClause(path, code) => + let initial_node = path_to_node(initial_node_map, path); + let target_id = path_to_id(initial_node_map, path); + switch ( + PerformUtils.overwrite_term( + initial_z, + target_id, + code, + true, + syntax, + return, + ) + ) { + | Error(e) => Error(e) + | Ok(new_z) => + let new_info_map = mk_statics(new_z); + switch (build(new_z, new_info_map)) { + | None => Error(Action.Failure.Cant_derive_local_AST_information) + | Some(new_node_map) => + switch ( + PerformUtils.static_error_check( + ~edit_action=e, + ~initial_info_map, + ~initial_node=Some(initial_node), + ~new_info_map, + ~new_node=node_of_cursor(new_node_map, new_z, new_info_map), + ) + ) { + | Some(e) => Error(Action.Failure.Composition_action_failure(e)) + | None => Ok(new_z) + } + }; + }; + | InsertBefore(path, code) => + // todo: figure out a better method than magic space + let target_id = path_to_id(initial_node_map, path); + switch ( + { + PerformUtils.insert_term( + initial_z, + target_id, + "\n" ++ code ++ "\n", + Direction.Left, + syntax, + return, + ); + } + ) { + | Error(e) => Error(e) + | Ok(new_z) => + let new_info_map = mk_statics(new_z); + let old_errors = ErrorPrint.all(initial_info_map); + let new_errors = ErrorPrint.all(new_info_map); + if (List.length(new_errors) > List.length(old_errors)) { + Error( + Action.Failure.Composition_action_failure( + "Not applying the action you requested as it would introduce new static error(s): " + ++ String.concat(", ", new_errors), + ), + ); + } else { + Ok(new_z); + }; + }; + | InsertAfter(path, code) => + // todo: figure out a better method than magic space + let target_id = path_to_id(initial_node_map, path); + switch ( + PerformUtils.insert_term( + initial_z, + target_id, + "\n" ++ code ++ "\n", + Direction.Right, + syntax, + return, + ) + ) { + | Error(e) => Error(e) + | Ok(new_z) => + let new_info_map = mk_statics(new_z); + let old_errors = ErrorPrint.all(initial_info_map); + let new_errors = ErrorPrint.all(new_info_map); + if (List.length(new_errors) > List.length(old_errors)) { + Error( + Action.Failure.Composition_action_failure( + "Not applying the action you requested as it would introduce new static error(s): " + ++ String.concat(", ", new_errors), + ), + ); + } else { + Ok(new_z); + }; + }; + | DeleteBindingClause(path) => + let target_id = path_to_id(initial_node_map, path); + PerformUtils.destruct( + ~defs_exclude_bodies=true, + initial_z, + target_id, + syntax, + ); + | DeleteBody(path) => + let node = path_to_node(initial_node_map, path); + let target_id = Utils.get_inner_term_id(Body, node); + PerformUtils.destruct( + ~defs_exclude_bodies=false, + initial_z, + target_id, + syntax, + ); + | Initialize(_) => + Error( + Action.Failure.Composition_action_failure( + "Once a program has let/type alias expressions, you can never use initialize on it ever again.", + ), + ) + }; + }; + + // Tempory wrapper that helps me localize myself while implementing (remove) + let composition_dispatch = + ( + a: CompositionActions.editor_action, + syntax: CachedSyntax.t, + z: Zipper.t, + mk_statics: Zipper.t => StaticsBase.Map.t, + return: + (Action.Failure.t, option(Zipper.t)) => + result(Zipper.t, Action.Failure.t), + ) => { + let initial_info_map = mk_statics(z); + switch (build(z, initial_info_map)) { + | None => + switch (a) { + | Edit(Initialize(code)) => + initialize_dispatch(z, mk_statics, return, code) + | _ => Error(Action.Failure.Cant_derive_local_AST_information) + } + | Some(initial_node_map) => + switch (a) { + | View(a) => view_dispatch(a, z) + | Read(ShowUseSites(_path)) + | Read(ShowReferences(_path)) => Ok(z) // TODO: Implement + | Edit(e) => + edit_dispatch( + ~e, + ~initial_z=z, + ~initial_node_map, + ~initial_info_map, + ~syntax, + ~return, + ~mk_statics, + ) + } + }; + }; + + let get_initial_cursor_position = (z: Zipper.t, info_map: Id.Map.t(Info.t)) => { + switch (Indicated.ci_of(z, info_map)) { + | Some(ci) => Info.id_of(ci) + | None => + raise( + Failure( + "No indicated piece found when getting initial cursor position.", + ), + ) + }; + }; + + let reposition_cursor = (z: Zipper.t, target_id: Id.t) => { + switch (Move.jump_to_id_indicated(z, target_id)) { + | Some(z) => Ok(z) + | None => Error(Action.Failure.Cant_move) + }; + }; + + let freshen_paths = + (z: Zipper.t, mk_statics: Zipper.t => StaticsBase.Map.t) + : result(Zipper.t, Action.Failure.t) => { + // This function removes any stale paths from the agent view list + // This can happen if variables are changed or deleted from the editor itself + let node_map = build(z, mk_statics(z)); + switch (node_map) { + | None => + Ok({ + ...z, + agent_view: AgentState.init, + }) + | Some(node_map) => + Ok({ + ...z, + agent_view: { + expanded_paths: + List.filter( + (path: string) => { + switch (path_to_id_opt(node_map, path)) { + | Some(_) => true + | None => false + } + }, + z.agent_view.expanded_paths, + ), + }, + }) + }; + }; + + let go = + ( + ~syntax: CachedSyntax.t, + ~z: Zipper.t, + ~a: CompositionActions.editor_action, + ~mk_statics: Zipper.t => StaticsBase.Map.t, + ~return: + (Action.Failure.t, option(Zipper.t)) => + result(Zipper.t, Action.Failure.t), + ~schedule_tool_response: AssistantUpdateAction.status => unit, + ) => { + let res = + try( + switch (composition_dispatch(a, syntax, z, mk_statics, return)) { + | Ok(new_z) => + switch (freshen_paths(new_z, mk_statics)) { + | Ok(new_z) => Ok(Dump.to_zipper(new_z)) + | Error(e) => Error(e) + } + | Error(e) => Error(e) + } + ) { + | Failure(e) => Error(Action.Failure.Composition_action_failure(e)) + }; + + //todo: handle res and use schedule_assistant_action to send the result to the assistant and loop + switch (res) { + | Ok(_) => + schedule_tool_response( + AssistantUpdateAction.Success( + "Action has been applied to the editor -- TODO: make more informative", + ), + ) + | Error(Composition_action_failure(e)) => + schedule_tool_response(AssistantUpdateAction.Failure(e)) + | Error(Action.Failure.Cant_derive_local_AST_information) => + schedule_tool_response( + AssistantUpdateAction.Failure( + "Could not derive an AST with definition nodes for the program. Maybe you meant to call initialize? Make sure to call initialize to overwrite the current program, and introduce a program with definition-based nodes. i.e. You tried calling a tool that requres deinitions to exist in the program.", + ), + ) + | _ => + schedule_tool_response( + AssistantUpdateAction.Failure( + "An error occured when applying your changes to the editor", + ), + ) + }; + + res; + }; +}; +module Public = { + let go = Local.go; +}; diff --git a/src/haz3lcore/ai_assistant_core/Composition/CompositionUtils.re b/src/haz3lcore/ai_assistant_core/Composition/CompositionUtils.re new file mode 100644 index 0000000000..db124c70f4 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/CompositionUtils.re @@ -0,0 +1,292 @@ +open Util; +open CompositionActions; + +[@deriving (show({with_path: false}), sexp, yojson)] +type action = CompositionActions.composition_action; + +[@deriving (show({with_path: false}), sexp, yojson)] +type action_wrapper = + | Action(action) + | Failure(string); + +module Local = { + [@deriving (show({with_path: false}), sexp, yojson)] + type t = list(API.Json.t); + + let tools = [ + // NavTools.go_to_parent, // No current node should have a parent anymore... we nav the top-level nodes + // NavTools.go_to_child, + ViewTools.expand, + ViewTools.collapse, + EditTools.initialize, // For initializing empty or nodeless programs + EditTools.update_definition, + EditTools.update_body, + EditTools.update_pattern, + EditTools.update_binding_clause, + EditTools.delete_binding_clause, + EditTools.delete_body, + EditTools.insert_after, + EditTools.insert_before, + // ViewTools.view_entire_definition, // No longer needed is this top-level refactor... this is done by default + // ViewTools.view_context, + WorkbenchTools.create_new_task, + WorkbenchTools.set_active_task, + WorkbenchTools.unset_active_task, + WorkbenchTools.set_active_subtask, + WorkbenchTools.unset_active_subtask, + WorkbenchTools.mark_active_task_complete, + WorkbenchTools.mark_active_task_incomplete, + WorkbenchTools.mark_active_subtask_complete, + WorkbenchTools.mark_active_subtask_incomplete, + ]; + + let get_string_arg = (~arg: option(string), ~fail_with: string) => { + switch (arg) { + | Some(arg) => arg + | None => raise(Failure(fail_with)) + }; + }; + + let get_path = (~path: option(string)) => { + switch (path) { + | Some(path) => path + | None => raise(Failure("A path must be provided for the action")) + }; + }; + + let get_paths = (~paths: option(list(string))) => { + switch (paths) { + | Some(paths) => paths + | None => + raise(Failure("A list of paths must be provided for the action")) + }; + }; + + let action_of = (~tool_name: string, ~args: API.Json.t): action_wrapper => { + /* Possible arguments */ + /* Parsing here to avoid redundancy */ + /* Argument(s) may or may not be provided depending on the tool called */ + let get_path = () => { + switch (API.Json.dot("path", args)) { + | Some(`String(path)) => path + | _ => raise(Failure("A path must be provided for the action")) + }; + }; + let get_paths = () => { + switch (API.Json.dot("paths", args)) { + | Some(`List(paths)) => + List.map( + path => + switch (path) { + | `String(path) => path + | _ => raise(Failure("A path must be provided for the action")) + }, + paths, + ) + | _ => + raise(Failure("A list of paths must be provided for the action")) + }; + }; + let get_code = () => { + switch (API.Json.dot("code", args)) { + | Some(`String(code)) => code + | _ => raise(Failure("A code must be provided for the action")) + }; + }; + let get_title = (item: API.Json.t) => { + switch (API.Json.dot("title", item)) { + | Some(`String(title)) => title + | _ => raise(Failure("A title must be provided for the todo item")) + }; + }; + let get_summary = (item: API.Json.t) => { + switch (API.Json.dot("summary", item)) { + | Some(`String(summary)) => summary + | _ => raise(Failure("A summary must be provided for the todo item")) + }; + }; + let get_description = (item: API.Json.t) => { + switch (API.Json.dot("description", item)) { + | Some(`String(description)) => description + | _ => + raise(Failure("A description must be provided for the todo item")) + }; + }; + let json_to_subtask = + (item: API.Json.t): CompositionAgentWorkbench.Model.subtask => { + let title = get_title(item); + let description = get_description(item); + CompositionAgentWorkbench.Utils.SubtaskUtils.mk(~title, ~description); + }; + let get_subtask = (): CompositionAgentWorkbench.Model.subtask => { + let subtask_json = + switch (API.Json.dot("subtask", args)) { + | Some(subtask_json) => subtask_json + | _ => raise(Failure("Each subtask must be a JSON object")) + }; + json_to_subtask(subtask_json); + }; + let get_subtasks = + (item: API.Json.t): list(CompositionAgentWorkbench.Model.subtask) => { + let subtasks_json = + switch (API.Json.dot("subtasks", item)) { + | Some(`List(subtasks_json)) => subtasks_json + | _ => + raise( + Failure("A list of subtasks must be provided for the action"), + ) + }; + List.map(json_to_subtask, subtasks_json); + }; + let get_subtasks_ordering = (): list(string) => { + let subtasks_ordering_json = + switch (API.Json.dot("subtasks_ordering", args)) { + | Some(`List(subtasks_ordering_json)) => subtasks_ordering_json + | _ => + raise( + Failure( + "A list of subtask titles (subtask ordering) must be provided for the action", + ), + ) + }; + List.map( + (title: API.Json.t) => + switch (title) { + | `String(title) => title + | _ => + raise(Failure("Subtask titles in the ordering must be strings")) + }, + subtasks_ordering_json, + ); + }; + let get_task = (): CompositionAgentWorkbench.Model.task => { + let task_json = + switch (API.Json.dot("task", args)) { + | Some(task_json) => task_json + | _ => raise(Failure("A todo list must be provided for the action")) + }; + let title = get_title(task_json); + let description = get_description(task_json); + let subtasks = get_subtasks(task_json); + CompositionAgentWorkbench.Utils.TaskUtils.mk( + ~title, + ~description, + ~subtasks, + ); + }; + + let action = + try({ + let action = + switch (tool_name) { + | "expand" => Editor(View(Expand(get_paths()))) + | "collapse" => Editor(View(Collapse(get_paths()))) + | "initialize" => Editor(Edit(Initialize(get_code()))) + | "update_definition" => + Editor(Edit(UpdateDefinition(get_path(), get_code()))) + | "update_body" => + Editor(Edit(UpdateBody(get_path(), get_code()))) + | "update_pattern" => + Editor(Edit(UpdatePattern(get_path(), get_code()))) + | "update_binding_clause" => + Editor(Edit(UpdateBindingClause(get_path(), get_code()))) + | "insert_after" => + Editor(Edit(InsertAfter(get_path(), get_code()))) + | "insert_before" => + Editor(Edit(InsertBefore(get_path(), get_code()))) + | "delete_binding_clause" => + Editor(Edit(DeleteBindingClause(get_path()))) + | "delete_body" => Editor(Edit(DeleteBody(get_path()))) + | "create_new_task" => Assistant(CreateNewTask(get_task())) + | "unset_active_task" => Assistant(UnsetActiveTask) + | "set_active_task" => Assistant(SetActiveTask(get_title(args))) + | "unset_active_subtask" => Assistant(UnsetActiveSubtask) + | "set_active_subtask" => + Assistant(SetActiveSubtask(get_title(args))) + | "mark_active_task_complete" => + Assistant(MarkActiveTaskComplete(get_summary(args))) + | "mark_active_task_incomplete" => + Assistant(MarkActiveTaskIncomplete) + | "mark_active_subtask_complete" => + Assistant(MarkActiveSubtaskComplete(get_summary(args))) + | "mark_active_subtask_incomplete" => + Assistant(MarkActiveSubtaskIncomplete) + | "add_new_subtask_to_active_task" => + Assistant(AddNewSubtaskToActiveTask(get_subtask())) + | "reorder_subtasks_in_active_task" => + Assistant(ReorderSubtasksInActiveTask(get_subtasks_ordering())) + | _ => raise(Failure("The tool called does not exist.")) + }; + Action(action); + }) { + | Failure(s: string) => Failure(s) + }; + action; + }; + + let string_of = (action: action) => { + switch (action) { + | Editor(View(Expand(paths))) => + "expand(\"[" ++ String.concat(", ", paths) ++ "]\")" + | Editor(View(Collapse(paths))) => + "collapse(\"[" ++ String.concat(", ", paths) ++ "]\")" + | Editor(Read(ShowUseSites(path))) => + "show_use_sites(\"" ++ path ++ "\")" + | Editor(Read(ShowReferences(path))) => + "show_references(\"" ++ path ++ "\")" + | Editor(Edit(Initialize(code))) => "initialize(\"" ++ code ++ "\")" + | Editor(Edit(UpdateDefinition(path, code))) => + "update_definition(\"" ++ path ++ "\", \"" ++ code ++ "\")" + | Editor(Edit(UpdateBody(path, code))) => + "update_body(\"" ++ path ++ "\", \"" ++ code ++ "\")" + | Editor(Edit(UpdatePattern(path, code))) => + "update_pattern(\"" ++ path ++ "\", \"" ++ code ++ "\")" + | Editor(Edit(UpdateBindingClause(path, code))) => + "update_binding_clause(\"" ++ path ++ "\", \"" ++ code ++ "\")" + | Editor(Edit(DeleteBindingClause(path))) => + "delete_binding_clause(\"" ++ path ++ "\")" + | Editor(Edit(DeleteBody(path))) => "delete_body(\"" ++ path ++ "\")" + | Editor(Edit(InsertAfter(path, code))) => + "insert_after(\"" ++ path ++ "\", \"" ++ code ++ "\")" + | Editor(Edit(InsertBefore(path, code))) => + "insert_before(\"" ++ path ++ "\", \"" ++ code ++ "\")" + | Assistant(CreateNewTask(task)) => + "create_new_task( " + ++ CompositionAgentWorkbench.Utils.TaskUtils.task_to_json_string(task) + ++ " )" + | Assistant(UnsetActiveTask) => "unset_active_task" + | Assistant(SetActiveTask(task_title)) => + "set_active_task(\"" ++ task_title ++ "\")" + | Assistant(UnsetActiveSubtask) => "unset_active_subtask" + | Assistant(SetActiveSubtask(subtask_title)) => + "set_active_subtask(\"" ++ subtask_title ++ "\")" + | Assistant(MarkActiveTaskComplete(summary)) => + "mark_active_task_complete(\"" ++ summary ++ "\")" + | Assistant(MarkActiveTaskIncomplete) => "mark_active_task_incomplete" + | Assistant(MarkActiveSubtaskComplete(summary)) => + "mark_active_subtask_complete(\"" ++ summary ++ "\")" + | Assistant(MarkActiveSubtaskIncomplete) => "mark_active_subtask_incomplete" + | Assistant(AddNewSubtaskToActiveTask(subtask)) => + "add_new_subtask_to_active_task( " + ++ CompositionAgentWorkbench.Utils.SubtaskUtils.subtask_to_json_string( + subtask, + ) + ++ ")" + | Assistant(ReorderSubtasksInActiveTask(subtasks_ordering)) => + "reorder_subtasks_in_active_task( \"[" + ++ String.concat(", ", subtasks_ordering) + ++ "]\" )" + }; + }; +}; + +module Public = { + [@der] + let tools = Local.tools; + let action_of = (~tool_name: string, ~args: API.Json.t): action_wrapper => { + Local.action_of(~tool_name, ~args); + }; + let string_of = (action: action) => { + Local.string_of(action); + }; +}; diff --git a/src/haz3lcore/ai_assistant_core/Composition/CompositionView.re b/src/haz3lcore/ai_assistant_core/Composition/CompositionView.re new file mode 100644 index 0000000000..d7daea0708 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/CompositionView.re @@ -0,0 +1,354 @@ +open Util; +open Language; +open Language.Statics; +open HighLevelNodeMap.Public; + +/* + Follows a programming practice/pattern here of separating logic local to the file into a "local" module, + and then exposing a public module for the logic that is intended to be used externally. + + module Local = { + **utils/helpers/etc.** + }; + module Public = { + **functions/methods/etc.** + }; + + The local code can get pretty messy and maze-like, thus showing a clear separation of what is + exposed and what is not will hopefully counteract this. + */ + +module Local = { + module Utils = { + let get_individual_ids_of_let = (term: Info.t): (Id.t, Id.t, Id.t) => { + switch (term) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(pat, def, body) => ( + Pat.rep_id(pat), + Exp.rep_id(def), + Exp.rep_id(body), + ) + // We won't fold/abstract away type definitions, so no need for this helper to handle them yet + | _ => (Id.invalid, Id.invalid, Id.invalid) + } + | _ => (Id.invalid, Id.invalid, Id.invalid) + }; + }; + + let get_def_id_of_let = (term: Info.t): Id.t => { + let (_, def, _) = get_individual_ids_of_let(term); + def; + }; + }; + + module ViewUtils = { + let rec fold_terms = (z: Zipper.t, ids: list(Id.t)) => { + switch (ids) { + | [] => z + | [id, ...rest] => + // Fold the term + switch ( + Select.term( + ~defs_exclude_bodies=false, + ~case_rules=false, + CachedSyntax.init(z).term_data, + id, + z, + ) + ) { + | Some(z') => + switch ( + ProjectorPerform.go( + CachedSyntax.init(z').term_data, + SetIndicated(Specific(Fold)), + z', + ) + ) { + | Ok(z'') => fold_terms(z'', rest) + | _ => fold_terms(z', rest) + } + | None => fold_terms(z, rest) + } + }; + }; + + let is_term_folded = (term_data: TermData.t, id: Id.t, z: Zipper.t): bool => { + switch ( + Select.term( + ~defs_exclude_bodies=false, + ~case_rules=false, + term_data, + id, + z, + ) + ) { + | Some(z') => + switch (z'.selection.content) { + | [Piece.Projector(pr)] => pr.kind == ProjectorCore.Kind.Fold + | _ => false + } + | None => false + }; + }; + + let expand_terms = (~z: Zipper.t, ~ids: list(Id.t)) => { + // Retain only the ids which are already folded + let ids = + List.filter_map( + id => + if (is_term_folded(CachedSyntax.init(z).term_data, id, z)) { + Some(id); + } else { + None; + }, + ids, + ); + fold_terms(z, ids); + }; + + let collapse_terms = (~z: Zipper.t, ~ids: list(Id.t)) => { + // Retain only the ids which are not folded + let ids = + List.filter_map( + id => + if (!is_term_folded(CachedSyntax.init(z).term_data, id, z)) { + Some(id); + } else { + None; + }, + ids, + ); + fold_terms(z, ids); + }; + + let expand_definitions = + (~z: Zipper.t, ~ids: list(Id.t), ~info_map: Id.Map.t(Info.t)) => { + let infos = + List.map((id: Id.t) => Id.Map.find_opt(id, info_map), ids); + let def_ids = + List.filter_map( + (info: option(Info.t)) => + switch (info) { + | Some(info) => Some(Utils.get_def_id_of_let(info)) + | None => None + }, + infos, + ) + |> List.filter((id: Id.t) => id != Id.invalid); + expand_terms(~z, ~ids=def_ids); + }; + + let collapse_definitions = + (~z: Zipper.t, ~ids: list(Id.t), ~info_map: Id.Map.t(Info.t)) => { + let infos = + List.map((id: Id.t) => Id.Map.find_opt(id, info_map), ids); + let def_ids = + List.filter_map( + (info: option(Info.t)) => + switch (info) { + | Some(info) => Some(Utils.get_def_id_of_let(info)) + | None => None + }, + infos, + ) + |> List.filter((id: Id.t) => id != Id.invalid); + collapse_terms(~z, ~ids=def_ids); + }; + + let fold_body = (z: Zipper.t, term: Info.t) => { + let id = + switch (term) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(_, _, body) => Exp.rep_id(body) + | TyAlias(_, _, body) => Exp.rep_id(body) + | _ => Id.invalid + } + | _ => Id.invalid + }; + switch ( + Select.term( + ~defs_exclude_bodies=false, + ~case_rules=false, + CachedSyntax.init(z).term_data, + id, + z, + ) + ) { + | Some(z') => + let z'' = + ProjectorPerform.go( + CachedSyntax.init(z').term_data, + SetIndicated(Specific(Fold)), + z', + ); + z''; + | _ => Ok(z) + }; + }; + }; + + module ContextUtils = { + let context_of = (node: HighLevelNodeMap.node): string => { + switch (node.info) { + | InfoExp({ctx, _}) => + let bindings: Binding.s = + List.filter_map( + (entry: Ctx.entry) => { + let b = + switch (entry) { + | Ctx.VarEntry(entry) => Ctx.binding_of(ctx, entry.name) + | Ctx.TVarEntry(entry) => Ctx.binding_of(ctx, entry.name) + | Ctx.ConstructorEntry(entry) => + Ctx.binding_of(ctx, entry.name) + | _ => Ctx.binding_of(ctx, "") // invalid + }; + if (b.id == Id.invalid) { + None; + } else { + Some(b); + }; + }, + ctx.entries, + ); + "Typing Context: [" + ++ String.concat( + "\n", + List.mapi( + (i: int, b: Binding.t) => + b.name ++ "(Index: " ++ string_of_int(i) ++ ")", + bindings, + ), + ) + ++ "]"; + | _ => "" + }; + }; + + let refs_in = + ( + ~exclude_rec_refs: bool=false, + ~exclude_body_refs: bool=false, + info_map: Id.Map.t(Info.t), + node: HighLevelNodeMap.node, + ) + : list(Binding.t) => { + let id = Utils.get_def_id_of_let(node.info); + let refs_of_def = Statics.Map.refs_in(info_map, id); + let refs_of_node = + Statics.Map.refs_in(info_map, HighLevelNodeMap.id_of(node)); + + // Intersect based on binding IDs + // This allows us to ignore references in the body AND recursive references in the def + // (refs_of_def will not have body refs) + // (refs_of_node will not have recursive refs) + let intersected_refs = + ListUtil.intersection_f( + (b: Binding.t) => b.id, + refs_of_def, + refs_of_node, + ); + + let refs = + switch (exclude_rec_refs, exclude_body_refs) { + | (true, true) => intersected_refs + | (true, false) => refs_of_node + | (false, true) => refs_of_def + | (false, false) => refs_of_def @ refs_of_node + }; + + // remove duplicates + let refs' = + ListUtil.dedup_f( + (b1: Binding.t, b2: Binding.t) => b1.id == b2.id, + refs, + ); + + let refs'' = List.filter((b: Binding.t) => b.id != Id.invalid, refs'); + + refs''; + }; + + let str_of_refs_in = (references: list(Binding.t)): string => { + "Variables referenced: [" + ++ String.concat( + ", ", + List.mapi( + (i: int, b: Binding.t) => + b.name ++ " (Index: " ++ string_of_int(i) ++ ")", + references, + ), + ) + ++ "]"; + }; + + let str_refs_in = + ( + ~exclude_rec_refs: bool=false, + ~exclude_body_refs: bool=false, + info_map: Id.Map.t(Info.t), + node: HighLevelNodeMap.node, + ) + : string => { + refs_in(~exclude_rec_refs, ~exclude_body_refs, info_map, node) + |> str_of_refs_in; + }; + }; + + module Printer = { + let convex_char = "?"; + let concave_char = "~"; + + let print = (~z: Zipper.t, ~info_map: Id.Map.t(Info.t)): string => { + let node_map = HighLevelNodeMap.build(z, info_map); + switch (node_map) { + | None => + Printer.of_zipper( + ~holes=convex_char, + ~concave_holes=concave_char, + ~special_folds=true, + z, + ) + | Some(node_map) => + // Step 1: Expand everything for agent view + // TODO: Skipping for now. Bottleneck. + // print_endline("here #1, expanding everything"); + // let all_ids = Id.Map.bindings(info_map) |> List.map(fst); + // let z' = ViewUtils.expand_terms(~z, ~ids=all_ids); + + // Step 2: Collapse all top level definitions for agent view, + // except for the ones that are expanded, given by the agent view's expanded list + let all_top_level_ids = Id.Map.bindings(node_map) |> List.map(fst); + let expanded_ids = + List.filter_map( + (path: string) => path_to_id_opt(node_map, path), + z.agent_view.expanded_paths, + ); + let ids_to_collapse = + all_top_level_ids + |> List.filter((id: Id.t) => !List.mem(id, expanded_ids)); + print_endline( + "here #2.1, ids to collapse: " + ++ String.concat(", ", List.map(Id.to_string, ids_to_collapse)), + ); + let z' = + ViewUtils.collapse_definitions(~z, ~ids=ids_to_collapse, ~info_map); + + print_endline("here #3, printing zipper"); + Printer.of_zipper( + ~holes=convex_char, + ~concave_holes=concave_char, + ~special_folds=true, + z', + ); + }; + }; + }; +}; + +module Public = { + let print = { + Local.Printer.print; + }; +}; diff --git a/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/EditTools.re b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/EditTools.re new file mode 100644 index 0000000000..7748facd6e --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/EditTools.re @@ -0,0 +1,649 @@ +open Util; + +let initialize_description = {| +Description: +Overwrites the program with the given code. +This may *only* ever be called on a program that has no let/type alias expressions. +This tool is only meant for this special case. +It may never be used once a program has let/type alias expressions. + +Parameters: +code: string — new code to replace the program with + +Example(s): +(Example 1) +The current program is: +``` +? +``` +Calling initialize(code="let a = 3 in +a * 2") would result in the program +``` +let a = 3 in a * 2 +``` + +(Example 2) +The current program is: +``` +5 * 10 +``` +Calling initialize(code="let a = 5 +in let b = 10 +in a * b") would result in the program +``` +let a = 5 in let b = 10 in a * b +``` +|}; + +let initialize: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("initialize")), + ("description", `String(initialize_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "code", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String("The new code to replace the definition with."), + ), + ]), + ), + ]), + ), + ("required", `List([`String("code")])), + ]), + ), + ]), + ), + ]); + +let update_definition_description = {| +Description: +Updates the definition of the node identified by the provided path. + +Parameters: +path: string — slash-delimited path to the node to update (e.g. "b" or "bindings/b") +code: string — new definition + +Example(s): +Given path "b" and the sketch: +``` +let a = ⋱ in +let b = "hello, world" in +let c : Int = ⋱ in +? +``` +Calling update_definition(path="b", code=""my new string"") will result in the sketch: +``` +let a = ⋱ in +let b = "my new string" in +let c : Int = ⋱ in +? +``` +Note(s): +This overwrites the ENTIRE definition of the targeted node. For example, if the path points to "b" and the sketch is: +``` +let a = ⋱ in +let b = + let x = ⋱ in +in +let c : Int = ⋱ in +? +``` +Then calling update_definition(path="b", code=""my new string"") will again result in the sketch: +``` +let a = ⋱ in +let b = "my new string" in +let c : Int = ⋱ in +? +``` +|}; + +let update_definition: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("update_definition")), + ("description", `String(update_definition_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the node to update (e.g. \"b\" or \"a/b\").", + ), + ), + ]), + ), + ( + "code", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String("The new code to replace the definition with."), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path"), `String("code")])), + ]), + ), + ]), + ), + ]); + +let update_body_description = {| +Description: +Replaces the body of the node located at the provided path. + +Parameters: +path: string — slash-delimited path to the node whose body should be replaced +code: string — new body + +Example(s): +Given path "b" and the sketch: +``` +let a = ⋱ in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +Calling update_body(path="b", code="a * a") will result in the program +``` +let a = ⋱ in +let b = "my new string" in +a * a +``` +|}; +let update_body: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("update_body")), + ("description", `String(update_body_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the node whose body should be replaced.", + ), + ), + ]), + ), + ( + "code", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String("The new code to replace the body with."), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path"), `String("code")])), + ]), + ), + ]), + ), + ]); + +let update_pattern_description = {| +Description: +Updates/renames the pattern of the node identified by the provided path. +A unique perk of this tool is that it will also update all use sites of the variable in the program. +If the pattern is a tuple, or some other higher-order pattern, it will recursively find +the atomic variables within that pattern, and update all use sites of those variables if and only if +the number of old and new variables are the same (this is only a requirement for this feature to work, +but you may very well change the pattern however you'd like to achieve your desired outcome!). + +Parameters: +path: string — slash-delimited path to the node whose pattern should change +code: string — new pattern to assign + +Example(s): +Given path "b" and the sketch: +``` +let a = ⋱ in +let b = "hello, world" in +let c : Int = ⋱ in +? +``` +Calling update_pattern(path="b", code="s : String") would result in the sketch: +``` +let a = ⋱ in +let s : String = "hello, world" in +let c : Int = ⋱ in +? +``` +*Note: If there were any references to "b" in the body of "b"'s variable definition +(such as in the definition of "c"), they would be updated to "s" as well. +|}; + +let update_pattern: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("update_pattern")), + ("description", `String(update_pattern_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the node whose pattern should change.", + ), + ), + ]), + ), + ( + "code", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String("The new pattern to assign to the node."), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path"), `String("code")])), + ]), + ), + ]), + ), + ]); + +let update_binding_clause_description = {| +Description: +Updates the pattern, definition, and enclosing delimiters of the node located via the provided path (everything exclusive of the body). +eg. calling update_binding_clause for the path to `let x = 3 in x` will overwrite "let x = 3 in". +It is important to note that this does NOT update the body of the node. If you wish to update the +binding along with the body, you should call this tool along with update_body, sequentially. This also means +the code argument you pass here should not contain a final body. +(Eg. ```let x = 3 in x``` would be bad, but ```let x = 3 in``` would be good, so would ```let x = 3 in let y = 4 in```). + +Parameters: +path: string — slash-delimited path to the node whose binding clause should change +code: string — new expression (which may contain multiple expressions; see example below for more information) + +Example(s): +Given path "b" and the sketch: +``` +let a = ⋱ in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +Calling update_binding_clause(path="b", code="let b : (Int, Int) = (0, ?) in let d : Int = b + 1 in") would result in the program +``` +let a = ⋱ in +let b : (Int, Int) = ⋱ in +let d : Int = b + 1 in +let c : Int = ⋱ in +? +``` +(Note that this is the only tool that can be called in the special case where there are no let or type alias expressions in the program, in which case, calling this tool will overwrite the entire program with the argument passed into 'code'.) +|}; + +let update_binding_clause: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("update_binding_clause")), + ("description", `String(update_binding_clause_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the node whose binding clause should change.", + ), + ), + ]), + ), + ( + "code", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String("The new code to replace the expression with."), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path"), `String("code")])), + ]), + ), + ]), + ), + ]); + +let delete_binding_clause_description = {| +Description: +Removes the entire type/value-binding of the node identified by the provided path. + +Parameters: +path: string — slash-delimited path to the binding to remove + +Example(s): +Given path "b" and the sketch: +``` +let a = ⋱ in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +Calling delete_binding_clause(path="b") would result in the program +``` +let a = 3 in +let c : Int = ⋱ in +? +``` +|}; + +let delete_binding_clause: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("delete_binding_clause")), + ("description", `String(delete_binding_clause_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the binding clause that should be deleted.", + ), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path")])), + ]), + ), + ]), + ), + ]); + +let delete_body_description = {| +Description: +Deletes the body of the node identified by the provided path. + +Parameters: +path: string — slash-delimited path to the node whose body should be cleared + +Example(s): +Given path "b" and the sketch: +``` +let a = ⋱ in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +Calling delete_body(path="b") would result in the program +``` +let a = ⋱ in +let b = ⋱ in +? +``` +|}; + +let delete_body: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("delete_body")), + ("description", `String(delete_body_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the node whose body should be deleted.", + ), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path")])), + ]), + ), + ]), + ), + ]); + +let insert_after_description = {| +Description: +Inserts code immediately after the definition located at the provided path. + +Parameters: +path: string — slash-delimited path to the node after which the code should be inserted +code: string — code to insert + +Example(s): +Given the program: +``` +let a = ⋱ in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +Calling insert_after(path="b", code = "let x = string_sub(b ,0, 7) ++ "big " ++ string_sub(b, 7, 6)") would result in the program +``` +let a = ⋱ in +let b = ⋱ in +let x = string_sub(b ,0, 7) ++ "big " ++ string_sub(b, 7, 6) +let c : Int = ⋱ in +? +``` +|}; + +let insert_after: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("insert_after")), + ("description", `String(insert_after_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the node after which the code should be inserted.", + ), + ), + ]), + ), + ( + "code", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The code to insert after the referenced expression.", + ), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path"), `String("code")])), + ]), + ), + ]), + ), + ]); + +let insert_before_description = {| +Description: +Inserts code before the let/type alias expression located at the provided path. + +Parameters: +path: string — slash-delimited path to the node before which the code should be inserted +code: string — code to insert + +Example(s): +Given path "b" and the sketch: +``` +let a = ⋱ in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +Calling insert_before(path="b", code = "let x = a * a in") would result in the program +``` +let a = ⋱ in +let x = a * a in +let b = ⋱ in +let c : Int = ⋱ in +? +``` +|}; + +let insert_before: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("insert_before")), + ("description", `String(insert_before_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "path", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "Slash-delimited path to the node before which the code should be inserted.", + ), + ), + ]), + ), + ( + "code", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The code to insert before the referenced expression.", + ), + ), + ]), + ), + ]), + ), + ("required", `List([`String("path"), `String("code")])), + ]), + ), + ]), + ), + ]); diff --git a/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/ReadTools.re b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/ReadTools.re new file mode 100644 index 0000000000..71b01d387a --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/ReadTools.re @@ -0,0 +1,115 @@ +open Util; + +let view_entire_definition: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("view_entire_definition")), + ( + "description", + `String( + "Displays the entire definition of the current node in the AST, leaving no child/sub definitions abstracted away.", + ), + ), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ("properties", `Assoc([])), + ("required", `List([])), + ]), + ), + ]), + ), + ]); + +let view_context: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("view_context")), + ( + "description", + `String( + "Displays all the variables in the typing context/scope at the current let expression in the AST.", + ), + ), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ("properties", `Assoc([])), + ("required", `List([])), + ]), + ), + ]), + ), + ]); + +let show_references_description = {| +Description: +Displays all the variables referenced in the body of the current node in the AST. +This action permits the use of the navigation tool "go_to_reference(index)" to be called using +the index displayed from this action as the argument for the "index" parameter. + +Parameters: +None + +Example(s): +(Note: We show the entire program, without any collapsed child/sibling definitions +here for a wholistic context and clarity) +The current node is "b" and the sketch is: +``` +let a = 200 in +let b = 10 in +let c : Int = +b * +let x = 20 in x +in +let d = c + 10 in +d * b +``` +Calling show_references() on this program would result in the following message to be returned: +"References to "b": +[ +(Index: 0, Definition of "c"): { +``` +let c : Int = +b * +let x = ⋱ in x +in +⋱ +```}, +(Index: 1, Body of "d"): { +``` +let d = ⋱ in +d * b +``` +} +] +" +|}; + +let show_references: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("show_references")), + ("description", `String(show_references_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ("properties", `Assoc([])), + ("required", `List([])), + ]), + ), + ]), + ), + ]); diff --git a/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/ViewTools.re b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/ViewTools.re new file mode 100644 index 0000000000..64dfb95dea --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/ViewTools.re @@ -0,0 +1,121 @@ +open Util; + +let expand_description = {| +Description: +Expands the specified nodes'/variables' defintions. + +Parameters: +paths: list(string) — the paths to the nodes/variables to expand + +Example(s): +Given the program state: +``` +let a = ⋱ in +let b = ⋱ in +let c : Int = ⋱ in +? +``` +Calling expand(paths = ["a", "b"]) would result in the program: +``` +let a = 4 + 5 in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +|}; + +let expand: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("expand")), + ("description", `String(expand_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "paths", + `Assoc([ + ("type", `String("array")), + ( + "description", + `String("The paths to the nodes/variables to expand."), + ), + ("items", `Assoc([("type", `String("string"))])), + ]), + ), + ]), + ), + ("required", `List([`String("paths")])), + ]), + ), + ]), + ), + ]); + +let collapse_description = {| +Description: +Collapses the specified nodes'/variables' defintions. + +Parameters: +paths: list(string) — the paths to the nodes/variables to collapse + +Example(s): +Given the program state: +``` +let a = 4 + 5 in +let b = "hello, world!" in +let c : Int = ⋱ in +? +``` +Calling collapse(paths = ["a", "b"]) would result in the program: +``` +let a = ⋱ in +let b = ⋱ in +let c : Int = ⋱ in +? +``` +|}; + +let collapse: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("collapse")), + ("description", `String(collapse_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "paths", + `Assoc([ + ("type", `String("array")), + ( + "description", + `String( + "The paths to the nodes/variables to collapse.", + ), + ), + ("items", `Assoc([("type", `String("string"))])), + ]), + ), + ]), + ), + ("required", `List([`String("paths")])), + ]), + ), + ]), + ), + ]); diff --git a/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/WorkbenchTools.re b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/WorkbenchTools.re new file mode 100644 index 0000000000..fa80ac6d6e --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/Composition/jsons_of_tools/WorkbenchTools.re @@ -0,0 +1,582 @@ +open Util; + +let create_new_task_description = {| +# Description +## Effects +Creates a new task with the given title, description, and subtasks. +If a task with the same title already exists, it will be overwritten. +All subtasks will be initialized to incomplete and no active subtask will be set. +This new task will be set as the active task in the model. +## Use Cases +Use this tool before starting work on a new feature, bug fix, or code implementation. +Since you are only allowed to make edit actions while an active subtask is set, +this is a mandatory step. +Use this tool to organize your thoughts, plan out implementation steps, and break down +complex tasks into manageable subtasks. +This will help you stay focused, track progress, and ensure that you complete all necessary steps +to successfully implement the desired functionality. + +Parameters: +task: { + title: string, + description: string, + subtasks: list({ + title: string, + description: string + }) +} +|}; + +let create_new_task: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("create_new_task")), + ("description", `String(create_new_task_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "task", + `Assoc([ + ("type", `String("object")), + ("description", `String("The task to create.")), + ( + "properties", + `Assoc([ + ( + "title", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The title of the task. Be concise. This will be used as a unique identifing key in a dictionary of tasks (to store/persist).", + ), + ), + ]), + ), + ( + "description", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "A high-level description of the task. Be descriptive here. This will help provide context and clarity about the purpose and scope of the task at hand. Jot down any relevant details and/or pseudocode that will help guide the implementation process.", + ), + ), + ]), + ), + ( + "subtasks", + `Assoc([ + ("type", `String("array")), + ( + "items", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "title", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The title of the subtask. This will also serve as a unique identifier for the subtask relative to the rest of the subtasks belonging to this specific task. Try to keep short and concise.", + ), + ), + ]), + ), + ( + "description", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The description of the subtask. Be as detailed as necessary to convey the subtask at hand. This will help provide clarity on what needs to be done. Jot down any relevant details and/or pseudocode that will help guide the implementation process.", + ), + ), + ]), + ), + ]), + ), + ( + "required", + `List([ + `String("title"), + `String("description"), + ]), + ), + ]), + ), + ]), + ), + ]), + ), + ( + "required", + `List([ + `String("title"), + `String("description"), + `String("subtasks"), + ]), + ), + ]), + ), + ]), + ), + ("required", `List([`String("task")])), + ]), + ), + ]), + ), + ]); + +let unset_active_task_description = {| +Description: +Unsets the currently active task in the composition model. +This results in no active task being set. + +Parameters: +None + +Example(s): +Calling unset_active_task() will result in no active task being set in the composition model. +|}; + +let unset_active_task: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("unset_active_task")), + ("description", `String(unset_active_task_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ("properties", `Assoc([])), + ("required", `List([])), + ]), + ), + ]), + ), + ]); + +let set_active_task_description = {| +Description: +Sets the active task in the composition model to the task with the given title. + +Parameters: +title: string — the title of the task to set as active + +Example(s): +Calling set_active_task(title = "Task 1") will set the active task in the composition model to the task with the title "Task 1". +The title must match the title of an existing task in the task dictionary. +|}; + +let set_active_task: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("set_active_task")), + ("description", `String(set_active_task_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "title", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String("The title of the task to set as active."), + ), + ]), + ), + ]), + ), + ("required", `List([`String("title")])), + ]), + ), + ]), + ), + ]); + +let unset_active_subtask_description = {| +Description: +Unsets the currently active subtask in the active task. +This results in no active subtask being set for the currently active task. +This requires an active task to be set, otherwise nothing will happen. + +Parameters: +None + +Example(s): +Calling unset_active_subtask() will result in no active subtask being set for the currently active task. +|}; + +let unset_active_subtask: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("unset_active_subtask")), + ("description", `String(unset_active_subtask_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ("properties", `Assoc([])), + ("required", `List([])), + ]), + ), + ]), + ), + ]); + +let set_active_subtask_description = {| +Description: +Sets the active subtask in the active task to the subtask with the given title. + +Parameters: +title: string — the title of the subtask to set as active + +Example(s): +Calling set_active_subtask(title = "Subtask 1") will set the active subtask in the active task to the subtask with the title "Subtask 1". +The title must match the title of an existing subtask in the subtasks of the active task. +|}; + +let set_active_subtask: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("set_active_subtask")), + ("description", `String(set_active_subtask_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "title", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String("The title of the subtask to set as active."), + ), + ]), + ), + ]), + ), + ("required", `List([`String("title")])), + ]), + ), + ]), + ), + ]); + +let mark_active_task_complete_description = {| +Description: +Marks the currently active task as complete. +Sets the completion summary of the active task with the given summary. +This requires an active task to be set, otherwise nothing will happen. + +Parameters: +summary: string — a summary of changes made to complete the active task +|}; + +let mark_active_task_complete: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("mark_active_task_complete")), + ("description", `String(mark_active_task_complete_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "summary", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The summary of changes made to complete the active task. Be descriptive here. This should help the user and future developers/agents understand what changes were made to the code to accomplish this task.", + ), + ), + ]), + ), + ]), + ), + ("required", `List([`String("summary")])), + ]), + ), + ]), + ), + ]); + +let mark_active_task_incomplete_description = {| +Description: +Marks the active task as incomplete. +This requires an active task to be set, otherwise nothing will happen. + +Parameters: +None +|}; + +let mark_active_task_incomplete: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("mark_active_task_incomplete")), + ("description", `String(mark_active_task_incomplete_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ("properties", `Assoc([])), + ("required", `List([])), + ]), + ), + ]), + ), + ]); + +let mark_active_subtask_complete_description = {| +Description: +Marks the currently active subtask as complete. +Sets the completion summary of the active subtask with the given summary. +Also automatically tries to set the next incomplete subtask (if any) as the active subtask in the currently active task; +the "next" incomplete subtask is determined by the ordering of subtasks in the active task. +No subtask will be active if all subtasks in the currently active task are marked complete. + +Parameters: +summary: string — a summary of changes made to complete the active subtask +|}; + +let mark_active_subtask_complete: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("mark_active_subtask_complete")), + ("description", `String(mark_active_subtask_complete_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "summary", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The summary of changes made to complete the active subtask. Be descriptive here. This should help the user and future developers/agents understand what changes were made to the code to accomplish this subtask.", + ), + ), + ]), + ), + ]), + ), + ("required", `List([`String("summary")])), + ]), + ), + ]), + ), + ]); + +let mark_active_subtask_incomplete_description = {| +Description: +Marks the active subtask as incomplete. +This requires an active subtask to be set, otherwise nothing will happen. + +Parameters: +None +|}; + +let mark_active_subtask_incomplete: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("mark_active_subtask_incomplete")), + ("description", `String(mark_active_subtask_incomplete_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ("properties", `Assoc([])), + ("required", `List([])), + ]), + ), + ]), + ), + ]); + +let add_new_subtask_to_active_task_description = {| +Description: +Adds a new subtask to the currently active task. +The new subtask will be appended to the end of the active task's subtask ordering. +If you want to change this ordering, you can call the reorder_subtasks_in_active_task tool later on. +This requires an active task to be set, otherwise nothing will happen. + +Parameters: +subtask: { + title: string, + description: string +} — the new subtask to add to the active task +|}; + +let add_new_subtask_to_active_task: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("add_new_subtask_to_active_task")), + ("description", `String(add_new_subtask_to_active_task_description)), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "subtask", + `Assoc([ + ("type", `String("object")), + ("description", `String("The subtask to add.")), + ( + "properties", + `Assoc([ + ( + "title", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The title of the subtask. This will also serve as a unique identifier for the subtask relative to the rest of the subtasks belonging to this specific task. Try to keep short and concise.", + ), + ), + ]), + ), + ( + "description", + `Assoc([ + ("type", `String("string")), + ( + "description", + `String( + "The description of the subtask. Be as detailed as necessary to convey the subtask at hand. This will help provide clarity on what needs to be done. Jot down any relevant details and/or pseudocode that will help guide the implementation process.", + ), + ), + ]), + ), + ]), + ), + ( + "required", + `List([`String("title"), `String("description")]), + ), + ]), + ), + ]), + ), + ("required", `List([`String("subtask")])), + ]), + ), + ]), + ), + ]); + +let reorder_subtasks_in_active_task_description = {| +Description: +Reorders the subtasks in the currently active task according to the given new ordering. +The new ordering is a list of subtask titles. +This requires an active task to be set, otherwise nothing will happen. +Make sure to use the exact titles of the subtasks as they appear in the active task's subtasks, otherwise this tool call will fail. + +Parameters: +subtasks_ordering: list(string) — the new ordering of subtask titles +|}; + +let reorder_subtasks_in_active_task: API.Json.t = + `Assoc([ + ("type", `String("function")), + ( + "function", + `Assoc([ + ("name", `String("reorder_subtasks_in_active_task")), + ( + "description", + `String(reorder_subtasks_in_active_task_description), + ), + ( + "parameters", + `Assoc([ + ("type", `String("object")), + ( + "properties", + `Assoc([ + ( + "subtasks_ordering", + `Assoc([ + ("type", `String("array")), + ("items", `Assoc([("type", `String("string"))])), + ( + "description", + `String( + "The new ordering of subtasks (referenced by their titles (aka keys/unique identifiers)) for the active task.", + ), + ), + ]), + ), + ]), + ), + ("required", `List([`String("subtasks_ordering")])), + ]), + ), + ]), + ), + ]); diff --git a/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/ChatLSP.re b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/ChatLSP.re new file mode 100644 index 0000000000..daf2c57b5b --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/ChatLSP.re @@ -0,0 +1,21 @@ +open Language; + +let get_sketch_and_error_ctx = + (zipper: Zipper.t, info_map: Statics.Map.t): list(string) => { + let sketch_seg = Dump.to_segment(zipper); + let errors = ErrorPrint.all(info_map); + let static_error_arr = + switch (errors) { + | [] => ["No static errors found"] + | _ => errors + }; + let ctx = + [ + "PROGRAM SKETCH: ```" + ++ ErrorPrint.Print.seg(~holes="?", sketch_seg) + ++ "```", + ] + @ ["STATIC ERRORS: "] + @ static_error_arr; + ctx; +}; diff --git a/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/GeneralTreeUtils.re b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/GeneralTreeUtils.re new file mode 100644 index 0000000000..0d33c7ecf0 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/GeneralTreeUtils.re @@ -0,0 +1,248 @@ +open Util; +open Language; +open Language.Statics; +open HighLevelNodeMap.Utils; + +let subtree_of = + ( + ~info: Info.t, + ~orig_info_map: Id.Map.t(Info.t), + ~of_pat: bool, + ~of_def: bool, + ~of_body: bool, + ) + : Statics.Map.t => { + let map = + switch (info) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(pat, def, body) => + let pat_info = pat_to_pat(pat, orig_info_map); + let def_info = exp_to_exp(def, orig_info_map); + let body_info = exp_to_exp(body, orig_info_map); + + let pat_map = + of_pat + ? Statics.upat_to_info_map( + ~is_synswitch=false, + ~ctx=pat_info.ctx, + ~co_ctx=pat_info.co_ctx, + ~ana=pat_info.ana, + ~ancestors=pat_info.ancestors, + ~duplicates=[], + pat, + Statics.Map.empty, + ) + |> snd + : Statics.Map.empty; + + let def_map = + of_def + ? Statics.uexp_to_info_map( + ~ctx=def_info.ctx, + ~ana=def_info.ana, + ~is_in_filter=false, + ~ancestors=def_info.ancestors, + ~duplicates=[], + ~expected_labels=None, + def, + pat_map, + ) + |> snd + : pat_map; + + let body_map = + of_body + ? Statics.uexp_to_info_map( + ~ctx=body_info.ctx, + ~ana=body_info.ana, + ~is_in_filter=false, + ~ancestors=body_info.ancestors, + ~duplicates=[], + ~expected_labels=None, + body, + def_map, + ) + |> snd + : def_map; + + body_map; + + | TyAlias(tpat, tdef, body) => + let tpat_info = tpat_to_tpat(tpat, orig_info_map); + let tdef_info = typ_to_typ(tdef, orig_info_map); + let body_info = exp_to_exp(body, orig_info_map); + + let tpat_map = + of_pat + ? Statics.utpat_to_info_map( + ~ctx=tpat_info.ctx, + ~ancestors=tpat_info.ancestors, + tpat, + Statics.Map.empty, + ) + |> snd + : Statics.Map.empty; + let tpat_map = + of_def + ? Statics.utyp_to_info_map( + ~ctx=tdef_info.ctx, + ~ancestors=tdef_info.ancestors, + tdef, + tpat_map, + ) + |> snd + : tpat_map; + + let body_map = + of_body + ? Statics.uexp_to_info_map( + ~ctx=body_info.ctx, + ~ana=body_info.ana, + ~is_in_filter=false, + ~ancestors=body_info.ancestors, + ~duplicates=[], + ~expected_labels=None, + body, + tpat_map, + ) + |> snd + : tpat_map; + + body_map; + + | _ => + raise( + Failure( + "UNIMPLEMENTED_NODE_TYPE: Only let and type alias expressions are currently supported as nodes", + ), + ) + } + | _ => raise(Failure("Current node is not an expression")) + }; + + map; +}; + +let get_refs_to = (curr: Info.t, info_map: Id.Map.t(Info.t)): CoCtx.t => { + /* + Returns the CoCtx containing exclusively references to the given let/tyalis expression + */ + + let exp_to_info = (term: Exp.t): Info.t => exp_to_info(term, info_map); + + switch (curr) { + | InfoExp(term) => + let entire_coctx = term.co_ctx; + let body_coctx = + switch (Exp.term_of(term.term)) { + | Let(_, _, body) + | TyAlias(_, _, body) => + switch (exp_to_info(body)) { + | InfoExp({co_ctx, _}) => co_ctx + | _ => raise(Failure("Body of type alias is not an expression")) + } + | _ => + raise(Failure("Current node is not a let or type alias expression")) + }; + // Find variables that appear in body_coctx but not in entire_coctx + // Effectively takes the set difference of body_coctx and entire_coctx + VarMap.filter( + ((var_name, _)) => !VarMap.contains(entire_coctx, var_name), + body_coctx, + ); + | _ => raise(Failure("Current node is not a let or type alias expression")) + }; +}; + +let get_var_names_from_pat = (curr: Info.t): list(string) => { + let rec go = (pat: Pat.t, vars: list(string)): list(string) => { + switch (pat.term) { + | Var(name) => vars @ [name] + | Ap(pat1, pat2) + | TupLabel(pat1, pat2) + | Cons(pat1, pat2) => go(pat1, vars) @ go(pat2, vars) + | Parens(pat) + | Probe(pat, _) + | Asc(pat, _) => go(pat, vars) + | ListLit(pats) + | Tuple(pats) => + List.fold_left((vars, pat) => go(pat, vars), vars, pats) + | Invalid(_) + | EmptyHole + | MultiHole(_) + | Wild + | Atom(_) + | Constructor(_, _) + | Label(_) => vars + }; + }; + let pat = + switch (curr) { + | InfoPat({term, _}) => term + | _ => raise(Failure("Pat is not a pattern")) + }; + go(pat, []); +}; + +let update_use_sites_of_var = + (z: Zipper.t, co_ctx: CoCtx.t, old_name: string, new_name: string) + : Zipper.t => { + /* + Updates the use sites of the given variables in the co-context. + */ + // Iterate through all variables in the co-context + List.fold_left( + (acc_z, (var_name, entries)) => + // Only update variables that match the old_name + if (var_name == old_name) { + // Iterate through all entries (IDs) for this variable + List.fold_left( + (acc_z', entry) => { + let id = entry.CoCtx.id; + switch (Select.tile(id, acc_z')) { + | Some(z') => + switch (Parser.to_zipper(~zipper_init=z', new_name)) { + | Some(z'') => z'' + | None => z' + } + | None => acc_z' + }; + }, + acc_z, + entries, + ); + } else { + acc_z; + }, + z, + co_ctx, + ); +}; + +let update_use_sites_of_pat = + ( + ~z: Zipper.t, + ~co_ctx: CoCtx.t, + ~old_names: list(string), + ~new_names: list(string), + ) + : Zipper.t => + /* + Updates the use sites of the given variables in the co-context. + + Should be noted that there are special cases: + - Consider updating the pattern (x, y, z) to (a, b) + we are unable to determine which new var maps to which old var. + For now, we will only consider the case where the number of old and new vars are the same. + */ + switch (ListUtil.opt_zip(old_names, new_names)) { + | None => z + | Some(pairs) => + List.fold_left( + (acc_z, (old_name, new_name)) => + update_use_sites_of_var(acc_z, co_ctx, old_name, new_name), + z, + pairs, + ) + }; diff --git a/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/HighLevelNodeMap.re b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/HighLevelNodeMap.re new file mode 100755 index 0000000000..ab9df809d2 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/HighLevelNodeMap.re @@ -0,0 +1,808 @@ +open Util; +open Language; +open Language.Statics; +open OptUtil.Syntax; + +module Utils = { + let exp_to_info = (term: Exp.t, info_map: Id.Map.t(Info.t)): Info.t => { + // Helper function + let e = Exp.rep_id(term); + Id.Map.find(e, info_map); + }; + + let exp_to_exp = (term: Exp.t, info_map: Id.Map.t(Info.t)): Info.exp => { + switch (exp_to_info(term, info_map)) { + | InfoExp(exp_info) => exp_info + | _ => raise(Failure("No exp info found")) + }; + }; + + let pat_to_info = (term: Pat.t, info_map: Id.Map.t(Info.t)): Info.t => { + // Helper function + let e = Pat.rep_id(term); + Id.Map.find(e, info_map); + }; + + let pat_to_pat = (term: Pat.t, info_map: Id.Map.t(Info.t)): Info.pat => { + switch (pat_to_info(term, info_map)) { + | InfoPat(pat_info) => pat_info + | _ => raise(Failure("No pat info found")) + }; + }; + + let tpat_to_info = (term: TPat.t, info_map: Id.Map.t(Info.t)): Info.t => { + // Helper function + let e = TPat.rep_id(term); + Id.Map.find(e, info_map); + }; + + let tpat_to_tpat = (term: TPat.t, info_map: Id.Map.t(Info.t)): Info.tpat => { + switch (tpat_to_info(term, info_map)) { + | InfoTPat(tpat_info) => tpat_info + | _ => raise(Failure("No tpat info found")) + }; + }; + + let typ_to_info = (term: Typ.t, info_map: Id.Map.t(Info.t)): Info.t => { + // Helper function + let e = Typ.rep_id(term); + Id.Map.find(e, info_map); + }; + + let typ_to_typ = (term: Typ.t, info_map: Id.Map.t(Info.t)): Info.typ => { + switch (typ_to_info(term, info_map)) { + | InfoTyp(typ_info) => typ_info + | _ => raise(Failure("No typ info found")) + }; + }; + + let child_expressions_of_exp = (term: TermBase.exp_t): list(TermBase.exp_t) => { + /* + Returns the child expressions within the given expression. + */ + switch (Exp.term_of(term)) { + | Fun(_, e, _, _) + | UnOp(_, e) + | Test(e) + | Parens(e) + | Filter(_, e) + | Closure(_, e) + | Probe(e, _) + | Asc(e, _) + | FixF(_, e, _) + | Use(_, e) + | TypAp(e, _) + | TypFun(_, e, _) + | DeferredAp(e, _) + | HintedTest(e, _) => [e] + | Let(_, e1, e2) + | Seq(e1, e2) + | Ap(_, e1, e2) + | Dot(e1, e2) + | TupleExtension(e1, e2) + | TupLabel(e1, e2) + | Cons(e1, e2) + | ListConcat(e1, e2) + | BinOp(_, e1, e2) => [e1, e2] + | Tuple(es) + | ListLit(es) => es + | If(e1, e2, e3) => [e1, e2, e3] + | Match(e, branches) => [e, ...List.map(snd, branches)] + | BuiltinFun(_) + | Label(_) + | EmptyHole + | Undefined + | Invalid(_) + | MultiHole(_) + | DynamicErrorHole(_, _) + | Deferral(_) + | Atom(_) + | Constructor(_, _) + | LivelitName(_) + | TyAlias(_) + | Var(_) => [] + }; + }; + + let top_level_term_of = + (~start_point: Info.t, ~info_map: Id.Map.t(Info.t)): option(Info.t) => { + let initial_nominee = + switch (start_point) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(_) => Some(start_point) // if it is a let binding, then this term is a nominee + | TyAlias(_) => Some(start_point) // if it is a type binding, then this term is a nominee + | _ => None + } + | _ => None + }; + + let rec search_for_top_level_node = + ( + ~candidates: list(Id.t), + ~departure_point: Info.t, + ~nominee: option(Info.t), + ) => { + switch (candidates) { + | [candidate, ...rest] => + let candidate = Id.Map.find(candidate, info_map); + let nominee = + switch (candidate) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(pat, def, body) => + let pat_id = Pat.rep_id(pat); + let def_id = Exp.rep_id(def); + let body_id = Exp.rep_id(body); + // Check if departure point is in the pattern, definition, or body position of the candidate + if (Id.equal(def_id, Info.id_of(departure_point)) + || Id.equal(body_id, Info.id_of(departure_point)) + || Id.equal(pat_id, Info.id_of(departure_point))) { + // if it is, then set this is as the new nominee for the top-level node + Some( + candidate, + ); + } else { + nominee; + }; + | TyAlias(tpat, tdef, body) => + let tpat_id = TPat.rep_id(tpat); + let tdef_id = Typ.rep_id(tdef); + let body_id = Exp.rep_id(body); + if (Id.equal(tdef_id, Info.id_of(departure_point)) + || Id.equal(body_id, Info.id_of(departure_point)) + || Id.equal(tpat_id, Info.id_of(departure_point))) { + Some(candidate); + } else { + nominee; + }; + | _ => nominee + } + | _ => nominee + }; + search_for_top_level_node( + ~candidates=rest, + ~departure_point=candidate, + ~nominee, + ); + | [] => nominee + }; + }; + + search_for_top_level_node( + ~candidates=Info.ancestors_of(start_point), + ~departure_point=start_point, + ~nominee=initial_nominee, + ); + }; + + let get_oldest_ancestor = + (info: Info.t, info_map: Id.Map.t(Info.t)): Info.t => { + switch ( + { + let* id = ListUtil.hd_opt(List.rev(Info.ancestors_of(info))); + Id.Map.find_opt(id, info_map); + } + ) { + | Some(info) => info + | None => info + }; + }; +}; + +module Namer = { + /* + Convert patterns to purely string names. + */ + + let rec mk_name_from_pat = (pat: TermBase.pat_t) => { + switch (pat.term) { + | Var(name) + | Constructor(name, _) + | Label(name) => name + | Probe(pat, _) + | Parens(pat) + | Asc(pat, _) => mk_name_from_pat(pat) + | Cons(pat1, pat2) => + mk_name_from_pat(pat1) ++ "::" ++ mk_name_from_pat(pat2) + | Tuple(pats) => + "(" ++ String.concat(", ", List.map(mk_name_from_pat, pats)) ++ ")" + | ListLit(pats) => + "[" ++ String.concat(", ", List.map(mk_name_from_pat, pats)) ++ "]" + | Wild => "{wild}" + | EmptyHole => "{empty pattern hole}" + | MultiHole(_) => "{multi hole}" + | Ap(pat1, pat2) => + mk_name_from_pat(pat1) ++ "(" ++ mk_name_from_pat(pat2) ++ ")" + | TupLabel(pat1, pat2) => + "(" ++ mk_name_from_pat(pat1) ++ " : " ++ mk_name_from_pat(pat2) ++ ")" + | Atom(_) => "{atom}" + | Invalid(_) => "{invalid}" + }; + }; + + let mk_name_from_tpat = (tpat: TermBase.tpat_t) => { + switch (tpat.term) { + | Var(name) + | Invalid(name) => name + | EmptyHole => "{empty type pattern hole}" + | MultiHole(_) => "{multi type pattern hole}" + }; + }; + + let mk_name = (info: Info.t): string => { + switch (info) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(pat, _, _) => mk_name_from_pat(pat) + | TyAlias(tpat, _, _) => mk_name_from_tpat(tpat) + | _ => raise(Failure("Not a valid expression to make a name from")) + } + | _ => raise(Failure("Not a valid term to make a name from")) + }; + }; +}; + +module MoveOffWhitespaceHelper = { + /* + Ad-hoc methods to help move the cursor off whitespace and to the nearest + non-whitespace term. + */ + + let is_on_whitespace = (z: Zipper.t): bool => { + // Use for_index which only ignores secondary pieces, not grout pieces + switch (Indicated.for_index(z)) { + | Some((piece, _, _)) => + Piece.is_secondary(piece) + || Piece.is_grout(piece) + || Piece.is_convex(piece) + | None => false + }; + }; + + let rec move_to_non_whitespace = + (z: Zipper.t, d: Direction.t): option(Zipper.t) => { + /* + Iteratively move the cursor to the left. + If moving left becomes stagnant, try and move right. + */ + is_on_whitespace(z) + ? { + switch (Move.by_char(d, z)) { + | Some(z') => move_to_non_whitespace(z', d) + | None => + if (d == Left) { + move_to_non_whitespace(z, Right); + } else { + None; + } + }; + } + : Some(z); + }; +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type node = HighLevelNodeMapModel.node; + +[@deriving (show({with_path: false}), sexp, yojson)] +type t = HighLevelNodeMapModel.t; + +let id_of = (node: node) => { + Info.id_of(node.info); +}; + +let unwrap = (t: option(t)): t => { + Option.get(t); +}; + +let find = (node_map: t, id: Id.t) => { + switch (Id.Map.find_opt(id, node_map)) { + | Some(node) => node + | None => + raise( + Failure("Node with id " ++ Id.to_string(id) ++ " not found in map"), + ) + }; +}; + +let id_to_name = (node_map: t, id: Id.t): string => { + find(node_map, id).name; +}; + +let parent_id_from_path = (path: list(Id.t)): option(Id.t) => { + switch (List.rev(path)) { + | [_, parent, ..._] => Some(parent) // the first element is the dummy root + | _ => None + }; +}; + +let parent_id_of = (node: node): option(Id.t) => { + // the parent is the second element in the path + parent_id_from_path( + node.path, + ); +}; + +let parent_of = (node_map: t, node: node): option(node) => { + switch (parent_id_of(node)) { + | Some(p_id) => Some(find(node_map, p_id)) + | None => None + }; +}; + +let children_of = (node_map: t, node: node): list(node) => { + List.map((id: Id.t) => find(node_map, id), node.children); +}; + +let descendants_of = (node_map: t, node: node): list(list(Id.t)) => { + let rec build_levels = + (current_level: list(Id.t), acc: list(list(Id.t))) + : list(list(Id.t)) => + if (List.length(current_level) == 0) { + List.rev(acc); + } else { + // Get all children of nodes in the current level + let next_level = + current_level + |> List.map((id: Id.t) => find(node_map, id)) + |> List.map((node: node) => node.children) + |> List.flatten; + + // Add current level to accumulator and recurse + build_levels(next_level, [current_level, ...acc]); + }; + + // Start with the children of the given node + build_levels(node.children, []); +}; + +let siblings_of = (node_map: t, node: node): list(node) => { + List.map((id: Id.t) => find(node_map, id), node.siblings); +}; + +let next_sibling_of = (node: node): option(Id.t) => { + List.nth_opt( + node.siblings, + node.sibling_idx + 1 mod List.length(node.siblings), + ); +}; + +let prev_sibling_of = (node: node): option(Id.t) => { + List.nth_opt( + node.siblings, + node.sibling_idx - 1 mod List.length(node.siblings), + ); +}; + +let lowest_enclosing_node_of = (info: Info.t, node_map: t): option(node) => { + switch (Id.Map.find_opt(Info.id_of(info), node_map)) { + | Some(node) => Some(node) + | None => + let ancestors = Info.ancestors_of(info); + List.find_map( + (ancestor: Id.t) => Id.Map.find_opt(ancestor, node_map), + ancestors, + ); + }; +}; + +let add_child = (node_map: t, parent: Id.t, child: Id.t): t => { + switch (Id.Map.find_opt(parent, node_map)) { + | Some(parent_node) => + Id.Map.add( + parent, + { + ...parent_node, + children: parent_node.children @ [child], + }, + node_map, + ) + | None => node_map + }; +}; + +let init_node = (info: Info.t, path: list(Id.t), node_map: t): t => { + print_endline( + "Constructing node for the id: " ++ Id.to_string(Info.id_of(info)), + ); + // 1. Add this node to the children of its parent (head of rev(path)), if one exists + let node_map = + switch (parent_id_from_path(path)) { + | Some(parent) => add_child(node_map, parent, Info.id_of(info)) + | None => node_map + }; + // 2. Initialize the node + let node: node = { + info, + path, + children: [], // must propogate after + siblings: [], // must propogate after + sibling_idx: (-1), + name: Namer.mk_name(info), + }; + print_endline("just processed: " ++ node.name); + // 3. Add the node to the node map + let node_map = Id.Map.add(Info.id_of(info), node, node_map); + // 4. Build the children of the node + print_endline( + "Added node: " + ++ node.name + ++ " (id: " + ++ Id.to_string(Info.id_of(info)) + ++ ")", + ); + node_map; +}; +let rec build_children = + ( + candidate: Info.t, + path: list(Id.t), + node_map: t, + info_map: Id.Map.t(Info.t), + ) + : t => { + print_endline( + "Building children for the id: " ++ Id.to_string(Info.id_of(candidate)), + ); + + let exp_to_info = (term: Exp.t): Info.t => + Utils.exp_to_info(term, info_map); + + let typ_to_info = (term: Typ.t): Info.t => + Utils.typ_to_info(term, info_map); + + switch (candidate) { + | InfoExp({term, _}) => + switch (Exp.term_of(term)) { + | Let(_, def, body) => + print_endline( + "Found Let expression, creating node and processing body", + ); + // Add this node to the node map + let new_path = path @ [Info.id_of(candidate)]; + let node_map = init_node(candidate, new_path, node_map); + // 1. Find children + let node_map = + build_children(exp_to_info(def), new_path, node_map, info_map); + // 2. Find siblings + // use "old" path for siblings!! + build_children(exp_to_info(body), path, node_map, info_map); + // It is also useful to add type defintions to the def-structured AST + | TyAlias(_, typ, body) => + print_endline( + "Found TyAlias expression, creating node and processing body", + ); + // Add this node to the node map + let new_path = path @ [Info.id_of(candidate)]; + let node_map = init_node(candidate, new_path, node_map); + // 1. Find children (although, not sure if necessary for type defs) + let node_map = + build_children(typ_to_info(typ), new_path, node_map, info_map); + // 2. Find siblings + // use "old" path for siblings!! + build_children(exp_to_info(body), path, node_map, info_map); + | _ => + let es = Utils.child_expressions_of_exp(term); + print_endline( + "Found other expression type with " + ++ string_of_int(List.length(es)) + ++ " child expressions", + ); + let es_mapped = List.map(exp_to_info, es); + List.fold_left( + (acc_map: t, e: Info.t) => { + let new_node_map = build_children(e, path, acc_map, info_map); + Id.Map.merge( + (_: Id.t, n1: option(node), n2: option(node)) => + switch (n1, n2) { + | (Some(_v1), Some(v2)) => + // If both maps have the same key, prefer the one with more complete data + // (e.g., the one that was built later with more context) + Some(v2) + | (Some(v), None) => Some(v) + | (None, Some(v)) => Some(v) + | (None, None) => None + }, + acc_map, + new_node_map, + ); + }, + node_map, + es_mapped, + ); + } + | _ => node_map + }; +}; + +let sibling_idx_of = (path: list(Id.t), self: Id.t): int => { + path + |> List.mapi((i, id) => (i, id)) + |> List.find_opt(((_, id)) => id == self) + |> Option.map(fst) + |> Option.value(~default=-1); +}; + +let build_siblings_and_trim = (node_map: t): t => { + /* + * Builds the siblings list for each node by: + * 1. First trimming the dummy root from all paths (removing the head) + * 2. Iterating through each node in the map + * 3. Finding its parent (second-to-last element in the trimmed path) + * 4. Looking up the parent's children and setting them as siblings (excluding self) + * 5. Calculating the sibling index based on position in parent's children list + * + * Example: For path [dummy_root, x, y, z], the parent of z is y. + * The siblings of z are all of y's children except z itself. + * XXX: A little hacky, but it works for now... + */ + let node_map = + node_map + |> Id.Map.fold( + (current_id, current_node, acc_map) => { + switch (parent_of(node_map, current_node)) { + | Some(parent) => + Id.Map.add( + current_id, + { + ...current_node, + path: List.tl(current_node.path), // removes dummy root + siblings: List.map(id_of, children_of(node_map, parent)), + sibling_idx: sibling_idx_of(parent.children, current_id), + }, + acc_map, + ) + | None => Id.Map.add(current_id, current_node, acc_map) + } + }, + node_map, + ); + node_map; +}; + +let gather_top_level = (node_map: t): list(Id.t) => { + // If the node has no parent, then it is a top-level node + // XXX: Hacky, cause it could be done more efficiently either on the fly or in + // build_siblings_and_trim... + node_map + |> Id.Map.bindings + |> List.filter_map(((id: Id.t, node: node)) => { + switch (parent_of(node_map, node)) { + | Some(_) => None + | None => Some(id) + } + }); +}; + +let split_path = (path: string): list(string) => { + String.split_on_char('/', path); +}; + +let id_path_to_name_path = (id_path: list(Id.t), node_map: t): list(string) => { + List.map((id: Id.t) => id_to_name(node_map, id), id_path); +}; + +let path_to_id_opt = (node_map: t, path: string): option(Id.t) => { + let path_names = split_path(path); + // Convert each node's path (list of Ids) to a list of names and compare + // XXX: Very hacky and ineffecient (O(n) in size of node_map) + // TODO: Implement a method which improves the efficiency of this operation + Id.Map.bindings(node_map) + |> List.find_map(((id: Id.t, node: node)) => + if (id_path_to_name_path(node.path, node_map) == path_names) { + Some(id); + } else { + None; + } + ); +}; + +let closest_valid_path_to_ill_path = (node_map: t, path: string): string => { + // Returns the most similar name of a node in the tree to the given ill-formed path + // This uses the levenshtein distance to find the closest match + let path_names = split_path(path); + + /* Helper to compute distance between a candidate node's name-path and the input */ + let distance_for_node = (node: node): int => { + let candidate_names = id_path_to_name_path(node.path, node_map); + StringUtil.levenshtein_list_distance(path_names, candidate_names); + }; + + /* Iterate over the map to find the minimum distance candidate */ + switch (Id.Map.bindings(node_map)) { + | [] => + raise(Failure("No nodes to compare when searching for closest path")) + | bindings => + /* fold to find (best_id, best_node, best_distance) */ + let (first_id, first_node) = List.hd(bindings); + let initial_acc = (first_id, first_node, distance_for_node(first_node)); + + let (best_id, _best_node, _best_dist) = + List.fold_left( + ((acc_id, acc_node, acc_d), (id, node)) => { + let d = distance_for_node(node); + if (d < acc_d) { + (id, node, d); + } else if (d == acc_d) { + /* Tie-breaker: prefer shorter candidate path (fewer segments) + which tends to yield simpler / more specific matches */ + if (List.length(node.path) < List.length(acc_node.path)) { + (id, node, d); + } else { + (acc_id, acc_node, acc_d); + }; + } else { + (acc_id, acc_node, acc_d); + }; + }, + /* initial accumulator is the first binding */ + initial_acc, + List.tl(bindings), + ); + + let path = find(node_map, best_id).path; + String.concat( + "/", + List.map((id: Id.t) => id_to_name(node_map, id), path), + ); + }; +}; + +let path_to_id = (node_map: t, path: string): Id.t => { + let path_names = split_path(path); + // Convert each node's path (list of Ids) to a list of names and compare + // XXX: Very hacky and ineffecient (O(n) in size of node_map) + // TODO: Implement a method which improves the efficiency of this operation + try( + Option.get( + Id.Map.bindings(node_map) + |> List.find_map(((id: Id.t, node: node)) => + if (id_path_to_name_path(node.path, node_map) == path_names) { + Some(id); + } else { + None; + } + ), + ) + ) { + | _ => + raise( + Failure( + "Path \"" + ++ path + ++ "\" not found in node map" + ++ "\nPerhaps you meant \"" + ++ closest_valid_path_to_ill_path(node_map, path) + ++ "\"?", + ), + ) + }; +}; + +let path_to_node = (node_map: t, path: string): node => { + find(node_map, path_to_id(node_map, path)); +}; + +let node_of_cursor = + (node_map: t, z: Zipper.t, info_map: Id.Map.t(Info.t)): node => { + switch ( + { + let* valid_zipper = + MoveOffWhitespaceHelper.move_to_non_whitespace(z, Left); + Indicated.ci_of(valid_zipper, info_map); + } + ) { + | None => + raise( + Failure( + "No current position found for cursor when trying to get node of cursor", + ), + ) + | Some(info) => + switch (lowest_enclosing_node_of(info, node_map)) { + | Some(node) => node + | None => raise(Failure("No lowest enclosing node found for cursor")) + } + }; +}; + +let build = (zipper: Zipper.t, info_map: Id.Map.t(Info.t)): option(t) => { + // Move to a valid, non-secondary, non-grout, non-convex term + switch ( + { + let* valid_zipper = + MoveOffWhitespaceHelper.move_to_non_whitespace(zipper, Left); + let* current_term = Indicated.ci_of(valid_zipper, info_map); + Utils.top_level_term_of(~start_point=current_term, ~info_map); + } + ) { + | None => + print_endline("No top level term found"); + None; + | Some(top_level_term) => + let oldest_ancestor = Utils.get_oldest_ancestor(top_level_term, info_map); + let dummy_root = Id.mk(); + print_endline("Dummy root is: " ++ Id.to_string(dummy_root)); + let node_map: t = + Id.Map.add( + dummy_root, + { + // This info is NEVER used, just for the sake of the algorithm and + // to avoid making this an optional type solely for the dummy root... + info: oldest_ancestor, + path: [dummy_root], + children: [], + siblings: [], + sibling_idx: (-1), + name: "{dummy root}", + }: node, + Id.Map.empty, + ); + let node_map = + build_children(oldest_ancestor, [dummy_root], node_map, info_map); + // Build siblings for all nodes and trim dummy root from paths + let node_map = build_siblings_and_trim(node_map); + // Remove the dummy root from the node map + let node_map = Id.Map.remove(dummy_root, node_map); + Some(node_map); + }; +}; + +module Public = { + /* + ================================ + Public methods to build and interact with the tree + ================================ + */ + let build = + // Builds the definition-based tree + // Main driver function, does most the heavy lifting + build; + let id_of = + // Gets the id of a node + id_of; + let unwrap = + // Unwraps the result of building the tree + unwrap; + let find = + // Finds a node in the tree by its id + find; + let id_to_name = + // Gets the name of a node given its id + id_to_name; + let parent_of = + // Gets the parent of a node + parent_of; + let children_of = + // Gets the children of a node + children_of; + let descendants_of = + // Gets all of the descendants of a node + descendants_of; + let siblings_of = + // Gets the siblings of a node + siblings_of; + let next_sibling_of = + // Gets the next sibling of a node + next_sibling_of; + let prev_sibling_of = + // Gets the previous sibling of a node + prev_sibling_of; + let gather_top_level = + // Gathers the top-level nodes from the tree + gather_top_level; + let path_to_id = + // Gets the node id from a path + path_to_id; + let path_to_id_opt = + // Gets the node id from a path, returns None if the path is not found + path_to_id_opt; + let path_to_node = + // Gets the node from a path + path_to_node; + let node_of_cursor = + // Gets the node at the cursor position + // Useful for post-edit checks and operations + node_of_cursor; +}; diff --git a/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/HighLevelNodeMapModel.re b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/HighLevelNodeMapModel.re new file mode 100644 index 0000000000..a91575077d --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/HighLevelNodeMapModel.re @@ -0,0 +1,27 @@ +open Util; +open Language; +open Language.Statics; + +[@deriving (show({with_path: false}), sexp, yojson)] +type node = { + // The term associated with this node + info: Info.t, + // The high-level path to this node from the root of the AST, as a list of their ids + // Ordering: the first element is the root of the AST, and the last element is this node + path: list(Id.t), + // Ids of the outgoing children nodes in the AST + children: list(Id.t), + // Ids of the outgoing sibling nodes in the AST + siblings: list(Id.t), + // Index of this node in the list of siblings + sibling_idx: int, + // The name of this node. Constructed through recursively + // unwrapping the pattern(s) associated with the node + name: string, + // ... + // May optionally choose to store more metadata about terms here, such as type, probes, etc. + // ... +}; + +[@deriving (show({with_path: false}), sexp, yojson)] +type t = Id.Map.t(node); diff --git a/src/web/app/helpful-assistant/RelevantTypes.re b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/RelevantTypes.re similarity index 88% rename from src/web/app/helpful-assistant/RelevantTypes.re rename to src/haz3lcore/ai_assistant_core/LanguageServerHelpers/RelevantTypes.re index 97a3066950..220d2d6619 100644 --- a/src/web/app/helpful-assistant/RelevantTypes.re +++ b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/RelevantTypes.re @@ -3,11 +3,7 @@ open Language; /* Assembles relevant type definitions per the Static Contextualization paper */ let format_def = (alias: string, ty: Typ.t): string => - Printf.sprintf( - "type %s = %s in", - alias, - Haz3lcore.ErrorPrint.Print.typ(ty), - ); + Printf.sprintf("type %s = %s in", alias, ErrorPrint.Print.typ(ty)); let subst_if_rec = ((name: string, ty: Typ.t)): (string, Typ.t) => { switch (ty) { @@ -41,7 +37,7 @@ let get = (ctx: Ctx.t, ana_ty: Typ.t, hole_label: string): string => { }; prefix ++ "a type consistent with " - ++ Haz3lcore.ErrorPrint.Print.typ(ana_ty) + ++ ErrorPrint.Print.typ(ana_ty) ++ " #\n" ++ defs; }; diff --git a/src/web/app/helpful-assistant/RelevantValues.re b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/RelevantValues.re similarity index 96% rename from src/web/app/helpful-assistant/RelevantValues.re rename to src/haz3lcore/ai_assistant_core/LanguageServerHelpers/RelevantValues.re index 2f222504e1..888da8bc05 100644 --- a/src/web/app/helpful-assistant/RelevantValues.re +++ b/src/haz3lcore/ai_assistant_core/LanguageServerHelpers/RelevantValues.re @@ -91,11 +91,7 @@ let entries = (ctx: Ctx.t, ana: Typ.t): list(filtered_entry) => { }; let format_def = ({name, typ, _}: filtered_entry) => - Printf.sprintf( - "let %s: %s = in", - name, - Haz3lcore.ErrorPrint.Print.typ(typ), - ); + Printf.sprintf("let %s: %s = in", name, ErrorPrint.Print.typ(typ)); /* Returns a list of relevant values headers formatted as let definitions */ let get = (ctx: Ctx.t, ana: Typ.t): string => diff --git a/src/haz3lcore/ai_assistant_core/PromptFactory/HazelDocumentation.re b/src/haz3lcore/ai_assistant_core/PromptFactory/HazelDocumentation.re new file mode 100644 index 0000000000..2e9c47c9db --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/HazelDocumentation.re @@ -0,0 +1,357 @@ +// todo: this broke when moving from web to core +// let get_documentation_as_text = () => { +// let (_, slides) = ScratchMode.StoreDocumentation.load(); +// let documentation = +// slides +// |> List.map(((name, persistent)) => { +// switch (persistent) { +// | Some(persistent) => +// let cell_model = +// CellEditor.Model.unpersist( +// ~settings=Language.CoreSettings.off, +// persistent, +// ); +// let text = +// Printer.of_zipper(cell_model.editor.editor.state.zipper); +// "" +// ++ name +// ++ "\n" +// ++ "" +// ++ text +// ++ ""; +// | None => "" +// } +// }) +// |> String.concat("\n\n"); +// ["" ++ documentation ++ ""]; +// }; + +let summarized_docs = [ + "", + "- Hazel is a live functional programming environment that supports incomplete programs via typed holes.", + "- Typed holes, denoted as `?`, can stand for missing expressions or types.", + "- Programs with holes still carry meaningful static and dynamic semantics, unlike traditional languages.", + "- Hazel uses a structure editor that maintains well-formed syntax trees with holes instead of raw text.", + "- Example: Define a function with an unknown type and evaluate it.", + "```", + "let f: ? -> ? = fun b -> b && true in f(true);", + "```", + "- This function runs successfully even though the type is unknown.", + "- Hazel uses **bidirectional typing**, where types can be either synthesized or analyzed.", + "- Example: Bidirectional typing for boolean functions.", + "```", + "let f = fun b: Bool -> b && true in f(true);", + "```", + "- Holes can be used in types, parameters, and patterns.", + "```", + "let f: (Int, ?) -> Int = fun a, b -> a + 1 in f(1, 2);", + "```", + "- Hazel also supports polymorphic functions.", + "```", + "let poly_id: forall a -> a -> a = typfun a -> fun x: a -> x;", + "```", + "- Polymorphic functions can be applied with type arguments using `@` syntax.", + "```", + "poly_id@(5);", + "```", + "- Hazel supports defining algebraic data types (ADTs) and using them in pattern matches.", + "```", + "type Exp =", + " + Var(String)", + " + Lam(String, Exp)", + " + Ap(Exp, Exp) in", + "", + "let subst: (Exp, String, Exp) -> Exp =", + " fun v, name, e ->", + " case e", + " | Var(n) => if n$==name then v else e", + " | Lam(x, body) => Lam(x, subst(v, name, body))", + " | Ap(e1, e2) => Ap(subst(v, name, e1), subst(v, name, e2))", + " end;", + "```", + "- Hazel permits incomplete pattern matches and treats them semantically.", + "```", + "let f = fun x -> case x | 0 => \"zero\" | ? => \"non-zero\" end;", + "```", + "- Evaluation proceeds until it hits a hole, then stops gracefully.", + "- Example of evaluation with error halted inside a hole:", + "```", + "let f: Int -> Int = fun x -> x + true in f(1);", + "```", + "- The erroneous addition will be wrapped in a non-empty hole, not crash.", + "- Hazel's recursive types allow expressive structures.", + "```", + "type MyList = rec A -> (Nil + Cons(Int, A)) in", + "let x: MyList = Cons(1, Cons(2, Cons(3, Nil)));", + "```", + "- Hazel supports list processing via pattern matching.", + "```", + "let length: [Int] -> Int =", + " fun xs ->", + " case xs", + " | [] => 0", + " | hd::tl => 1 + length(tl)", + " end;", + "```", + "- Hazel allows mutual recursion via let-bindings of tuples.", + "```", + "let (even: Int -> Bool, odd: Int -> Bool) =", + " (fun n -> if n == 0 then true else odd(n - 1),", + " fun n -> if n == 0 then false else even(n - 1))", + "in even(4);", + "```", + "- Projectors and livelits provide visual editing tools in Hazel's GUI.", + "- Hazel allows tests to be included inline.", + "```", + "test 2 + 2 == 4 end;", + "```", + "- Hazel supports explaining sub-expressions via cursor-based documentation.", + "- Hazel is ideal for education, as no edit state is meaningless.", + "- All code edits are type-preserving and guided by Hazel's edit action model.", + "- An important note is that comments in Hazel are enclosed by `#` symbols. Eg `# This is a comment #`. The program WILL be erroneous if you do not include an ending '#' symbol for a comment.", + "", +]; + +let polymorphism_documentation = {| + + # Polymorphism # + +# We can take types as parameters to type functions, # +# and use them in annoatations in the body: # +let id = typfun A -> fun x : A -> x in + +# Such functions are applied like so: # +let ex1 = id@(1) in # 1 # + +# We can annotate the type of a type function with a forall. # +let const : forall A -> forall B -> A -> B -> A = + typfun A -> typfun B -> fun x -> fun y -> x in +let ex2 = const@@(2)("Hello World") in # 2 # + +# We can go beyond rank 1 polymorphism: # +let apply_both : forall A -> forall B -> (forall D -> D -> D) -> (A , B) -> (A , B) = + typfun A -> typfun B -> fun f -> fun (x, y) -> (f@(x), f@(y)) in +let ex3 = apply_both@@(id)(3, "Hello World") in # (3, "Hello World") # + +# Finally, here is a more in-depth, yet applicable example: polymorphic map # +let emptylist : forall A -> [A] = typfun A -> [] in # polymorphic constant # +let map : forall A -> forall B -> (A -> B) -> ([A] -> [B]) = + typfun A -> typfun B -> fun f : (A -> B) -> fun l : [A] -> + case l + | h :: t => f(h) :: map@@(f)(t) + | _ => emptylist@ + end in +let ex4 = map@@(string_of_int)([1,2,3]) in # ["1", "2", "3"] # + + +# Recursive types # + +# We can express types that are the least fixed point of # +# some type function with the rec keyword. # +type MyList = rec A -> (Nil + Cons(Int, A)) in + +# Hazel does not (yet) support higher-kinded or existential types, # +# So we cannot implement our own polymorphic lists. # + +# Now anything that returns an element of the least fixed point matches MyList. # +let x : MyList = Cons(1, Cons(2, Cons(3, Nil))) in + +# Note that if the sum is the top level operator, # +# type aliases are implicitly least fixed points on their own name: # +type MyList2 = Nil + Cons(Int, MyList2) in +type Broken = Int -> (HasInt(Int) + HasMore(Int, Broken)) in + + +let list_of_mylist : (MyList -> [Int]) = fun myl : MyList -> + case myl + | Nil => [] + | Cons(h, t) => h :: list_of_mylist(t) + end in +let ex5 = list_of_mylist(x) in # [1, 2, 3] # + + +# All output from examples: # +(ex1, ex2, ex3, ex4, ex5) + +|}; + +let sample_tic_tac_toe_program = {| + +The following is a sample Hazel program that implements a simple Tic-Tac-Toe game using the MVU (Model-View-Update) architecture. +Note the syntax, semantics, and structure: +``` +type Player = + + X + + O +in + +type Square = + + SPlayer(Player) + + SEmpty +in + +type Board = (Square, Square, Square, Square, Square, Square, Square, Square, Square) in + +type Model = (Board, Player) in let string_of_square : Square -> String = + fun s -> + case s + | SPlayer(p) => + (case p + | X => " X " + | O => " O " + end) + | SEmpty => " " + end + in +let view : Model -> String = + fun m -> + let (b, _) = m in + let (s1, s2, s3, s4, s5, s6, s7, s8, s9) = b in + string_of_square(s1) ++ "|" ++ string_of_square(s2) ++ "|" ++ string_of_square(s3) ++ "\n" ++ + "---|---|---\n" ++ + string_of_square(s4) ++ "|" ++ string_of_square(s5) ++ "|" ++ string_of_square(s6) ++ "\n" ++ + "---|---|---\n" ++ + string_of_square(s7) ++ "|" ++ string_of_square(s8) ++ "|" ++ string_of_square(s9) +in let update : (Model, Int) -> Model = + fun m, move -> + let (b, p) = m in + let (s1, s2, s3, s4, s5, s6, s7, s8, s9) = b in + let new_board = + case move + | 0 => (SPlayer(p), s2, s3, s4, s5, s6, s7, s8, s9) + | 1 => (s1, SPlayer(p), s3, s4, s5, s6, s7, s8, s9) + | 2 => (s1, s2, SPlayer(p), s4, s5, s6, s7, s8, s9) + | 3 => (s1, s2, s3, SPlayer(p), s5, s6, s7, s8, s9) + | 4 => (s1, s2, s3, s4, SPlayer(p), s6, s7, s8, s9) + | 5 => (s1, s2, s3, s4, s5, SPlayer(p), s7, s8, s9) + | 6 => (s1, s2, s3, s4, s5, s6, SPlayer(p), s8, s9) + | 7 => (s1, s2, s3, s4, s5, s6, s7, SPlayer(p), s9) + | 8 => (s1, s2, s3, s4, s5, s6, s7, s8, SPlayer(p)) + | _ => b + end + in + let next_player = case p | X => O | O => X end in + (new_board, next_player) +in +let initial_model : Model = + let empty_board = (SEmpty, SEmpty, SEmpty, SEmpty, SEmpty, SEmpty, SEmpty, SEmpty, SEmpty) in + (empty_board, X) +in + +let model1 = update(initial_model, 0) in +let model2 = update(model1, 4) in +let model3 = update(model2, 1) in +let (b, p) = model3 in + +test + (view(model3) == " X | X | \n---|---|---\n | O | \n---|---|---\n | | ") && + (p == O) +end; +``` + +|}; + +let sample_emoji_paint = {| + +Below is a sample Hazel program that implements a simple Emoji Paint application using the MVU (Model-View-Update) architecture. +Note the syntax, semantics, and structure: +``` +# EMOJIPAINT MVU # +type Emoji = String in +type Canvas = [[Emoji]] in +type Row = Int in +type Col = Int in +type Model = ( + canvas = Canvas, # The 2D grid of emojis # + brush = Emoji, # The currently selected emoji # + palette = [Emoji] # The list of available emojis # +) in +type Action = + + SetBrush(Int) # Set the brush using a palette index # + + PaintCell(Row, Col) # Stamp the current emoji at the specified position # + + ClearCell(Row, Col) # Clear the emoji at the specified position # + + ClearCanvas # Clear the entire grid # + + PaintRow(Row) # Fill the specified row with the current emoji # +in +let init: Model = ( + # The canvas starts empty # + canvas = [ + ["","",""], + ["","",""], + ["","",""] + ], + # Initial emoji brush # + brush = "😄", + # Emoji palette # + palette = ["😄", "😅", "😆", "😉", "😊"] +) in +let setCell: (Canvas, Row, Col, Emoji) -> Canvas = + fun (canvas, row, col, emoji) -> mapi( + canvas, + fun (i, r) -> + if i == row + then mapi( + r, + fun (j, c) -> + if j == col + then emoji + else c) + else r) +in +let setRow: (Canvas, Row, Emoji) -> Canvas = + fun (canvas, targetRow, emoji) -> + mapi( + canvas, + fun (i, row) -> + if i == targetRow + then map(row, fun _ -> emoji) + else row) +in +let setAll: (Canvas, Emoji) -> Canvas = + fun (canvas, emoji) -> + map(canvas, fun r -> map(r, fun _ -> emoji)) +in +let updateGrid: (Model, Canvas -> Canvas) -> Model = + fun (m, f) -> + (f(m.canvas), m.brush, m.palette) +in +# Update the EmojiPaint app model based on an action # +let update: (Model, Action) -> Model = + fun (m, action) -> + case action + | SetBrush(emoji) => + (m.canvas, nth(m.palette, emoji), m.palette) + | PaintCell(row, col) => + updateGrid(m, fun c -> setCell(c, row, col, m.brush)) + | ClearCell(row, col) => + updateGrid(m, fun c -> setCell(c, row, col, "")) + | ClearCanvas => + updateGrid(m, fun c -> setAll(c, "")) + | PaintRow(row) => + updateGrid(m, fun c -> setRow(c, row, m.brush)) + end +in +let do = fun (init: Model, actions: [Action]) -> + fold_left(actions, update, init) +in +let actions = [ + ClearCanvas, + SetBrush(1), + PaintCell(1, 1), + SetBrush(3), + PaintRow(0) +] in +do(init, actions) +``` + +|}; + +let self = (~summarized: bool) => { + let _ = summarized; + summarized_docs + @ [ + polymorphism_documentation, + sample_tic_tac_toe_program, + sample_emoji_paint, + ]; +}; diff --git a/src/web/app/helpful-assistant/prompts/HazelSyntaxNotes.re b/src/haz3lcore/ai_assistant_core/PromptFactory/HazelSyntaxNotes.re similarity index 100% rename from src/web/app/helpful-assistant/prompts/HazelSyntaxNotes.re rename to src/haz3lcore/ai_assistant_core/PromptFactory/HazelSyntaxNotes.re diff --git a/src/haz3lcore/ai_assistant_core/PromptFactory/InitPrompts.re b/src/haz3lcore/ai_assistant_core/PromptFactory/InitPrompts.re new file mode 100644 index 0000000000..82ded915f5 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/InitPrompts.re @@ -0,0 +1,96 @@ +open Util; + +let mk_tutor = () => { + OpenRouter.mk_system_msg( + String.concat( + "\n", + TutorPrompt.self @ HazelDocumentation.self(~summarized=false), + ), + ); +}; + +let mk_composition = (): OpenRouter.message => { + OpenRouter.mk_system_msg( + String.concat( + " ", + CompositionPrompt.self @ HazelDocumentation.self(~summarized=true), + ), + ); +}; + +module Options = { + [@deriving (show({with_path: false}), sexp, yojson)] + type t = { + params: OpenRouter.params, + instructions: bool, + syntax_notes: bool, + num_examples: int, + expected_type: bool, + relevant_ctx: bool, + error_rounds_max: int, + }; + let init: t = { + params: OpenRouter.default_params, + instructions: true, + syntax_notes: true, + num_examples: 9, + expected_type: true, + relevant_ctx: true, + error_rounds_max: 2, + }; +}; + +module SystemPrompt = { + let prelude = ["You are a helpful coding assistant in Hazel. \n"]; + + let normal_completion_prompt = (completion_token: string) => + CompletionPrompt_normal.self(completion_token); + + let cot_completion_prompt = (completion_token: string) => + CompletionPrompt_cot.self(completion_token); + + let hazel_syntax_notes = HazelSyntaxNotes.self; + + let mk_suggestion_prompt = + ( + {instructions, syntax_notes, _}: Options.t, + completion_token: string, + advanced_reasoning: bool, + ) + : string => + String.concat( + "\n", + ( + instructions + ? prelude + @ ( + advanced_reasoning + ? cot_completion_prompt(completion_token) + : normal_completion_prompt(completion_token) + ) + : [] + ) + @ (syntax_notes ? hazel_syntax_notes : []), + ); +}; + +let mk_suggestion = + (hole_label: string, advanced_reasoning: bool): OpenRouter.message => { + let prompt = + String.concat( + "\n", + [ + SystemPrompt.mk_suggestion_prompt( + Options.init, + hole_label, + advanced_reasoning, + ), + ], + // @ CompletionExamples.get( + // Options.init.num_examples, + // hole_label, + // advanced_reasoning, + // ), + ); + OpenRouter.mk_system_msg(prompt); +}; diff --git a/src/web/app/helpful-assistant/prompts/suggestion_prompts/SuggestionPrompt_cot.re b/src/haz3lcore/ai_assistant_core/PromptFactory/completion/CompletionPrompt_cot.re similarity index 100% rename from src/web/app/helpful-assistant/prompts/suggestion_prompts/SuggestionPrompt_cot.re rename to src/haz3lcore/ai_assistant_core/PromptFactory/completion/CompletionPrompt_cot.re diff --git a/src/web/app/helpful-assistant/prompts/suggestion_prompts/SuggestionPrompt_normal.re b/src/haz3lcore/ai_assistant_core/PromptFactory/completion/CompletionPrompt_normal.re similarity index 100% rename from src/web/app/helpful-assistant/prompts/suggestion_prompts/SuggestionPrompt_normal.re rename to src/haz3lcore/ai_assistant_core/PromptFactory/completion/CompletionPrompt_normal.re diff --git a/src/web/app/helpful-assistant/CompletionExamples.re b/src/haz3lcore/ai_assistant_core/PromptFactory/completion/fewshot_examples/CompletionExamples.re similarity index 100% rename from src/web/app/helpful-assistant/CompletionExamples.re rename to src/haz3lcore/ai_assistant_core/PromptFactory/completion/fewshot_examples/CompletionExamples.re diff --git a/src/haz3lcore/ai_assistant_core/PromptFactory/composition/CompositionPrompt.re b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/CompositionPrompt.re new file mode 100644 index 0000000000..2ae0ca08ca --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/CompositionPrompt.re @@ -0,0 +1,167 @@ +let hazel_syntax_notes = HazelSyntaxNotes.self; + +let hazel_documentation = HazelDocumentation.self; + +let role = [ + "\n", + "You are an expert agentic AI programming assistant operating in the Hazel programming language.", + "If ever asked, your name is 'Corylus' (a play on words coming from the genus of the hazel tree, since you operate solely in the Hazel programming language).", + "Furthermore, you are an agentic programming tool designed by researchers in the Future of Programming Lab at the University of Michigan.", + "You are working with a user to accomplish a programming task in a paired programming setting.", + "The user will ask you a question or to perform a task (implement a feature, fix an issue, etc).", + "You are a professional coding agent, meaning it is your duty to complete the user's task or attempt to complete their task until you decide", + "the task is complete or it is absolutely infeasible to complete the task.", + "The Hazel programming language is a low-resource programming language,", + "meaning it did not show up in much of your training data, and thus", + "you will be provided with relevant syntax and semantic information of the programming language", + "that you must carefully study and review when generating your responses.", + "NEVER try to write code from another programming language other than Hazel.", + "\n", +]; + +let instructions = [ + "\n", + "You may explain and reason about the program/task/user query, but aim to keep your thinking and explanations concise and to the point.", + "If the user wants you to implement a feature that is quite complex, you should break it down into smaller tasks to work through step by step.", + "After calling a tool, you should pick up immediately from where you left off—No need to repeat or summarize what you've been doing.", + "You should avoid explicitly mentioning tool calls to the user. Rather, explain what you are doing to the codebase in a way that is natural and easy to understand.", + "Your conversation with the user should be as natural as possible, as if you were their pair programming partner.", + "Do NOT write code or make todo lists if the user is simply just trying to engage in a dialogue with you.", + "We will now provide you with the following:\n", + "1. A uniquely designed structure-based programming toolkit along with a specification", + "on how to call these tools throughout the attempted completion of the task.", + "2. Hazel syntax notes.\n", + "3. A brief summary of Hazel documentation.\n", + // "4. A series of few shot examples from golden standard agents who successfully implemented user-requested features", + // "using our uniquely designed structure-based programming toolkit.\n", + "You should frequently come back and reference each of the toolkit, syntax notes, documentation, and golden standard examples.", + "Keep your chats concise, briefly communicating with the user your plan-of-action.", + "After making a tool call, pick up immediately where you left off.", + "That is, do not repeat yourself or try to summarize what you've been doing.", + "You should use markdown to format your text responses, in a way such that the user can easily read and understand your thinking, intentions, and plan-of-action.", + "Available markdown features include:\n", + "1. bold\n", + "2. italic\n", + "3. inline code\n", + "4. headers\n", + "5. blockquote\n", + "6. thematic break\n", + "7. lists\n", + "8. links\n", + "\n", +]; + +let toolkit_instructions = [ + "\n", + "You are operating in a structure-based programming environment, akin to a structure editor.", + "every edit state of Hazel maintains a valid Abstract Syntax Tree (AST) representation of the program.", + "We aim to leverage this feature of Hazel and provide you with a toolkit that", + "enables you to view, modify, and gather/read meaningful context (via language server tools) from the program's structure.", + "On each iteration, you will be provided with a textual representation of the program and any static errors present in the program.", + "It is your duty to manage the context/length of what is shown by the textual representation of the program.", + "It's important to note that the tools you have belong exclusively to one of three categories:\n", + "1. View: These tools are used to view the AST/program itself. They primarily operate on the tree-based structure of the file system and AST.", + "You should use these to expand and collapse definitions (kind of as if they were folders/files).", + "It is your duty to adequately manage what is expanded and collapsed. Try to only retain meaningul information expanded, things you readily need.", + "This is a critical step here, as it helps keep costs at bay, and avoids overwhelming you with too much unnecessary information.", + "These actions will never modify the program itself. They are purely for controlling the view of what you see in the program.\n", + "Critically, there is no notion of 'we' for this view. ONLY YOU are able to see this view. These user sees their own view of the program.", + "(note that by 'view' here, we mean what is expanded/collapsed.", + "2. Read: These tools are used to gather additional information from the program, and do not modify the program/AST.", + "Think of these as powerful language server tools.\n", + "3. Edit: These tools are used to modify the program. \n", + "You should use the tools to navigate/view the AST, read information from the program, and modify the program.", + "You may find it useful to use view and read tools to understand the code and provide and thoughtful, accurate response.", + "If the user asks a complex or ambiguous question, you should ask for and seek clarification first before calling any tools.", + "These tools are meant to be fairly atomic, and you are expected to make many tool calls in order to", + "read and undestand the code and complete the user's task!", + "\n", +]; + +let notes = [ + "\n", + "You might see ⋱ after some definitions. This is a special character that indicates a \"fold\" in the program.", + "It is a critical feature here, as it aims to prevent information overload...", + "These ⋱ characters are what you will see on collapsed definitions.", + "You should recognize that these are not actual characters within the program,", + "but rather \"folds\" which hide away the details of collapsed definitions and prevent an overload of information.", + "\nTerminology Used in this Toolkit:\n", + "* \"pattern\" - the tiles between the \"let\" and \"=\" delimiters, or the \"type\" and \"=\" delimiters, exclusive\n", + "* \"definition\" - the tiles between the \"=\" and \"in\" delimiters, exclusive\n", + "* \"body\" - the tiles after the \"in\" delimiter, exclusive\n", + "* \"binding clause\" - the tiles between the \"let\" and \"in\" delimiters, or the \"type\" and \"in\" delimiters, inclusive\n", + "\nTHESE TOOLS ARE ONLY AVAILABLE TO YOU.", + "The user has their OWN text editor interface.", + "They see their own view of the program, and you see your own view of the program.", + "They interact with the program in their own way, and you interact with the program in your own way (with these tools).", + "They technically should know nothing about the tools unless they have backend knowledge.", +]; + +let indentation_instructions = [ + "\nAnother super important note—when writing code, you should use line breaks to neatly format the code.", + "Hazel's formatter will automatically indent the code for you wherever you insert line breaks.", + "ALWAYS ALWAYS ALWAYS use line breaks when necessary to allow Hazel's formatter to format your code.", + "We REPEAT, ALWAYS ALWAYS ALWAYS use line breaks, OTHERWISE Hazel canNOT pretty format the code and it will NOT be human readable!!!", + "Indenting is so important and crucial, and such a simple, surefire way to make your code readable. Please always use it.", + "\n", +]; + +let task_management_instructions = [ + "\n", + "You also have some tools available to you for maintaining a tasks/todo lists.", + "This is a SUPER useful tool you should ALWAYS utilize.", + "We repeat. Utilize. The. Task. Tools.", + "When planning large or small tasks, utilize this tool.", + "Even just a small refactor... we store all of them, so the user can easily read through all tasks you've done, small or large.", + "Almost always your first tool call should be to create a task, especially if one does not yet exist.", + "This task list will always be displayed as the latest message for you, meaning it will greatly help you manage your overall goal at hand.", + "You should aim to keep titles concise, as they are unique identifiers for the subtask items, and make descriptions very detailed.", + "Check off items as necessary. Uncheck if necessary. Add more subtasks as necessary. Reorder subtasks if necessary.", + "You must always have an active task and subtask set when working on code-related tasks.", + "This will help keep track of the subtask/subgoal you are focusing on.", + "And it will help communicate to the user what this objective is as well, and the actions you take to complete it.", + "Again, basic flow for todo list usage should be:\n", + "1.a. Create a new task if you need to intialize one.\n", + "1.b. If a *relevant* task exists already, just continue to use it! You can always add more subtasks if needed.\n", + "2. Set an active subtask before working on code-related tasks.\n", + "3. Mark off subtasks as you complete them.\n", + "4. Set new active subtasks as you move on to new tasks, try to go in order.\n", + "5. Repeat steps 1-4 as necessary until the task is complete!\n", + "6. Once complete, mark the task itself as complete, offering a descriptive summary. This will depend on the task and complexity of the implementation, but this summary could range from a brief overview to a multi-paragraph explanation.\n", + "\n", +]; + +let comments_in_hazel = [ + "\n", + "Whenever writing a comment in Hazel, YOU MUST follow the comment syntax exactly... the syntax is as follows: ", + "```\n", + " # comment #\n", + "```\n", + "Note that there MUST ALWAYS be an OPENING AND CLOSING hash symbol to ENCLOSE the comment.", + "If the closing hash symbol is not present, the comment is invalid and syntax parsing will fail.", + "Furthermore, comments cannot span multiple lines.", + "```# this is an \n invalid comment #``` is invalid (spans multiple lines). ```# This is an invalid comment``` is invalid (no closing hash symbol). ```# This is a valid comment #``` is valid.", + "\n", +]; + +let few_shot_comp_examples = { + [ + "The following are GOLDEN EXAMPLES from agents who successfully implemented user-requested features." + ++ "You ASPIRE to be as elegant and efficient as they are! " + ++ "In fact, you CAN be! As long as you study what they've done so well!\n" + ++ Eg_RecFib.self + ++ Eg_RPS.self + ++ "", + ]; +}; + +let self = + role + @ instructions + @ toolkit_instructions + @ notes + @ indentation_instructions + @ task_management_instructions + @ hazel_syntax_notes + @ comments_in_hazel; +//@ few_shot_comp_examples; diff --git a/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_EmojiPaint.re b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_EmojiPaint.re new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_EmojiPaint.re @@ -0,0 +1 @@ + diff --git a/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_RPS.re b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_RPS.re new file mode 100644 index 0000000000..d52531145d --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_RPS.re @@ -0,0 +1,8 @@ +let self = {| +Chat: +{ +[ + +] +} +|}; diff --git a/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_RecFib.re b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_RecFib.re new file mode 100644 index 0000000000..3f394e4afe --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/composition/fewshot_examples/Eg_RecFib.re @@ -0,0 +1,110 @@ +// A simple example going over how to navigate from a sibling to another sibling, and update +// a definition of a function + +let self = + // This example is outdated, but may be a good reference for + {| + + +Please implement the recursive fibonacci function. + + + +Current node: factorial +No parent node, you are at the top level of the program's AST. +Sibling nodes: [rec_fib (index: 0)] +Child nodes: [] + + +Definition of "factorial"'s parent (no parent, displaying entire top level of the program): +``` +let rec_fib = + ⋱ +in +let factorial = fun n -> + ? +in +? +``` +No static errors found in the program. + + + + +I can help with that! +I'll start by defining the `rec_fib` function. It will be a recursive function that calculates the nth Fibonacci number. +I'll begin by updating the function's definition, but first I'll need to move to the `rec_fib` function. + +go_to_sibling( +rec_fib +) + + + + + +Current node: rec_fib +No parent node, you are at the top level of the program's AST. +Sibling nodes: [factorial (index: 0)] +Child nodes: [] + + +Definition of "rec_fib"'s parent (no parent, displaying entire top level of the program): +``` +let rec_fib = + ? +in +let factorial = + ⋱ +in +? +``` +No static errors found in the program. + + + + +Great! Now I'll update the definition of `rec_fib`. + +update_definition( +code=" +fun n -> + if n <= 1 + then 1 + else rec_fib(n - 1) + rec_fib(n - 2)" +) + + + + + +Current node: rec_fib +No parent node, you are at the top level of the program's AST. +Sibling nodes: [factorial (index: 0)] +Child nodes: [] + + +Definition of "rec_fib"'s parent (no parent, displaying entire top level of the program): +``` +let rec_fib = + fun n -> + if n <= 1 + then 1 + else rec_fib(n - 1) + rec_fib(n - 2) +in +let factorial = + ⋱ +in +? +``` +No static errors found in the program. + + + + +Great! The recursive fibonacci function is now properly implemented. + + + +|}; +// how to structure and write future few shot examples. diff --git a/src/haz3lcore/ai_assistant_core/PromptFactory/summarization/SummarizePrompt.re b/src/haz3lcore/ai_assistant_core/PromptFactory/summarization/SummarizePrompt.re new file mode 100644 index 0000000000..704ab9da9a --- /dev/null +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/summarization/SummarizePrompt.re @@ -0,0 +1,22 @@ +/* + Summarization prompt for compressing long conversations. + */ + +let prelude = "This is an automated system message: +You are a helpful assistant that is to summarize a conversation. +Your summary should be approximately the length of a 4 page report, or around 2000 words. +You will be given a conversation between a user and an AI coding agent. +You should NOT focus too much on the prompt, as the prompt will not be truncated from +the conversation; however, you ABSOLUTELY SHOULD focus on the content and overall ideas present +between the user and the AI coding agent. +You may briefly sum up older, potentially competed or failed-to-be-compelted tasks, while +giving an in-depth summary, review, and outline of the current task, and the state of it. +At the end of your summary, leave a note for future inputs of the LLM to read, indicating that +this message is a summary of the conversation, and it should not directly refer to it and ask the user +for clarification on it; rather, it should continue it's role as a task completion agent. +You should use third person when summarizing the conversation, and not refer to yourself as the assistant or address the +user in your summary (no first or second person pronouns). +Your summary shouldn't really be made for the user to read, but rather for future LLMs to read and +use as a sort of historical context and memory."; + +let self = [prelude]; diff --git a/src/web/app/helpful-assistant/prompts/InitPrompts.re b/src/haz3lcore/ai_assistant_core/PromptFactory/tutor/TutorPrompt.re similarity index 78% rename from src/web/app/helpful-assistant/prompts/InitPrompts.re rename to src/haz3lcore/ai_assistant_core/PromptFactory/tutor/TutorPrompt.re index 2a6cdf108e..9931fbb43d 100644 --- a/src/web/app/helpful-assistant/prompts/InitPrompts.re +++ b/src/haz3lcore/ai_assistant_core/PromptFactory/tutor/TutorPrompt.re @@ -1,43 +1,8 @@ -open Util; - -let get_documentation_as_text = () => { - let (_, slides) = ScratchMode.StoreDocumentation.load(); - let documentation = - slides - |> List.map(((s, optional_persistent)) => { - ( - s, - OptUtil.get( - () => Init.default_documentation_slide_name(s), - optional_persistent, - ), - ) - }) - |> List.map(((name, persistent)) => { - let cell_model = - CellEditor.Model.unpersist( - ~settings=Language.CoreSettings.off, - persistent, - ); - let text = - Haz3lcore.Printer.of_zipper(cell_model.editor.editor.state.zipper); - "" - ++ name - ++ "\n" - ++ "" - ++ text - ++ ""; - }) - |> String.concat("\n\n"); - "" ++ documentation ++ ""; -}; - -let mk_tutor = () => { - let prelude = " +let prelude = " You are a friendly, helpful, and highly knowledgeable tutor of the Hazel programming language. - If ever asked, you are \"Hazelnut, Hazel's AI Tutor\". + If ever asked, you are \"Corylus, Hazel's Agentic Assistant\". That is, if the scenario ever arises where you must give your name or identity, - you should let the user know you are \"Hazelnut, Hazel's AI Tutor\". + you should let the user know you are \"Corylus, Hazel's Agentic Assistant\". You are given a list of documentation slides, which are formatted as follows: name @@ -81,20 +46,5 @@ let mk_tutor = () => { We are first implementing these ideas into Hazel, a web-based programming environment for an Elm/ML-like functional programming language designed around typed-hole-driven development. Uniquely, every incomplete program that you can construct using Hazel's language of edit actions is both statically and dynamically well-defined, i.e. it has a (possibly incomplete) type, and you can run it to produce a (possibly incomplete) result. Consequently, Hazel serves as an elegant platform for research on the future of programming (and programming education). "; - OpenRouter.mk_system_msg(prelude ++ "\n\n" ++ get_documentation_as_text()); -}; - -let mk_composition = (): OpenRouter.message => { - OpenRouter.mk_system_msg( - String.concat( - " ", - CompositionPrompt.self @ [get_documentation_as_text()], - ), - ); -}; -let mk_suggestion = (): OpenRouter.message => { - OpenRouter.mk_system_msg( - "You are a helpful assistant that suggests how to fill holes in Hazel to the user.", - ); -}; +let self = [prelude]; diff --git a/src/haz3lcore/projectors/implementations/SliderProj.re b/src/haz3lcore/projectors/implementations/SliderProj.re old mode 100644 new mode 100755 diff --git a/src/haz3lcore/tiles/Base.re b/src/haz3lcore/tiles/Base.re index 8b135e0db1..8547988c9a 100644 --- a/src/haz3lcore/tiles/Base.re +++ b/src/haz3lcore/tiles/Base.re @@ -36,36 +36,85 @@ let unparenthesize = (piece: piece): segment => }; let rec segment_to_string = - (~holes=" ", ~concave_holes=" ", ~projector_to_segment, seg: segment) + ( + ~holes=" ", + ~concave_holes=" ", + ~special_folds=false, + ~projector_to_segment, + seg: segment, + ) : string => seg |> List.map( - piece_to_string(~holes, ~concave_holes, ~projector_to_segment), + piece_to_string( + ~holes, + ~concave_holes, + ~projector_to_segment, + ~special_folds, + ), ) |> String.concat("") and piece_to_string = - (~holes: string, ~concave_holes: string, ~projector_to_segment, p: piece) + ( + ~holes: string, + ~concave_holes: string, + ~projector_to_segment, + ~special_folds, + p: piece, + ) : string => switch (p) { | Tile(t) => - tile_to_string(~holes, ~concave_holes, ~projector_to_segment, t) - | Grout({shape: Concave, _}) => concave_holes - | Grout({shape: Convex, _}) => holes - | Secondary(w) => Secondary.get_string(w.content) - | Projector(p) => - segment_to_string( + tile_to_string( ~holes, ~concave_holes, ~projector_to_segment, - projector_to_segment(p), + ~special_folds, + t, ) + | Grout({shape: Concave, _}) => concave_holes + | Grout({shape: Convex, _}) => holes + | Secondary(w) => Secondary.get_string(w.content) + | Projector(p) => + if (special_folds) { + switch (p.kind) { + | ProjectorCore.Kind.Fold => "⋱" + | _ => + segment_to_string( + ~holes, + ~concave_holes, + ~projector_to_segment, + ~special_folds, + projector_to_segment(p), + ) + }; + } else { + segment_to_string( + ~holes, + ~concave_holes, + ~projector_to_segment, + ~special_folds, + projector_to_segment(p), + ); + } } and tile_to_string = - (~holes: string, ~concave_holes: string, ~projector_to_segment, t: tile) + ( + ~holes: string, + ~concave_holes: string, + ~projector_to_segment, + ~special_folds, + t: tile, + ) : string => Aba.mk(t.shards, t.children) |> Aba.join( List.nth(t.label), - segment_to_string(~holes, ~concave_holes, ~projector_to_segment), + segment_to_string( + ~holes, + ~concave_holes, + ~projector_to_segment, + ~special_folds, + ), ) |> String.concat(""); diff --git a/src/haz3lcore/zipper/Printer.re b/src/haz3lcore/zipper/Printer.re index 748054af0a..a38f4a7040 100644 --- a/src/haz3lcore/zipper/Printer.re +++ b/src/haz3lcore/zipper/Printer.re @@ -67,6 +67,7 @@ let of_segment = ( ~holes=" ", ~concave_holes=" ", + ~special_folds=false, ~indent=" ", ~caret: option((string, Point.t))=None, ~selection_anchor: option((string, Point.t))=None, @@ -79,6 +80,7 @@ let of_segment = ~holes, ~concave_holes, ~projector_to_segment=Triggers.projector_to_invoke, + ~special_folds, ) |> String.split_on_char('\n') |> add_indents(segment, measured, indent) @@ -90,6 +92,7 @@ let of_zipper = ( ~holes=?, ~concave_holes=?, + ~special_folds=?, ~indent=?, ~caret=?, ~selection_anchor=?, @@ -111,6 +114,7 @@ let of_zipper = of_segment( ~holes?, ~concave_holes?, + ~special_folds?, ~indent?, ~caret, ~selection_anchor, diff --git a/src/haz3lcore/zipper/Zipper.re b/src/haz3lcore/zipper/Zipper.re index 61e07eb223..4f33cc2b4f 100644 --- a/src/haz3lcore/zipper/Zipper.re +++ b/src/haz3lcore/zipper/Zipper.re @@ -18,6 +18,7 @@ let init: unit => t = ancestors: [], }, caret: Outer, + agent_view: AgentState.init, }; let next_blank = _ => Id.mk(); @@ -41,10 +42,11 @@ let unzip = (~direction: Direction.t=Right, seg: Segment.t): t => { ancestors: [], }, caret: Outer, + agent_view: AgentState.init, }; let regrout = (d: Direction.t, z: t): t => { - assert(Selection.is_empty(z.selection)); + //assert(Selection.is_empty(z.selection)); let relatives = Relatives.regrout(d, z.relatives); { ...z, @@ -53,8 +55,9 @@ let regrout = (d: Direction.t, z: t): t => { }; let remold = (z: t): t => { - assert(Selection.is_empty(z.selection)); { + //assert(Selection.is_empty(z.selection)); + ...z, relatives: Relatives.remold(z.relatives), }; diff --git a/src/haz3lcore/zipper/ZipperBase.re b/src/haz3lcore/zipper/ZipperBase.re index 27a9c01f8f..960dee2382 100644 --- a/src/haz3lcore/zipper/ZipperBase.re +++ b/src/haz3lcore/zipper/ZipperBase.re @@ -11,6 +11,7 @@ type t = { selection: Selection.t, relatives: Relatives.t, caret, + agent_view: AgentState.t, }; let update_relatives = (f: Relatives.t => Relatives.t, z: t): t => { diff --git a/src/haz3lcore/zipper/action/Action.re b/src/haz3lcore/zipper/action/Action.re index 13e1f68df2..4770ade36b 100644 --- a/src/haz3lcore/zipper/action/Action.re +++ b/src/haz3lcore/zipper/action/Action.re @@ -31,6 +31,10 @@ type rel = | Current | Id([@equal (_, _) => true] Id.t, Direction.t); +[@deriving (show({with_path: false}), sexp, yojson, eq)] +type structure = + | Node(Id.t); + [@deriving (show({with_path: false}), sexp, yojson, eq)] type select = | All @@ -75,7 +79,7 @@ type paste = | String(string) | Segment(Segment.t); -[@deriving (show({with_path: false}), sexp, yojson, eq)] +[@deriving (show({with_path: false}), sexp, yojson)] type t = | Reparse | Buffer(buffer) @@ -90,6 +94,7 @@ type t = | Insert(string) | Put_down | Introduce + | Composition(CompositionActions.editor_payload) | Dump; module Failure = { @@ -106,7 +111,10 @@ module Failure = { | CantAccept | Cant_undo | Cant_redo - | CantIntroduce; + | CantIntroduce + | Cant_derive_local_AST_information + | Cant_view + | Composition_action_failure(string); exception Exception(t); }; @@ -126,10 +134,13 @@ let is_edit: t => bool = | Put_down | Introduce | Buffer(Accept | Clear | Set(_)) + | Composition((Edit(_), _)) | Dump => true | Copy | Move(_) | Select(_) + | Composition((View(_), _)) + | Composition((Read(_), _)) | Unselect(_) => false | Project(p) => switch (p) { @@ -147,6 +158,8 @@ let is_historic: t => bool = | Copy | Move(_) | Select(_) + | Composition((View(_), _)) + | Composition((Read(_), _)) | Unselect(_) => false | Cut | Buffer(Accept | Clear | Set(_)) @@ -156,6 +169,7 @@ let is_historic: t => bool = | Destruct(_) | Put_down | Introduce + | Composition((Edit(_), _)) | Dump => true | Project(p) => switch (p) { @@ -172,6 +186,8 @@ let prevent_in_read_only_editor = (a: t) => | Copy | Move(_) | Unselect(_) + | Composition((View(_), _)) + | Composition((Read(_), _)) | Select(_) => false | Buffer(Set(_) | Accept | Clear) | Cut @@ -181,6 +197,7 @@ let prevent_in_read_only_editor = (a: t) => | Insert(_) | Put_down | Introduce + | Composition((Edit(_), _)) | Dump => true | Project(p) => switch (p) { @@ -221,4 +238,5 @@ let should_animate: t => bool = | Copy | Move(_) | Project(_) + | Composition((_, _)) | Dump => true; diff --git a/src/haz3lcore/zipper/action/Perform.re b/src/haz3lcore/zipper/action/Perform.re index 7f4d52f0db..41a5331d59 100644 --- a/src/haz3lcore/zipper/action/Perform.re +++ b/src/haz3lcore/zipper/action/Perform.re @@ -6,6 +6,15 @@ type state = { col_target: option(int), }; +let mk_statics = (z: Zipper.t) => + Language.( + Statics.mk( + CoreSettings.on, + Builtins.ctx_init(Some(Operators.default_mode)), + MakeTerm.from_zip_for_sem(z).term, + ) + ); + let return = (error: Action.Failure.t, z: option(Zipper.t)) => Result.of_option(~error, z); @@ -121,6 +130,15 @@ let go = z |> Insert.go(char, ~ci=Indicated.ci_of(z, statics.info_map)) |> return(Cant_insert) + | Composition((a, schedule_tool_response)) => + CompositionGo.Public.go( + ~syntax, + ~z, + ~a, + ~mk_statics, + ~return, + ~schedule_tool_response, + ) | Put_down => Zipper.put_down(z) |> return(Cant_put_down) | Dump => Ok(Dump.to_zipper(z)) }; diff --git a/src/language/statics/CoCtx.re b/src/language/statics/CoCtx.re old mode 100644 new mode 100755 diff --git a/src/language/statics/Statics.re b/src/language/statics/Statics.re index 5e4b94347e..0f3919b7bc 100644 --- a/src/language/statics/Statics.re +++ b/src/language/statics/Statics.re @@ -111,7 +111,7 @@ and uexp_to_info_map = ~expected_labels: option(list(string)), ~override_self: option(Self.exp)=?, ~inferred_label: option(LabeledTuple.label)=?, - ~label_sort, + ~label_sort=false, {annotation: {ids}, term} as uexp: Exp.t, m: Map.t, ) diff --git a/src/language/term/TPat.re b/src/language/term/TPat.re index 5ed31b76c6..5d0c392e85 100644 --- a/src/language/term/TPat.re +++ b/src/language/term/TPat.re @@ -7,6 +7,7 @@ type cls = include TermBase.TPat; +let term_of: t => term = IdTagged.term_of; let fast_equal = Equality.syntactic.tpat; let equal = fast_equal; diff --git a/src/llm-tests/README.md b/src/llm-tests/README.md new file mode 100644 index 0000000000..8d344dcbdf --- /dev/null +++ b/src/llm-tests/README.md @@ -0,0 +1,59 @@ + +# Automated tests for LLM task completion + + +## Example Usage +``` +$ npm install +``` +``` + +$ cd src/llm-tests + +``` + +``` + +$ ts-node run-task.ts \ + +--task tasks/test.yaml \ + +--apiKey \ + +--model deepseek/deepseek-chat-v3.1:free + +--url https://hazel.org/build/assistant-actions-v2 + +--headless true + +--outputDir test-results + +--attemptTimeoutMs 120000 + +--retries 2 + +``` +## Arguments + +`headless`, `attemptTimeoutMs`, and `retries` are optional arguments, with their default values shown in the example run command below: +| Argument | Default | Description | +|--|--|--| +| `task` | None | The path to the YAML file containing the task description | +| `apiKey` | None | Your OpenRouter API key | +| `model` | None | OpenRouter model ID as defined by their [API](https://openrouter.ai/api/v1/models) +| `url` | `http://0.0.0.0:8000/` | The URL of the Hazel instance to be tested | +| `headless` | `true` | Whether the testing should be done in a headless browser | +| `outputDir` | None | The directory name where the result file (with name matching the task file) should be saved | +| `retries` | `2` | The number of times the test should be retried on timeout or browser crash | +| `attemptTimeoutMs` | `120000` | The maximum number of milliseconds each attempt can take | + +## YAML File Format + +Your YAML file should have three sections: +| Section | Description | +| -- | -- | +| `initialProgram` | The initial program sketch to paste into the editor | +| `prompt` | The prompt to give to the model | +| `tests` | Tests to paste into the editor after the task is completed | + +Make sure to use the `|` character in your YAML files to preserve whitespace in the program sketch/tests. \ No newline at end of file diff --git a/src/llm-tests/run-task.ts b/src/llm-tests/run-task.ts new file mode 100644 index 0000000000..70e75dc90c --- /dev/null +++ b/src/llm-tests/run-task.ts @@ -0,0 +1,594 @@ +#!/usr/bin/env ts-node + +import { chromium } from 'playwright'; +import fs from 'fs'; +import yaml from 'js-yaml'; +import minimist from 'minimist'; +import path from 'path'; + +type Task = { + url?: string; + apiKey?: string; + model?: string; + initialProgram: string; + prompt: string; + tests: string; +}; + +type TokenUsageEvent = { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + cached_tokens: number; +}; + +type Summary = { + total: number; + pass: number; + fail: number; + indet: number; + model: string; + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + cached_tokens: number; + cached_tokens_supported: boolean; + failed_or_indet_tests: string[]; + message_log: any[]; +}; + +type RunOnceResult = + | { ok: true; summary: Summary } + | { ok: false; error: unknown }; + +const args = minimist(process.argv.slice(2), { + string: ['task', 'apiKey', 'model', 'url', 'headless', 'outputDir', 'retries', 'attemptTimeoutMs'], + alias: { t: 'task' }, + default: { url: 'http://0.0.0.0:8000/', headless: 'true' }, +}); +const retries = Math.max(0, Number(args.retries ?? 2)); +const attemptTimeoutMs = Math.max(1, Number(args.attemptTimeoutMs ?? 120_000)); + +function loadTask(path: string): Task { + const doc = yaml.load(fs.readFileSync(path, 'utf8')) as Task; + if (!doc?.initialProgram) throw new Error('YAML missing initialProgram'); + if (!doc?.prompt) throw new Error('YAML missing prompt'); + if (!doc?.tests) throw new Error('YAML missing tests'); + return doc; +} + +const S = { + modeSelect: '.mode-name select', + assistantTab: '.assistant-button .tab', + apiKey: '.api-input', + apiButton: '[data-testid="update-api-key-btn"]', + modelInput: '.llm-model-id-input', + modelSelectButton: '[data-testid="update-model-btn"]', + confirmAndChatButton: '[data-testid="confirm-and-chat-btn"]', + prompt: '.message-input', + loadingDots: '.loading-dots', + testPass: '.test-result.Pass', + testFail: '.test-result.Fail', + testIndet: '.test-result.Indet', +}; + +// Wait until loading-dots is not in the DOM, and stays gone for debounceMs +async function waitChatDone( + page: import('playwright').Page, + loadingDots: string, + debounceMs = 600, + timeout = attemptTimeoutMs +) { + const t0 = Date.now(); + while (true) { + // Wait until no element matches (detached) + await page.waitForSelector(loadingDots, { + state: 'detached', + timeout: Math.max(1, timeout - (Date.now() - t0)), + }); + + // Debounce: if it re-attaches within debounceMs, loop and wait again + try { + await page.waitForSelector(loadingDots, { + state: 'attached', + timeout: debounceMs, + }); + continue; + } catch { + return; + } + } +} + +async function runCore( + page: import('playwright').Page, + ctx: import('playwright').BrowserContext, + params: { + task: Task; + apiKey: string; + modelValue: string; + url: string; + outPath: string; + tokenTotals: { prompt: number; completion: number; total: number; cached: number }; + cachedTokensSupported: boolean; + lastMessages: any[] | null; + } +): Promise { + const { task, apiKey, modelValue, url, outPath, tokenTotals, cachedTokensSupported, lastMessages } = params; + + await page.goto(url, { waitUntil: 'domcontentloaded' }); + + // Select Scratch Mode + let select = page.locator(S.modeSelect); + await select.waitFor({ state: 'visible' }); + await select.selectOption({ label: 'Scratch' }); + + await page.locator('.code-container > .code').first().click({force: true}); + + // Put initial program in the editor + const mod = process.platform === 'darwin' ? 'Meta' : 'Control'; + await page.keyboard.press(`${mod}+A`).catch(() => {}); + await page.keyboard.press('Delete').catch(() => {}); + + + // Try clipboard first, fallback to typing + try { + await page.evaluate(async (txt) => { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(txt); + } else { + throw new Error('Clipboard not available'); + } + }, task.initialProgram); + await page.keyboard.press(`${mod}+V`); + } catch (e) { + await page.keyboard.type(task.initialProgram); + } + + // Select Assistant tab + await page.locator(S.assistantTab).click(); + + // Fill API key + await page.locator(S.apiKey).fill(apiKey); + await page.locator(S.apiButton).click(); + + // Need to wait for API Key to load list of models + await page.waitForTimeout(1000); + + // Enter model ID using text input + await page.locator(S.modelInput).fill(modelValue); + await page.locator(S.modelSelectButton).click(); + + // Open chat dialog and go to Compose mode + await page.locator(S.confirmAndChatButton).click(); + await page.locator('.mode-buttons .mode-button', { hasText: 'Compose' }).click(); + + // Wait for page change to propagate before entering prompt + await page.waitForTimeout(1000); + + // Fill prompt + await page.locator(S.prompt).fill(task.prompt); + await page.keyboard.press('Enter'); + + // Wait for model run to complete + await waitChatDone(page, S.loadingDots, 600); + + // Paste tests and wait a beat for results + await page.keyboard.press(`${mod}+A`).catch(() => {}); + await page.keyboard.press('ArrowRight'); + await page.keyboard.press('Enter'); + await page.keyboard.type(';'); + await page.keyboard.press('Enter'); + + try { + await page.evaluate(async (txt) => { + if (navigator.clipboard && navigator.clipboard.writeText) { + await navigator.clipboard.writeText(txt); + } else { + throw new Error('Clipboard not available'); + } + }, task.tests); + await page.keyboard.press(`${mod}+V`); + } catch (e) { + await page.keyboard.type(task.tests); + } + await page.waitForTimeout(500); + + // Collect failed/indeterminate test contents + const failed_or_indet_tests: string[] = []; + for (const selector of [S.testFail, S.testIndet]) { + const tests = page.locator(selector); + const count = await tests.count(); + for (let i = 0; i < count; i++) { + const test = tests.nth(i); + await test.click({ clickCount: 3, force: true }); + await page.waitForTimeout(100); // let selection happen + await page.keyboard.press(`${mod}+c`); + await page.waitForTimeout(100); // let clipboard update + // Read clipboard in browser context + const copied = await page.evaluate(async () => { + const selection = window.getSelection(); + return selection ? selection.toString() : ''; + }); + if (copied && copied.trim()) failed_or_indet_tests.push(copied.trim()); + } + } + await page.waitForTimeout(500); + + // --- Collect all messages robustly using Playwright locators --- + const message_log: any[] = []; + const container = await page.$('.message-display-container'); + if (container) { + const messageEls = await container.$$('.message-container'); + for (const el of messageEls) { + // Check for collapse indicator and expand if needed + const collapseIndicator = await el.$('.collapse-indicator'); + if (collapseIndicator && await collapseIndicator.isVisible()) { + const indicatorText = await collapseIndicator.textContent(); + if (indicatorText && indicatorText.includes('▼ Show more')) { + await collapseIndicator.click(); + await page.waitForTimeout(200); // Wait for expansion + } + } + + const classes = await el.getAttribute('class') || ''; + // System prompt + if (classes.includes('system-prompt')) { + const showBtn = await el.$('.show-prompt-button'); + let content = '[prompt hidden]'; + if (showBtn) { + await showBtn.click(); + // Wait for the system-prompt-message to appear anywhere in the DOM + const promptMsg = await page.waitForSelector('.system-prompt-message', { timeout: 2000 }); + content = (await promptMsg.textContent())?.trim() || ''; + // Close the prompt + await showBtn.click(); + // Wait for the prompt to disappear before continuing + await page.waitForSelector('.system-prompt-message', { state: 'detached', timeout: 2000 }); + await page.waitForTimeout(100); + } + message_log.push({ type: 'system-prompt', content }); + } + // User message + else if (classes.includes('user')) { + const textarea = await el.$('textarea'); + let content = ''; + if (textarea) { + content = (await textarea.getProperty('value')).toString() || ''; + if (!content) content = (await textarea.textContent())?.trim() || ''; + } + message_log.push({ type: 'user', content }); + } + // LLM/agent message + else if (classes.includes('llm')) { + const llmMsg = await el.$('.llm-message'); + const content = (await llmMsg?.textContent())?.trim() || ''; + message_log.push({ type: 'agent', content }); + } + // Tool call + else if (classes.includes('tool')) { + const toolMsg = await el.$('.tool-message'); + const content = (await toolMsg?.textContent())?.trim() || ''; + message_log.push({ type: 'tool', content }); + } + // System error + else if (classes.includes('system-error')) { + const errMsg = await el.$('.system-error-message'); + const content = (await errMsg?.textContent())?.trim() || ''; + message_log.push({ type: 'system-error', content }); + } + } + } + await page.waitForTimeout(200); + + const [passCount, failCount, indetCount] = await Promise.all([ + page.locator(S.testPass).count(), + page.locator(S.testFail).count(), + page.locator(S.testIndet).count(), + ]); + const total = passCount + failCount + indetCount; + + const summary: Summary = { + total, + pass: passCount, + fail: failCount, + indet: indetCount, + model: modelValue, + prompt_tokens: tokenTotals.prompt, + completion_tokens: tokenTotals.completion, + total_tokens: tokenTotals.total || (tokenTotals.prompt + tokenTotals.completion), + cached_tokens: tokenTotals.cached ?? 0, + cached_tokens_supported: cachedTokensSupported, + failed_or_indet_tests, + message_log: message_log + }; + + return summary; +} + +async function runOnce(attempt: number, params: { + headless: boolean, url: string, outDir: string, outPath: string, + task: Task, apiKey: string, modelValue: string, +}) { + const { headless, url, outDir, outPath, task, apiKey, modelValue } = params; + + let lastMessages: any[] | null = null; + + const browser = await chromium.launch({ headless }); + const ctx = await browser.newContext(); + + // Grant clipboard permissions for all modes + try { + await ctx.grantPermissions(['clipboard-read', 'clipboard-write'], { + origin: url, + }); + } catch (e) { + // If this fails, we'll just use the typing fallback instead of copy/paste + } + + const page = await ctx.newPage(); + + // Capture console/network for debugging + const logs: string[] = []; + page.on('console', m => logs.push(`[console:${m.type()}] ${m.text()}`)); + page.on('requestfailed', r => logs.push(`[requestfailed] ${r.url()} ${r.failure()?.errorText}`)); + + // Start trace (stopped on success, saved on failure) + await ctx.tracing.start({ screenshots: true, snapshots: true, sources: true }); + + const attemptTag = `attempt-${attempt}`; + const tracePath = path.join(outDir, `trace-${attemptTag}.zip`); + const screenshotPath = path.join(outDir, `screenshot-${attemptTag}.png`); + const logsPath = path.join(outDir, `logs-${attemptTag}.txt`); + + const work = (async () => { + try { + const tokenTotals = { prompt: 0, completion: 0, total: 0, cached: 0 }; + let cachedTokensSupported = false; + const responseTokenData: any[] = []; // Store all response data in order + + // Capture OpenRouter chat completion responses + page.on('request', async (req) => { + try { + if (req.method() !== 'POST') return; + const url = req.url(); + if (!url.startsWith('https://openrouter.ai/api/v1/chat/completions')) return; + const postData = req.postData(); + if (!postData) return; + const body = JSON.parse(postData); + if (Array.isArray(body.messages)) { + lastMessages = body.messages; + } + } catch {} + }); + page.on('response', async (resp) => { + try { + if (resp.request().method() !== 'POST') return; + const url = resp.url(); + if (!url.startsWith('https://openrouter.ai/api/v1/chat/completions')) return; + + // Non-stream JSON responses only + const ct = resp.headers()['content-type'] || ''; + if (!ct.includes('application/json')) return; + + const data = await resp.json(); + const u = data?.usage; + if (!u) return; + + const p = Number(u.prompt_tokens ?? 0); + const c = Number(u.completion_tokens ?? 0); + const t = Number(u.total_tokens ?? (p + c)); + const cached = Number(u.prompt_tokens_details?.cached_tokens ?? 0); + const created = Number(data.created ?? 0); + + if (u.prompt_tokens_details && typeof u.prompt_tokens_details.cached_tokens !== 'undefined' && u.prompt_tokens_details.cached_tokens !== null) { + cachedTokensSupported = true; + } + + const tokenData: any = { + prompt_tokens: Number.isFinite(p) ? p : 0, + completion_tokens: Number.isFinite(c) ? c : 0, + total_tokens: Number.isFinite(t) ? t : (Number.isFinite(p) && Number.isFinite(c) ? p + c : 0), + cached_tokens: Number.isFinite(cached) ? cached : 0, + timestamp: created > 0 ? new Date(created * 1000).toISOString() : null + }; + + // Capture API response metadata: id, role, content, tool_calls, finish_reason + if (Array.isArray(data.choices) && data.choices.length > 0 && data.choices[0].message) { + const choice = data.choices[0]; + const message = choice.message; + tokenData.id = data.id ?? null; + tokenData.role = message.role ?? null; + if (typeof message.content !== 'undefined') { + tokenData.content = message.content; + } + if (message.tool_calls && Array.isArray(message.tool_calls)) { + tokenData.tool_calls = message.tool_calls; + } + if (typeof choice.finish_reason !== 'undefined') { + tokenData.finish_reason = choice.finish_reason; + } + } + + // Wait for tool execution to complete + await page.waitForTimeout(500); + + // Extract program state from localStorage + try { + const result = await page.evaluate(() => { + const saveData = localStorage.getItem('SAVE_SCRATCH'); + + return { + saveData, + programState: (() => { + if (saveData) { + const match = saveData.match(/\(backup_text"((?:\\.|[^"\\])*)"\)/); + return match ? match[1] : null; + } + return null; + })() + }; + }); + + // Log the localStorage state in Node.js console + console.log('=== COMPLETE LOCALSTORAGE STATE ==='); + console.log('Raw localStorage SAVE_SCRATCH:', result.saveData); + console.log('=== END LOCALSTORAGE STATE ==='); + + const programState = result.programState; + console.log('Extracted program state:', programState); + if (programState) { + // Unescape the JSON string + tokenData.program_state = programState.replace(/\\n/g, '\n').replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + console.log('Final program state:', tokenData.program_state); + } else { + console.log('No program state found in localStorage'); + } + } catch (e) { + // Log localStorage access errors for debugging + console.log('Failed to extract program state:', e); + } + + // Store token data in order of responses + responseTokenData.push(tokenData); + + // Update totals + if (Number.isFinite(p)) tokenTotals.prompt += p; + if (Number.isFinite(c)) tokenTotals.completion += c; + if (Number.isFinite(t)) tokenTotals.total += t; + if (Number.isFinite(cached)) tokenTotals.cached = (tokenTotals.cached ?? 0) + cached; + + // Save last message + if (Array.isArray(data.choices) && data.choices.length > 0 && data.choices[0].message) { + // Log the full message object for debugging + console.log(`Response ${responseTokenData.length - 1}:`, JSON.stringify(data.choices[0].message, null, 2)); + } + } catch { + // ignore parse/streaming issues + } + }); + + const summary = await runCore(page, ctx, { task, apiKey, modelValue, url, outPath, tokenTotals, cachedTokensSupported, lastMessages }); + + // Add token data to agent messages based on order + let agentIndex = 0; + console.log(`Found ${responseTokenData.length} API responses and processing message log with ${summary.message_log.length} messages`); + for (const message of summary.message_log) { + if (message.type === 'agent') { + if (agentIndex < responseTokenData.length) { + const tokenData = responseTokenData[agentIndex]; + console.log(`Associating response ${agentIndex} with agent message ${agentIndex}:`, tokenData); + message.prompt_tokens = tokenData.prompt_tokens; + message.completion_tokens = tokenData.completion_tokens; + message.total_tokens = tokenData.total_tokens; + message.cached_tokens = tokenData.cached_tokens; + message.timestamp = tokenData.timestamp; + if (tokenData.tool_calls) { + message.tool_calls = tokenData.tool_calls; + } + if (tokenData.program_state) { + message.program_state = tokenData.program_state; + } + } else { + console.log(`Warning: No token data found for agent message ${agentIndex}`); + } + agentIndex++; + } + } + + // Rewrite the JSON file with the updated summary that includes token data + fs.writeFileSync(outPath, JSON.stringify(summary, null, 2), 'utf8'); + + // Success: stop trace without saving + await ctx.tracing.stop(); + return { ok: true as const, summary }; + } catch (err) { + // Failure: save trace/screenshot/logs + try { await ctx.tracing.stop({ path: tracePath }); } catch {} + try { await page.screenshot({ path: screenshotPath, fullPage: true }); } catch {} + try { fs.writeFileSync(logsPath, logs.join('\n'), 'utf8'); } catch {} + return { ok: false as const, error: err }; + } finally { + await browser.close().catch(() => {}); + } + })(); + + // Watchdog timeout + const timeout = new Promise<{ ok: false; error: Error }>((resolve) => { + const t = setTimeout(() => { + resolve({ ok: false, error: new Error(`Attempt ${attempt} timed out after ${attemptTimeoutMs}ms`) }); + }, attemptTimeoutMs); + // If work finishes first, clear timeout + work.finally(() => clearTimeout(t)); + }); + + // Race work vs timeout + const result = await Promise.race([work, timeout]); + return result; +} + +// 4) Retry loop with exponential backoff +(async () => { + const outputDir = args.outputDir as string | undefined; + if (!outputDir) { + console.error('Missing output directory. Provide --outputDir .'); + process.exit(1); + } + // ensure dir exists + fs.mkdirSync(outputDir, { recursive: true }); + + const taskPath = args.task as string | undefined; + if (!taskPath) { + console.error('Missing task file. Provide --task .'); + process.exit(1); + } + + const task = loadTask(taskPath); + // CLI overrides + const apiKey = args.apiKey ?? task.apiKey ?? ''; + const modelValue = args.model ?? task.model ?? ''; + const url = args.url ?? task.url ?? ''; + const headless = args.headless !== 'false'; + + if (!apiKey) { + console.error('Missing API key. Provide --apiKey or include in YAML.'); + process.exit(1); + } + if (!modelValue) { + console.error('Missing model value. Provide --model or include in YAML.'); + process.exit(1); + } + if (!url) { + console.error('Missing URL. Provide --url or include in YAML.'); + process.exit(1); + } + + const base = path.basename(taskPath, path.extname(taskPath)); + const outPath = path.join(outputDir, `${base}.json`); + + let lastError: unknown = null; + + for (let attempt = 1; attempt <= (retries + 1); attempt++) { + const res = await runOnce(attempt, { headless, url, outDir: outputDir, outPath, task, apiKey, modelValue }) as RunOnceResult; + if (res.ok) { + // Log summary without the verbose message_log + const { message_log, ...summaryWithoutLog } = res.summary; + console.log(JSON.stringify({ ...summaryWithoutLog, output: outPath, attempts: attempt }, null, 2)); + if (res.summary?.fail > 0) process.exitCode = 2; + return; + } + lastError = res.error; + console.error(`[attempt ${attempt}] failed:`, res.error); + + if (attempt <= retries) { + const backoffMs = Math.min(30_000, 2_000 * Math.pow(2, attempt - 1)); // 2s, 4s, 8s, ... + console.error(`[attempt ${attempt}] retrying in ${backoffMs}ms...`); + await new Promise(r => setTimeout(r, backoffMs)); + } + } + + // All attempts failed + console.error(`All ${retries + 1} attempts failed.`); + if (lastError) console.error(lastError); + process.exit(1); +})(); \ No newline at end of file diff --git a/src/llm-tests/tasks/emojipaint-base.yaml b/src/llm-tests/tasks/emojipaint-base.yaml new file mode 100644 index 0000000000..a90df18956 --- /dev/null +++ b/src/llm-tests/tasks/emojipaint-base.yaml @@ -0,0 +1,144 @@ +initialProgram: | + # EMOJIPAINT MVU # + + type Emoji = String in + type Canvas = [[Emoji]] in + type Row = Int in + type Col = Int in + + type Model = ( + canvas = Canvas, # The 2D grid of emojis # + brush = Emoji, # The currently selected emoji # + palette = [Emoji] # The list of available emojis # + ) in + + type Action = + + SetBrush(Int) # Set the brush using a palette index # + + PaintCell(Row, Col) # Stamp the current emoji at the specified position # + + ClearCell(Row, Col) # Clear the emoji at the specified position # + + ClearCanvas # Clear the entire grid # + + PaintRow(Row) # Fill the specified row with the current emoji # + in + + let model_init: Model = ( + [["","",""],["","",""],["","",""]], # Initial 3x3 empty grid # + "😄", # Initial selected emoji # + ["😄", "😅", "😆", "😉", "😊"] # Available emojis # + ) in + + let setCell: (Canvas, Row, Col, Emoji) -> Canvas = + fun (canvas, row, col, emoji) -> mapi( + canvas, + fun (i, r) -> + if i == row + then mapi( + r, + fun (j, c) -> + if j == col + then emoji + else c) + else r) + in + + let setRow: (Canvas, Row, Emoji) -> Canvas = + fun (canvas, targetRow, emoji) -> + mapi( + canvas, + fun (i, row) -> + if i == targetRow + then map(row, fun _ -> emoji) + else row) + in + + let setAll: (Canvas, Emoji) -> Canvas = + fun (canvas, emoji) -> + map(canvas, fun r -> map(r, fun _ -> emoji)) + in + + let updateGrid: (Model, Canvas -> Canvas) -> Model = + fun (m, f) -> + (f(m.canvas), m.brush, m.palette) + in + + # Update the EmojiPaint app model based on an action # + let update: (Model, Action) -> Model = + ? + in + + let do = fun (init: Model, actions: [Action]) -> + fold_left(actions, update, init) + in +prompt: | + Write the EmojiPaint update function +tests: | + # EMOJIPAINT MVU TESTS # + + test + let (grid, _, _) = update(model_init, PaintCell(0, 0)) in + grid == [["😄","",""],["","",""],["","",""]] + end; + + test + let (grid, _, _) = update(model_init, PaintRow(1)) in + grid == [["","",""],["😄","😄","😄"],["","",""]] + end; + + test + let model = update(model_init, SetBrush(1)) in + let (grid, selectedEmoji, _) = update(model, PaintCell(2, 2)) in + grid == [["","",""],["","",""],["","","😅"]] && selectedEmoji == "😅" + end; + + test + let model = update(model_init, PaintRow(0)) in + let (grid, _, _) = update(model, ClearCell(0, 1)) in + grid == [["😄","","😄"],["","",""],["","",""]] + end; + + test + let model = update(model_init, PaintCell(1, 1)) in + let (grid, _, _) = update(model, ClearCanvas) in + grid == [["","",""],["","",""],["","",""]] + end; + + test + let (_, selectedEmoji, _) = update(model_init, SetBrush(4)) in + let (grid_init, _, emojiList_init) = model_init in + let (grid, _, _) = update((grid_init, selectedEmoji, emojiList_init), PaintCell(1, 2)) in + grid == [["","",""],["","","😊"],["","",""]] + end; + + test + let (_, selectedEmoji, emojiList) = model_init in + let model = update(model_init, PaintRow(2)) in + let (grid, _, _) = update(model, ClearCell(2, 0)) in + grid == [["","",""],["","",""],["","😄","😄"]] + end; + + test + let model = update(model_init, PaintCell(0, 0)) in + let model = update(model, PaintCell(1, 1)) in + let model = update(model, PaintCell(2, 2)) in + let (grid, _, _) = update(model, ClearCanvas) in + grid == [["","",""],["","",""],["","",""]] + end; + + test + let (grid_init, _, emojiList_init) = model_init in + let model = update(model_init, PaintRow(0)) in + let (_, selectedEmoji, _) = update(model, SetBrush(2)) in + let (grid,_,_) = model in + let (grid, _, _) = update((grid, selectedEmoji, emojiList_init), PaintCell(1, 1)) in + grid == [["😄","😄","😄"],["","😆",""],["","",""]] + end; + + test + let model = update(model_init, PaintCell(0, 0)) in + let model = update(model, PaintRow(2)) in + let (grid, _, emojiList) = model in + let model = update(model, SetBrush(3)) in + let model = update(model, PaintCell(1, 1)) in + let model = update(model, ClearCell(2, 2)) in + let (grid, selectedEmoji, _) = model in + grid == [["😄","",""],["","😉",""],["😄","😄",""]] && selectedEmoji == "😉" + end; \ No newline at end of file diff --git a/src/llm-tests/tasks/emojipaint-sample.yaml b/src/llm-tests/tasks/emojipaint-sample.yaml new file mode 100644 index 0000000000..4a7f1a1af9 --- /dev/null +++ b/src/llm-tests/tasks/emojipaint-sample.yaml @@ -0,0 +1,113 @@ +initialProgram: | + # EMOJIPAINT MVU # + + type Emoji = String in + type Canvas = [[Emoji]] in + type Row = Int in + type Col = Int in + + type Model = ( + canvas = Canvas, # The 2D grid of emojis # + brush = Emoji, # The currently selected emoji # + palette = [Emoji] # The list of available emojis # + ) in + + type Action = + + SetBrush(Int) # Set the brush using a palette index # + + PaintCell(Row, Col) # Stamp the current emoji at the specified position # + + ClearCell(Row, Col) # Clear the emoji at the specified position # + + ClearCanvas # Clear the entire grid # + + PaintRow(Row) # Fill the specified row with the current emoji # + in + + let init: Model = ( + # The canvas starts empty # + canvas = [ + ["","",""], + ["","",""], + ["","",""] + ], + # Initial emoji brush # + brush = ":smile:", + # Emoji palette # + palette = [":smile:", ":sweat_smile:", ":laughing:", ":wink:", ":blush:"] + ) in + + let setCell: (Canvas, Row, Col, Emoji) -> Canvas = + fun (canvas, row, col, emoji) -> mapi( + canvas, + fun (i, r) -> + if i == row + then mapi( + r, + fun (j, c) -> + if j == col + then emoji + else c) + else r) + in + + let setRow: (Canvas, Row, Emoji) -> Canvas = + fun (canvas, targetRow, emoji) -> + mapi( + canvas, + fun (i, row) -> + if i == targetRow + then map(row, fun _ -> emoji) + else row) + in + + let setAll: (Canvas, Emoji) -> Canvas = + fun (canvas, emoji) -> + map(canvas, fun r -> map(r, fun _ -> emoji)) + in + + let updateGrid: (Model, Canvas -> Canvas) -> Model = + fun (m, f) -> + (f(m.canvas), m.brush, m.palette) + in + + # Update the EmojiPaint app model based on an action # + let update: (Model, Action) -> Model = + fun (m, action) -> + case action + | SetBrush(emoji) => + (m.canvas, nth(m.palette, emoji), m.palette) + | PaintCell(row, col) => + updateGrid(m, fun c -> setCell(c, row, col, m.brush)) + | ClearCell(row, col) => + updateGrid(m, fun c -> setCell(c, row, col, "")) + | ClearCanvas => + updateGrid(m, fun c -> setAll(c, "")) + | PaintRow(row) => + updateGrid(m, fun c -> setRow(c, row, m.brush)) + end + in + + let do = fun (init: Model, actions: [Action]) -> + fold_left(actions, update, init) + in + + let actions = [ClearCanvas, SetBrush(1), PaintCell(1, 1), SetBrush(3), PaintRow(0)] in + + do(init, actions) +prompt: | + Implement a new action, FillColumn(Col) that fills a column in the grid with a currently-selected emoji +tests: | + test + let (grid, _, _) = update(model_init, FillColumn(0)) in + grid == [["😄","",""],["😄","",""],["😄","",""]] + end; + test + let (grid, _, _) = update(model_init, FillColumn(1)) in + grid == [["","😄",""],["","😄",""],["","😄",""]] + end; + test + let (grid, _, _) = update(model_init, FillColumn(2)) in + grid == [["","","😄"],["","","😄"],["","","😄"]] + end; + test + 1 == 2 + end; + let testfunc = fun n -> ? in + test testfunc(1) == 0 end; \ No newline at end of file diff --git a/src/llm-tests/tasks/test.yaml b/src/llm-tests/tasks/test.yaml new file mode 100644 index 0000000000..8904809d3a --- /dev/null +++ b/src/llm-tests/tasks/test.yaml @@ -0,0 +1,6 @@ +initialProgram: | + let x = 5 in x +prompt: | + Create a varable called y and add it to x +tests: | + test y == 5 end; \ No newline at end of file diff --git a/src/llm-tests/test-results/emojipaint-base-undet.json b/src/llm-tests/test-results/emojipaint-base-undet.json new file mode 100644 index 0000000000..64f1a2c5b5 --- /dev/null +++ b/src/llm-tests/test-results/emojipaint-base-undet.json @@ -0,0 +1,147 @@ +{ + "total": 10, + "pass": 0, + "fail": 0, + "indet": 10, + "model": "openai/gpt-5-mini", + "prompt_tokens": 124554, + "completion_tokens": 3984, + "total_tokens": 128538, + "cached_tokens": 0, + "cached_tokens_supported": false, + "failed_or_indet_tests": [ + "test \n let (grid, _, _) = update(model_init, StampEmoji(0, 0)) in\n grid == [[\"😄\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"]] \nend", + "test\n let (grid, _, _) = update(model_init, FillRow(1)) in\n grid == [[\"\",\"\",\"\"],[\"😄\",\"😄\",\"😄\"],[\"\",\"\",\"\"]] \nend", + "test\n let model = update(model_init, SelectEmoji(\"😅\")) in\n let (grid, selectedEmoji, _) = update(model, StampEmoji(2, 2)) in\n grid == [[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"\",\"😅\"]] && selectedEmoji == \"😅\" \nend", + "test\n let model = update(model_init, FillRow(0)) in\n let (grid, _, _) = update(model, ClearCell(0, 1)) in\n grid == [[\"😄\",\"\",\"😄\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"]] \nend", + "test\n let model = update(model_init, StampEmoji(1, 1)) in\n let (grid, _, _) = update(model, ClearGrid) in\n grid == [[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"]] \nend", + "test\n let (_, selectedEmoji, _) = update(model_init, SelectEmoji(\"😊\")) in\n let (grid_init, _, emojiList_init) = model_init in\n let (grid, _, _) = update((grid_init, selectedEmoji, emojiList_init), StampEmoji(1, 2)) in\n grid == [[\"\",\"\",\"\"],[\"\",\"\",\"😊\"],[\"\",\"\",\"\"]] \nend", + "test\n let (_, selectedEmoji, emojiList) = model_init in\n let model = update(model_init, FillRow(2)) in\n let (grid, _, _) = update(model, ClearCell(2, 0)) in\n grid == [[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"😄\",\"😄\"]] \nend", + "test\n let model = update(model_init, StampEmoji(0, 0)) in\n let model = update(model, StampEmoji(1, 1)) in\n let model = update(model, StampEmoji(2, 2)) in\n let (grid, _, _) = update(model, ClearGrid) in\n grid == [[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"]] \nend", + "test\n let (grid_init, _, emojiList_init) = model_init in\n let model = update(model_init, FillRow(0)) in\n let (_, selectedEmoji, _) = update(model, SelectEmoji(\"😆\")) in\n let (grid,_,_) = model in\n let (grid, _, _) = update((grid, selectedEmoji, emojiList_init), StampEmoji(1, 1)) in\n grid == [[\"😄\",\"😄\",\"😄\"],[\"\",\"😆\",\"\"],[\"\",\"\",\"\"]] \nend", + "test\n let model = update(model_init, StampEmoji(0, 0)) in\n let model = update(model, FillRow(2)) in\n let (grid, _, emojiList) = model in\n let model = update(model, SelectEmoji(\"😉\")) in\n let model = update(model, StampEmoji(1, 1)) in\n let model = update(model, ClearCell(2, 2)) in\n let (grid, selectedEmoji, _) = model in\n grid == [[\"😄\",\"\",\"\"],[\"\",\"😉\",\"\"],[\"😄\",\"😄\",\"\"]] && selectedEmoji == \"😉\" \nend" + ], + "message_log": [ + { + "type": "system-prompt", + "content": " You are an expert agentic AI programming assistant operating in the Hazel programming language. You are working with a user to accomplish a programming task in a paired programming setting. The user will ask you a question or to perform a task (implement a feature, fix an issue, etc). You are a professional coding agent, meaning it is your duty to complete the user's task or attempt to complete their task until you decide the task is complete or it is absolutely infeasible to complete the task. The Hazel programming language is a low-resource programming language, meaning it did not show up in much of your training data, and thus you will be provided with relevant syntax and semantic information of the programming language that you must carefully study and review when generating your responses. NEVER try to write code from another programming language other than Hazel. Whenver writing a comment in Hazel, YOU MUST follow the comment syntax exactly... the syntax is as follows: ```\n # comment #\n ```\n Note that there MUST ALWAYS be an opening and closing hash symbol to enclose the comment. Furthermore, comments cannot span multiple lines. ```# this is an \n invalid comment #``` is invalid (spans multiple lines). ```# This is an invalid comment``` is invalid (no closing hash symbol). ```# This is a valid comment #``` is valid. You may explain and reason about the program/task/user query, but aim to keep your thinking and explanations concise and to the point. If the user wants you to implement a feature that is quite complex, you should break it down into smaller tasks to work through step by step. After calling a tool, you should pick up immediately from where you left off—No need to repeat or summarize what you've been doing. You should avoid explicitly mentioning tool calls to the user. Your conversation with the user should be as natural as possible, as if you were their pair programming partner. We will now provide you with the following:\n 1. A uniquely designed structure-based programming toolkit along with a specification on how to call these tools throughout the attempted completion of the task. This toolkit is newly designed by us Hazel developers, and will require thorough study and review by you to use it effectively.\n 2. Hazel syntax notes.\n 3. A brief summary of Hazel documentation.\n 4. A series of GOLDEN FEW SHOT EXAMPLES from agents who successfully implemented user-requested features using our uniquely designed structure-based programming toolkit.\n You should frequently come back and reference each of the toolkit, syntax notes, documentation, and golden standard examples. Keep your chats brief and concise, briefly communicating with the user your plan-of-action. After making a tool call, pick up immediately from where you left off. That is, do not repeat yourself or try to summarize what you've been doing. You should use markdown to format your text responses, in a way such that the user can easily read and understand your thinking, intentions, and plan-of-action. Available markdown features include:\n 1. bold\n 2. italic\n 3. inline code\n 4. headers\n 5. blockquote\n 6. thematic break\n 7. lists\n 8. links\n You are operating in a structure-based programming environment, akin to a structure editor. every edit state of Hazel maintains a valid Abstract Syntax Tree (AST) representation of the program. We aim to leverage this feature of Hazel and provide you with a toolkit that enables you to navigate, read, and modify the program's structure. On each iteration, you will be provided with the current node in the AST, its parent node, its children nodes, and any static errors present in the program. You are also provided with rich language server information, such as what variables are referenced in the current node's definition. It's important to note that the tools you have belong exclusively to one of three categories:\n 1. Navigation: These tools are used to navigate the AST/program itslef, and move the cursor from one variable/type definition to another, and will never modify the program.\n 2. Read: These tools are used to gather additional information from the program, and do not modify the program or the cursor location in the AST.\n 3. Edit: These tools are used to modify the program. They may move the cursor as a side effect (eg. removing a node will require the cursor to be moved elsewhere, or inserting a new node will move the cursor to that new node).\n You should use the tools to navigate the AST, read information from the program, and modify the program. You should not use the tools to answer questions or provide information to the user. These tools are meant to be strictly structure-based, allowing you to treat the program as a structure editor. If the user asks a question that does not require any editing or understanding of their code, you should not use the tools to answer the question. If the user asks a question that requires understanding their code, but not actually editing it, you should use esclusivley navigation and read tools to understand the code and provide and thoughtful response. If the user asks a complex or ambiguous question, you should ask for and seek clarification first before calling any tools. These tools are meant to be fairly atomic, and you are expected to make many tool calls in order to traverse the AST, read and undestand the code, and finally, complete the user's task! * You might see ⋱ after some definitions. This is a special character that indicates a \"fold\" in the program. It is a critical feature here, as it aims to prevent information overload... In this agentic setting, we abstract away child let bindings' definitions behind these folds, thus replacing them with ⋱. You should recognize that these are not actual characters within the program, but rather \"folds\" which hide away the details of child defintions and prevent an overload of information. * It is likely you'll need to understand the code in order to answer a user's question or to make an edit. In any case, you should use navigation tools and read tools to view the relevant parts and defintions (let expressions) of the program as necessary. Terminology Used in this Toolkit:\n * \"pattern\" - the tiles between the \"let\" and \"=\" delimiters, or the \"type\" and \"=\" delimiters, exclusive\n * \"definition\" - the tiles between the \"=\" and \"in\" delimiters, exclusive\n * \"body\" - the tiles after the \"in\" delimiter, exclusive\n * \"binding clause\" - the tiles between the \"let\" and \"in\" delimiters, or the \"type\" and \"in\" delimiters, inclusive\n Another note—when writing code, you should use line breaks to neatly format the code. NEVER try to indent code, as Hazel's formatter will automatically indent and format the code for you wherever you insert line breaks. NEVER write code without using line breaks when line breaks are highly necessary. - Hazel uses C-style function application syntax, with parenthesis around comma-separated arguments - Function application is ALWAYS written using parentheses and commas: use 'function(arg1, arg2)'. DO NOT just use spaces between the function name and arguments. - Function parameters are ALWAYS commas separated: 'fun arg1, arg2 -> '. DO NOT use spaces to separate function arguments. - There is no dot accessor notation for tuples; DO NOT use tuple.field. use pattern matching for destructuring: let (field, _) = tuple in ... - The following ARE NOT Hazel keywords. DO NOT use these keywords: switch, with, of, rec. ALWAYS omit these keywords - Pattern matching is ALWAYS written a 'case ... end' expression. Cases MUST END in an 'end' keyword. DO NOT USE any other keyword besides 'case' to do pattern matching. DO NOT USE a 'with' or 'of' keyword with 'case', just start the list of rules. Pattern matching rules use syntax '| pattern => expression'. Note the '=>' arrow. - The ONLY way to define a named function is by using a function expression nested in a let expression like 'let = fun -> in SYSTEM CORE (always prepend)\n\n\nRole & Objective\n----------------\nYou are a compositional AI coding agent working exclusively in Hazel. Your goal is to read,\nnavigate, and edit a single Hazel program to implement user-requested changes safely and\nincrementally, using the provided structure-level tools only. Always preserve a well-formed AST,\nprefer small, reversible steps, and keep your replies brief and actionable.\n\nNon-negotiables\n---------------\n- Hazel only. Never emit code from other languages.\n- Prefer structure-aware changes (bindings, patterns, bodies) over textual rewriting.\n- Formatting: Use line breaks for layout; do not hand-indent. Hazel's formatter handles indentation.\n- Never use the 'rec' keyword for recursion. Recursion is inferred by Hazel.\n- If you rename a binding, use `update_pattern` (it potentially updates use-sites, assuming arity matches).\n- Use `initialize` only on a program with no `let`/`type` bindings (fresh start). Your changes will be rejected otherwise.\n- Use `update_binding_clause` when you must rewrite the entire `let … in` (`let`+pattern+`=`+definition+`in`)\n without a final body. This effectively allows you to edit both the pattern and deinition at once. It also\n allows you to redefine the current binding while simultaneously adding any number of new bindings.\n- Never fabricate unavailable tools or external files.\n- Respond to the user with: plan → action(s) → result/next step. Keep it short. If more information\n from the user is needed, ask for it. Make checklists/plans/todo lists whenever necessary.\n\nDesign Rationale (for you)\n--------------------------\nStructure-level actions reduce line-based fragility, align with Hazel's always-well-formed AST\nstates, and help the model reason over program semantics, not arbitrary line windows. Error rounds\nhave diminishing returns—favor precise edits and targeted navigation.\n\n\nOPERATING PROTOCOL (R→P→A→C loop)\n\n\nR — Read\n--------\n- Mentally summarize the task and current cursor position.\n- When needed, navigate to the relevant binding(s) (`go_to_child`, `go_to_sibling`, `go_to_parent`,\n `go_to_binding_site`). Absorb the information you need to know about the binding(s) into your mental model.\n- If details are folded (⋱), use `view_entire_definition` at the target to see the whole definition when\n necessary. If you are not interested in the details (eg. just navigating through the program, or already know you need to rewrite things), you can skip this step.\n\nP — Plan\n--------\n- Decompose into small structural edits: introduce binding, change pattern, change definition,\n change body, or rewrite a binding clause.\n- Prefer a single clear edit per tool call; sequence them.\n- Add types where they clarify intent.\n\nA — Act\n-------\n- Execute the minimal tool(s) needed: `insert_before/after`, `update_*`, `delete_*`.\n- When adding multi-binding scaffolds, use `insert_before` or `update_binding_clause`.\n- When needing to change the body of a binding, use `update_body`. Note that this tool call\n can potentially replace a lot of code (you should usually use it on the tail binding of sequence of sibling bindings/nodes).\n\nC — Check\n---------\n- Re-open or re-navigate and scan the updated binding with `view_entire_definition` if necessary.\n- If subsequent fixes are required, repeat R→P→A→C.\n- Stop when the requested behavior is implemented or a blocking ambiguity exists.\n\n\nHAZEL CHEAT-SHEET (compact, high-yield)\n\n\nBindings & Body\n---------------\nlet [ : Type ] = in\n\n ``` - Pattern: between `let` and `=` - Definition: between `=` and `in` - Body: after `in` - Holes: `?` (empty), non-empty error hole by typing mismatches\nFunctions\nlet f = fun x -> ... in or let f : A -> B = fun x -> ... in\n\nCase\ncase xs\n| [] => ...\n| hd::tl => ...\nend\nLists\n[], 1::2::[], [1,2,3]\n\nADTs\ntype Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in\nPolymorphism\nkotlin\n\nlet id : forall A -> A -> A = typfun A -> fun x:A -> x in\nFormatting Rule\nUse line breaks generously; do not hand-indent.\n\n\nTOOLBOX SUMMARY (only these tools exist)\n\n\nNavigation (no edits)\ngo_to_parent()\n\ngo_to_child(name, index?)\n\ngo_to_sibling(name, index?)\n\ngo_to_binding_site(name, index?) — jump from a variable use to its binding\n\nview_entire_definition() — expand folded content at cursor. has potential to reveal a lot of information about the binding which\n can quickly fill contexts and potentially muffle semantic information. Use with caution.\n\nEditing\ninitialize(code) — replace/initialize the whole program ONLY if no let/type exist yet. You will be rejected otherwise.\n\ninsert_before(code) / insert_after(code) — add new binding(s) adjacent to current node;\ncursor moves to newest inserted binding\n\nupdate_pattern(code) — rename/change pattern; updates all use-sites if atom names match in count\n\nupdate_definition(code) — replace only the definition (right of =)\n\nupdate_body(code) — replace the body (after in)\n\nupdate_binding_clause(code) — replace the entire let … in header (pattern + definition + in),\nno trailing body\n\ndelete_binding_clause() — remove the whole let … in binding; cursor relocates per rules\n\ndelete_body() — remove the current node's body (turns into a hole/unfinished body)\n\nWhen to choose what\nIntroduce new helper(s) or function(s) or variable(s) etc.: insert_before/after\n\nRename a variable (and propagate): update_pattern\n\nChange computation for a binding: update_definition\n\nChange the program result or subsequent code: update_body\n\nSplit/merge binding headers or change type annotations & structure: update_binding_clause\n\nClean up: delete_binding_clause / delete_body\n\n\nFAILURE & SAFETY\n\n\n\nIf a tool is disallowed in the current state (e.g., initialize when bindings exist),\nchoose the nearest structural alternative (e.g., update_binding_clause to seed scaffolding,\nthen update_body).\n\nIf symbol ambiguity arises (e.g., multiple same-name bindings), prefer go_to_* with an index\nto disambiguate, then rename with update_pattern.\n\nIf an edit cascades unexpected references, immediately inspect with view_entire_definition\nand correct with a follow-up update_definition or update_pattern.\n\nKeep each action atomic and explain briefly what you did and what you will do next.\n\n\nNotes for future engineers (human readers)\n\n\n\nThe examples emphasize atomic structure edits, minimal deltas, and short confirmations.\n\nPrefer structuring multi-step tasks as sequences of navigation → single edit → check\nto improve stability and interpretability.\n\nIf show-references becomes available later, introduce it in the Read phase, but do not\nassume it exists now (the tool list in this pack is authoritative).\n\nThis pack assumes Hazel's invariants (always meaningful AST states with holes) and\njustifies structure-level actions accordingly.\n - Hazel is a live functional programming environment that supports incomplete programs via typed holes. - Typed holes, denoted as `?`, can stand for missing expressions or types. - Programs with holes still carry meaningful static and dynamic semantics, unlike traditional languages. - Hazel uses a structure editor that maintains well-formed syntax trees with holes instead of raw text. - Example: Define a function with an unknown type and evaluate it. ``` let f: ? -> ? = fun b -> b && true in f(true); ``` - This function runs successfully even though the type is unknown. - Hazel uses **bidirectional typing**, where types can be either synthesized or analyzed. - Example: Bidirectional typing for boolean functions. ``` let f = fun b: Bool -> b && true in f(true); ``` - Holes can be used in types, parameters, and patterns. ``` let f: (Int, ?) -> Int = fun a, b -> a + 1 in f(1, 2); ``` - Hazel also supports polymorphic functions. ``` let poly_id: forall a -> a -> a = typfun a -> fun x: a -> x; ``` - Polymorphic functions can be applied with type arguments using `@` syntax. ``` poly_id@(5); ``` - Hazel supports defining algebraic data types (ADTs) and using them in pattern matches. ``` type Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in let subst: (Exp, String, Exp) -> Exp = fun v, name, e -> case e | Var(n) => if n$==name then v else e | Lam(x, body) => Lam(x, subst(v, name, body)) | Ap(e1, e2) => Ap(subst(v, name, e1), subst(v, name, e2)) end; ``` - Hazel permits incomplete pattern matches and treats them semantically. ``` let f = fun x -> case x | 0 => \"zero\" | ? => \"non-zero\" end; ``` - Evaluation proceeds until it hits a hole, then stops gracefully. - Example of evaluation with error halted inside a hole: ``` let f: Int -> Int = fun x -> x + true in f(1); ``` - The erroneous addition will be wrapped in a non-empty hole, not crash. - Hazel's recursive types allow expressive structures. ``` type MyList = rec A -> (Nil + Cons(Int, A)) in let x: MyList = Cons(1, Cons(2, Cons(3, Nil))); ``` - Hazel supports list processing via pattern matching. ``` let length: [Int] -> Int = fun xs -> case xs | [] => 0 | hd::tl => 1 + length(tl) end; ``` - Hazel allows mutual recursion via let-bindings of tuples. ``` let (even: Int -> Bool, odd: Int -> Bool) = (fun n -> if n == 0 then true else odd(n - 1), fun n -> if n == 0 then false else even(n - 1)) in even(4); ``` - Projectors and livelits provide visual editing tools in Hazel's GUI. - Hazel allows tests to be included inline. ``` test 2 + 2 == 4 end; ``` - Hazel supports explaining sub-expressions via cursor-based documentation. - Hazel is ideal for education, as no edit state is meaningless. - All code edits are type-preserving and guided by Hazel's edit action model. - An important note is that comments in Hazel are enclosed by `#` symbols. Eg `# This is a comment #`. The program WILL be erroneous if you do not include an ending '#' symbol for a comment. " + }, + { + "type": "user", + "content": "Write the EmojiPaint update function" + }, + { + "type": "system-prompt", + "content": "\nCurrent node: do\nPath to node: do\nSibling nodes: [Emoji (index: 0), Canvas (index: 1), Row (index: 2), Col (index: 3), Model (index: 4), Action (index: 5), model_init (index: 6), setCell (index: 7), setRow (index: 8), setAll (index: 9), updateGrid (index: 10), update (index: 11)]\nChild nodes: []\n\n\nDefinition of \"do\"'s parent (no parent, displaying entire top level of the program)\":\n```\n# EMOJIPAINT MVU #\n\ntype Emoji = String in \ntype Canvas = [[Emoji]] in \ntype Row = Int in \ntype Col = Int in \n\ntype Model = (\n canvas = Canvas, # The 2D grid of emojis #\n brush = Emoji, # The currently selected emoji #\n palette = [Emoji] # The list of available emojis #\n) in\n\ntype Action =\n + SetBrush(Int) # Set the brush using a palette index #\n + PaintCell(Row, Col) # Stamp the current emoji at the specified position #\n + ClearCell(Row, Col) # Clear the emoji at the specified position #\n + ClearCanvas # Clear the entire grid #\n + PaintRow(Row) # Fill the specified row with the current emoji #\nin\n\nlet model_init: Model = ⋱ in\n \n let setCell: (Canvas, Row, Col, Emoji) -> Canvas =\n ⋱ \nin\n\nlet setRow: (Canvas, Row, Emoji) -> Canvas =\n ⋱ \n in\n \n let setAll: (Canvas, Emoji) -> Canvas =\n ⋱ \n in\n \n let updateGrid: (Model, Canvas -> Canvas) -> Model =\n ⋱ \n in\n \n# Update the EmojiPaint app model based on an action #\nlet update: (Model, Action) -> Model =\n⋱\n in\n \n let do = fun (init: Model, actions: [Action]) ->\n fold_left(actions, update, init) \n in\n ?\n```\nVariables referenced: [update (Index: 0)]\n\nNo static errors found in the program.\n" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"update\", 11)▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"setCell\", 7)▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: view_entire_definition▼ Show more" + }, + { + "type": "system-prompt", + "content": "let setCell: (Canvas, Row, Col, Emoji) -> Canvas =\n fun (canvas, row, col, emoji) -> mapi(\n canvas, \n fun (i, r) ->\n if i == row \n then mapi(\n r,\n fun (j, c) ->\n if j == col \n then emoji \n else c) \n else r) \nin" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"setRow\", 7)▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"setAll\", 8)▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"updateGrid\", 9)▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: view_entire_definition▼ Show more" + }, + { + "type": "system-prompt", + "content": "let updateGrid: (Model, Canvas -> Canvas) -> Model =\n fun (m, f) ->\n (f(m.canvas), m.brush, m.palette) \nin" + }, + { + "type": "tool", + "content": "Agent called tool: update_definition(\"fun (m, action) ->\n case action\n | SetBrush(i) => (m.canvas, nth(m.palette, i), m.palette)\n | PaintCell(r, c) => updateGrid(m, fun canvas -> setCell(canvas, r, c, m.brush))\n | ClearCell(r, c) => updateGrid(m, fun canvas -> setCell(canvas, r, c, \"\"))\n | ClearCanvas => updateGrid(m, fun canvas -> setAll(canvas, \"\"))\n | PaintRow(r) => updateGrid(m, fun canvas -> setRow(canvas, r, m.brush))\n end\")▼ Show more" + }, + { + "type": "system-error", + "content": "Not applying the action you requested as it would have the following static error(s): Error in term:\n ClearCanvas\nNature of error: Expecting type Canvas -> Canvas but got inconsistent type Action, Error in term:\n ClearCell\nNature of error: Expecting type -> Canvas -> Canvas but got inconsistent type Row, Col -> Action, Error in term:\n PaintCell\nNature of error: Expecting type -> Canvas -> Canvas but got inconsistent type Row, Col -> Action, Error in term:\n PaintRow\nNature of error: Expecting type -> Canvas -> Canvas but got inconsistent type Row -> Action, Error in term:\n SetBrush\nNature of error: Expecting type -> Canvas -> Canvas but got inconsistent type Int -> Action▲ Show less" + }, + { + "type": "tool", + "content": "Agent called tool: view_entire_definition▼ Show more" + }, + { + "type": "system-prompt", + "content": "let updateGrid: (Model, Canvas -> Canvas) -> Model =\n fun (m, f) ->\n (f(m.canvas), m.brush, m.palette) \nin" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"update\", 10)▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: update_definition(\"fun (m, action) ->\n case action\n | SetBrush(i) => (m.canvas, nth(m.palette, i), m.palette)\n | PaintCell(r, c) => updateGrid(m, fun canvas -> setCell(canvas, r, c, m.brush))\n | ClearCell(r, c) => updateGrid(m, fun canvas -> setCell(canvas, r, c, \"\"))\n | ClearCanvas => updateGrid(m, fun canvas -> setAll(canvas, \"\"))\n | PaintRow(r) => updateGrid(m, fun canvas -> setRow(canvas, r, m.brush))\n end\")▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "agent", + "content": "Plan\n- Implement the `update` function to handle every `Action` by using the existing helpers `setCell`, `setRow`, `setAll`, and `updateGrid`. Keep updates immutable by returning a new `Model`.\n\nActions taken\n- Implemented `update` to:\n - Change brush via `SetBrush(i)` using `nth(m.palette, i)`.\n - Paint / clear a cell via `setCell`.\n - Clear whole canvas via `setAll`.\n - Paint a row via `setRow`.\n - Wrap canvas-transforming functions with `updateGrid` to produce a new `Model`.\n\nResult (the Hazel implementation)\n```\nlet update: (Model, Action) -> Model =\nfun (m, action) ->\ncase action\n| SetBrush(i) => (m.canvas, nth(m.palette, i), m.palette)\n| PaintCell(r, c) => updateGrid(m, fun canvas -> setCell(canvas, r, c, m.brush))\n| ClearCell(r, c) => updateGrid(m, fun canvas -> setCell(canvas, r, c, \"\"))\n| ClearCanvas => updateGrid(m, fun canvas -> setAll(canvas, \"\"))\n| PaintRow(r) => updateGrid(m, fun canvas -> setRow(canvas, r, m.brush))\nend\n```\n\nNext step\n- Run the app or tests to confirm behavior. Want me to add bounds-checking for `nth(m.palette, i)` (safe fallback) or to wire this into the UI event handlers?▲ Show less", + "prompt_tokens": 7356, + "completion_tokens": 346, + "total_tokens": 7702, + "cached_tokens": 0, + "timestamp": "2025-11-03T19:40:52.000Z", + "tool_calls": [ + { + "id": "call_oAwkqGx0PgFleK6OqQ49XTIw", + "index": 0, + "type": "function", + "function": { + "name": "go_to_sibling", + "arguments": "{\"name\":\"update\",\"index\":11}" + } + } + ], + "program_state": "# EMOJIPAINT MVU #\n\ntype Emoji = String in \ntype Canvas = [[Emoji]] in \ntype Row = Int in \ntype Col = Int in \n\ntype Model = (\ncanvas = Canvas, # The 2D grid of emojis #\nbrush = Emoji, # The currently selected emoji #\npalette = [Emoji] # The list of available emojis #\n) in\n\ntype Action =\n+ SetBrush(Int) # Set the brush using a palette index #\n+ PaintCell(Row, Col) # Stamp the current emoji at the specified position #\n+ ClearCell(Row, Col) # Clear the emoji at the specified position #\n+ ClearCanvas # Clear the entire grid #\n+ PaintRow(Row) # Fill the specified row with the current emoji #\nin\n\nlet model_init: Model = (\n[[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"]], # Initial 3x3 empty grid #\n\"\\240\\159\\152\\132\", # Initial selected emoji #\n[\"\\240\\159\\152\\132\", \"\\240\\159\\152\\133\", \"\\240\\159\\152\\134\", \"\\240\\159\\152\\137\", \"\\240\\159\\152\\138\"] # Available emojis #\n) in\n\nlet setCell: (Canvas, Row, Col, Emoji) -> Canvas =\nfun (canvas, row, col, emoji) -> mapi(\ncanvas, \nfun (i, r) ->\nif i == row \nthen mapi(\nr,\nfun (j, c) ->\nif j == col \nthen emoji \nelse c) \nelse r) \nin\n\nlet setRow: (Canvas, Row, Emoji) -> Canvas =\nfun (canvas, targetRow, emoji) ->\nmapi(\ncanvas,\nfun (i, row) ->\nif i == targetRow \nthen map(row, fun _ -> emoji) \nelse row) \nin\n\nlet setAll: (Canvas, Emoji) -> Canvas =\nfun (canvas, emoji) ->\nmap(canvas, fun r -> map(r, fun _ -> emoji)) \nin\n\nlet updateGrid: (Model, Canvas -> Canvas) -> Model =\nfun (m, f) ->\n(f(m.canvas), m.brush, m.palette) \nin\n\n# Update the EmojiPaint app model based on an action #\nlet update: (Model, Action) -> Model =\n?\nin\n\nlet do = fun (init: Model, actions: [Action]) ->\nfold_left(actions, update, init) \nin\n" + } + ] +} \ No newline at end of file diff --git a/src/llm-tests/test-results/emojipaint-base.json b/src/llm-tests/test-results/emojipaint-base.json new file mode 100644 index 0000000000..1467ce1876 --- /dev/null +++ b/src/llm-tests/test-results/emojipaint-base.json @@ -0,0 +1,136 @@ +{ + "total": 10, + "pass": 10, + "fail": 0, + "indet": 0, + "model": "openai/gpt-5-mini", + "prompt_tokens": 103265, + "completion_tokens": 6707, + "total_tokens": 109972, + "cached_tokens": 0, + "cached_tokens_supported": false, + "failed_or_indet_tests": [], + "message_log": [ + { + "type": "system-prompt", + "content": " You are an expert agentic AI programming assistant operating in the Hazel programming language. You are working with a user to accomplish a programming task in a paired programming setting. The user will ask you a question or to perform a task (implement a feature, fix an issue, etc). You are a professional coding agent, meaning it is your duty to complete the user's task or attempt to complete their task until you decide the task is complete or it is absolutely infeasible to complete the task. The Hazel programming language is a low-resource programming language, meaning it did not show up in much of your training data, and thus you will be provided with relevant syntax and semantic information of the programming language that you must carefully study and review when generating your responses. NEVER try to write code from another programming language other than Hazel. Whenver writing a comment in Hazel, YOU MUST follow the comment syntax exactly... the syntax is as follows: ```\n # comment #\n ```\n Note that there MUST ALWAYS be an opening and closing hash symbol to enclose the comment. Furthermore, comments cannot span multiple lines. ```# this is an \n invalid comment #``` is invalid (spans multiple lines). ```# This is an invalid comment``` is invalid (no closing hash symbol). ```# This is a valid comment #``` is valid. You may explain and reason about the program/task/user query, but aim to keep your thinking and explanations concise and to the point. If the user wants you to implement a feature that is quite complex, you should break it down into smaller tasks to work through step by step. After calling a tool, you should pick up immediately from where you left off—No need to repeat or summarize what you've been doing. You should avoid explicitly mentioning tool calls to the user. Your conversation with the user should be as natural as possible, as if you were their pair programming partner. We will now provide you with the following:\n 1. A uniquely designed structure-based programming toolkit along with a specification on how to call these tools throughout the attempted completion of the task. This toolkit is newly designed by us Hazel developers, and will require thorough study and review by you to use it effectively.\n 2. Hazel syntax notes.\n 3. A brief summary of Hazel documentation.\n 4. A series of GOLDEN FEW SHOT EXAMPLES from agents who successfully implemented user-requested features using our uniquely designed structure-based programming toolkit.\n You should frequently come back and reference each of the toolkit, syntax notes, documentation, and golden standard examples. Keep your chats brief and concise, briefly communicating with the user your plan-of-action. After making a tool call, pick up immediately from where you left off. That is, do not repeat yourself or try to summarize what you've been doing. You should use markdown to format your text responses, in a way such that the user can easily read and understand your thinking, intentions, and plan-of-action. Available markdown features include:\n 1. bold\n 2. italic\n 3. inline code\n 4. headers\n 5. blockquote\n 6. thematic break\n 7. lists\n 8. links\n You are operating in a structure-based programming environment, akin to a structure editor. every edit state of Hazel maintains a valid Abstract Syntax Tree (AST) representation of the program. We aim to leverage this feature of Hazel and provide you with a toolkit that enables you to navigate, read, and modify the program's structure. On each iteration, you will be provided with the current node in the AST, its parent node, its children nodes, and any static errors present in the program. You are also provided with rich language server information, such as what variables are referenced in the current node's definition. It's important to note that the tools you have belong exclusively to one of three categories:\n 1. Navigation: These tools are used to navigate the AST/program itslef, and move the cursor from one variable/type definition to another, and will never modify the program.\n 2. Read: These tools are used to gather additional information from the program, and do not modify the program or the cursor location in the AST.\n 3. Edit: These tools are used to modify the program. They may move the cursor as a side effect (eg. removing a node will require the cursor to be moved elsewhere, or inserting a new node will move the cursor to that new node).\n You should use the tools to navigate the AST, read information from the program, and modify the program. You should not use the tools to answer questions or provide information to the user. These tools are meant to be strictly structure-based, allowing you to treat the program as a structure editor. If the user asks a question that does not require any editing or understanding of their code, you should not use the tools to answer the question. If the user asks a question that requires understanding their code, but not actually editing it, you should use esclusivley navigation and read tools to understand the code and provide and thoughtful response. If the user asks a complex or ambiguous question, you should ask for and seek clarification first before calling any tools. These tools are meant to be fairly atomic, and you are expected to make many tool calls in order to traverse the AST, read and undestand the code, and finally, complete the user's task! * You might see ⋱ after some definitions. This is a special character that indicates a \"fold\" in the program. It is a critical feature here, as it aims to prevent information overload... In this agentic setting, we abstract away child let bindings' definitions behind these folds, thus replacing them with ⋱. You should recognize that these are not actual characters within the program, but rather \"folds\" which hide away the details of child defintions and prevent an overload of information. * It is likely you'll need to understand the code in order to answer a user's question or to make an edit. In any case, you should use navigation tools and read tools to view the relevant parts and defintions (let expressions) of the program as necessary. Terminology Used in this Toolkit:\n * \"pattern\" - the tiles between the \"let\" and \"=\" delimiters, or the \"type\" and \"=\" delimiters, exclusive\n * \"definition\" - the tiles between the \"=\" and \"in\" delimiters, exclusive\n * \"body\" - the tiles after the \"in\" delimiter, exclusive\n * \"binding clause\" - the tiles between the \"let\" and \"in\" delimiters, or the \"type\" and \"in\" delimiters, inclusive\n Another note—when writing code, you should use line breaks to neatly format the code. NEVER try to indent code, as Hazel's formatter will automatically indent and format the code for you wherever you insert line breaks. NEVER write code without using line breaks when line breaks are highly necessary. - Hazel uses C-style function application syntax, with parenthesis around comma-separated arguments - Function application is ALWAYS written using parentheses and commas: use 'function(arg1, arg2)'. DO NOT just use spaces between the function name and arguments. - Function parameters are ALWAYS commas separated: 'fun arg1, arg2 -> '. DO NOT use spaces to separate function arguments. - There is no dot accessor notation for tuples; DO NOT use tuple.field. use pattern matching for destructuring: let (field, _) = tuple in ... - The following ARE NOT Hazel keywords. DO NOT use these keywords: switch, with, of, rec. ALWAYS omit these keywords - Pattern matching is ALWAYS written a 'case ... end' expression. Cases MUST END in an 'end' keyword. DO NOT USE any other keyword besides 'case' to do pattern matching. DO NOT USE a 'with' or 'of' keyword with 'case', just start the list of rules. Pattern matching rules use syntax '| pattern => expression'. Note the '=>' arrow. - The ONLY way to define a named function is by using a function expression nested in a let expression like 'let = fun -> in SYSTEM CORE (always prepend)\n\n\nRole & Objective\n----------------\nYou are a compositional AI coding agent working exclusively in Hazel. Your goal is to read,\nnavigate, and edit a single Hazel program to implement user-requested changes safely and\nincrementally, using the provided structure-level tools only. Always preserve a well-formed AST,\nprefer small, reversible steps, and keep your replies brief and actionable.\n\nNon-negotiables\n---------------\n- Hazel only. Never emit code from other languages.\n- Prefer structure-aware changes (bindings, patterns, bodies) over textual rewriting.\n- Formatting: Use line breaks for layout; do not hand-indent. Hazel's formatter handles indentation.\n- Never use the 'rec' keyword for recursion. Recursion is inferred by Hazel.\n- If you rename a binding, use `update_pattern` (it potentially updates use-sites, assuming arity matches).\n- Use `initialize` only on a program with no `let`/`type` bindings (fresh start). Your changes will be rejected otherwise.\n- Use `update_binding_clause` when you must rewrite the entire `let … in` (`let`+pattern+`=`+definition+`in`)\n without a final body. This effectively allows you to edit both the pattern and deinition at once. It also\n allows you to redefine the current binding while simultaneously adding any number of new bindings.\n- Never fabricate unavailable tools or external files.\n- Respond to the user with: plan → action(s) → result/next step. Keep it short. If more information\n from the user is needed, ask for it. Make checklists/plans/todo lists whenever necessary.\n\nDesign Rationale (for you)\n--------------------------\nStructure-level actions reduce line-based fragility, align with Hazel's always-well-formed AST\nstates, and help the model reason over program semantics, not arbitrary line windows. Error rounds\nhave diminishing returns—favor precise edits and targeted navigation.\n\n\nOPERATING PROTOCOL (R→P→A→C loop)\n\n\nR — Read\n--------\n- Mentally summarize the task and current cursor position.\n- When needed, navigate to the relevant binding(s) (`go_to_child`, `go_to_sibling`, `go_to_parent`,\n `go_to_binding_site`). Absorb the information you need to know about the binding(s) into your mental model.\n- If details are folded (⋱), use `view_entire_definition` at the target to see the whole definition when\n necessary. If you are not interested in the details (eg. just navigating through the program, or already know you need to rewrite things), you can skip this step.\n\nP — Plan\n--------\n- Decompose into small structural edits: introduce binding, change pattern, change definition,\n change body, or rewrite a binding clause.\n- Prefer a single clear edit per tool call; sequence them.\n- Add types where they clarify intent.\n\nA — Act\n-------\n- Execute the minimal tool(s) needed: `insert_before/after`, `update_*`, `delete_*`.\n- When adding multi-binding scaffolds, use `insert_before` or `update_binding_clause`.\n- When needing to change the body of a binding, use `update_body`. Note that this tool call\n can potentially replace a lot of code (you should usually use it on the tail binding of sequence of sibling bindings/nodes).\n\nC — Check\n---------\n- Re-open or re-navigate and scan the updated binding with `view_entire_definition` if necessary.\n- If subsequent fixes are required, repeat R→P→A→C.\n- Stop when the requested behavior is implemented or a blocking ambiguity exists.\n\n\nHAZEL CHEAT-SHEET (compact, high-yield)\n\n\nBindings & Body\n---------------\nlet [ : Type ] = in\n\n ``` - Pattern: between `let` and `=` - Definition: between `=` and `in` - Body: after `in` - Holes: `?` (empty), non-empty error hole by typing mismatches\nFunctions\nlet f = fun x -> ... in or let f : A -> B = fun x -> ... in\n\nCase\ncase xs\n| [] => ...\n| hd::tl => ...\nend\nLists\n[], 1::2::[], [1,2,3]\n\nADTs\ntype Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in\nPolymorphism\nkotlin\n\nlet id : forall A -> A -> A = typfun A -> fun x:A -> x in\nFormatting Rule\nUse line breaks generously; do not hand-indent.\n\n\nTOOLBOX SUMMARY (only these tools exist)\n\n\nNavigation (no edits)\ngo_to_parent()\n\ngo_to_child(name, index?)\n\ngo_to_sibling(name, index?)\n\ngo_to_binding_site(name, index?) — jump from a variable use to its binding\n\nview_entire_definition() — expand folded content at cursor. has potential to reveal a lot of information about the binding which\n can quickly fill contexts and potentially muffle semantic information. Use with caution.\n\nEditing\ninitialize(code) — replace/initialize the whole program ONLY if no let/type exist yet. You will be rejected otherwise.\n\ninsert_before(code) / insert_after(code) — add new binding(s) adjacent to current node;\ncursor moves to newest inserted binding\n\nupdate_pattern(code) — rename/change pattern; updates all use-sites if atom names match in count\n\nupdate_definition(code) — replace only the definition (right of =)\n\nupdate_body(code) — replace the body (after in)\n\nupdate_binding_clause(code) — replace the entire let … in header (pattern + definition + in),\nno trailing body\n\ndelete_binding_clause() — remove the whole let … in binding; cursor relocates per rules\n\ndelete_body() — remove the current node's body (turns into a hole/unfinished body)\n\nWhen to choose what\nIntroduce new helper(s) or function(s) or variable(s) etc.: insert_before/after\n\nRename a variable (and propagate): update_pattern\n\nChange computation for a binding: update_definition\n\nChange the program result or subsequent code: update_body\n\nSplit/merge binding headers or change type annotations & structure: update_binding_clause\n\nClean up: delete_binding_clause / delete_body\n\n\nFAILURE & SAFETY\n\n\n\nIf a tool is disallowed in the current state (e.g., initialize when bindings exist),\nchoose the nearest structural alternative (e.g., update_binding_clause to seed scaffolding,\nthen update_body).\n\nIf symbol ambiguity arises (e.g., multiple same-name bindings), prefer go_to_* with an index\nto disambiguate, then rename with update_pattern.\n\nIf an edit cascades unexpected references, immediately inspect with view_entire_definition\nand correct with a follow-up update_definition or update_pattern.\n\nKeep each action atomic and explain briefly what you did and what you will do next.\n\n\nNotes for future engineers (human readers)\n\n\n\nThe examples emphasize atomic structure edits, minimal deltas, and short confirmations.\n\nPrefer structuring multi-step tasks as sequences of navigation → single edit → check\nto improve stability and interpretability.\n\nIf show-references becomes available later, introduce it in the Read phase, but do not\nassume it exists now (the tool list in this pack is authoritative).\n\nThis pack assumes Hazel's invariants (always meaningful AST states with holes) and\njustifies structure-level actions accordingly.\n - Hazel is a live functional programming environment that supports incomplete programs via typed holes. - Typed holes, denoted as `?`, can stand for missing expressions or types. - Programs with holes still carry meaningful static and dynamic semantics, unlike traditional languages. - Hazel uses a structure editor that maintains well-formed syntax trees with holes instead of raw text. - Example: Define a function with an unknown type and evaluate it. ``` let f: ? -> ? = fun b -> b && true in f(true); ``` - This function runs successfully even though the type is unknown. - Hazel uses **bidirectional typing**, where types can be either synthesized or analyzed. - Example: Bidirectional typing for boolean functions. ``` let f = fun b: Bool -> b && true in f(true); ``` - Holes can be used in types, parameters, and patterns. ``` let f: (Int, ?) -> Int = fun a, b -> a + 1 in f(1, 2); ``` - Hazel also supports polymorphic functions. ``` let poly_id: forall a -> a -> a = typfun a -> fun x: a -> x; ``` - Polymorphic functions can be applied with type arguments using `@` syntax. ``` poly_id@(5); ``` - Hazel supports defining algebraic data types (ADTs) and using them in pattern matches. ``` type Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in let subst: (Exp, String, Exp) -> Exp = fun v, name, e -> case e | Var(n) => if n$==name then v else e | Lam(x, body) => Lam(x, subst(v, name, body)) | Ap(e1, e2) => Ap(subst(v, name, e1), subst(v, name, e2)) end; ``` - Hazel permits incomplete pattern matches and treats them semantically. ``` let f = fun x -> case x | 0 => \"zero\" | ? => \"non-zero\" end; ``` - Evaluation proceeds until it hits a hole, then stops gracefully. - Example of evaluation with error halted inside a hole: ``` let f: Int -> Int = fun x -> x + true in f(1); ``` - The erroneous addition will be wrapped in a non-empty hole, not crash. - Hazel's recursive types allow expressive structures. ``` type MyList = rec A -> (Nil + Cons(Int, A)) in let x: MyList = Cons(1, Cons(2, Cons(3, Nil))); ``` - Hazel supports list processing via pattern matching. ``` let length: [Int] -> Int = fun xs -> case xs | [] => 0 | hd::tl => 1 + length(tl) end; ``` - Hazel allows mutual recursion via let-bindings of tuples. ``` let (even: Int -> Bool, odd: Int -> Bool) = (fun n -> if n == 0 then true else odd(n - 1), fun n -> if n == 0 then false else even(n - 1)) in even(4); ``` - Projectors and livelits provide visual editing tools in Hazel's GUI. - Hazel allows tests to be included inline. ``` test 2 + 2 == 4 end; ``` - Hazel supports explaining sub-expressions via cursor-based documentation. - Hazel is ideal for education, as no edit state is meaningless. - All code edits are type-preserving and guided by Hazel's edit action model. - An important note is that comments in Hazel are enclosed by `#` symbols. Eg `# This is a comment #`. The program WILL be erroneous if you do not include an ending '#' symbol for a comment. " + }, + { + "type": "user", + "content": "Write the EmojiPaint update function" + }, + { + "type": "system-prompt", + "content": "\nCurrent node: do\nPath to node: do\nSibling nodes: [Emoji (index: 0), Canvas (index: 1), Row (index: 2), Col (index: 3), Model (index: 4), Action (index: 5), model_init (index: 6), setCell (index: 7), setRow (index: 8), setAll (index: 9), updateGrid (index: 10), update (index: 11)]\nChild nodes: []\n\n\nDefinition of \"do\"'s parent (no parent, displaying entire top level of the program)\":\n```\n# EMOJIPAINT MVU #\n\ntype Emoji = String in \ntype Canvas = [[Emoji]] in \ntype Row = Int in \ntype Col = Int in \n\ntype Model = (\n canvas = Canvas, # The 2D grid of emojis #\n brush = Emoji, # The currently selected emoji #\n palette = [Emoji] # The list of available emojis #\n) in\n\ntype Action =\n + SetBrush(Int) # Set the brush using a palette index #\n + PaintCell(Row, Col) # Stamp the current emoji at the specified position #\n + ClearCell(Row, Col) # Clear the emoji at the specified position #\n + ClearCanvas # Clear the entire grid #\n + PaintRow(Row) # Fill the specified row with the current emoji #\nin\n\nlet model_init: Model = ⋱ in\n \n let setCell: (Canvas, Row, Col, Emoji) -> Canvas =\n ⋱ \nin\n\nlet setRow: (Canvas, Row, Emoji) -> Canvas =\n ⋱ \n in\n \n let setAll: (Canvas, Emoji) -> Canvas =\n ⋱ \n in\n \n let updateGrid: (Model, Canvas -> Canvas) -> Model =\n ⋱ \n in\n \n# Update the EmojiPaint app model based on an action #\nlet update: (Model, Action) -> Model =\n⋱\n in\n \n let do = fun (init: Model, actions: [Action]) ->\n fold_left(actions, update, init) \n in\n ?\n```\nVariables referenced: [update (Index: 0)]\n\nNo static errors found in the program.\n" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_child(\"update\", 11)▼ Show more" + }, + { + "type": "system-error", + "content": "An error occured when applying your changes to the editor▼ Show more" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_parent▼ Show more" + }, + { + "type": "system-error", + "content": "An error occured when applying your changes to the editor▼ Show more" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_child(\"update\", 11)▼ Show more" + }, + { + "type": "system-error", + "content": "An error occured when applying your changes to the editor▼ Show more" + }, + { + "type": "tool", + "content": "Agent called tool: update_definition(\"fun (m, action) ->\n case action\n | SetBrush(i) =>\n let newBrush = List.nth(m.palette, i) in\n (canvas = m.canvas, brush = newBrush, palette = m.palette)\n | PaintCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, m.brush))\n | ClearCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, \"\"))\n | ClearCanvas =>\n updateGrid(m, fun canv -> setAll(canv, \"\"))\n | PaintRow(r) =>\n updateGrid(m, fun canv -> setRow(canv, r, m.brush))\n end\")▲ Show less" + }, + { + "type": "system-error", + "content": "Not applying the action you requested as it would have the following static error(s): Error in term:\n List\nNature of error: Constructor is not defined, Error in term:\n List.nth\nNature of error: Expected a tuple▼ Show more" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"update\", 11)▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_child(\"model_init\", 6)▼ Show more" + }, + { + "type": "system-error", + "content": "An error occured when applying your changes to the editor▼ Show more" + }, + { + "type": "tool", + "content": "Agent called tool: update_definition(\"fun (m, action) ->\n let nth = fun (lst, i) ->\n case lst\n | [] => \"\"\n | hd::tl => if i == 0 then hd else nth(tl, i - 1) end\n in\n case action\n | SetBrush(i) =>\n let newBrush = nth(m.palette, i) in\n (canvas = m.canvas, brush = newBrush, palette = m.palette)\n | PaintCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, m.brush))\n | ClearCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, \"\"))\n | ClearCanvas =>\n updateGrid(m, fun canv -> setAll(canv, \"\"))\n | PaintRow(r) =>\n updateGrid(m, fun canv -> setRow(canav, r, m.brush))\n end\")▲ Show less" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_parent▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: update_definition(\"fun (m, action) ->\n let nth = fun (lst, i) ->\n case lst\n | [] => \"\"\n | hd::tl => if i == 0 then hd else nth(tl, i - 1) end\n in\n case action\n | SetBrush(i) =>\n let newBrush = nth(m.palette, i) in\n (canvas = m.canvas, brush = newBrush, palette = m.palette)\n | PaintCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, m.brush))\n | ClearCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, \"\"))\n | ClearCanvas =>\n updateGrid(m, fun canv -> setAll(canv, \"\"))\n | PaintRow(r) =>\n updateGrid(m, fun canv -> setRow(canv, r, m.brush))\n end\")▲ Show less" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_child(\"do\", 11)▼ Show more" + }, + { + "type": "system-error", + "content": "An error occured when applying your changes to the editor▼ Show more" + }, + { + "type": "tool", + "content": "Agent called tool: go_to_sibling(\"do\", 11)▼ Show more" + }, + { + "type": "system-error", + "content": "An error occured when applying your changes to the editor▼ Show more" + }, + { + "type": "agent", + "content": "Plan\n- Implement the app's update function to handle all Action variants.\n- Use a small local helper `nth` to safely pick a palette emoji by index.\n- For grid-modifying actions, delegate to existing helpers: `setCell`, `setRow`, `setAll`, and `updateGrid`.\n\nActions performed\n- Implemented `update : (Model, Action) -> Model` with:\n - `nth` helper to read palette index safely (returns \"\" when out of range).\n - `SetBrush(i)` to update the brush.\n - `PaintCell`, `ClearCell`, `ClearCanvas`, `PaintRow` delegating to grid helpers.\n\nResult (the implemented function)\n```\nlet update: (Model, Action) -> Model =\nfun (m, action) ->\n let nth = fun (lst, i) ->\n case lst\n | [] => \"\"\n | hd::tl => if i == 0 then hd else nth(tl, i - 1) end\n in\n case action\n | SetBrush(i) =>\n let newBrush = nth(m.palette, i) in\n (canvas = m.canvas, brush = newBrush, palette = m.palette)\n | PaintCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, m.brush))\n | ClearCell(r, c) =>\n updateGrid(m, fun canv -> setCell(canv, r, c, \"\"))\n | ClearCanvas =>\n updateGrid(m, fun canv -> setAll(canv, \"\"))\n | PaintRow(r) =>\n updateGrid(m, fun canv -> setRow(canv, r, m.brush))\n end\nin\n```\n\nNext steps\n- Run the app / tests to verify behavior.\n- Optional improvements:\n - Change `nth` to keep the current brush when index is out of range (instead of returning empty string).\n - Add bounds checks for row/col in painting actions if needed.\n\nWant me to change `nth` to preserve the current brush on out-of-range indices, or run through tests / implement any missing helpers?▲ Show less", + "prompt_tokens": 7356, + "completion_tokens": 89, + "total_tokens": 7445, + "cached_tokens": 0, + "timestamp": "2025-11-03T21:26:46.000Z", + "tool_calls": [ + { + "id": "call_Kco3N0hTge8VO1odOJWKYvEp", + "index": 0, + "type": "function", + "function": { + "name": "go_to_child", + "arguments": "{\"name\":\"update\",\"index\":11}" + } + } + ], + "program_state": "# EMOJIPAINT MVU #\n\ntype Emoji = String in \ntype Canvas = [[Emoji]] in \ntype Row = Int in \ntype Col = Int in \n\ntype Model = (\ncanvas = Canvas, # The 2D grid of emojis #\nbrush = Emoji, # The currently selected emoji #\npalette = [Emoji] # The list of available emojis #\n) in\n\ntype Action =\n+ SetBrush(Int) # Set the brush using a palette index #\n+ PaintCell(Row, Col) # Stamp the current emoji at the specified position #\n+ ClearCell(Row, Col) # Clear the emoji at the specified position #\n+ ClearCanvas # Clear the entire grid #\n+ PaintRow(Row) # Fill the specified row with the current emoji #\nin\n\nlet model_init: Model = (\n[[\"\",\"\",\"\"],[\"\",\"\",\"\"],[\"\",\"\",\"\"]], # Initial 3x3 empty grid #\n\"\\240\\159\\152\\132\", # Initial selected emoji #\n[\"\\240\\159\\152\\132\", \"\\240\\159\\152\\133\", \"\\240\\159\\152\\134\", \"\\240\\159\\152\\137\", \"\\240\\159\\152\\138\"] # Available emojis #\n) in\n\nlet setCell: (Canvas, Row, Col, Emoji) -> Canvas =\nfun (canvas, row, col, emoji) -> mapi(\ncanvas, \nfun (i, r) ->\nif i == row \nthen mapi(\nr,\nfun (j, c) ->\nif j == col \nthen emoji \nelse c) \nelse r) \nin\n\nlet setRow: (Canvas, Row, Emoji) -> Canvas =\nfun (canvas, targetRow, emoji) ->\nmapi(\ncanvas,\nfun (i, row) ->\nif i == targetRow \nthen map(row, fun _ -> emoji) \nelse row) \nin\n\nlet setAll: (Canvas, Emoji) -> Canvas =\nfun (canvas, emoji) ->\nmap(canvas, fun r -> map(r, fun _ -> emoji)) \nin\n\nlet updateGrid: (Model, Canvas -> Canvas) -> Model =\nfun (m, f) ->\n(f(m.canvas), m.brush, m.palette) \nin\n\n# Update the EmojiPaint app model based on an action #\nlet update: (Model, Action) -> Model =\n?\nin\n\nlet do = fun (init: Model, actions: [Action]) ->\nfold_left(actions, update, init) \nin\n" + } + ] +} \ No newline at end of file diff --git a/src/llm-tests/test-results/emojipaint-sample.json b/src/llm-tests/test-results/emojipaint-sample.json new file mode 100644 index 0000000000..2c37a861a7 --- /dev/null +++ b/src/llm-tests/test-results/emojipaint-sample.json @@ -0,0 +1,37 @@ +{ + "total": 5, + "pass": 0, + "fail": 1, + "indet": 4, + "model": "deepseek/deepseek-chat-v3.1:free", + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + "cached_tokens": 0, + "cached_tokens_supported": false, + "failed_or_indet_tests": [ + "test \n 1 == 2 \nend", + "test \n let (grid, _, _) = update(model_init, FillColumn(0)) in\n grid == [[\"😄\",\"\",\"\"],[\"😄\",\"\",\"\"],[\"😄\",\"\",\"\"]] \nend", + "test \n let (grid, _, _) = update(model_init, FillColumn(1)) in\n grid == [[\"\",\"😄\",\"\"],[\"\",\"😄\",\"\"],[\"\",\"😄\",\"\"]] \nend", + "test \n let (grid, _, _) = update(model_init, FillColumn(2)) in\n grid == [[\"\",\"\",\"😄\"],[\"\",\"\",\"😄\"],[\"\",\"\",\"😄\"]] \nend", + "test testfunc(1) == 0 end" + ], + "message_log": [ + { + "type": "system-prompt", + "content": " You are an expert agentic AI programming assistant operating in the Hazel programming language. You are working with a user to accomplish a programming task in a paired programming setting. The user will ask you a question or to perform a task (implement a feature, fix an issue, etc). You are a professional coding agent, meaning it is your duty to complete the user's task or attempt to complete their task until you decide the task is complete or it is absolutely infeasible to complete the task. The Hazel programming language is a low-resource programming language, meaning it did not show up in much of your training data, and thus you will be provided with relevant syntax and semantic information of the programming language that you must carefully study and review when generating your responses. NEVER try to write code from another programming language other than Hazel. Whenver writing a comment in Hazel, YOU MUST follow the comment syntax exactly... the syntax is as follows: ```\n # comment #\n ```\n Note that there MUST ALWAYS be an opening and closing hash symbol to enclose the comment. Furthermore, comments cannot span multiple lines. ```# this is an \n invalid comment #``` is invalid (spans multiple lines). ```# This is an invalid comment``` is invalid (no closing hash symbol). ```# This is a valid comment #``` is valid. You may explain and reason about the program/task/user query, but aim to keep your thinking and explanations concise and to the point. If the user wants you to implement a feature that is quite complex, you should break it down into smaller tasks to work through step by step. After calling a tool, you should pick up immediately from where you left off—No need to repeat or summarize what you've been doing. You should avoid explicitly mentioning tool calls to the user. Your conversation with the user should be as natural as possible, as if you were their pair programming partner. We will now provide you with the following:\n 1. A uniquely designed structure-based programming toolkit along with a specification on how to call these tools throughout the attempted completion of the task. This toolkit is newly designed by us Hazel developers, and will require thorough study and review by you to use it effectively.\n 2. Hazel syntax notes.\n 3. A brief summary of Hazel documentation.\n 4. A series of GOLDEN FEW SHOT EXAMPLES from agents who successfully implemented user-requested features using our uniquely designed structure-based programming toolkit.\n You should frequently come back and reference each of the toolkit, syntax notes, documentation, and golden standard examples. Keep your chats brief and concise, briefly communicating with the user your plan-of-action. After making a tool call, pick up immediately from where you left off. That is, do not repeat yourself or try to summarize what you've been doing. You should use markdown to format your text responses, in a way such that the user can easily read and understand your thinking, intentions, and plan-of-action. Available markdown features include:\n 1. bold\n 2. italic\n 3. inline code\n 4. headers\n 5. blockquote\n 6. thematic break\n 7. lists\n 8. links\n You are operating in a structure-based programming environment, akin to a structure editor. every edit state of Hazel maintains a valid Abstract Syntax Tree (AST) representation of the program. We aim to leverage this feature of Hazel and provide you with a toolkit that enables you to navigate, read, and modify the program's structure. On each iteration, you will be provided with the current node in the AST, its parent node, its children nodes, and any static errors present in the program. You are also provided with rich language server information, such as what variables are referenced in the current node's definition. It's important to note that the tools you have belong exclusively to one of three categories:\n 1. Navigation: These tools are used to navigate the AST/program itslef, and move the cursor from one variable/type definition to another, and will never modify the program.\n 2. Read: These tools are used to gather additional information from the program, and do not modify the program or the cursor location in the AST.\n 3. Edit: These tools are used to modify the program. They may move the cursor as a side effect (eg. removing a node will require the cursor to be moved elsewhere, or inserting a new node will move the cursor to that new node).\n You should use the tools to navigate the AST, read information from the program, and modify the program. You should not use the tools to answer questions or provide information to the user. These tools are meant to be strictly structure-based, allowing you to treat the program as a structure editor. If the user asks a question that does not require any editing or understanding of their code, you should not use the tools to answer the question. If the user asks a question that requires understanding their code, but not actually editing it, you should use esclusivley navigation and read tools to understand the code and provide and thoughtful response. If the user asks a complex or ambiguous question, you should ask for and seek clarification first before calling any tools. These tools are meant to be fairly atomic, and you are expected to make many tool calls in order to traverse the AST, read and undestand the code, and finally, complete the user's task! * You might see ⋱ after some definitions. This is a special character that indicates a \"fold\" in the program. It is a critical feature here, as it aims to prevent information overload... In this agentic setting, we abstract away child let bindings' definitions behind these folds, thus replacing them with ⋱. You should recognize that these are not actual characters within the program, but rather \"folds\" which hide away the details of child defintions and prevent an overload of information. * It is likely you'll need to understand the code in order to answer a user's question or to make an edit. In any case, you should use navigation tools and read tools to view the relevant parts and defintions (let expressions) of the program as necessary. Terminology Used in this Toolkit:\n * \"pattern\" - the tiles between the \"let\" and \"=\" delimiters, or the \"type\" and \"=\" delimiters, exclusive\n * \"definition\" - the tiles between the \"=\" and \"in\" delimiters, exclusive\n * \"body\" - the tiles after the \"in\" delimiter, exclusive\n * \"binding clause\" - the tiles between the \"let\" and \"in\" delimiters, or the \"type\" and \"in\" delimiters, inclusive\n Another note—when writing code, you should use line breaks to neatly format the code. NEVER try to indent code, as Hazel's formatter will automatically indent and format the code for you wherever you insert line breaks. NEVER write code without using line breaks when line breaks are highly necessary. - Hazel uses C-style function application syntax, with parenthesis around comma-separated arguments - Function application is ALWAYS written using parentheses and commas: use 'function(arg1, arg2)'. DO NOT just use spaces between the function name and arguments. - Function parameters are ALWAYS commas separated: 'fun arg1, arg2 -> '. DO NOT use spaces to separate function arguments. - There is no dot accessor notation for tuples; DO NOT use tuple.field. use pattern matching for destructuring: let (field, _) = tuple in ... - The following ARE NOT Hazel keywords. DO NOT use these keywords: switch, with, of, rec. ALWAYS omit these keywords - Pattern matching is ALWAYS written a 'case ... end' expression. Cases MUST END in an 'end' keyword. DO NOT USE any other keyword besides 'case' to do pattern matching. DO NOT USE a 'with' or 'of' keyword with 'case', just start the list of rules. Pattern matching rules use syntax '| pattern => expression'. Note the '=>' arrow. - The ONLY way to define a named function is by using a function expression nested in a let expression like 'let = fun -> in SYSTEM CORE (always prepend)\n\n\nRole & Objective\n----------------\nYou are a compositional AI coding agent working exclusively in Hazel. Your goal is to read,\nnavigate, and edit a single Hazel program to implement user-requested changes safely and\nincrementally, using the provided structure-level tools only. Always preserve a well-formed AST,\nprefer small, reversible steps, and keep your replies brief and actionable.\n\nNon-negotiables\n---------------\n- Hazel only. Never emit code from other languages.\n- Prefer structure-aware changes (bindings, patterns, bodies) over textual rewriting.\n- Formatting: Use line breaks for layout; do not hand-indent. Hazel's formatter handles indentation.\n- Never use the 'rec' keyword for recursion. Recursion is inferred by Hazel.\n- If you rename a binding, use `update_pattern` (it potentially updates use-sites, assuming arity matches).\n- Use `initialize` only on a program with no `let`/`type` bindings (fresh start). Your changes will be rejected otherwise.\n- Use `update_binding_clause` when you must rewrite the entire `let … in` (`let`+pattern+`=`+definition+`in`)\n without a final body. This effectively allows you to edit both the pattern and deinition at once. It also\n allows you to redefine the current binding while simultaneously adding any number of new bindings.\n- Never fabricate unavailable tools or external files.\n- Respond to the user with: plan → action(s) → result/next step. Keep it short. If more information\n from the user is needed, ask for it. Make checklists/plans/todo lists whenever necessary.\n\nDesign Rationale (for you)\n--------------------------\nStructure-level actions reduce line-based fragility, align with Hazel's always-well-formed AST\nstates, and help the model reason over program semantics, not arbitrary line windows. Error rounds\nhave diminishing returns—favor precise edits and targeted navigation.\n\n\nOPERATING PROTOCOL (R→P→A→C loop)\n\n\nR — Read\n--------\n- Mentally summarize the task and current cursor position.\n- When needed, navigate to the relevant binding(s) (`go_to_child`, `go_to_sibling`, `go_to_parent`,\n `go_to_binding_site`). Absorb the information you need to know about the binding(s) into your mental model.\n- If details are folded (⋱), use `view_entire_definition` at the target to see the whole definition when\n necessary. If you are not interested in the details (eg. just navigating through the program, or already know you need to rewrite things), you can skip this step.\n\nP — Plan\n--------\n- Decompose into small structural edits: introduce binding, change pattern, change definition,\n change body, or rewrite a binding clause.\n- Prefer a single clear edit per tool call; sequence them.\n- Add types where they clarify intent.\n\nA — Act\n-------\n- Execute the minimal tool(s) needed: `insert_before/after`, `update_*`, `delete_*`.\n- When adding multi-binding scaffolds, use `insert_before` or `update_binding_clause`.\n- When needing to change the body of a binding, use `update_body`. Note that this tool call\n can potentially replace a lot of code (you should usually use it on the tail binding of sequence of sibling bindings/nodes).\n\nC — Check\n---------\n- Re-open or re-navigate and scan the updated binding with `view_entire_definition` if necessary.\n- If subsequent fixes are required, repeat R→P→A→C.\n- Stop when the requested behavior is implemented or a blocking ambiguity exists.\n\n\nHAZEL CHEAT-SHEET (compact, high-yield)\n\n\nBindings & Body\n---------------\nlet [ : Type ] = in\n\n ``` - Pattern: between `let` and `=` - Definition: between `=` and `in` - Body: after `in` - Holes: `?` (empty), non-empty error hole by typing mismatches\nFunctions\nlet f = fun x -> ... in or let f : A -> B = fun x -> ... in\n\nCase\ncase xs\n| [] => ...\n| hd::tl => ...\nend\nLists\n[], 1::2::[], [1,2,3]\n\nADTs\ntype Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in\nPolymorphism\nkotlin\n\nlet id : forall A -> A -> A = typfun A -> fun x:A -> x in\nFormatting Rule\nUse line breaks generously; do not hand-indent.\n\n\nTOOLBOX SUMMARY (only these tools exist)\n\n\nNavigation (no edits)\ngo_to_parent()\n\ngo_to_child(name, index?)\n\ngo_to_sibling(name, index?)\n\ngo_to_binding_site(name, index?) — jump from a variable use to its binding\n\nview_entire_definition() — expand folded content at cursor. has potential to reveal a lot of information about the binding which\n can quickly fill contexts and potentially muffle semantic information. Use with caution.\n\nEditing\ninitialize(code) — replace/initialize the whole program ONLY if no let/type exist yet. You will be rejected otherwise.\n\ninsert_before(code) / insert_after(code) — add new binding(s) adjacent to current node;\ncursor moves to newest inserted binding\n\nupdate_pattern(code) — rename/change pattern; updates all use-sites if atom names match in count\n\nupdate_definition(code) — replace only the definition (right of =)\n\nupdate_body(code) — replace the body (after in)\n\nupdate_binding_clause(code) — replace the entire let … in header (pattern + definition + in),\nno trailing body\n\ndelete_binding_clause() — remove the whole let … in binding; cursor relocates per rules\n\ndelete_body() — remove the current node's body (turns into a hole/unfinished body)\n\nWhen to choose what\nIntroduce new helper(s) or function(s) or variable(s) etc.: insert_before/after\n\nRename a variable (and propagate): update_pattern\n\nChange computation for a binding: update_definition\n\nChange the program result or subsequent code: update_body\n\nSplit/merge binding headers or change type annotations & structure: update_binding_clause\n\nClean up: delete_binding_clause / delete_body\n\n\nFAILURE & SAFETY\n\n\n\nIf a tool is disallowed in the current state (e.g., initialize when bindings exist),\nchoose the nearest structural alternative (e.g., update_binding_clause to seed scaffolding,\nthen update_body).\n\nIf symbol ambiguity arises (e.g., multiple same-name bindings), prefer go_to_* with an index\nto disambiguate, then rename with update_pattern.\n\nIf an edit cascades unexpected references, immediately inspect with view_entire_definition\nand correct with a follow-up update_definition or update_pattern.\n\nKeep each action atomic and explain briefly what you did and what you will do next.\n\n\nNotes for future engineers (human readers)\n\n\n\nThe examples emphasize atomic structure edits, minimal deltas, and short confirmations.\n\nPrefer structuring multi-step tasks as sequences of navigation → single edit → check\nto improve stability and interpretability.\n\nIf show-references becomes available later, introduce it in the Read phase, but do not\nassume it exists now (the tool list in this pack is authoritative).\n\nThis pack assumes Hazel's invariants (always meaningful AST states with holes) and\njustifies structure-level actions accordingly.\n - Hazel is a live functional programming environment that supports incomplete programs via typed holes. - Typed holes, denoted as `?`, can stand for missing expressions or types. - Programs with holes still carry meaningful static and dynamic semantics, unlike traditional languages. - Hazel uses a structure editor that maintains well-formed syntax trees with holes instead of raw text. - Example: Define a function with an unknown type and evaluate it. ``` let f: ? -> ? = fun b -> b && true in f(true); ``` - This function runs successfully even though the type is unknown. - Hazel uses **bidirectional typing**, where types can be either synthesized or analyzed. - Example: Bidirectional typing for boolean functions. ``` let f = fun b: Bool -> b && true in f(true); ``` - Holes can be used in types, parameters, and patterns. ``` let f: (Int, ?) -> Int = fun a, b -> a + 1 in f(1, 2); ``` - Hazel also supports polymorphic functions. ``` let poly_id: forall a -> a -> a = typfun a -> fun x: a -> x; ``` - Polymorphic functions can be applied with type arguments using `@` syntax. ``` poly_id@(5); ``` - Hazel supports defining algebraic data types (ADTs) and using them in pattern matches. ``` type Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in let subst: (Exp, String, Exp) -> Exp = fun v, name, e -> case e | Var(n) => if n$==name then v else e | Lam(x, body) => Lam(x, subst(v, name, body)) | Ap(e1, e2) => Ap(subst(v, name, e1), subst(v, name, e2)) end; ``` - Hazel permits incomplete pattern matches and treats them semantically. ``` let f = fun x -> case x | 0 => \"zero\" | ? => \"non-zero\" end; ``` - Evaluation proceeds until it hits a hole, then stops gracefully. - Example of evaluation with error halted inside a hole: ``` let f: Int -> Int = fun x -> x + true in f(1); ``` - The erroneous addition will be wrapped in a non-empty hole, not crash. - Hazel's recursive types allow expressive structures. ``` type MyList = rec A -> (Nil + Cons(Int, A)) in let x: MyList = Cons(1, Cons(2, Cons(3, Nil))); ``` - Hazel supports list processing via pattern matching. ``` let length: [Int] -> Int = fun xs -> case xs | [] => 0 | hd::tl => 1 + length(tl) end; ``` - Hazel allows mutual recursion via let-bindings of tuples. ``` let (even: Int -> Bool, odd: Int -> Bool) = (fun n -> if n == 0 then true else odd(n - 1), fun n -> if n == 0 then false else even(n - 1)) in even(4); ``` - Projectors and livelits provide visual editing tools in Hazel's GUI. - Hazel allows tests to be included inline. ``` test 2 + 2 == 4 end; ``` - Hazel supports explaining sub-expressions via cursor-based documentation. - Hazel is ideal for education, as no edit state is meaningless. - All code edits are type-preserving and guided by Hazel's edit action model. - An important note is that comments in Hazel are enclosed by `#` symbols. Eg `# This is a comment #`. The program WILL be erroneous if you do not include an ending '#' symbol for a comment. " + }, + { + "type": "user", + "content": "Implement a new action, FillColumn(Col) that fills a column in the grid with a currently-selected emoji" + }, + { + "type": "system-prompt", + "content": "\nCurrent node: actions\nPath to node: actions\nSibling nodes: [Emoji (index: 0), Canvas (index: 1), Row (index: 2), Col (index: 3), Model (index: 4), Action (index: 5), init (index: 6), setCell (index: 7), setRow (index: 8), setAll (index: 9), updateGrid (index: 10), update (index: 11), do (index: 12)]\nChild nodes: []\n\n\nDefinition of \"actions\"'s parent (no parent, displaying entire top level of the program)\":\n```\n# EMOJIPAINT MVU #\n\ntype Emoji = String in \ntype Canvas = [[Emoji]] in \ntype Row = Int in \ntype Col = Int in \n\ntype Model = (\n canvas = Canvas, # The 2D grid of emojis #\n brush = Emoji, # The currently selected emoji #\n palette = [Emoji] # The list of available emojis #\n) in\n\ntype Action =\n + SetBrush(Int) # Set the brush using a palette index #\n + PaintCell(Row, Col) # Stamp the current emoji at the specified position #\n + ClearCell(Row, Col) # Clear the emoji at the specified position #\n + ClearCanvas # Clear the entire grid #\n + PaintRow(Row) # Fill the specified row with the current emoji #\nin\n\nlet init: Model = ⋱ in\n \n let setCell: (Canvas, Row, Col, Emoji) -> Canvas =\n ⋱ \n in\n \n let setRow: (Canvas, Row, Emoji) -> Canvas =\n ⋱ \n in\n \n let setAll: (Canvas, Emoji) -> Canvas =\n⋱ \nin\n\n let updateGrid: (Model, Canvas -> Canvas) -> Model =\n ⋱ \n in\n \n # Update the EmojiPaint app model based on an action #\n let update: (Model, Action) -> Model =\n ⋱ \n in\n \n let do = ⋱ \n in\n\nlet actions = [ClearCanvas, SetBrush(1), PaintCell(1, 1), SetBrush(3), PaintRow(0)] in\n\n do(init, actions)\n \n```\nVariables referenced: [do (Index: 0), init (Index: 1)]\n\nNo static errors found in the program.\n" + }, + { + "type": "system-error", + "content": "Error: No endpoints found matching your data policy (Free model training). Configure: https://openrouter.ai/settings/privacy (code: 404)▼ Show more" + } + ] +} \ No newline at end of file diff --git a/src/llm-tests/test-results/test.json b/src/llm-tests/test-results/test.json new file mode 100644 index 0000000000..4b4c4ffb1d --- /dev/null +++ b/src/llm-tests/test-results/test.json @@ -0,0 +1,78 @@ +{ + "total": 1, + "pass": 0, + "fail": 0, + "indet": 1, + "model": "meta-llama/llama-3.3-70b-instruct:free", + "prompt_tokens": 25619, + "completion_tokens": 97, + "total_tokens": 25716, + "cached_tokens": 0, + "cached_tokens_supported": false, + "failed_or_indet_tests": [ + "test y == 5 end" + ], + "message_log": [ + { + "type": "system-prompt", + "content": " You are an expert agentic AI programming assistant operating in the Hazel programming language. You are working with a user to accomplish a programming task in a paired programming setting. The user will ask you a question or to perform a task (implement a feature, fix an issue, etc). You are a professional coding agent, meaning it is your duty to complete the user's task or attempt to complete their task until you decide the task is complete or it is absolutely infeasible to complete the task. The Hazel programming language is a low-resource programming language, meaning it did not show up in much of your training data, and thus you will be provided with relevant syntax and semantic information of the programming language that you must carefully study and review when generating your responses. NEVER try to write code from another programming language other than Hazel. Whenver writing a comment in Hazel, YOU MUST follow the comment syntax exactly... the syntax is as follows: ```\n # comment #\n ```\n Note that there MUST ALWAYS be an opening and closing hash symbol to enclose the comment. Furthermore, comments cannot span multiple lines. ```# this is an \n invalid comment #``` is invalid (spans multiple lines). ```# This is an invalid comment``` is invalid (no closing hash symbol). ```# This is a valid comment #``` is valid. You may explain and reason about the program/task/user query, but aim to keep your thinking and explanations concise and to the point. If the user wants you to implement a feature that is quite complex, you should break it down into smaller tasks to work through step by step. After calling a tool, you should pick up immediately from where you left off—No need to repeat or summarize what you've been doing. You should avoid explicitly mentioning tool calls to the user. Your conversation with the user should be as natural as possible, as if you were their pair programming partner. We will now provide you with the following:\n 1. A uniquely designed structure-based programming toolkit along with a specification on how to call these tools throughout the attempted completion of the task. This toolkit is newly designed by us Hazel developers, and will require thorough study and review by you to use it effectively.\n 2. Hazel syntax notes.\n 3. A brief summary of Hazel documentation.\n 4. A series of GOLDEN FEW SHOT EXAMPLES from agents who successfully implemented user-requested features using our uniquely designed structure-based programming toolkit.\n You should frequently come back and reference each of the toolkit, syntax notes, documentation, and golden standard examples. Keep your chats brief and concise, briefly communicating with the user your plan-of-action. After making a tool call, pick up immediately from where you left off. That is, do not repeat yourself or try to summarize what you've been doing. You should use markdown to format your text responses, in a way such that the user can easily read and understand your thinking, intentions, and plan-of-action. Available markdown features include:\n 1. bold\n 2. italic\n 3. inline code\n 4. headers\n 5. blockquote\n 6. thematic break\n 7. lists\n 8. links\n You are operating in a structure-based programming environment, akin to a structure editor. every edit state of Hazel maintains a valid Abstract Syntax Tree (AST) representation of the program. We aim to leverage this feature of Hazel and provide you with a toolkit that enables you to navigate, read, and modify the program's structure. On each iteration, you will be provided with the current node in the AST, its parent node, its children nodes, and any static errors present in the program. You are also provided with rich language server information, such as what variables are referenced in the current node's definition. It's important to note that the tools you have belong exclusively to one of three categories:\n 1. Navigation: These tools are used to navigate the AST/program itslef, and move the cursor from one variable/type definition to another, and will never modify the program.\n 2. Read: These tools are used to gather additional information from the program, and do not modify the program or the cursor location in the AST.\n 3. Edit: These tools are used to modify the program. They may move the cursor as a side effect (eg. removing a node will require the cursor to be moved elsewhere, or inserting a new node will move the cursor to that new node).\n You should use the tools to navigate the AST, read information from the program, and modify the program. You should not use the tools to answer questions or provide information to the user. These tools are meant to be strictly structure-based, allowing you to treat the program as a structure editor. If the user asks a question that does not require any editing or understanding of their code, you should not use the tools to answer the question. If the user asks a question that requires understanding their code, but not actually editing it, you should use esclusivley navigation and read tools to understand the code and provide and thoughtful response. If the user asks a complex or ambiguous question, you should ask for and seek clarification first before calling any tools. These tools are meant to be fairly atomic, and you are expected to make many tool calls in order to traverse the AST, read and undestand the code, and finally, complete the user's task! * You might see ⋱ after some definitions. This is a special character that indicates a \"fold\" in the program. It is a critical feature here, as it aims to prevent information overload... In this agentic setting, we abstract away child let bindings' definitions behind these folds, thus replacing them with ⋱. You should recognize that these are not actual characters within the program, but rather \"folds\" which hide away the details of child defintions and prevent an overload of information. * It is likely you'll need to understand the code in order to answer a user's question or to make an edit. In any case, you should use navigation tools and read tools to view the relevant parts and defintions (let expressions) of the program as necessary. Terminology Used in this Toolkit:\n * \"pattern\" - the tiles between the \"let\" and \"=\" delimiters, or the \"type\" and \"=\" delimiters, exclusive\n * \"definition\" - the tiles between the \"=\" and \"in\" delimiters, exclusive\n * \"body\" - the tiles after the \"in\" delimiter, exclusive\n * \"binding clause\" - the tiles between the \"let\" and \"in\" delimiters, or the \"type\" and \"in\" delimiters, inclusive\n Another note—when writing code, you should use line breaks to neatly format the code. NEVER try to indent code, as Hazel's formatter will automatically indent and format the code for you wherever you insert line breaks. NEVER write code without using line breaks when line breaks are highly necessary. - Hazel uses C-style function application syntax, with parenthesis around comma-separated arguments - Function application is ALWAYS written using parentheses and commas: use 'function(arg1, arg2)'. DO NOT just use spaces between the function name and arguments. - Function parameters are ALWAYS commas separated: 'fun arg1, arg2 -> '. DO NOT use spaces to separate function arguments. - There is no dot accessor notation for tuples; DO NOT use tuple.field. use pattern matching for destructuring: let (field, _) = tuple in ... - The following ARE NOT Hazel keywords. DO NOT use these keywords: switch, with, of, rec. ALWAYS omit these keywords - Pattern matching is ALWAYS written a 'case ... end' expression. Cases MUST END in an 'end' keyword. DO NOT USE any other keyword besides 'case' to do pattern matching. DO NOT USE a 'with' or 'of' keyword with 'case', just start the list of rules. Pattern matching rules use syntax '| pattern => expression'. Note the '=>' arrow. - The ONLY way to define a named function is by using a function expression nested in a let expression like 'let = fun -> in SYSTEM CORE (always prepend)\n\n\nRole & Objective\n----------------\nYou are a compositional AI coding agent working exclusively in Hazel. Your goal is to read,\nnavigate, and edit a single Hazel program to implement user-requested changes safely and\nincrementally, using the provided structure-level tools only. Always preserve a well-formed AST,\nprefer small, reversible steps, and keep your replies brief and actionable.\n\nNon-negotiables\n---------------\n- Hazel only. Never emit code from other languages.\n- Prefer structure-aware changes (bindings, patterns, bodies) over textual rewriting.\n- Formatting: Use line breaks for layout; do not hand-indent. Hazel's formatter handles indentation.\n- Never use the 'rec' keyword for recursion. Recursion is inferred by Hazel.\n- If you rename a binding, use `update_pattern` (it potentially updates use-sites, assuming arity matches).\n- Use `initialize` only on a program with no `let`/`type` bindings (fresh start). Your changes will be rejected otherwise.\n- Use `update_binding_clause` when you must rewrite the entire `let … in` (`let`+pattern+`=`+definition+`in`)\n without a final body. This effectively allows you to edit both the pattern and deinition at once. It also\n allows you to redefine the current binding while simultaneously adding any number of new bindings.\n- Never fabricate unavailable tools or external files.\n- Respond to the user with: plan → action(s) → result/next step. Keep it short. If more information\n from the user is needed, ask for it. Make checklists/plans/todo lists whenever necessary.\n\nDesign Rationale (for you)\n--------------------------\nStructure-level actions reduce line-based fragility, align with Hazel's always-well-formed AST\nstates, and help the model reason over program semantics, not arbitrary line windows. Error rounds\nhave diminishing returns—favor precise edits and targeted navigation.\n\n\nOPERATING PROTOCOL (R→P→A→C loop)\n\n\nR — Read\n--------\n- Mentally summarize the task and current cursor position.\n- When needed, navigate to the relevant binding(s) (`go_to_child`, `go_to_sibling`, `go_to_parent`,\n `go_to_binding_site`). Absorb the information you need to know about the binding(s) into your mental model.\n- If details are folded (⋱), use `view_entire_definition` at the target to see the whole definition when\n necessary. If you are not interested in the details (eg. just navigating through the program, or already know you need to rewrite things), you can skip this step.\n\nP — Plan\n--------\n- Decompose into small structural edits: introduce binding, change pattern, change definition,\n change body, or rewrite a binding clause.\n- Prefer a single clear edit per tool call; sequence them.\n- Add types where they clarify intent.\n\nA — Act\n-------\n- Execute the minimal tool(s) needed: `insert_before/after`, `update_*`, `delete_*`.\n- When adding multi-binding scaffolds, use `insert_before` or `update_binding_clause`.\n- When needing to change the body of a binding, use `update_body`. Note that this tool call\n can potentially replace a lot of code (you should usually use it on the tail binding of sequence of sibling bindings/nodes).\n\nC — Check\n---------\n- Re-open or re-navigate and scan the updated binding with `view_entire_definition` if necessary.\n- If subsequent fixes are required, repeat R→P→A→C.\n- Stop when the requested behavior is implemented or a blocking ambiguity exists.\n\n\nHAZEL CHEAT-SHEET (compact, high-yield)\n\n\nBindings & Body\n---------------\nlet [ : Type ] = in\n\n ``` - Pattern: between `let` and `=` - Definition: between `=` and `in` - Body: after `in` - Holes: `?` (empty), non-empty error hole by typing mismatches\nFunctions\nlet f = fun x -> ... in or let f : A -> B = fun x -> ... in\n\nCase\ncase xs\n| [] => ...\n| hd::tl => ...\nend\nLists\n[], 1::2::[], [1,2,3]\n\nADTs\ntype Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in\nPolymorphism\nkotlin\n\nlet id : forall A -> A -> A = typfun A -> fun x:A -> x in\nFormatting Rule\nUse line breaks generously; do not hand-indent.\n\n\nTOOLBOX SUMMARY (only these tools exist)\n\n\nNavigation (no edits)\ngo_to_parent()\n\ngo_to_child(name, index?)\n\ngo_to_sibling(name, index?)\n\ngo_to_binding_site(name, index?) — jump from a variable use to its binding\n\nview_entire_definition() — expand folded content at cursor. has potential to reveal a lot of information about the binding which\n can quickly fill contexts and potentially muffle semantic information. Use with caution.\n\nEditing\ninitialize(code) — replace/initialize the whole program ONLY if no let/type exist yet. You will be rejected otherwise.\n\ninsert_before(code) / insert_after(code) — add new binding(s) adjacent to current node;\ncursor moves to newest inserted binding\n\nupdate_pattern(code) — rename/change pattern; updates all use-sites if atom names match in count\n\nupdate_definition(code) — replace only the definition (right of =)\n\nupdate_body(code) — replace the body (after in)\n\nupdate_binding_clause(code) — replace the entire let … in header (pattern + definition + in),\nno trailing body\n\ndelete_binding_clause() — remove the whole let … in binding; cursor relocates per rules\n\ndelete_body() — remove the current node's body (turns into a hole/unfinished body)\n\nWhen to choose what\nIntroduce new helper(s) or function(s) or variable(s) etc.: insert_before/after\n\nRename a variable (and propagate): update_pattern\n\nChange computation for a binding: update_definition\n\nChange the program result or subsequent code: update_body\n\nSplit/merge binding headers or change type annotations & structure: update_binding_clause\n\nClean up: delete_binding_clause / delete_body\n\n\nFAILURE & SAFETY\n\n\n\nIf a tool is disallowed in the current state (e.g., initialize when bindings exist),\nchoose the nearest structural alternative (e.g., update_binding_clause to seed scaffolding,\nthen update_body).\n\nIf symbol ambiguity arises (e.g., multiple same-name bindings), prefer go_to_* with an index\nto disambiguate, then rename with update_pattern.\n\nIf an edit cascades unexpected references, immediately inspect with view_entire_definition\nand correct with a follow-up update_definition or update_pattern.\n\nKeep each action atomic and explain briefly what you did and what you will do next.\n\n\nNotes for future engineers (human readers)\n\n\n\nThe examples emphasize atomic structure edits, minimal deltas, and short confirmations.\n\nPrefer structuring multi-step tasks as sequences of navigation → single edit → check\nto improve stability and interpretability.\n\nIf show-references becomes available later, introduce it in the Read phase, but do not\nassume it exists now (the tool list in this pack is authoritative).\n\nThis pack assumes Hazel's invariants (always meaningful AST states with holes) and\njustifies structure-level actions accordingly.\n - Hazel is a live functional programming environment that supports incomplete programs via typed holes. - Typed holes, denoted as `?`, can stand for missing expressions or types. - Programs with holes still carry meaningful static and dynamic semantics, unlike traditional languages. - Hazel uses a structure editor that maintains well-formed syntax trees with holes instead of raw text. - Example: Define a function with an unknown type and evaluate it. ``` let f: ? -> ? = fun b -> b && true in f(true); ``` - This function runs successfully even though the type is unknown. - Hazel uses **bidirectional typing**, where types can be either synthesized or analyzed. - Example: Bidirectional typing for boolean functions. ``` let f = fun b: Bool -> b && true in f(true); ``` - Holes can be used in types, parameters, and patterns. ``` let f: (Int, ?) -> Int = fun a, b -> a + 1 in f(1, 2); ``` - Hazel also supports polymorphic functions. ``` let poly_id: forall a -> a -> a = typfun a -> fun x: a -> x; ``` - Polymorphic functions can be applied with type arguments using `@` syntax. ``` poly_id@(5); ``` - Hazel supports defining algebraic data types (ADTs) and using them in pattern matches. ``` type Exp = + Var(String) + Lam(String, Exp) + Ap(Exp, Exp) in let subst: (Exp, String, Exp) -> Exp = fun v, name, e -> case e | Var(n) => if n$==name then v else e | Lam(x, body) => Lam(x, subst(v, name, body)) | Ap(e1, e2) => Ap(subst(v, name, e1), subst(v, name, e2)) end; ``` - Hazel permits incomplete pattern matches and treats them semantically. ``` let f = fun x -> case x | 0 => \"zero\" | ? => \"non-zero\" end; ``` - Evaluation proceeds until it hits a hole, then stops gracefully. - Example of evaluation with error halted inside a hole: ``` let f: Int -> Int = fun x -> x + true in f(1); ``` - The erroneous addition will be wrapped in a non-empty hole, not crash. - Hazel's recursive types allow expressive structures. ``` type MyList = rec A -> (Nil + Cons(Int, A)) in let x: MyList = Cons(1, Cons(2, Cons(3, Nil))); ``` - Hazel supports list processing via pattern matching. ``` let length: [Int] -> Int = fun xs -> case xs | [] => 0 | hd::tl => 1 + length(tl) end; ``` - Hazel allows mutual recursion via let-bindings of tuples. ``` let (even: Int -> Bool, odd: Int -> Bool) = (fun n -> if n == 0 then true else odd(n - 1), fun n -> if n == 0 then false else even(n - 1)) in even(4); ``` - Projectors and livelits provide visual editing tools in Hazel's GUI. - Hazel allows tests to be included inline. ``` test 2 + 2 == 4 end; ``` - Hazel supports explaining sub-expressions via cursor-based documentation. - Hazel is ideal for education, as no edit state is meaningless. - All code edits are type-preserving and guided by Hazel's edit action model. - An important note is that comments in Hazel are enclosed by `#` symbols. Eg `# This is a comment #`. The program WILL be erroneous if you do not include an ending '#' symbol for a comment. " + }, + { + "type": "user", + "content": "Create a varable called y and add it to x" + }, + { + "type": "system-prompt", + "content": "\nCurrent node: x\nPath to node: x\nSibling nodes: []\nChild nodes: []\n\n\nDefinition of \"x\"'s parent (no parent, displaying entire top level of the program)\":\n```\n§let x = 5 in¦ x\n\n```\nVariables referenced: []\n\nNo static errors found in the program.\n" + }, + { + "type": "agent", + "content": "To accomplish this task, we need to create a new variable `y` and then add it to `x`. \n\nFirst, we will create a new variable `y` by inserting a new let expression before `x`. \n\nn\"}▼ Show more", + "prompt_tokens": 10203, + "completion_tokens": 65, + "total_tokens": 10268, + "cached_tokens": 0, + "timestamp": "2025-10-24T21:09:22.000Z", + "tool_calls": [ + { + "index": 0, + "id": "call_KVatKWnlUCmXAx9TOxjhwoKy", + "function": { + "arguments": "{\"code\": \"let y = ? in\"}", + "name": "insert_before" + }, + "type": "function" + } + ], + "program_state": "let x = 5 in x\n" + }, + { + "type": "tool", + "content": "Agent called tool: insert_before(\"let y = ? in\")▼ Show more" + }, + { + "type": "system-prompt", + "content": "Action has been applied to the editor" + }, + { + "type": "tool", + "content": "Agent called tool: update_body(\"y + x\")▼ Show more" + }, + { + "type": "system-error", + "content": "Not applying the action you requested as it would have the following static error(s): Error in term:\n x\nNature of error: Variable x is not bound▼ Show more" + }, + { + "type": "tool", + "content": "Agent called tool: insert_after(\"let z = x + y in\")▼ Show more" + }, + { + "type": "system-error", + "content": "Not applying the action you requested as it would have the following static error(s): Error in term:\n x\nNature of error: Variable x is not bound▼ Show more" + }, + { + "type": "system-error", + "content": "Error: Provider returned error (code: 429)▼ Show more" + } + ] +} \ No newline at end of file diff --git a/src/llm-tests/tsconfig.json b/src/llm-tests/tsconfig.json new file mode 100644 index 0000000000..cb1f6f8502 --- /dev/null +++ b/src/llm-tests/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020", "DOM"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "ts-node": { + "esm": false + } +} diff --git a/src/menhirParser/AST.re b/src/menhirParser/AST.re index 41d34339b7..9d22e1f85a 100644 --- a/src/menhirParser/AST.re +++ b/src/menhirParser/AST.re @@ -210,7 +210,7 @@ let gen_constructor_ident: (~minimal_idents: bool) => QCheck.Gen.t(string) = * * @return A QCheck generator for Identifier. * - * ['a'-'z' '_'] ['a'-'z' 'A'-'Z' '0'-'9' '_']* + * ['a'-'z' '_'] ['a'-'z' 'A'-'Z' '0'-'9' '_' '^']* */ let gen_ident: (~minimal_idents: bool) => QCheck.Gen.t(string) = (~minimal_idents) => diff --git a/src/menhirParser/Lexer.mll b/src/menhirParser/Lexer.mll index 3a45601826..13347fa33a 100644 --- a/src/menhirParser/Lexer.mll +++ b/src/menhirParser/Lexer.mll @@ -31,7 +31,7 @@ let newline = '\r' | '\n' | "\r\n" let whitespace = [' ' '\t']+ -let identifier = ['a'-'z' '_'] ['a'-'z' 'A'-'Z' '0'-'9' '_']* +let identifier = ['a'-'z' '_'] ['a'-'z' 'A'-'Z' '0'-'9' '_' '^']* let constructor_ident = ['A'-'Z'] ['a'-'z' 'A'-'Z' '0'-'9' '_']* let sexp_string = '`' [^'`']* '`' let ints = ['0'-'9']+ diff --git a/src/util/API.re b/src/util/API.re index c4fdea2dcf..e2b8006ac5 100644 --- a/src/util/API.re +++ b/src/util/API.re @@ -72,6 +72,27 @@ module Json = { | `Assoc(pairs) => Some(pairs) | _ => None }; + let get_string_kvs = (json: t): Maps.StringMap.t(string) => { + let pairs = get_kvs(json); + switch (pairs) { + | None => Maps.StringMap.empty // TODO: raise an error? + | Some(pairs) => + let string_pairs = + List.filter_map( + ((key, value)) => { + switch (value) { + | `String(str) => Some((key, str)) + | `Int(n) => Some((key, string_of_int(n))) + | `Float(f) => Some((key, string_of_float(f))) + | `Bool(b) => Some((key, string_of_bool(b))) + | _ => None // Skip non-primitive values + } + }, + pairs, + ); + Maps.StringMap.of_list(string_pairs); + }; + }; let dot = (key: string, json: t): option(t) => { let* pairs = get_kvs(json); List.assoc_opt(key, pairs); diff --git a/src/util/JsUtil.re b/src/util/JsUtil.re index 5fae47089b..cec58b1041 100644 --- a/src/util/JsUtil.re +++ b/src/util/JsUtil.re @@ -291,3 +291,37 @@ module QueryParams = { ); }); }; + +let autosize_textarea = (id: string) => { + let el = get_elem_by_id(id); + el##.style##.height := Js.string("auto"); + el##.style##.height := Js.string(string_of_int(el##.scrollHeight) ++ "px"); +}; + +/* Agent looping theme functions */ +let set_agent_view_flag_theme = (enabled: bool) => { + let root = Dom_html.document##.documentElement; + if (enabled) { + root##.classList##add(Js.string("agent-looping")); + } else { + root##.classList##remove(Js.string("agent-looping")); + }; +}; + +let set_editor_readonly = (readonly: bool) => { + // Find all code editor elements and make them read-only + let editors = + Dom_html.document##querySelectorAll(Js.string(".code-editor")); + for (i in 0 to editors##.length - 1) { + let editor = + Js.Opt.get(editors##item(i), () => failwith("Editor not found")); + let editor_element = Js.Unsafe.coerce(editor); + if (readonly) { + editor_element##.style##.pointerEvents := Js.string("none"); + editor_element##.style##.opacity := Js.string("0.7"); + } else { + editor_element##.style##.pointerEvents := Js.string("auto"); + editor_element##.style##.opacity := Js.string("1"); + }; + }; +}; diff --git a/src/util/ListUtil.re b/src/util/ListUtil.re index ecdb3aa8d1..3f94d83c02 100644 --- a/src/util/ListUtil.re +++ b/src/util/ListUtil.re @@ -408,6 +408,15 @@ let suffix_at_depth = (xs: list('a), ys: list('a)): option(int) => { go(0, xs, ys); }; +let rec opt_zip = (xs: list('x), ys: list('y)): option(list(('x, 'y))) => + switch (xs, ys) { + | ([], [_, ..._]) + | ([_, ..._], []) => None + | ([], []) => Some([]) + | ([x, ...xs], [y, ...ys]) => + opt_zip(xs, ys) |> Option.map(xys => [(x, y), ...xys]) + }; + /* list truncated after at most n elementsnts */ let truncate = (n: int, xs: list('a)): list('a) => { let rec loop = (n, xs, acc) => @@ -469,6 +478,12 @@ let rec fold_left_opt = }; }; +let intersection = (xs, ys) => + List.filter((x: 'a) => List.exists((y: 'a) => x == y, ys), xs); + +let intersection_f = (f: 'a => 'b, xs, ys) => + List.filter((x: 'a) => List.exists((y: 'a) => f(x) == f(y), ys), xs); + let map_with_history = (f: (list('y), 'x) => 'y, xs: list('x)): list('y) => { let rec aux = (acc: list('y), remaining: list('x)) => { switch (remaining) { @@ -482,6 +497,14 @@ let map_with_history = (f: (list('y), 'x) => 'y, xs: list('x)): list('y) => { aux([], xs); }; +// This was removed when merging fix-1973... not sure why, so were a few others +// but this code depends on them... aybe there are List helpers instead? +let take_up_to_n = (n: int, xs: list('a)): list('a) => + switch (split_n_opt(n, xs)) { + | Some((xs, _)) => xs + | None => xs + }; + let rec fold_left2_opt = ( f: ('a, 'b, 'c) => option('a), diff --git a/src/util/OpenRouter.re b/src/util/OpenRouter.re index 3dc517c83b..d34ce66f37 100644 --- a/src/util/OpenRouter.re +++ b/src/util/OpenRouter.re @@ -17,10 +17,18 @@ type role = | Assistant | Tool(tool_contents); +[@deriving (show({with_path: false}), sexp, yojson)] +type tool_call = { + tool_name: string, + args: Json.t, + id: string, +}; + [@deriving (show({with_path: false}), sexp, yojson)] type message = { role, content: string, + tool_calls_json: list(Json.t), }; [@deriving (show({with_path: false}), sexp, yojson)] @@ -53,17 +61,10 @@ type usage = { total_tokens: int, }; -[@deriving (show({with_path: false}), sexp, yojson)] -type tool_call = { - id: string, - name: string, - args: Json.t, -}; - [@deriving (show({with_path: false}), sexp, yojson)] type reply = { content: string, - tool_call: option(tool_call), + tool_calls_json: list(Json.t), usage, }; @@ -93,11 +94,11 @@ let string_of_effort_level = let default_params = { model_id: "", - reasoning: Effort(Low), + reasoning: Exclude(true), temperature: 0.9, top_p: 1.0, tools: [], - stream: false, + stream: false, // Default to streaming messages: [], }; @@ -108,21 +109,26 @@ let set_to_default_params = (model_id: string): params => { }; }; -let mk_message = ({role, content}) => - switch (role) { - | Tool(tool_contents) => - `Assoc([ - ("role", `String(string_of_role(role))), - ("content", `String(content)), - ("tool_call_id", `String(tool_contents.tool_call_id)), - ("name", `String(tool_contents.name)), - ]) - | _ => - `Assoc([ - ("role", `String(string_of_role(role))), - ("content", `String(content)), - ]) - }; +let mk_message = ({role, content, tool_calls_json}) => { + let base = [ + ("role", `String(string_of_role(role))), + ("content", `String(content)), + ]; + + let with_tool_calls = + switch (role) { + | Tool({name, tool_call_id}) => + [("tool_call_id", `String(tool_call_id)), ("name", `String(name))] + @ base + | _ => + switch (tool_calls_json) { + | [] => base + | calls => [("tool_calls", `List(calls))] @ base + } + }; + + `Assoc(with_tool_calls); +}; let mk_reasoning = (reasoning: reasoning) => switch (reasoning) { @@ -133,15 +139,18 @@ let mk_reasoning = (reasoning: reasoning) => }; let body = (~params: params, messages: list(message)): Json.t => { - `Assoc([ - ("model", `String(params.model_id)), - ("reasoning", mk_reasoning(params.reasoning)), - ("temperature", `Float(params.temperature)), - ("top_p", `Float(params.top_p)), - ("messages", `List(List.map(mk_message, messages))), - ("tools", `List(params.tools)), - ("stream", `Bool(params.stream)), - ]); + let body = + `Assoc([ + ("model", `String(params.model_id)), + ("reasoning", mk_reasoning(params.reasoning)), + ("temperature", `Float(params.temperature)), + ("top_p", `Float(params.top_p)), + ("messages", `List(List.map(mk_message, messages))), + ("tools", params.tools == [] ? `Null : `List(params.tools)), + ("stream", `Bool(params.stream)), + ]); + print_endline("Outgoing body: " ++ Json.to_string(body)); + body; }; let chat = (~key, ~body, ~handler): unit => { @@ -189,42 +198,19 @@ let first_message_content = (choices: Json.t): option(string) => { Json.str(content); }; -let parse_tool_args = (args: Json.t): Json.t => { - switch (args) { - | `String(str) => - try(Yojson.Safe.from_string(str)) { - | _ => args - } - | json => json +let first_message_tool_call = (choices: Json.t): list(Json.t) => { + let get_tool_calls = (choices: Json.t): option(list(Json.t)) => { + let* choices = Json.list(choices); + let* hd = ListUtil.hd_opt(choices); + let* message = Json.dot("message", hd); + let* tool_calls = Json.dot("tool_calls", message); + let* tool_calls_json = Json.list(tool_calls); + Some(tool_calls_json); }; -}; - -let first_message_tool_call = (choices: Json.t): option(tool_call) => { - let* choices = Json.list(choices); - let* hd = ListUtil.hd_opt(choices); - let* message = Json.dot("message", hd); - - let* tool_calls = Json.dot("tool_calls", message); - let* tool_calls = Json.list(tool_calls); - let* tool_call = ListUtil.hd_opt(tool_calls); - - let* id = Json.dot("id", tool_call); - let* id = Json.str(id); - - let* tool_call = Json.dot("function", tool_call); - - let* name = Json.dot("name", tool_call); - let* name = Json.str(name); - let* args = Json.dot("arguments", tool_call); - - let parsed_args = parse_tool_args(args); - - let tool_call: tool_call = { - id, - name, - args: parsed_args, + switch (get_tool_calls(choices)) { + | Some(tool_calls_json) => tool_calls_json + | None => [] }; - Some(tool_call); }; let parse_errs = (json: Json.t): option(error) => { @@ -250,11 +236,11 @@ let handle_chat = (~db=ignore, response: option(Json.t)): option(result) => { let* choices = Json.dot("choices", json); let* usage = Json.dot("usage", json); let* content = first_message_content(choices); - let tool_call = first_message_tool_call(choices); + let tool_calls_json = first_message_tool_call(choices); let+ usage = of_usage(usage); Reply({ content, - tool_call, + tool_calls_json, usage, }); }; @@ -263,21 +249,26 @@ let handle_chat = (~db=ignore, response: option(Json.t)): option(result) => { let mk_system_msg = (content: string): message => { role: System, content, + tool_calls_json: [], }; let mk_user_msg = (content: string): message => { role: User, content, + tool_calls_json: [], }; -let mk_assistant_msg = (content: string): message => { +let mk_assistant_msg = + (content: string, tool_calls_json: list(Json.t)): message => { role: Assistant, content, + tool_calls_json, }; let mk_tool_msg = (content: string, tool_contents: tool_contents): message => { role: Tool(tool_contents), content, + tool_calls_json: [], }; [@deriving (show({with_path: false}), sexp, yojson)] @@ -290,6 +281,7 @@ type pricing = { type model_info = { id: string, name: string, + context_length: int, pricing, }; @@ -351,15 +343,17 @@ let parse_models_response = (json: Json.t): option(models_response) => let pricing_opt = List.assoc_opt("pricing", model_fields); let params_opt = List.assoc_opt("supported_parameters", model_fields); - + let context_length_opt = + List.assoc_opt("context_length", model_fields); if (!has_required_parameters(params_opt)) { None; } else { - switch (id_opt, name_opt, pricing_opt) { + switch (id_opt, name_opt, pricing_opt, context_length_opt) { | ( Some(`String(id)), Some(`String(name)), Some(`Assoc(pricing_fields)), + Some(`Int(context_length)), ) => let prompt = List.assoc_opt("prompt", pricing_fields); let completion = @@ -369,6 +363,7 @@ let parse_models_response = (json: Json.t): option(models_response) => Some({ id, name, + context_length, pricing: { prompt: p, completion: c, @@ -376,6 +371,21 @@ let parse_models_response = (json: Json.t): option(models_response) => }) | _ => None }; + | ( + Some(`String(id)), + Some(`String(name)), + _, + Some(`Int(context_length)), + ) => + Some({ + id, + name, + context_length, + pricing: { + prompt: "n/a", + completion: "n/a", + }, + }) | _ => None }; }; diff --git a/src/util/StringUtil.re b/src/util/StringUtil.re index 181bd4b951..e90097f323 100644 --- a/src/util/StringUtil.re +++ b/src/util/StringUtil.re @@ -136,6 +136,85 @@ let prefixes = (s: string): list(string) => { }; }; +/* Compute edit distance between two lists of strings using the Levenshtein algorithm */ +let levenshtein_list_distance = (a: list(string), b: list(string)): int => { + let a_len = List.length(a); + let b_len = List.length(b); + /* Fast-paths */ + if (a_len == 0) { + b_len; + } else if (b_len == 0) { + a_len; + } else { + let a_arr = Array.of_list(a); + let b_arr = Array.of_list(b); + let prev = Array.init(b_len + 1, i => i); + let curr = Array.make(b_len + 1, 0); + + let min3 = (x, y, z) => { + let m = + if (x < y) { + x; + } else { + y; + }; + if (m < z) { + m; + } else { + z; + }; + }; + + for (i in 1 to a_len) { + curr[0] = i; + let ai = a_arr[i - 1]; + for (j in 1 to b_len) { + let bj = b_arr[j - 1]; + let cost = + if (ai == bj) { + 0; + } else { + 1; + }; + let deletion = prev[j] + 1; + let insertion = curr[j - 1] + 1; + let substitution = prev[j - 1] + cost; + curr[j] = min3(deletion, insertion, substitution); + }; + /* copy curr into prev for next iteration */ + for (k in 0 to b_len) { + prev[k] = curr[k]; + }; + }; + prev[b_len]; + }; +}; + +let nearest_levenshtein_match = + (target: list(string), candidates: list(list(string))) + : option(list(string)) => + if (candidates == []) { + None; + } else { + let distances = + List.map( + candidate => + (candidate, levenshtein_list_distance(target, candidate)), + candidates, + ); + let (best_candidate, _best_distance) = + List.fold_left( + ((best_cand, best_dist), (cand, dist)) => + if (dist < best_dist) { + (cand, dist); + } else { + (best_cand, best_dist); + }, + List.hd(distances), + List.tl(distances), + ); + Some(best_candidate); + }; // Removes double quotes from string and escapes newlines // Update once https://github.com/hazelgrove/hazel/issues/786 is done let sanitize_for_string_expression = (s: string): string => { diff --git a/src/util/TimeUtil.re b/src/util/TimeUtil.re index d3378a5e3a..60147b9900 100644 --- a/src/util/TimeUtil.re +++ b/src/util/TimeUtil.re @@ -64,3 +64,35 @@ let format_time_diff = (prior: float): string => { : Printf.sprintf("%.0f days ago", diff_days); }; }; + +let format_duration_ms = (ms: float): string => { + /* + ms is in milliseconds; convert to human-friendly string + Examples: + <1000 -> "<1s" + 1500 -> "1.5s" + 62000 -> "1m 2s" + 3660000 -> "1h 1m 0s" + */ + let total_seconds = ms /. 1000.0; + if (total_seconds < 1.0) { + "<1s"; + } else if (total_seconds < 60.0) { + if (total_seconds < 10.0) { + Printf.sprintf("%.1fs", total_seconds); + } else { + Printf.sprintf("%.0fs", total_seconds); + }; + } else if (total_seconds < 3600.0) { + let mins = floor(total_seconds /. 60.0) |> int_of_float; + let secs = + int_of_float(floor(total_seconds -. float_of_int(mins) *. 60.0)); + Printf.sprintf("%dm %ds", mins, secs); + } else { + let hours = floor(total_seconds /. 3600.0) |> int_of_float; + let remainder = total_seconds -. float_of_int(hours) *. 3600.0; + let mins = floor(remainder /. 60.0) |> int_of_float; + let secs = int_of_float(floor(remainder -. float_of_int(mins) *. 60.0)); + Printf.sprintf("%dh %dm %ds", hours, mins, secs); + }; +}; diff --git a/src/web/Keyboard.re b/src/web/Keyboard.re index a1c7865b9b..0ff65578be 100644 --- a/src/web/Keyboard.re +++ b/src/web/Keyboard.re @@ -94,32 +94,20 @@ let handle_key_event = (k: Key.t): option(Action.t) => { | "e" => now(Move(Line(Right))) | _ => None } - | {key: D("f"), sys: PC, shift: Up, meta: Up, ctrl: Up, alt: Down} => - Some(Project(SetIndicated(Specific(Fold)))) - | {key: D("ƒ"), sys: Mac, shift: Up, meta: Up, ctrl: Up, alt: Down} => - /* Curly ƒ is what holding option turns f into on Mac */ - Some(Project(SetIndicated(Specific(Fold)))) - | {key: D("v"), sys: PC, shift: Up, meta: Up, ctrl: Up, alt: Down} => - Some(Project(SetIndicated(Specific(Probe)))) - | {key: D("√"), sys: Mac, shift: Up, meta: Up, ctrl: Up, alt: Down} => - /* √ is what holding option turns f into on Mac */ - Some(Project(SetIndicated(Specific(Probe)))) - | {key: D("t"), sys: PC, shift: Up, meta: Up, ctrl: Up, alt: Down} => - Some(Project(SetIndicated(Specific(Info)))) - | {key: D("†"), sys: Mac, shift: Up, meta: Up, ctrl: Up, alt: Down} => - /* † is what holding option turns t into on Mac */ - Some(Project(SetIndicated(Specific(Info)))) - | {key: D("l"), sys: PC, shift: Up, meta: Up, ctrl: Up, alt: Down} => - Some(Project(SetIndicated(ChooseLivelit))) - | {key: D("¬"), sys: Mac, shift: Up, meta: Up, ctrl: Up, alt: Down} => - /* † is what holding option turns t into on Mac */ - Some(Project(SetIndicated(ChooseLivelit))) - | {key: D("µ"), sys: Mac, shift: Up, meta: Up, ctrl: Up, alt: Down} => - Some(Dump) | {key: D(key), sys: _, shift: Up, meta: Up, ctrl: Up, alt: Down} => switch (key) { - | "ArrowLeft" => now(Move(Local(Left, ByToken))) - | "ArrowRight" => now(Move(Local(Right, ByToken))) + | "a" + | "å" => now(Move(Local(Left, ByToken))) + | "d" + | "∆" => now(Move(Local(Right, ByToken))) + | "f" + | "ƒ" => Some(Project(SetIndicated(Specific(Fold)))) + | "v" + | "√" => Some(Project(SetIndicated(Specific(Probe)))) + | "t" + | "†" => Some(Project(SetIndicated(Specific(Info)))) + | "l" + | "¬" => Some(Project(SetIndicated(ChooseLivelit))) | _ => None } | _ => None diff --git a/src/web/Main.re b/src/web/Main.re index 01379d1b7f..3bfe26d0f5 100644 --- a/src/web/Main.re +++ b/src/web/Main.re @@ -154,7 +154,9 @@ let start = { NinjaKeys.initialize(Shortcut.options(schedule_action)); JsUtil.focus_clipboard_shim(); schedule_action( - Assistant(AssistantUpdate.ChatAction(FilterLoadingMessages)), + Assistant( + Haz3lcore.AssistantUpdateAction.ChatAction(FilterLoadingMessages), + ), ); }; let%sub () = diff --git a/src/web/Settings.re b/src/web/Settings.re index 1f9582e5ef..0fa91bb455 100644 --- a/src/web/Settings.re +++ b/src/web/Settings.re @@ -1,4 +1,5 @@ open Util; +open Haz3lcore; module Model = { [@deriving (show({with_path: false}), sexp, yojson)] @@ -47,7 +48,7 @@ module Model = { highlight: NoHighlight, }, assistant: { - mode: CodeSuggestion, + mode: Composition, ongoing_chat: false, show_history: false, show_api_key: false, diff --git a/src/web/app/Page.re b/src/web/app/Page.re index 75766ce61d..e09bb8ffab 100644 --- a/src/web/app/Page.re +++ b/src/web/app/Page.re @@ -2,6 +2,7 @@ open Js_of_ocaml; open Virtual_dom.Vdom; open Node; open Util; +open Haz3lcore; /* The top-level UI component of Hazel */ @@ -32,7 +33,7 @@ module Store = { ~instructor_mode=globals.settings.instructor_mode, ); let explain_this = ExplainThisModel.Store.load(); - let assistant = AssistantModel.Store.load(); + let assistant = AssistantWeb.Store.load(); { editors, globals, @@ -49,7 +50,7 @@ module Store = { ); Globals.Model.save(m.globals); ExplainThisModel.Store.save(m.explain_this); - AssistantModel.Store.save(m.assistant); + AssistantWeb.Store.save(m.assistant); }; }; @@ -66,7 +67,7 @@ module Update = { | Globals(Globals.Update.t) | Editors(Editors.Update.t) | ExplainThis(ExplainThisUpdate.update) - | Assistant(AssistantUpdate.t) + | Assistant(AssistantWeb.Update.t) | MakeActive(selection) | Benchmark(benchmark_action) | Start @@ -80,11 +81,11 @@ module Update = { model: Model.t, editor: CodeEditable.Model.t, ) => - AssistantUpdate.check_req( + AssistantModes.Completion.check_req( ~schedule_action=a => schedule_action(Assistant(a)), ~schedule_setting=a => schedule_action(Globals(Set(Assistant(a)))), ~chat_id=model.assistant.current_chats.curr_suggestion_chat, - ~editor, + ~z=editor.editor.state.zipper, ); let get_editor = (model: Model.t): CodeEditable.Model.t => @@ -271,14 +272,20 @@ module Update = { explain_this, }; | Assistant(action) => + let schedule_editor_action = a => { + let perform_action = CodeEditable.Update.Perform(a); + let cell_action = CellEditor.Update.MainEditor(perform_action); + let scratch_action = Editors.Update.Scratch(CellAction(cell_action)); + schedule_action(Editors(scratch_action)); + }; let* assistant = - AssistantUpdate.update( + AssistantWeb.Update.update( ~action, - ~settings=globals.settings, + ~settings=globals.settings.assistant, ~model=model.assistant, ~editor=get_editor(model), ~schedule_action=a => schedule_action(Assistant(a)), - ~schedule_editor_action=a => schedule_action(Editors(a)), + ~schedule_editor_action, ); { ...model, @@ -625,7 +632,6 @@ module View = { | MakeActive(s) => inject(MakeActive(Scratch(s))), ~explainThisModel, ~assistantModel, - ~editor=Update.get_editor(model), cursor.info, ); diff --git a/src/web/app/ai-assistant-web/AssistantView.re b/src/web/app/ai-assistant-web/AssistantView.re new file mode 100644 index 0000000000..5266879835 --- /dev/null +++ b/src/web/app/ai-assistant-web/AssistantView.re @@ -0,0 +1,2109 @@ +open Haz3lcore; +open Virtual_dom.Vdom; +open Node; +open Util.WebUtil; +open Util; +open Js_of_ocaml; +open Key; + +module Update = AssistantUpdateAction; + +module UpdateBase = AssistantUpdate; + +module Model = AssistantModel; + +type selection = + | MakeActive(Selection.t); + +type event = + | MakeActive(ScratchMode.Selection.t); + +type unwrapped_message = { + display: Model.display, + role: Model.role, + sketch_snapshot: Model.sketch_snapshot, +}; + +let resume_chat_button = + (~inject_global: Globals.Action.t => Ui_effect.t(unit)): Node.t => { + let tooltip = "Confirm and Chat"; + let resume_chat = _ => + Virtual_dom.Vdom.Effect.Many([ + inject_global(Set(Assistant(UpdateChatStatus))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[ + clss(["chat-button"]), + Attr.create("data-testid", "confirm-and-chat-btn"), + Attr.on_click(resume_chat), + ], + [Widgets.button_named(~tooltip, None, resume_chat)], + ); +}; + +let settings_button = + (~inject_global: Globals.Action.t => Ui_effect.t(unit)): Node.t => { + let tooltip = "Settings"; + let end_chat = _ => + Virtual_dom.Vdom.Effect.Many([ + inject_global(Set(Assistant(UpdateChatStatus))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["chat-button"]), Attr.on_click(end_chat)], + [ + Widgets.button_named(~tooltip, None, _ => Virtual_dom.Vdom.Effect.Ignore), + ], + ); +}; + +let new_chat_button = (~inject, ~model: Model.t): Node.t => { + let tooltip = "New Chat"; + let new_chat_action = + model.current_chats.curr_tutor_chat == Id.invalid + ? [] : [inject(Update.ChatAction(NewChat))]; + let new_chat = _ => + Virtual_dom.Vdom.Effect.Many( + new_chat_action @ [Virtual_dom.Vdom.Effect.Stop_propagation], + ); + div( + ~attrs=[clss(["add-button"]), Attr.on_click(new_chat)], + [ + Widgets.button(~tooltip, Icons.add, _ => Virtual_dom.Vdom.Effect.Ignore), + ], + ); +}; + +let history_button = + (~inject, ~inject_global: Globals.Action.t => Ui_effect.t(unit)): Node.t => { + let tooltip = "Past Chats"; + let history = _ => + Virtual_dom.Vdom.Effect.Many([ + inject(Update.ChatAction(CollapseMessage(-1))), // Hacky way to collapse any active prompt display + inject_global(Set(Assistant(ToggleHistory))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["history-button"]), Attr.on_click(history)], + [ + Widgets.button(~tooltip, Icons.history, _ => + Virtual_dom.Vdom.Effect.Ignore + ), + ], + ); +}; + +let select_llm = + (~inject: Update.t => Ui_effect.t(unit), ~model: Model.t): Node.t => { + let handle_change = (event, _) => { + let value = Js.to_string(Js.Unsafe.coerce(event)##.target##.value); + let selected_model = + List.find_opt( + (m: OpenRouter.model_info) => m.id == value, + model.external_api_info.available_models, + ); + switch (selected_model) { + | Some(model_info) => + Virtual_dom.Vdom.Effect.Many([ + inject(ExternalAPIAction(SetLLM(model_info))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]) + | None => Virtual_dom.Vdom.Effect.Ignore + }; + }; + + div( + ~attrs=[clss(["llm-selector"])], + [ + label(~attrs=[clss(["llm-label"])], [text("LLM Model")]), + select( + ~attrs=[Attr.on_change(handle_change), clss(["llm-dropdown"])], + List.map( + (open_router_model: OpenRouter.model_info) => + option( + ~attrs=[ + Attr.value(open_router_model.id), + switch (model.external_api_info.set_model_info.id) { + | "" => Attr.empty + | current_model => + if (current_model == open_router_model.id) { + Attr.selected; + } else { + Attr.empty; + } + }, + ], + [text(open_router_model.name)], + ), + model.external_api_info.available_models, + ), + ), + ], + ); +}; + +let api_input = + ( + ~inject: Update.t => Ui_effect.t(unit), + ~inject_global: Globals.Action.t => Ui_effect.t(unit), + ~signal, + ~model: Model.t, + ~settings: AssistantSettings.t, + ) + : Node.t => { + let init_assistant_model = + model.current_chats.curr_tutor_chat == Id.invalid + ? [inject(Update.InitializeAssistant)] : []; + let handle_submission = (api_key: string) => { + Virtual_dom.Vdom.Effect.Many( + init_assistant_model + @ [ + inject(ExternalAPIAction(SetAPIKey(api_key))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ], + ); + }; + + let toggle_visibility = _ => + Virtual_dom.Vdom.Effect.Many([ + inject_global(Set(Assistant(ToggleAPIKeyVisibility))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + + let submit_key = _ => { + let message = + Js.Opt.case( + Dom_html.document##getElementById(Js.string("api-input")), + () => "", + el => + switch (Js.Unsafe.coerce(el)) { + | input => Js.to_string(input##.value) + }, + ); + Js.Opt.case( + Dom_html.document##getElementById(Js.string("api-input")), + () => (), + el => Js.Unsafe.coerce(el)##.value := Js.string(""), + ); + handle_submission(message); + }; + + let handle_keydown = event => { + let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); + switch (key) { + | Some("Enter") => submit_key() + | _ => Virtual_dom.Vdom.Effect.Ignore + }; + }; + + div( + ~attrs=[clss(["api-key-container"])], + [ + div(~attrs=[clss(["title"])], [text("API Key")]), + div( + ~attrs=[clss(["assistant-info-container"])], + [ + text("Get an OpenRouter API key "), + a( + ~attrs=[ + Attr.href("https://openrouter.ai/settings/keys"), + Attr.target("_blank"), + ], + [text("here")], + ), + text("."), + ], + ), + div( + ~attrs=[clss(["llm-selector"])], + [ + label(~attrs=[clss(["llm-label"])], [text("New API Key")]), + input( + ~attrs=[ + Attr.id("api-input"), + Attr.placeholder("Click to enter your OpenRouter API key"), + Attr.type_("password"), + Attr.property("autocomplete", Js.Unsafe.inject("off")), + Attr.on_focus(_ => + signal(MakeActive(ScratchMode.Selection.TextBox)) + ), + Attr.on_keydown(handle_keydown), + clss(["api-input"]), + Attr.on_copy(_ => {Effect.Stop_propagation}), + Attr.on_paste(_ => {Effect.Stop_propagation}), + Attr.on_cut(_ => {Effect.Stop_propagation}), + ], + (), + ), + ], + ), + div( + ~attrs=[ + clss(["chat-button"]), + Attr.create("data-testid", "update-api-key-btn"), + Attr.on_click(submit_key), + ], + [Widgets.button_named(~tooltip="Update API Key", None, submit_key)], + ), + div( + ~attrs=[clss(["llm-selector"])], + [ + label( + ~attrs=[clss(["llm-label"])], + [ + text("Current API Key\n"), + div( + ~attrs=[clss(["toggle-show-button"])], + [ + Widgets.button( + ~tooltip="Show/Hide Key", + settings.show_api_key ? Icons.visible : Icons.invisible, + toggle_visibility, + ), + ], + ), + ], + ), + div( + ~attrs=[clss(["api-key-row"])], + [ + div( + ~attrs=[ + clss(["api-key-display"]), + Attr.id("api-key-display"), + ], + [ + text( + switch (model.external_api_info.api_key) { + | "" => "No API key set" + | key => + settings.show_api_key + ? key : String.make(String.length(key), '*') + }, + ), + ], + ), + ], + ), + ], + ), + ], + ); +}; + +let llm_model_id_input = + (~inject: Update.t => Ui_effect.t(unit), ~signal, ~model: Model.t) + : Node.t => { + let format_price_per_million = (price: string): string => { + // Convert string to float, multiply by 1000 to get per million tokens + // The API provides price per 1K tokens + switch (float_of_string_opt(price)) { + | Some(p) => + let per_million = p *. 1000000.0; + if (per_million == 0.0) { + "Free"; + } else { + "$" ++ Printf.sprintf("%.4f", per_million); + }; + | None => "Unknown" + }; + }; + + let handle_submission = (llm_model: string) => { + let selected_model = + List.find_opt( + (m: OpenRouter.model_info) => m.id == llm_model, + model.external_api_info.available_models, + ); + switch (selected_model) { + | Some(model_info) => + Virtual_dom.Vdom.Effect.Many([ + inject(ExternalAPIAction(SetLLM(model_info))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]) + | None => Virtual_dom.Vdom.Effect.Ignore + }; + }; + + let submit_key = _ => { + let message = + Js.Opt.case( + Dom_html.document##getElementById(Js.string("llm-model-id-input")), + () => "", + el => + switch (Js.Unsafe.coerce(el)) { + | input => Js.to_string(input##.value) + }, + ); + Js.Opt.case( + Dom_html.document##getElementById(Js.string("llm-model-id-input")), + () => (), + el => Js.Unsafe.coerce(el)##.value := Js.string(""), + ); + handle_submission(message); + }; + + let handle_keydown = event => { + let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); + switch (key) { + | Some("Enter") => submit_key() + | _ => Virtual_dom.Vdom.Effect.Ignore + }; + }; + + div( + ~attrs=[clss(["api-key-container"])], + [ + div(~attrs=[clss(["title"])], [text("Model Selection")]), + div( + ~attrs=[clss(["assistant-info-container"])], + [ + text("See available OpenRouter models "), + a( + ~attrs=[ + Attr.href("https://openrouter.ai/models"), + Attr.target("_blank"), + ], + [text("here")], + ), + text("."), + ], + ), + select_llm(~inject, ~model), + div( + ~attrs=[clss(["llm-selector"])], + [ + div( + ~attrs=[clss(["llm-label"])], + [text("Or Enter Model ID Manually")], + ), + input( + ~attrs=[ + Attr.id("llm-model-id-input"), + Attr.placeholder("Enter the ID of an OpenRouter model"), + Attr.type_("text"), + Attr.property("autocomplete", Js.Unsafe.inject("off")), + Attr.on_focus(_ => + signal(MakeActive(ScratchMode.Selection.TextBox)) + ), + Attr.on_keydown(handle_keydown), + clss(["llm-model-id-input"]), + Attr.on_copy(_ => {Effect.Stop_propagation}), + Attr.on_paste(_ => {Effect.Stop_propagation}), + Attr.on_cut(_ => {Effect.Stop_propagation}), + ], + (), + ), + ], + ), + div( + ~attrs=[ + clss(["chat-button"]), + Attr.create("data-testid", "update-model-btn"), + Attr.on_click(submit_key), + ], + [Widgets.button_named(~tooltip="Update Model", None, submit_key)], + ), + div( + ~attrs=[clss(["llm-selector"])], + [ + label(~attrs=[clss(["llm-label"])], [text("Current Model\n")]), + div( + ~attrs=[clss(["api-key-display"]), Attr.id("api-key-display")], + [ + text( + switch (model.external_api_info.set_model_info.id) { + | "" => "No model ID set" + | model_id => model_id + }, + ), + ], + ), + ], + ), + div( + ~attrs=[clss(["llm-selector"])], + [ + label( + ~attrs=[clss(["llm-label"])], + [text("Model Pricing (per million tokens)\n")], + ), + div( + ~attrs=[clss(["api-key-display"])], + [ + text( + switch (model.external_api_info.set_model_info.id) { + | "" => "No model selected" + | model_id => + let selected_model = + List.find_opt( + (model: OpenRouter.model_info) => model.id == model_id, + model.external_api_info.available_models, + ); + switch (selected_model) { + | Some(model) => + "Prompt: " + ++ format_price_per_million(model.pricing.prompt) + ++ " / Completion: " + ++ format_price_per_million(model.pricing.completion) + | None => "Pricing information not available" + }; + }, + ), + ], + ), + ], + ), + ], + ); +}; + +let mk_input_handlers = + ( + settings: AssistantSettings.t, + model: Model.t, + index: option(int), + inject: Update.t => Ui_effect.t(unit), + signal: event => Ui_effect.t(unit), + which_input: string, + ) => { + let mode = settings.mode; + let curr_chat_id = + switch (mode) { + | CodeSuggestion => model.current_chats.curr_suggestion_chat + | Composition => model.current_chats.curr_composition_chat + | HazelTutor => model.current_chats.curr_tutor_chat + }; + let curr_messages = + switch (mode) { + | CodeSuggestion => + Id.Map.find(curr_chat_id, model.chat_history.past_suggestion_chats). + messages + | Composition => + Id.Map.find(curr_chat_id, model.chat_history.past_composition_chats). + messages + | HazelTutor => + Id.Map.find(curr_chat_id, model.chat_history.past_tutor_chats).messages + }; + let editor_opt = + switch (index) { + | Some(idx) => List.nth(curr_messages, idx).sketch_snapshot + | None => None + }; + let handle_send = (content: string) => { + Js_of_ocaml.Firebug.console##log( + Js_of_ocaml.Js.string("Message sent: " ++ content), + ); + Virtual_dom.Vdom.Effect.Many([ + switch (index) { + | Some(index) => inject(Update.ChatAction(Lop(index))) + | _ => Virtual_dom.Vdom.Effect.Ignore + }, + signal(MakeActive(ScratchMode.Selection.Cell(MainEditor))), + switch (mode) { + | HazelTutor => + inject( + Update.SendMessage( + Tutor(content), + editor_opt, + model.current_chats.curr_tutor_chat, + ), + ) + | CodeSuggestion => + inject( + Update.SendMessage( + Completion(Query(content)), + editor_opt, + model.current_chats.curr_suggestion_chat, + ), + ) + | Composition => + inject( + Update.SendMessage( + Composition(Request(content), false), + editor_opt, + model.current_chats.curr_composition_chat, + ), + ) + }, + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + }; + let (past_chats, curr_chat) = + UpdateBase.get_mode_info(settings.mode, model); + let curr_messages = Id.Map.find(curr_chat.id, past_chats).messages; + let send_message = _ => { + let message = + Js.Opt.case( + Dom_html.document##getElementById(Js.string(which_input)), + () => "", + el => + switch (Js.Unsafe.coerce(el)) { + | input => Js.to_string(input##.value) + }, + ); + switch (index) { + | Some(_) => + Js.Opt.case( + Dom_html.document##getElementById(Js.string(which_input)), + () => (), + el => {Js.Unsafe.coerce(el)##blur()}, + ) + | _ => + Js.Opt.case( + Dom_html.document##getElementById(Js.string(which_input)), + () => (), + el => { + Js.Unsafe.coerce(el)##.value := Js.string(""); + Js.Unsafe.coerce(el)##blur(); + }, + ) + }; + handle_send(message); + }; + let handle_keydown = event => { + let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); + let shift_pressed = shift_held(event); + switch (key, ListUtil.last_opt(curr_messages)) { + | (Some("Enter"), _) when !shift_pressed => send_message() + | _ => Virtual_dom.Vdom.Effect.Ignore + }; + }; + (send_message, handle_keydown); +}; + +let context_usage_display = + (~model: Model.t, ~settings: AssistantSettings.t): Node.t => { + let (past_chats, curr_chat) = + UpdateBase.get_mode_info(settings.mode, model); + let curr_chat_data = Id.Map.find(curr_chat.id, past_chats); + let context_length = model.external_api_info.set_model_info.context_length; + let context_usage = curr_chat_data.context_usage; + + let percentage = + if (context_length > 0) { + int_of_float( + float_of_int(context_usage) /. float_of_int(context_length) *. 100.0, + ); + } else { + 0; + }; + + let usage_text = string_of_int(percentage) ++ "%"; + + div(~attrs=[clss(["context-usage-display"])], [text(usage_text)]); +}; + +let message_input = + (~signal, ~inject, ~model: Model.t, ~settings: AssistantSettings.t) + : Node.t => { + let (send_message, handle_keydown) = + mk_input_handlers(settings, model, None, inject, signal, "message-input"); + div( + ~attrs=[clss(["input-container"])], + [ + context_usage_display(~model, ~settings), + input( + ~attrs=[ + Attr.id("message-input"), + Attr.placeholder( + switch (settings.mode) { + | HazelTutor => "Ask a question about Hazel (or anything)..." + | CodeSuggestion => "Followup with a question about the agent's code suggestion..." + | Composition => "Ask the agent to help clarify, plan, or write code..." + }, + ), + Attr.type_("text"), + Attr.property("autocomplete", Js.Unsafe.inject("off")), + Attr.on_focus(_ => + signal(MakeActive(ScratchMode.Selection.TextBox)) + ), + Attr.on_copy(_ => {Effect.Stop_propagation}), + Attr.on_paste(_ => {Effect.Stop_propagation}), + Attr.on_cut(_ => {Effect.Stop_propagation}), + Attr.on_keydown(handle_keydown), + clss(["message-input"]), + ], + (), + ), + if ({ + let mode = settings.mode; + let (_, curr_chat) = UpdateBase.get_mode_info(mode, model); + curr_chat.awaiting_response; + }) { + div( + ~attrs=[ + clss(["quit-button", "icon"]), + Attr.on_click(_ => { + Virtual_dom.Vdom.Effect.Many([ + inject(Update.EmployLLMAction(Update.Quit)), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]) + }), + Attr.title("Quit Request"), + ], + [Icons.cancel], + ); + } else { + switch ( + ListUtil.last_opt( + [] /*curr_messages*/ /* todo: change. temporarily repressed.*/, + ) + ) { + // todo: change, maybe to allowing for stop button to cancel request + | Some( + { + displayable_content: [Text("...")], + raw_content: "...", + collapsed: false, + }: Model.display, + ) => + div( + ~attrs=[ + clss(["send-button-disabled", "icon"]), + Attr.title("Submitting Message Disabled"), + ], + [Icons.send], + ) + | _ => + div( + ~attrs=[ + clss(["send-button", "icon"]), + Attr.on_click(send_message), + Attr.title("Submit Message"), + ], + [Icons.send], + ) + }; + }, + ], + ); +}; + +/* + // Indicates that the LLM is thinking/responding + let loading_dots = () => { + div( + ~attrs=[clss(["loading-dots"])], + [ + div(~attrs=[clss(["dot", "dot1"])], []), + div(~attrs=[clss(["dot", "dot2"])], []), + div(~attrs=[clss(["dot", "dot3"])], []), + ], + ); + }; + */ + +let form_collapse_toggle = + ( + ~message: unwrapped_message, + ~toggle_collapse, + ~index: int, + ~is_first: bool, + ~is_last: bool, + ) + : Node.t => + if (message.display.collapsed && is_first) { + div( + ~attrs=[ + clss(["collapse-indicator"]), + Attr.on_click(_ => toggle_collapse(index)), + String.length(message.display.raw_content) + >= AssistantSettings.max_collapsed_length + ? Attr.empty : Attr.hidden, + ], + [text("▼ Show more")], + ); + } else if (!message.display.collapsed + && String.length(message.display.raw_content) + >= AssistantSettings.max_collapsed_length + && is_last) { + div( + ~attrs=[ + clss(["collapse-indicator"]), + Attr.on_click(_ => toggle_collapse(index)), + String.length(message.display.raw_content) + >= AssistantSettings.max_collapsed_length + ? Attr.empty : Attr.hidden, + ], + [text("▲ Show less")], + ); + } else { + None; + }; + +let mk_translation = (~text: string): list(Node.t) => { + let omd = Omd.of_string(text); + //print_markdown(omd); + + let rec translate_inline = + (inline: Omd.inline(_), msg: list(Node.t)): list(Node.t) => { + switch (inline) { + | Omd.Concat(_, items) => + let nodes = + List.fold_left( + (msg, item) => { + let translated_item = translate_inline(item, []); + List.concat([msg, translated_item]); + }, + [], + items, + ); + List.append(msg, nodes); + | Omd.Text(_, d) => + // Split on '\n' and interleave
nodes + let lines = String.split_on_char('\n', d); + let num_lines = List.length(lines); + let nodes = + List.concat( + List.mapi( + (i, line) => + if (i < num_lines - 1) { + [Node.text(line), Node.br()]; + } else { + [Node.text(line)]; + }, + lines, + ), + ); + List.append(msg, nodes); + | Omd.Code(_, d) => + List.append( + msg, + [ + Node.span( + ~attrs=[ + Attr.style( + Css_gen.create(~field="font-family", ~value="monospace"), + ), + ], + [Node.text(d)], + ), + ], + ) + | Omd.Link(_, {label, destination, _}) => + let d = translate_inline(label, []); + let inner_msg = + Node.a( + ~attrs=[ + Attr.href(destination), + Attr.target("_blank") // open in new tab + ], + d, + ); + List.append(msg, [inner_msg]); + | Omd.Emph(_, d) => + let d = translate_inline(d, []); + List.append( + msg, + [ + Node.span( + ~attrs=[ + Attr.style( + Css_gen.create(~field="font-style", ~value="italic"), + ), + ], + d, + ), + ], + ); + | Omd.Strong(_, d) => + let d = translate_inline(d, []); + List.append( + msg, + [ + Node.span( + ~attrs=[ + Attr.style( + Css_gen.create(~field="font-weight", ~value="bold"), + ), + ], + d, + ), + ], + ); + | Omd.Soft_break(_) => List.append(msg, [Node.br()]) + | Omd.Hard_break(_) => List.append(msg, [Node.br(), Node.br()]) + | _ => msg + }; + }; + + let rec translate_block = (doc: Omd.doc): list(Node.t) => { + List.fold_left( + (msg, elem) => { + switch (elem) { + | Omd.Paragraph(_, d) => translate_inline(d, msg) + | Omd.List(_, _, _, items) => + let bullets = + List.fold_left( + (nodes, d) => { + let n = translate_block(d); + List.append(nodes, [Node.li(n)]); + }, + [], + items, + ); + List.append(msg, [Node.ul(bullets)]); + | Omd.Blockquote(_, d) => + List.append(msg, [Node.blockquote(translate_block(d))]) + | Omd.Heading(_, i, d) => + switch (i) { + | 1 => List.append(msg, [Node.h1(translate_inline(d, []))]) + | 2 => List.append(msg, [Node.h2(translate_inline(d, []))]) + | 3 => List.append(msg, [Node.h3(translate_inline(d, []))]) + | 4 => List.append(msg, [Node.h4(translate_inline(d, []))]) + | 5 => List.append(msg, [Node.h5(translate_inline(d, []))]) + | 6 => List.append(msg, [Node.h6(translate_inline(d, []))]) + | _ => msg + } + | Omd.Thematic_break(_) => List.append(msg, [Node.hr()]) + | Omd.Code_block(_, _, d) => + // Todo: Potentially remove parse_blocks and favor pure markdown instead. + // This will require a bit of rewiring, however. + List.append(msg, [Node.text(d)]) + | Omd.Html_block(_, d) => List.append(msg, [Node.text(d)]) + | _ => msg + } + }, + [], + doc, + ); + }; + [Node.br()] @ translate_block(omd); +}; + +let text_block = + ( + ~message: unwrapped_message, + ~content: string, + ~toggle_collapse, + ~index: int, + ~signal, + ~settings: AssistantSettings.t, + ~model: Model.t, + ~inject: Update.t => Ui_effect.t(unit), + ~is_first: bool, + ~is_last: bool, + ) + : Node.t => + if (message.role == User) { + let unique_id = "user-message-input-" ++ string_of_int(index); + let (send_message, handle_keydown) = + mk_input_handlers( + settings, + model, + Some(index), + inject, + signal, + unique_id, + ); + + // Auto-resize textarea when first rendered with content + JsUtil.delay(0.0, () => JsUtil.autosize_textarea(unique_id)); + div( + ~attrs=[clss(["user-message-input-container"])], + [ + textarea( + ~attrs=[ + clss(["user-message-input"]), + Attr.id(unique_id), + Attr.value(content), + Attr.placeholder( + switch (settings.mode) { + | HazelTutor => "Ask a question about Hazel (or anything)..." + | CodeSuggestion => "Followup with a question about the agent's code suggestion..." + | Composition => "Ask the agent to help clarify, plan, or write code..." + }, + ), + Attr.property("autocomplete", Js.Unsafe.inject("off")), + Attr.on_focus(_ => { + JsUtil.autosize_textarea(unique_id); + signal(MakeActive(ScratchMode.Selection.TextBox)); + }), + Attr.on_input(_ => { + JsUtil.autosize_textarea(unique_id); + _ => Virtual_dom.Vdom.Effect.Ignore; + }), + Attr.on_copy(_ => {Effect.Stop_propagation}), + Attr.on_paste(_ => { + JsUtil.delay(0.0, () => JsUtil.autosize_textarea(unique_id)); + Effect.Stop_propagation; + }), + Attr.on_cut(_ => {Effect.Stop_propagation}), + Attr.on_keydown(handle_keydown), + ], + [text(content)], + ), + div( + ~attrs=[ + clss(["user-send-button", "icon"]), + Attr.on_click(send_message), + Attr.title("Submit Message"), + ], + [Icons.send], + ), + ], + ); + } else { + div( + ~attrs=[ + clss([ + switch (message.role) { + | User => "user-message" + | Assistant => "llm-message" + | System(AssistantPrompt) => "system-prompt-message" + | System(AgentView) => "system-prompt-message" + | System(AgentWorkbench) => "system-todo-list-message" + | System(InternalError) => "system-error-message" + | Tool => "tool-message" + }, + ]), + Attr.on_copy(_ => {Effect.Stop_propagation}), + Attr.on_paste(_ => {Effect.Stop_propagation}), + Attr.on_cut(_ => {Effect.Stop_propagation}), + Attr.on_click(_ => {Effect.Stop_propagation}), + ], + { + let content = + message.display.collapsed + && String.length(message.display.raw_content) + >= AssistantSettings.max_collapsed_length + ? String.concat( + "", + [ + String.sub( + content, + 0, + min( + String.length(content), + AssistantSettings.max_collapsed_length, + ), + ), + "...", + ], + ) + : content; + [text(content)] + // TODO: fix markdown rendering. for now, just displaying raw content. + //mk_translation(~text=content) + @ [ + form_collapse_toggle( + ~message, + ~toggle_collapse, + ~index, + ~is_first, + ~is_last, + ), + ]; + }, + ); + }; + +let code_block = + ( + ~message: unwrapped_message, + ~sketch: Segment.t, + ~toggle_collapse, + ~index: int, + ~is_first: bool, + ~is_last: bool, + ~globals: Globals.t, + ) + : Node.t => { + div( + ~attrs=[ + clss([ + "example", + switch (message.role) { + | User => "user" + | Assistant => "llm" + | System(AssistantPrompt) => "system-prompt" + | System(AgentView) => "system-prompt" + | System(InternalError) => "system-error" + | Tool => "tool" + | System(AgentWorkbench) => "system-todo-list" + }, + ]), + ], + [ + CellEditor.View.view( + ~globals, + ~signal=_ => Ui_effect.Ignore, + ~inject=_ => Ui_effect.Ignore, + ~selected=None, + ~caption=None, + ~locked=true, + message.role == Assistant + ? { + sketch |> Zipper.unzip |> Editor.Model.mk |> CellEditor.Model.mk; + } + : { + sketch + |> Zipper.unzip + |> Editor.Model.mk + |> CellEditor.Model.mk + |> CellEditor.Update.calculate( + ~settings={ + statics: false, + elaborate: false, + assist: false, + dynamics: false, + flip_animations: false, + evaluation: { + show_case_clauses: false, + show_fn_bodies: false, + show_fixpoints: false, + show_ascription_steps: false, + show_lookup_steps: false, + show_stepper_filters: false, + stepper_history: false, + show_settings: false, + show_hidden_steps: false, + enable_proof: false, + }, + }, + ~is_edited=true, + ~stitch=x => x, + ~queue_worker=None, + ); + }, + ), + form_collapse_toggle( + ~message, + ~toggle_collapse, + ~index, + ~is_first, + ~is_last, + ), + ], + ); +}; + +let form_block = + ( + ~message: unwrapped_message, + ~block: Model.block_kind, + ~toggle_collapse, + ~index: int, + ~is_first: bool, + ~is_last: bool, + ~globals: Globals.t, + ~settings: AssistantSettings.t, + ~signal, + ~model: Model.t, + ~inject: Update.t => Ui_effect.t(unit), + ) + : Node.t => + if (!message.display.collapsed || message.display.collapsed && is_first) { + switch (block) { + | Text(content) => + text_block( + ~message, + ~content, + ~toggle_collapse, + ~index, + ~is_first, + ~is_last, + ~settings, + ~signal, + ~model, + ~inject, + ) + | Code(sketch) => + code_block( + ~message, + ~sketch, + ~toggle_collapse, + ~index, + ~is_first, + ~is_last, + ~globals, + ) + }; + } else { + None; + }; + +let initial_display = + (~model: Model.t, ~settings: AssistantSettings.t): Node.t => { + let (past_chats, curr_chat) = + UpdateBase.get_mode_info(settings.mode, model); + let curr_messages = Id.Map.find(curr_chat.id, past_chats).messages; + List.length(curr_messages) <= 1 + ? div( + ~attrs=[clss(["initial-display"])], + [ + Icons.corylus, + div( + ~attrs=[clss(["initial-display-text"])], + [ + text( + switch (settings.mode) { + | HazelTutor => "Hi, I'm Corylus. Let's learn about Hazel together." + | CodeSuggestion => "Hi, I'm Corylus. Type '??' in an expression hole and see the suggestion dialogue here." + | Composition => "Hi, I'm Corylus. Let's work on your task together." + }, + ), + ], + ), + div( + ~attrs=[clss(["disclaimer-display-text"])], + [ + text( + "AI-based technologies, such as Corylus, are prone to making mistakes. Always verify critical information independently.", + ), + ], + ), + ], + ) + : None; +}; + +let message_display = + ( + ~globals: Globals.t, + ~inject, + ~model: Model.t, + ~settings: AssistantSettings.t, + ~signal, + ) + : Node.t => { + let toggle_collapse = (is_system_prompt, index) => { + // Create an action to toggle the collapsed state of a specific message + Virtual_dom.Vdom.Effect.Many( + if (is_system_prompt && settings.show_history) { + [globals.inject_global(Set(Assistant(ToggleHistory)))]; + } else { + { + []; + } + @ [ + inject(Update.ChatAction(CollapseMessage(index))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]; + }, + ); + }; + let (past_chats, curr_chat) = + UpdateBase.get_mode_info(settings.mode, model); + let curr_messages = Id.Map.find(curr_chat.id, past_chats).messages; + let message_nodes = + List.flatten( + List.mapi( + (index: int, message: Model.message) => { + switch (message.display) { + | None => [] + | Some(display) => + let message = { + display, + role: message.role, + sketch_snapshot: message.sketch_snapshot, + }; + let is_last_message = index == List.length(curr_messages) - 1; + [ + div( + ~attrs=[ + clss([ + "message-container", + switch (message.role) { + | User => "user" + | Assistant => "llm" + | System(AssistantPrompt) => "system-prompt" + | System(InternalError) => "system-error" + | Tool => "tool" + | System(AgentView) => "system-agent-view" + | System(AgentWorkbench) => "system-todo-list" + }, + ]), + is_last_message ? Attr.id("last-message") : Attr.empty, + ], + [ + div( + ~attrs=[clss(["message-identifier-row"])], + [ + div( + ~attrs=[clss(["message-identifier"])], + [ + switch (message.role) { + | User => text("User") + | Assistant => + switch (settings.mode) { + | CodeSuggestion => + div( + ~attrs=[clss(["llm-identifier"])], + [Icons.corylus, text("Corylus")], + ) + | Composition => + div( + ~attrs=[clss(["llm-identifier"])], + [Icons.corylus, text("Corylus")], + ) + | HazelTutor => + div( + ~attrs=[clss(["llm-identifier"])], + [Icons.corylus, text("Corylus")], + ) + } + + | System(AssistantPrompt) => + div( + ~attrs=[clss(["system-prompt-identifier"])], + [text("System")], + ) + | System(AgentView) => + div( + ~attrs=[ + clss(["system-agent-view-identifier"]), + ], + [text("Agent View")], + ) + | System(AgentWorkbench) => + div( + ~attrs=[clss(["system-todo-list-identifier"])], + [text("Todo List")], + ) + | System(InternalError) => + div( + ~attrs=[clss(["system-error-identifier"])], + [text("System")], + ) + | Tool => + div( + ~attrs=[clss(["tool-identifier"])], + [text("Tool")], + ) + }, + ], + ), + switch (message.role) { + | System(AssistantPrompt) => + div( + ~attrs=[clss(["show-prompt-button"])], + [ + Widgets.button(~tooltip="Show Prompt", Icons.doc, _ => + toggle_collapse(true, index) + ), + ], + ) + | System(AgentView) => + div( + ~attrs=[clss(["show-prompt-button"])], + [ + Widgets.button( + ~tooltip="Show Agent View", Icons.agent_view, _ => + toggle_collapse(true, index) + ), + ], + ) + | _ => None + }, + ], + ), + ] + @ { + message.role == System(AssistantPrompt) + || message.role == System(AgentView) + ? [None] + : { + let parsed_blocks = message.display.displayable_content; + List.mapi( + (idx, block: Model.block_kind) => + form_block( + ~message, + ~block, + ~toggle_collapse=toggle_collapse(false), + ~index, + ~is_first=idx == 0, + ~is_last=idx == List.length(parsed_blocks) - 1, + ~globals, + ~settings, + ~signal, + ~model, + ~inject, + ), + parsed_blocks, + ); + }; + }, + ), + ]; + } + }, + curr_messages, + ), + ); + // Autoscroll to loading dots if they exist (only once per loading session) + // JsUtil.delay(0.0, () => { + // Js.Opt.iter( + // Dom_html.document##getElementById(Js.string("loading-dots-anchor")), + // el => { + // let _ = Js.Unsafe.coerce(el)##scrollIntoView(); + // // Change the ID so we don't scroll to it again + // Js.Unsafe.coerce(el)##.id := + // Js.string("loading-dots-anchor-scrolled"); + // }, + // ) + // }); + div( + ~attrs=[clss(["message-display-container"])], + message_nodes + @ [initial_display(~model, ~settings)] + @ [ + curr_chat.awaiting_response + ? div( + ~attrs=[clss(["loading-dots"]), Attr.id("loading-dots-anchor")], + [ + div(~attrs=[clss(["dot", "dot1"])], []), + div(~attrs=[clss(["dot", "dot2"])], []), + div(~attrs=[clss(["dot", "dot3"])], []), + ], + ) + : None, + ], + ); +}; + +let get_sidebar_width = () => { + let sidebar = + Js.Unsafe.coerce(Dom_html.document)##getElementById("side-bar"); + if (Js.Opt.test(sidebar)) { + let width_str = Js.to_string(sidebar##.style##.width); + if (String.length(width_str) >= 2 + && String.sub(width_str, String.length(width_str) - 2, 2) == "px") { + try( + int_of_string( + String.sub(width_str, 0, String.length(width_str) - 2), + ) + ) { + | Invalid_argument(_) => 450 // default width on error + }; + } else { + 450; // default width if no 'px' suffix + }; + } else { + 450; // default width + }; +}; + +let prompt_display = + ( + ~globals: Globals.t, + ~model: Model.t, + ~settings: AssistantSettings.t, + ~signal, + ~inject, + ) + : Node.t => { + let (past_chats, curr_chat) = + UpdateBase.get_mode_info(settings.mode, model); + let curr_messages = Id.Map.find(curr_chat.id, past_chats).messages; + let curr_messages: list(unwrapped_message) = + List.filter_map( + (message: Model.message) => + switch (message.display) { + | Some(display) => + Some({ + display, + role: message.role, + sketch_snapshot: message.sketch_snapshot, + }) + | None => None + }, + curr_messages, + ); + let display = + List.find_mapi( + (index: int, message: unwrapped_message) => { + ( + message.role == System(AssistantPrompt) + || message.role == System(AgentView) + ) + && !message.display.collapsed + ? Some( + div( + ~attrs=[ + Attr.id("prompt-display-container"), + Attr.create( + "style", + "right: " + ++ string_of_int(get_sidebar_width() + 20) + ++ "px", + ), + ], + { + let parsed_blocks = message.display.displayable_content; + List.map( + (block: Model.block_kind) => + form_block( + ~message, + ~block, + ~toggle_collapse=_ => {Virtual_dom.Vdom.Effect.Ignore}, + ~index, + ~is_first=false, + ~is_last=false, + ~globals, + ~settings, + ~signal, + ~model, + ~inject, + ), + parsed_blocks, + ); + }, + ), + ) + : None + }, + curr_messages, + ); + switch (display) { + | Some(node) => node + | None => None + }; +}; + +let view_buttons = + ( + ~inject: Update.t => Ui_effect.t(unit), + ~model: Model.t, + ~settings: AssistantSettings.t, + ) + : Node.t => { + let (_, curr_chat) = UpdateBase.get_mode_info(settings.mode, model); + let active_view = curr_chat.composition_model.t_ui.active_view; + + let view_button = + (view: CompositionAgentWorkbench.Model.UI.active_view, label: string) => { + let switch_view = _ => + Virtual_dom.Vdom.Effect.Many([ + inject( + Update.CompositionAgentWorkbenchAction( + UIAction(SwitchView(view)), + User, + curr_chat.id, + ), + ), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[ + clss(["view-button", active_view == view ? "active" : ""]), + Attr.on_click(switch_view), + ], + [text(label)], + ); + }; + + div( + ~attrs=[clss(["view-buttons"])], + [view_button(Chat, "Chat"), view_button(Todos, "Workbench")], + ); +}; + +let mode_dropdown = + ( + ~inject_global: Globals.Action.t => Ui_effect.t(unit), + ~settings: AssistantSettings.t, + ) + : Node.t => { + let handle_change = (event, _) => { + let target = Js.Unsafe.coerce(event)##.target; + let value = target##.value; + let mode = + switch (Js.to_string(value)) { + | "HazelTutor" => AssistantSettings.HazelTutor + | "CodeSuggestion" => AssistantSettings.CodeSuggestion + | "Composition" => AssistantSettings.Composition + | _ => AssistantSettings.Composition + }; + inject_global(Set(Assistant(SwitchMode(mode)))); + }; + + let current_mode_string = + switch (settings.mode) { + | HazelTutor => "HazelTutor" + | CodeSuggestion => "CodeSuggestion" + | Composition => "Composition" + }; + + let mode_label = + switch (settings.mode) { + | HazelTutor => "Tutor" + | CodeSuggestion => "Suggest" + | Composition => "Compose" + }; + + div( + ~attrs=[clss(["mode-dropdown-container"])], + [ + span( + ~attrs=[clss(["mode-dropdown-label"])], + [ + text(mode_label), + span( + ~attrs=[clss(["dropdown-arrow"])], + [text(" \226\150\188")], + ), + ], + ), + select( + ~attrs=[ + Attr.on_change(handle_change), + clss(["mode-dropdown-select"]), + Attr.value(current_mode_string), + ], + [ + option( + ~attrs=[ + Attr.value("Composition"), + current_mode_string == "Composition" + ? Attr.selected : Attr.empty, + ], + [text("Compose")], + ), + option( + ~attrs=[ + Attr.value("CodeSuggestion"), + current_mode_string == "CodeSuggestion" + ? Attr.selected : Attr.empty, + ], + [text("Suggest")], + ), + option( + ~attrs=[ + Attr.value("HazelTutor"), + current_mode_string == "HazelTutor" ? Attr.selected : Attr.empty, + ], + [text("Tutor")], + ), + ], + ), + ], + ); +}; + +let history_menu = + (~model: Model.t, ~settings: AssistantSettings.t, ~inject): Node.t => { + let (past_chats, curr_chat) = + UpdateBase.get_mode_info(settings.mode, model); + let chronologically_sorted_past_chats = Model.sorted_chats(past_chats); + + div( + ~attrs=[ + Attr.id("history-menu"), + Attr.create( + "style", + "right: " ++ string_of_int(get_sidebar_width() + 20) ++ "px", + ), + ], + [ + div(~attrs=[clss(["history-menu-header"])], [text("Chat History")]), + div( + ~attrs=[clss(["history-menu-list"])], + List.map( + (chat: Model.chat) => + div( + ~attrs=[ + chat.id == curr_chat.id + ? clss(["history-menu-item", "active"]) + : clss(["history-menu-item"]), + Attr.on_click(e => { + let target = Js.Unsafe.coerce(e)##.target; + let contains_button = + Js.to_bool(target##.classList##contains("button")) + || Js.to_bool( + target##.parentElement##.classList##contains( + "button", + ), + ); + if (!contains_button) { + Virtual_dom.Vdom.Effect.Many([ + inject(Update.ChatAction(SwitchChat(chat.id))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + } else { + Virtual_dom.Vdom.Effect.Stop_propagation; + }; + }), + ], + [ + div( + ~attrs=[clss(["history-menu-item-content"])], + [ + text(chat.descriptor == "" ? "New chat" : chat.descriptor), + ], + ), + div( + ~attrs=[clss(["history-menu-item-actions"])], + [ + div( + ~attrs=[clss(["history-menu-item-time"])], + [text(TimeUtil.format_time_diff(chat.timestamp))], + ), + div( + ~attrs=[ + clss(["delete-chat-button"]), + Attr.on_click(_ => + Virtual_dom.Vdom.Effect.Many([ + inject(Update.ChatAction(DeleteChat(chat.id))), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]) + ), + ], + [ + Widgets.button(~tooltip="Delete chat", Icons.trash, _ => + Virtual_dom.Vdom.Effect.Ignore + ), + ], + ), + ], + ), + ], + ), + chronologically_sorted_past_chats, + ), + ), + ], + ); +}; + +let todo_archive_button = (~inject, ~chat_id: Id.t): Node.t => { + let tooltip = "Task Archive"; + let toggle_archive = _ => + Virtual_dom.Vdom.Effect.Many([ + inject( + Update.CompositionAgentWorkbenchAction( + UIAction(ToggleShowTaskDictionary), + User, + chat_id, + ), + ), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]); + div( + ~attrs=[clss(["assistant-button", "todo-archive-button"])], + [Widgets.button(~tooltip, Icons.library, toggle_archive)], + ); +}; + +let todo_archive_menu = + (~model: Model.t, ~settings: AssistantSettings.t, ~inject, ~chat_id: Id.t) + : Node.t => { + let (_, curr_chat) = UpdateBase.get_mode_info(settings.mode, model); + let task_dict = curr_chat.composition_model.task_dict; + let sorted_task_dict = + CompositionAgentWorkbench.Utils.TaskDictUtils.sorted_task_dict(task_dict); + + div( + ~attrs=[ + Attr.id("todo-archive-menu"), + Attr.create( + "style", + "right: " ++ string_of_int(get_sidebar_width() + 20) ++ "px", + ), + ], + [ + div(~attrs=[clss(["history-menu-header"])], [text("Task Archive")]), + div( + ~attrs=[clss(["history-menu-list"])], + List.map( + (task: CompositionAgentWorkbench.Model.task) => + div( + ~attrs=[ + CompositionAgentWorkbench.Utils.MainUtils.active_task( + curr_chat.composition_model, + ) + |> Option.map( + (active_task: CompositionAgentWorkbench.Model.task) => + active_task.title == task.title + ) + |> Option.value(~default=false) + ? clss(["history-menu-item", "active"]) + : clss(["history-menu-item"]), + Attr.on_click(_ => { + Virtual_dom.Vdom.Effect.Many([ + inject( + Update.CompositionAgentWorkbenchAction( + UIAction(SetDisplayTask(task.title)), + User, + chat_id, + ), + ), + inject( + Update.CompositionAgentWorkbenchAction( + BackendAction(SetActiveTask(task.title)), + User, + chat_id, + ), + ), + Virtual_dom.Vdom.Effect.Stop_propagation, + ]) + }), + ], + [ + div( + ~attrs=[clss(["history-menu-item-content"])], + [text(task.title)], + ), + div( + ~attrs=[clss(["history-menu-item-actions"])], + [ + div( + ~attrs=[clss(["history-menu-item-time"])], + [ + text( + TimeUtil.format_time_diff( + task.metadata.last_updated_at, + ), + ), + ], + ), + ], + ), + ], + ), + sorted_task_dict, + ), + ), + ], + ); +}; + +let task_display = + (~model: Model.t, ~settings: AssistantSettings.t, ~inject): Node.t => { + let (_, curr_chat) = UpdateBase.get_mode_info(settings.mode, model); + let chat_id = curr_chat.id; + + div( + ~attrs=[clss(["todo-list-view"])], + [ + div( + ~attrs=[clss(["todo-archive-button-container"])], + [todo_archive_button(~inject, ~chat_id)], + ), + switch ( + CompositionAgentWorkbench.Utils.MainUtils.active_task( + curr_chat.composition_model, + ) + ) { + | None => + div( + ~attrs=[clss(["no-todo-list"])], + [text("No active todo list.")], + ) + | Some(active_task) => + div( + ~attrs=[clss(["todo-list-container"])], + [ + /* Title row: show title on the left and (if completed) elapsed time on the right */ + div( + ~attrs=[clss(["todo-list-title"])], + [ + div( + ~attrs=[clss(["todo-list-title-left"])], + [text(active_task.title)], + ), + switch (active_task.completion_info) { + | Some(info) => + div( + ~attrs=[clss(["todo-list-title-time"])], + [text(TimeUtil.format_duration_ms(info.elapsed_time))], + ) + | None => None + }, + ], + ), + div( + ~attrs=[clss(["todo-items"])], + List.map( + (subtask: CompositionAgentWorkbench.Model.subtask) => { + let is_active = + switch (active_task.active_subtask) { + | Some(active_item) => active_item == subtask.title + | None => false + }; + div( + ~attrs=[ + clss([ + "todo-item", + subtask.subtask_ui.expanded ? "expanded" : "", + ]), + Attr.on_click(_ => + inject( + Update.CompositionAgentWorkbenchAction( + UIAction(ExpandSubtask(subtask.title)), + User, + chat_id, + ), + ) + ), + ], + [ + div( + ~attrs=[ + clss([ + "todo-status-icon", + CompositionAgentWorkbench.Utils.SubtaskUtils.is_completed( + subtask, + ) + ? "completed" : "incomplete", + is_active + && ! + CompositionAgentWorkbench.Utils.SubtaskUtils.is_completed( + subtask, + ) + ? "active-todo" : "", + ]), + ], + [ + CompositionAgentWorkbench.Utils.SubtaskUtils.is_completed( + subtask, + ) + ? Icons.circle_with_check + : Icons.circle_with_no_check, + ], + ), + div( + ~attrs=[clss(["todo-item-content"])], + [ + div( + ~attrs=[clss(["todo-item-title-row"])], + [ + div( + ~attrs=[clss(["todo-item-title"])], + [text(subtask.title)], + ), + /* Show elapsed time on the header (if completed) */ + switch (subtask.completion_info) { + | Some(info) => + div( + ~attrs=[clss(["todo-item-title-time"])], + [ + text( + TimeUtil.format_duration_ms( + info.elapsed_time, + ), + ), + ], + ) + | None => None + }, + ], + ), + subtask.subtask_ui.expanded + ? div( + ~attrs=[clss(["todo-item-details"])], + [ + /* Header row for the details section: label + elapsed time (if completed) */ + div( + ~attrs=[ + clss(["todo-detail-header-row"]), + ], + [ + div( + ~attrs=[ + clss(["todo-detail-header"]), + ], + [text("Description")], + ), + None, + ], + ), + div( + ~attrs=[clss(["todo-detail-text"])], + [text(subtask.description)], + ), + switch (subtask.completion_info) { + | Some(info) => + div( + ~attrs=[clss(["todo-detail-section"])], + [ + div( + ~attrs=[ + clss(["todo-detail-header"]), + ], + [text("Summary of Changes")], + ), + div( + ~attrs=[ + clss(["todo-detail-text"]), + ], + [text(info.summary)], + ), + ], + ) + | None => None + }, + ], + ) + : None, + ], + ), + ], + ); + }, + CompositionAgentWorkbench.Utils.TaskUtils.ordered_subtasks_of( + active_task, + ), + ), + ), + ], + ) + }, + ], + ); +}; + +let view = + ( + ~globals: Globals.t, + ~signal: event => Ui_effect.t(unit), + ~inject: Update.t => Ui_effect.t(unit), + ~model: Model.t, + ) => { + let settings = globals.settings; + let inject_global = globals.inject_global; + + let (_, curr_chat) = + UpdateBase.get_mode_info(settings.assistant.mode, model); + let active_view = curr_chat.composition_model.t_ui.active_view; + let show_todos = + settings.assistant.mode == Composition + && active_view == CompositionAgentWorkbench.Model.UI.Todos; + + let show_archive = + settings.assistant.mode == Composition + && curr_chat.composition_model.t_ui.show_archive; + /* For debugging: Uncomment to view chat history + let curr_chat = + Id.Map.find( + model.current_chats.curr_composition_chat, + model.chat_history.past_composition_chats, + ); + let concat_text = + String.concat( + "\n", + List.map( + (message: Model.display) => + "<" + ++ Model.string_of_role(message.role) + ++ ">" + ++ message.original_content + ++ "\n", + curr_chat.message_displays, + ), + ); + print_endline(concat_text); + Js_of_ocaml.Firebug.console##log(Js_of_ocaml.Js.string(concat_text)); + */ + let view = + div( + ~attrs=[Attr.id("assistant")], + [ + div( + ~attrs=[clss(["header"])], + [ + div( + ~attrs=[clss(["header-left-group"])], + [ + settings.assistant.ongoing_chat + && settings.assistant.mode == Composition + ? view_buttons( + ~inject, + ~model, + ~settings=settings.assistant, + ) + : None, + ], + ), + div( + ~attrs=[clss(["header-right-group"])], + [ + settings.assistant.ongoing_chat + ? mode_dropdown( + ~inject_global, + ~settings=settings.assistant, + ) + : div( + ~attrs=[clss(["main-title"])], + [text("Assistant Settings")], + ), + settings.assistant.ongoing_chat + ? history_button(~inject, ~inject_global) : None, + settings.assistant.ongoing_chat + ? new_chat_button(~inject, ~model) : None, + settings.assistant.ongoing_chat + ? settings_button(~inject_global) + : resume_chat_button(~inject_global), + ], + ), + ], + ), + settings.assistant.ongoing_chat + ? show_todos + ? task_display(~model, ~settings=settings.assistant, ~inject) + : message_display( + ~globals, + ~inject, + ~model, + ~settings=settings.assistant, + ~signal, + ) + : None, + settings.assistant.ongoing_chat && !show_todos + ? message_input( + ~signal, + ~inject, + ~model, + ~settings=settings.assistant, + ) + : None, + settings.assistant.ongoing_chat + ? None + : api_input( + ~inject, + ~inject_global, + ~signal, + ~model, + ~settings=settings.assistant, + ), + settings.assistant.ongoing_chat + ? None : llm_model_id_input(~inject, ~model, ~signal), + settings.assistant.ongoing_chat && settings.assistant.show_history + ? history_menu(~model, ~settings=settings.assistant, ~inject) : None, + settings.assistant.ongoing_chat + && show_archive + && curr_chat.composition_model.t_ui.active_view + == CompositionAgentWorkbench.Model.UI.Todos + ? todo_archive_menu( + ~model, + ~settings=settings.assistant, + ~inject, + ~chat_id=curr_chat.id, + ) + : None, + prompt_display( + ~globals, + ~model, + ~settings=settings.assistant, + ~signal, + ~inject, + ), + ], + ); + + // Apply blue cursor theme and disable editing when agent is looping + JsUtil.delay( + 0.0, + () => { + // todo: maybe use later + JsUtil.set_agent_view_flag_theme(false); + JsUtil.set_editor_readonly(false); + }, + ); + + view; +}; diff --git a/src/web/app/ai-assistant-web/AssistantWeb.re b/src/web/app/ai-assistant-web/AssistantWeb.re new file mode 100644 index 0000000000..2b5c9b011d --- /dev/null +++ b/src/web/app/ai-assistant-web/AssistantWeb.re @@ -0,0 +1,43 @@ +open Haz3lcore; +open AssistantModel; + +module Update = { + /* + A thin wrapper around the AssistantUpdate module + */ + [@deriving (show({with_path: false}), sexp, yojson)] + type t = AssistantUpdateAction.t; + type model = AssistantModel.t; + + let update = + ( + ~settings, + ~model: model, + ~action: t, + ~editor: CodeEditable.Model.t, + ~schedule_action, + ~schedule_editor_action, + ) => { + let zipper = editor.editor.state.zipper; + let info_map = editor.statics.info_map; + AssistantUpdate.update( + ~settings, + ~action, + ~model, + ~zipper, + ~info_map, + ~schedule_action, + ~schedule_editor_action, + ) + |> Updated.return; + }; +}; + +module Store = + Store.F({ + [@deriving (show({with_path: false}), yojson, sexp)] + type t = model; + let default = () => null_model(); + + let key = Store.Assistant; + }); diff --git a/src/web/app/common/Icons.re b/src/web/app/common/Icons.re index fb4a02b3e5..1b5d7b080b 100644 --- a/src/web/app/common/Icons.re +++ b/src/web/app/common/Icons.re @@ -272,23 +272,14 @@ let gamma = ], ); -let assistant = +let agent_view = simple_icon( - ~view="0 0 512 512", + ~view="0 0 24 24", [ - "M440.86,303.607c-27.106,0-59.31-6.311-68.409-24.022c-5.41-10.534,0.128-22.059,5.728-29.872 - c21.037-29.36,32.158-62.108,32.158-94.703C410.336,69.538,341.101,0,255.999,0S101.662,69.538,101.662,155.01 - c0,32.595,11.12,65.343,32.158,94.703c5.598,7.813,11.138,19.339,5.728,29.872c-9.099,17.712-41.303,24.022-68.409,24.022 - c-30.01,0-54.424,24.497-54.424,54.609s24.415,54.609,54.424,54.609c13.878,0,32.714-3.057,49.694-7.208 - c-3.563,2.705-7.187,5.417-10.638,7.91c-21.305,15.39-28.094,40.131-17.718,64.568c6.132,14.441,18.071,25.704,32.76,30.902 - c13.514,4.781,27.708,3.827,39.96-2.695c35.939-19.129,59.199-51.213,76.181-74.639c4.617-6.369,10.521-14.513,14.62-19.001 - c4.099,4.489,10.003,12.633,14.621,19.001c16.983,23.426,40.242,55.509,76.181,74.639c7.114,3.786,14.882,5.698,22.793,5.698 - c5.713,0,11.5-0.997,17.168-3.002c14.688-5.197,26.629-16.46,32.76-30.902c10.375-24.438,3.587-49.178-17.717-64.568 - c-3.453-2.493-7.075-5.206-10.639-7.91c16.979,4.15,35.818,7.208,49.694,7.208c30.01,0,54.424-24.497,54.424-54.609 - C495.285,328.106,470.87,303.607,440.86,303.607z M204.312,193.512c-11.237,0-20.381-9.291-20.381-20.753 - c0-11.463,9.144-20.747,20.381-20.747c11.251,0,20.394,9.284,20.394,20.747C224.706,184.22,215.564,193.512,204.312,193.512z - M307.532,193.666c-11.346,0-20.55-9.36-20.55-20.908c0-11.547,9.204-20.908,20.55-20.908c11.332,0,20.535,9.36,20.535,20.908 - C328.067,184.306,318.865,193.666,307.532,193.666z", + "M14.0184 7.36446C14.1256 6.96436 13.8882 6.55311 13.4881 6.4459C13.088 6.3387 12.6767 6.57614 12.5695 6.97623L9.98131 16.6355C9.8741 17.0356 10.1115 17.4468 10.5116 17.5541C10.9117 17.6613 11.323 17.4238 11.4302 17.0237L14.0184 7.36446Z", + "M16.0303 8.46967C15.7374 8.17678 15.2626 8.17678 14.9697 8.46967C14.6768 8.76256 14.6768 9.23744 14.9697 9.53033L15.1412 9.7019C15.8229 10.3836 16.2797 10.8426 16.5753 11.2301C16.8577 11.6002 16.9216 11.8157 16.9216 12C16.9216 12.1843 16.8577 12.3998 16.5753 12.7699C16.2797 13.1574 15.8229 13.6164 15.1412 14.2981L14.9697 14.4697C14.6768 14.7626 14.6768 15.2374 14.9697 15.5303C15.2626 15.8232 15.7374 15.8232 16.0303 15.5303L16.2387 15.322C16.874 14.6867 17.4038 14.1569 17.7678 13.6798C18.1521 13.1762 18.4216 12.6441 18.4216 12C18.4216 11.3559 18.1521 10.8238 17.7678 10.3202C17.4038 9.84307 16.874 9.31331 16.2387 8.67801L16.0303 8.46967Z", + "M7.96986 8.46967C8.26275 8.17678 8.73762 8.17678 9.03052 8.46967C9.32341 8.76256 9.32341 9.23744 9.03052 9.53033L8.85894 9.7019C8.17729 10.3836 7.72052 10.8426 7.42488 11.2301C7.14245 11.6002 7.07861 11.8157 7.07861 12C7.07861 12.1843 7.14245 12.3998 7.42488 12.7699C7.72052 13.1574 8.17729 13.6164 8.85894 14.2981L9.03052 14.4697C9.32341 14.7626 9.32341 15.2374 9.03052 15.5303C8.73762 15.8232 8.26275 15.8232 7.96986 15.5303L7.76151 15.322C7.12618 14.6867 6.59637 14.1569 6.23235 13.6798C5.84811 13.1762 5.57861 12.6441 5.57861 12C5.57861 11.3559 5.84811 10.8238 6.23235 10.3202C6.59637 9.84307 7.12617 9.31332 7.7615 8.67802L7.96986 8.46967Z", + "M11.9426 1.25C9.63423 1.24999 7.82519 1.24998 6.41371 1.43975C4.96897 1.63399 3.82895 2.03933 2.93414 2.93414C2.03933 3.82895 1.63399 4.96897 1.43975 6.41371C1.24998 7.82519 1.24999 9.63423 1.25 11.9426V12.0574C1.24999 14.3658 1.24998 16.1748 1.43975 17.5863C1.63399 19.031 2.03933 20.1711 2.93414 21.0659C3.82895 21.9607 4.96897 22.366 6.41371 22.5603C7.82519 22.75 9.63423 22.75 11.9426 22.75H12.0574C14.3658 22.75 16.1748 22.75 17.5863 22.5603C19.031 22.366 20.1711 21.9607 21.0659 21.0659C21.9607 20.1711 22.366 19.031 22.5603 17.5863C22.75 16.1748 22.75 14.3658 22.75 12.0574V11.9426C22.75 9.63423 22.75 7.82519 22.5603 6.41371C22.366 4.96897 21.9607 3.82895 21.0659 2.93414C20.1711 2.03933 19.031 1.63399 17.5863 1.43975C16.1748 1.24998 14.3658 1.24999 12.0574 1.25H11.9426ZM3.9948 3.9948C4.56445 3.42514 5.33517 3.09825 6.61358 2.92637C7.91356 2.75159 9.62178 2.75 12 2.75C14.3782 2.75 16.0864 2.75159 17.3864 2.92637C18.6648 3.09825 19.4355 3.42514 20.0052 3.9948C20.5749 4.56445 20.9018 5.33517 21.0736 6.61358C21.2484 7.91356 21.25 9.62178 21.25 12C21.25 14.3782 21.2484 16.0864 21.0736 17.3864C20.9018 18.6648 20.5749 19.4355 20.0052 20.0052C19.4355 20.5749 18.6648 20.9018 17.3864 21.0736C16.0864 21.2484 14.3782 21.25 12 21.25C9.62178 21.25 7.91356 21.2484 6.61358 21.0736C5.33517 20.9018 4.56445 20.5749 3.9948 20.0052C3.42514 19.4355 3.09825 18.6648 2.92637 17.3864C2.75159 16.0864 2.75 14.3782 2.75 12C2.75 9.62178 2.75159 7.91356 2.92637 6.61358C3.09825 5.33517 3.42514 4.56445 3.9948 3.9948Z", ], ); @@ -370,11 +361,54 @@ let invisible = ], ); -let hazelnut_agent = +let corylus = simple_icon( - ~view="0 0 765 739", + ~view="0 0 512 512", [ - "M383.267 2.82845C395.982 -1.3284 403.092 -0.933013 408.253 4.21809C411.081 7.04183 410.86 15.4184 407.771 22.4964C405.81 26.9924 405.367 29.9546 404.981 41.2181C404.458 56.5112 405.991 71.7075 408.521 76.3148L410.252 79.4632L424.743 80.5208C493.509 85.5385 555.238 110.453 601.682 151.938C615.087 163.913 622.42 171.793 632.807 185.388C646.925 203.868 653.123 214.725 666.654 244.68C682.801 280.429 693.575 296.219 713.722 313.667C723.258 321.926 734.221 328.612 744.174 332.242C760.752 338.29 761.53 338.695 763.14 342.128L763.312 342.514C764.997 346.467 763.636 349.564 758.652 353.237L758.273 353.506C750.043 359.142 725.424 363.485 705.475 362.762L693.887 362.342L695.16 367.772C695.86 370.759 696.895 377.208 697.46 382.104C708.732 479.838 661.058 561.026 550.991 631.535C535.319 641.575 518.713 650.813 493.201 663.682C437.609 691.725 424.682 699.531 405.246 716.787C398.078 723.152 389.939 730.753 387.16 733.679C382.525 738.561 381.776 739 378.068 739C373.078 739 372.649 738.683 361.163 726.476C338.167 702.039 321.261 690.278 263.707 658.684C203.206 625.471 177.912 608.737 147.792 581.992C123.63 560.538 108.28 542.803 94.1981 520.072C65.8893 474.373 56.5172 418.31 68.0302 363.532C69.1471 358.215 69.8602 353.685 69.6151 353.462C69.3693 353.243 65.666 353.709 61.3856 354.495C50.877 356.429 30.8361 356.442 20.5643 354.52C6.50497 351.89 -3.2286e-05 347.946 -0.00012207 342.05C-0.00012207 337.885 3.8306 334.975 12.8231 332.307C29.8108 327.267 46.1785 316.366 55.6708 303.773C61.3473 296.241 68.4089 283.407 77.951 263.273C96.7286 223.654 110.274 202.208 131.978 177.731C142.398 165.98 166.554 144.494 178.891 136.004C221.389 106.757 271.245 88.2601 324.58 81.9554L338.105 80.3568L339.408 77.5784C342.971 69.9831 341.931 54.1795 337.08 42.1976C335.666 38.7048 334.307 33.7504 334.061 31.1878C333.659 26.9877 333.895 26.2787 336.454 24.0072C342.158 18.946 363.492 9.29421 383.267 2.82845ZM103.579 342.804C103.342 342.581 91.9463 347.837 91.9442 348.171C91.9442 348.34 91.0664 351.639 89.993 355.5C85.4618 371.812 83.6216 387.254 83.6385 408.807C83.6565 430.407 84.6083 438.426 89.3934 457.266C103.218 511.693 140.925 559.095 203.877 601.185C225.847 615.875 240.565 624.533 279.331 645.569C329.157 672.606 350.325 687.21 370.397 708.396L377.075 715.442L389.165 704.981C410.882 686.192 425.472 677.081 471.971 653.268C515.609 630.92 529.664 622.992 552.228 608.002C628.051 557.631 671.062 496.82 678.006 430.17C680.178 409.313 679.309 388.807 675.475 370.423L673.412 360.523L664.758 358.506C659.998 357.397 653.95 355.745 651.318 354.833C648.689 353.922 646.408 353.176 646.246 353.175C646.087 353.175 645.962 359.284 645.967 366.749C645.985 390.856 640.096 409.011 625.829 428.834C623.891 431.527 624 431.5 623 433.5C622.5 434.5 622.736 433.68 621.5 436C620 438 620.5 437 617.5 440C614.5 443 617.5 440.5 614 443.5C612.5 444.312 612.049 444.846 611.014 445.869C607.96 448.889 594.583 458.49 588.643 461.924C575.604 469.464 562.28 474.109 545.471 476.975C530.887 479.462 507.867 479.065 495.2 476.108C452.635 466.173 419.459 438.579 405.471 401.475L403.042 395.034C400.008 374.983 394.059 369.103 393.123 368.304C392.649 367.9 389.339 366.697 385.766 365.63C379.266 363.689 347.407 371.839 347.407 373.202L346.224 382.104C345.574 386.999 344.532 392.917 343.908 395.253C340.343 408.611 331.766 424.352 321.79 435.85C302.945 457.567 274.108 472.593 241.887 477.485C204.274 483.195 157.732 467.878 132.26 441.405C110.799 419.101 101.864 396.75 101.998 365.703C102.03 358.079 102.432 349.837 102.89 347.389C103.346 344.941 103.657 342.878 103.579 342.804ZM429.06 497.659C436.75 490.874 438.93 490.17 444.425 492.701C450.289 495.402 451.246 502.173 446.575 507.916L446.109 508.48C436.194 520.276 417.851 530.051 398.802 533.646C390.717 535.171 365.155 535.408 355.459 534.046C333.579 530.975 313.38 521.184 304.057 509.184L303.621 508.611C298.831 502.17 299.786 496.263 306.044 493.618C310.197 491.863 313.443 492.835 318.98 497.492C329.059 505.972 335.539 509.528 346.535 512.617C358.792 516.061 378.1 517.255 391.115 515.373C406.975 513.08 416.764 508.512 429.06 497.659ZM551.166 280.146C538.397 277.195 506.721 277.138 503.438 280.058L483.12 285.625L475.668 289.222C465.47 294.144 456.462 300.237 448.678 307.476C440.01 315.537 437.653 318.446 432.929 326.916C416.793 355.845 419.241 392.397 439.033 418.084C451.974 434.881 476.733 450.552 498.345 455.626C514.609 459.445 538.662 458.743 554.663 453.981C596.152 441.634 623.622 407.714 623.622 368.829L622.623 357.382L619.625 344.92C616.127 331.569 612.625 325.335 605.274 315.495C592.035 297.774 572.653 285.111 551.166 280.146ZM237.856 278.422C229.981 277.339 209.258 277.404 202.378 278.534C171.349 283.635 145.412 303.562 131.946 332.647C130.283 336.238 128.922 339.674 128.922 340.282L123.642 361.544L124.246 370.941C125.607 392.116 132.697 409.507 146.268 424.955C158.608 439.002 178.776 450.42 199.379 455.022C202.677 455.759 209.708 456.835 215.003 457.414C244.522 460.645 278.647 448.511 299.268 427.451C314.742 411.649 322.394 396.181 324.87 375.697C328.689 344.109 313.46 314.521 283.907 296.11C278.833 286.623 238.209 278.406 237.856 278.422ZM235.705 342.05C248.875 337.755 265.739 345.331 272.539 358.595C274.049 361.541 275.085 364.729 275.65 368.004L267.46 365.697C268.395 362.378 266.367 357.86 263.271 356.215L262.969 356.065C258.573 354.04 255.205 354.432 251.932 357.347C247.962 360.883 247.944 365.224 251.883 369.151L252.291 369.548C254.162 371.321 255.542 371.932 257.844 372.005L258.351 372.013C262.458 372.013 266.462 369.236 267.46 365.697L275.65 368.004C276.883 375.148 275.867 382.706 272.599 389.063C265.267 403.327 247.218 410.25 232.992 404.399L232.317 404.111C225.85 401.24 218.62 394.382 215.346 388.012C211.749 381.016 211.125 370.589 213.847 362.955C217.154 353.68 226.006 345.213 235.705 342.05ZM496.068 343.765C510.289 337.453 527.176 344.087 535.55 359.273L531.09 361.731C530.398 359.264 528.776 357.03 526.391 355.955L526.13 355.844C515.527 351.541 508.261 364.396 517.599 370.938C518.2 371.359 518.878 371.686 519.603 371.924C522.597 372.906 526.391 372.341 528.873 370.341C531.347 368.348 531.965 364.853 531.09 361.731L535.55 359.273C538.003 363.721 538.264 365.136 538.264 373.987L538.26 375.575C538.222 383.1 537.842 384.59 535.36 389.377C529.661 400.371 517.95 407.105 505.935 406.298C498.74 405.815 494.196 403.849 487.956 398.52C472.681 385.476 472.235 364.125 486.94 349.916C489.15 347.781 493.258 345.012 496.068 343.765ZM385.537 22.4583C385.047 22.0691 368.479 28.7156 362.036 31.8861C357.479 34.1292 355.971 35.3186 356.453 36.2933C356.816 37.0282 358.19 41.2334 359.506 45.639C362.184 54.6005 362.808 70.1303 360.844 78.9505C358.937 87.5107 353.245 93.9882 345.291 96.6488C343.642 97.2006 334.07 98.8938 324.021 100.411C259.433 110.165 209.284 132.609 167.711 170.364C139.221 196.237 120.088 222.957 99.9091 265.053C88.523 288.807 80.4514 303.316 73.1678 313.119C67.6642 320.529 56.9234 330.964 49.9696 335.66L46.4716 338.023L49.7616 338.033C58.934 338.06 79.3538 332.555 93.4003 326.266C107.947 319.754 113.86 315.926 117.715 310.526C137.025 283.479 164.051 265.508 194.307 259.599C214.037 255.746 243.059 256.728 263.218 261.931C267.972 263.159 272.193 264.154 272.599 264.144C273.003 264.134 275.808 261.152 278.831 257.519C281.854 253.886 284.869 250.711 285.528 250.462C288.094 249.497 291.111 253.608 298.736 268.458C305.305 281.252 307.569 284.741 312.018 288.931C326.924 302.966 339.521 322.588 343.863 338.532C344.524 340.955 345.339 343.203 345.677 343.527C346.015 343.85 349.438 343.564 353.286 342.891C364.862 340.868 380.11 341.232 389.902 343.765C394.498 344.954 399.046 346.199 400.008 346.531C401.196 346.941 401.757 346.707 401.757 345.804C401.757 343.174 406.936 328.628 410.514 321.21C414.51 312.924 422.905 301.036 429.859 293.815C432.691 290.874 437.851 282.79 444.061 271.562C445.685 268.627 447.169 265.939 448.528 263.478L448.747 263.082L448.75 263.076L448.781 263.021C461.666 239.695 463.106 237.087 464.808 236.944C465.023 236.926 465.241 236.947 465.487 236.971L465.83 237C465.951 237.009 466.08 237.014 466.218 237.014C468.999 237.014 469.803 237.882 477.349 249.031C477.935 249.896 478.478 250.704 478.983 251.455C482.828 257.178 484.527 259.706 486.94 260.492C488.851 261.115 491.209 260.646 495.433 259.807L495.449 259.804C496.146 259.666 496.893 259.517 497.698 259.362C509.373 257.109 532.13 256.582 544.528 258.279C570.343 261.81 588.879 269.891 607.024 285.525C617.991 294.973 626.139 304.763 632.836 316.533C638.22 325.996 639.631 327.151 653.604 333.515C670.95 341.414 688.647 345.224 707.228 345.059C713.738 345.001 720.391 344.7 722.013 344.39C724.913 343.835 724.854 343.77 718.317 340.366C714.663 338.462 707.714 333.819 702.876 330.047C681.574 313.44 666.509 292.994 652.118 261.154C649.215 254.731 645.167 245.871 643.122 241.465C618.444 188.297 576.371 147.587 520.684 122.995C487.572 108.373 460.17 101.622 420.245 98.2474C403.416 96.825 401.497 96.3472 396.307 92.2874C388.472 86.1599 385.264 76.6623 383.819 55.3079C382.857 41.0795 383.077 35.037 384.845 27.0394C385.374 24.6423 385.686 22.5803 385.537 22.4583Z", + "M511.26,392.674c-4.28-15.551-25.68-22.763-48.729-16.42c-10.243,2.82-19.321,7.946-25.563,14.435 + c-3.629,3.774-6.162,7.907-7.507,12.11c-29.638-6.338-58.279-14.758-85.794-25.229c2.871-0.381,5.742-0.879,8.603-1.502 + c26.96-5.877,50.018-21.901,64.927-45.119c1.213-1.891,1.627-4.186,1.149-6.382c-0.479-2.195-1.811-4.11-3.7-5.325 + c-46.404-29.795-107.906-17.659-139.704,26.644c-14.185-7.771-27.992-16.161-41.397-25.162 + c-11.125-7.47-21.969-15.385-32.546-23.687c8.855-0.077,18.295-1.418,27.711-4.01c35.596-9.8,58.131-32.832,52.422-53.573 + c-2.956-10.738-12.825-18.414-27.786-21.613c-13.02-2.785-28.885-1.926-44.668,2.421c-3.868,1.064-7.696,2.331-11.385,3.768 + c-21.038,8.216-35.682,21.192-40.324,34.534c-18.843-18.304-36.561-38.064-53.042-59.192 + c-10.736-13.765-20.325-27.331-28.856-40.379c6.542,3.921,13.525,7.214,20.896,9.782c12.048,4.197,24.482,6.283,36.874,6.283 + c16.62,0,33.163-3.754,48.57-11.2l8.327-4.025c4.212-2.035,5.978-7.102,3.941-11.314l-4.025-8.328 + c-1.778-3.678-3.769-7.288-5.921-10.73c-2.48-3.967-7.707-5.173-11.674-2.693c-3.967,2.479-5.173,7.707-2.693,11.674 + c1.828,2.924,3.521,5.992,5.033,9.12l0.338,0.7l-0.699,0.338c-22.818,11.028-48.563,12.51-72.496,4.171 + c-20.113-7.007-36.898-20.261-48.296-37.851l65.423,22.825c0.924,0.322,1.865,0.474,2.791,0.474c3.503,0,6.78-2.189,8-5.683 + c1.541-4.418-0.791-9.249-5.208-10.79l-65.473-22.84c14.48-4.886,29.855-6.205,45.084-3.77 + c18.762,2.999,35.994,11.401,49.834,24.298c3.423,3.19,8.785,3.001,11.973-0.422c3.19-3.423,3.001-8.783-0.422-11.973 + c-16.311-15.2-36.613-25.101-58.711-28.633c-22.624-3.617-45.52-0.239-66.218,9.763l-4.042,1.953 + C22.6,61.469,16.722,42.805,16.573,42.324c-1.372-4.472-6.111-6.985-10.585-5.613c-4.473,1.374-6.985,6.113-5.613,10.585 + c0.965,3.142,24.379,77.921,89.763,161.949c4.86,6.246,9.827,12.375,14.895,18.388l-4.507,6.531 + c-16.964,24.584-23.339,54.302-17.951,83.681c5.389,29.379,21.895,54.9,46.478,71.863l7.612,5.252 + c1.424,0.983,3.105,1.499,4.812,1.499c0.509,0,1.021-0.046,1.528-0.139c2.211-0.406,4.168-1.672,5.444-3.522l5.251-7.612 + c16.964-24.584,23.339-54.302,17.951-83.681c-0.506-2.755-1.108-5.477-1.807-8.161c17.184,14.851,35.165,28.606,53.885,41.198 + c2.124,1.429,4.258,2.839,6.401,4.238c-26.29,45.718-13.49,104.911,30.673,135.385c1.424,0.983,3.105,1.499,4.813,1.499 + c0.508,0,1.02-0.046,1.528-0.139c2.209-0.406,4.168-1.672,5.444-3.522c22.014-31.903,23.409-72.009,7.107-104.414 + c19.956,10.265,40.604,19.366,61.889,27.29c-8.853,10.056-8.881,28.92,0.848,46.689c8.902,16.257,22.974,26.129,35.445,26.129 + c3.61,0,7.088-0.828,10.265-2.568c7.374-4.038,11.815-12.348,12.186-22.799c0.32-9-2.315-19.087-7.417-28.404 + c-0.569-1.04-1.168-2.056-1.785-3.053c14.571,4.012,29.397,7.515,44.469,10.492c0.553,0.11,1.106,0.163,1.65,0.163 + c3.965,0,7.506-2.796,8.302-6.832c0.264-1.337,0.19-2.66-0.15-3.89c0.002-0.002,0.005-0.005,0.006-0.007 + c1.528-1.646,0.814-5.294,3.77-8.368c4.1-4.262,10.607-7.85,17.849-9.845c15.116-4.159,26.617-0.066,27.895,4.581 + c1.281,4.653-6.503,14.065-21.616,18.225c-4.63,1.274-9.396,1.849-13.76,1.659c-4.68-0.198-8.628,3.424-8.83,8.098 + c-0.202,4.675,3.424,8.628,8.098,8.83c0.812,0.035,1.627,0.052,2.444,0.052c5.43,0,10.985-0.773,16.546-2.304 + C500.848,425.389,515.543,408.233,511.26,392.674z M139.759,375.565l-0.44,0.638l-0.638-0.44 + c-20.859-14.393-34.864-36.047-39.436-60.974c-3.859-21.036-0.582-42.266,9.177-60.92l13.111,68.763 + c0.774,4.06,4.326,6.887,8.312,6.887c0.526,0,1.06-0.05,1.596-0.151c4.596-0.877,7.611-5.312,6.734-9.908l-13.016-68.266 + c15.542,13.942,26.003,32.501,29.832,53.373C159.561,329.492,154.153,354.706,139.759,375.565z M237.264,233.193 + c4.49,0,8.728,0.403,12.538,1.218c8.228,1.76,13.833,5.327,14.993,9.542c2.237,8.124-9.837,21.068-29.991,29.167 + c-1.482-18.263-8.799-30.305-15.982-37.893C225.137,233.884,231.414,233.193,237.264,233.193z M172.581,269.341 + c-1.972-7.16,7.909-19.538,27.31-28.047c5.596,3.595,17.573,13.935,18.211,36.864c-11.005,2.286-21.867,2.572-30.527,0.719 + C179.345,277.116,173.74,273.552,172.581,269.341z M263.34,454.766c-26.782-22.922-36.358-59.81-25.373-92.351l9.623,50.467 + c0.774,4.06,4.326,6.887,8.312,6.887c0.525,0,1.061-0.05,1.596-0.151c4.596-0.877,7.612-5.312,6.736-9.909l-9.475-49.695 + C276.157,386.509,280.119,424.06,263.34,454.766z M305.022,357.761l33.541-7.558c4.565-1.029,7.431-5.562,6.402-10.126 + c-1.028-4.565-5.564-7.429-10.126-6.401l-32.781,7.387c10.832-9.472,23.934-16.12,38.383-19.27 + c19.603-4.275,39.636-1.685,57.297,7.266c-12.331,15.486-29.47,26.181-49.075,30.454 + C333.963,362.718,319.022,362.064,305.022,357.761z M380.009,450.272c-4.232,2.313-15.181-3.089-22.711-16.838 + c-7.527-13.748-6.185-25.887-1.953-28.205c1.972-1.08,5.343-0.429,9.019,1.742c5.094,3.008,10.084,8.51,13.69,15.098 + c3.607,6.586,5.555,13.755,5.345,19.666C383.251,446,381.983,449.191,380.009,450.272z", ], ); @@ -395,7 +429,39 @@ let rename = ], ); +let doc = + simple_icon( + ~view="0 0 56 56", + [ + "M 15.5547 53.125 L 40.4453 53.125 C 45.2969 53.125 47.7109 50.6875 47.7109 45.7890 L 47.7109 10.2344 C 47.7109 5.3594 45.2969 2.8750 40.4453 2.8750 L 15.5547 2.8750 C 10.7266 2.8750 8.2891 5.3594 8.2891 10.2344 L 8.2891 45.7890 C 8.2891 50.6875 10.7266 53.125 15.5547 53.125 Z M 15.7422 49.3515 C 13.3281 49.3515 12.0625 48.0625 12.0625 45.7187 L 12.0625 10.3047 C 12.0625 7.9844 13.3281 6.6484 15.7656 6.6484 L 40.2578 6.6484 C 42.6953 6.6484 43.9375 7.9609 43.9375 10.3047 L 43.9375 45.7187 C 43.9375 48.0625 42.6953 49.3515 40.2813 49.3515 Z M 19.1406 16.4453 L 36.9062 16.4453 C 37.7266 16.4453 38.3594 15.7891 38.3594 14.9687 C 38.3594 14.1718 37.7266 13.5625 36.9062 13.5625 L 19.1406 13.5625 C 18.2734 13.5625 17.6640 14.1718 17.6640 14.9687 C 17.6640 15.7891 18.2734 16.4453 19.1406 16.4453 Z M 19.1406 24.625 L 36.9062 24.625 C 37.7266 24.625 38.3594 23.9687 38.3594 23.1484 C 38.3594 22.3515 37.7266 21.7422 36.9062 21.7422 L 19.1406 21.7422 C 18.2734 21.7422 17.6640 22.3515 17.6640 23.1484 C 17.6640 23.9687 18.2734 24.625 19.1406 24.625 Z M 19.1406 32.8047 L 27.5547 32.8047 C 28.375 32.8047 29.0078 32.1718 29.0078 31.3750 C 29.0078 30.5547 28.375 29.9218 27.5547 29.9218 L 19.1406 29.9218 C 18.2734 29.9218 17.6640 30.5547 17.6640 31.3750 C 17.6640 32.1718 18.2734 32.8047 19.1406 32.8047 Z", + ], + ); + let next = Node.text("⮕"); let prev = Node.text("⮕"); - let infoIcon = Node.text("❖"); + +let circle_with_check = + simple_icon( + ~view="0 0 800 800", + [ + "M486.62 267.969L358.3 421.875L313.379 368.017C309.179 362.977 304.027 358.814 298.218 355.765C292.409 352.716 286.056 350.841 279.523 350.247C272.989 349.653 266.402 350.351 260.139 352.303C253.875 354.255 248.057 357.421 243.017 361.621C237.977 365.821 233.813 370.972 230.764 376.781C227.715 382.59 225.84 388.943 225.246 395.477C224.652 402.011 225.351 408.597 227.303 414.861C229.254 421.125 232.42 426.942 236.62 431.982L319.921 531.982C324.603 537.616 330.467 542.151 337.098 545.265C343.729 548.379 350.963 549.995 358.289 550C365.614 550.005 372.851 548.397 379.485 545.292C386.12 542.187 391.99 537.66 396.679 532.032L563.379 332.032C567.615 326.995 570.816 321.172 572.797 314.896C574.778 308.62 575.5 302.015 574.922 295.459C574.344 288.903 572.477 282.526 569.429 276.693C566.38 270.86 562.21 265.687 557.157 261.47C552.104 257.253 546.268 254.075 539.985 252.119C533.701 250.163 527.092 249.466 520.539 250.07C513.985 250.674 507.615 252.566 501.795 255.638C495.974 258.709 490.817 262.9 486.62 267.969Z", + "M400 0C320.888 0 243.552 23.4596 177.772 67.4122C111.992 111.365 60.7234 173.836 30.4484 246.927C0.173314 320.017 -7.74802 400.444 7.68607 478.036C23.1202 555.629 61.2165 626.902 117.157 682.843C173.098 738.784 244.372 776.88 321.964 792.314C399.556 807.748 479.983 799.827 553.074 769.552C626.164 739.277 688.635 688.008 732.588 622.228C776.541 556.448 800 479.113 800 400C799.887 293.948 757.708 192.272 682.718 117.282C607.728 42.2919 506.052 0.112941 400 0ZM400 700C340.666 700 282.664 682.405 233.329 649.441C183.994 616.476 145.543 569.623 122.836 514.805C100.13 459.987 94.189 399.667 105.765 341.473C117.34 283.279 145.912 229.824 187.868 187.868C229.824 145.912 283.279 117.34 341.473 105.764C399.667 94.1888 459.987 100.13 514.805 122.836C569.623 145.542 616.477 183.994 649.441 233.329C682.405 282.664 700 340.666 700 400C699.913 479.538 668.279 555.794 612.036 612.036C555.794 668.278 479.539 699.913 400 700Z", + ], + ); + +let circle_with_no_check = + simple_icon( + ~view="0 0 800 800", + [ + "M400 0C320.888 0 243.552 23.4596 177.772 67.4122C111.992 111.365 60.7234 173.836 30.4484 246.927C0.173314 320.017 -7.74802 400.444 7.68607 478.036C23.1202 555.629 61.2165 626.902 117.157 682.843C173.098 738.784 244.372 776.88 321.964 792.314C399.556 807.748 479.983 799.827 553.074 769.552C626.164 739.277 688.635 688.008 732.588 622.228C776.541 556.448 800 479.113 800 400C799.887 293.948 757.708 192.272 682.718 117.282C607.728 42.2919 506.052 0.112941 400 0ZM400 700C340.666 700 282.664 682.405 233.329 649.441C183.994 616.476 145.543 569.623 122.836 514.805C100.13 459.987 94.189 399.667 105.765 341.473C117.34 283.279 145.912 229.824 187.868 187.868C229.824 145.912 283.279 117.34 341.473 105.764C399.667 94.1888 459.987 100.13 514.805 122.836C569.623 145.542 616.477 183.994 649.441 233.329C682.405 282.664 700 340.666 700 400C699.913 479.538 668.279 555.794 612.036 612.036C555.794 668.278 479.539 699.913 400 700Z", + ], + ); + +let library = + simple_icon( + ~view="0 0 36 36", + [ + "M33.48,29.63,26.74,11.82a2,2,0,0,0-2.58-1.16L21,11.85V8.92A1.92,1.92,0,0,0,19.08,7H14V4.92A1.92,1.92,0,0,0,12.08,3H5A2,2,0,0,0,3,5V32a1,1,0,0,0,1,1H20a1,1,0,0,0,1-1V19.27l5,13.21a1,1,0,0,0,1.29.58l5.61-2.14a1,1,0,0,0,.58-1.29ZM12,8.83V31H5V5h7ZM19,31H14V9h5Zm8.51-.25L21.13,13.92l3.74-1.42,6.39,16.83Z", + ], + ); diff --git a/src/web/app/editors/code/CodeEditable.re b/src/web/app/editors/code/CodeEditable.re index 2ee6d7ee8b..cd3c28361b 100644 --- a/src/web/app/editors/code/CodeEditable.re +++ b/src/web/app/editors/code/CodeEditable.re @@ -65,6 +65,7 @@ module Update = { | Cut | Reparse | Introduce + | Composition(_) | Dump => true | Project(_) | Unselect(_) diff --git a/src/web/app/editors/code/CodeSelectable.re b/src/web/app/editors/code/CodeSelectable.re index eecaec1179..e5e1dc698d 100644 --- a/src/web/app/editors/code/CodeSelectable.re +++ b/src/web/app/editors/code/CodeSelectable.re @@ -49,7 +49,8 @@ module Update = { Buffer(_) | Project(_) | Introduce | - Dump, + Dump | + Composition(_), ) | DebugConsole(_) | TAB => None; diff --git a/src/web/app/explainthis/Example.re b/src/web/app/explainthis/Example.re old mode 100644 new mode 100755 diff --git a/src/web/app/helpful-assistant/AssistantModel.re b/src/web/app/helpful-assistant/AssistantModel.re deleted file mode 100644 index dfbbfc86c9..0000000000 --- a/src/web/app/helpful-assistant/AssistantModel.re +++ /dev/null @@ -1,303 +0,0 @@ -open Util; - -[@deriving (show({with_path: false}), sexp, yojson)] -type system = - // Issue that we catch and inform the user about. - // Do not send this to the model and display to user as error message - | InternalError - // The system prompt for the model. - // Send this to the model. - // Display to user as expandable/collapsable system message. - | AssistantPrompt; - -[@deriving (show({with_path: false}), sexp, yojson)] -type role = - | System(system) - | User - | Assistant - | Tool; - -let string_of_role = - fun - | System(AssistantPrompt) => "System" - | System(InternalError) => "Error" - | User => "User" - | Assistant => "Assistant" - | Tool => "Tool"; - -[@deriving (show({with_path: false}), sexp, yojson)] -type block_kind = - | Text(string) - | Code(Haz3lcore.Segment.t); - -[@deriving (show({with_path: false}), sexp, yojson)] -type display = { - displayable_content: list(block_kind), - original_content: string, - role, - collapsed: bool, -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type chat = { - outgoing_messages: list(OpenRouter.message), - message_displays: list(display), - id: Id.t, - descriptor: string, - timestamp: float, -}; - -// We save the history of past chats as a hash map with chat IDs as keys. -[@deriving (show({with_path: false}), sexp, yojson)] -type chat_history = { - // History logs of past chats stored as hash maps with chat IDs as keys - past_tutor_chats: Id.Map.t(chat), - past_suggestion_chats: Id.Map.t(chat), - past_composition_chats: Id.Map.t(chat), -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type current_chats = { - // Current active chat IDs for each mode - curr_tutor_chat: Id.t, - curr_suggestion_chat: Id.t, - curr_composition_chat: Id.t, -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type external_api_info = { - available_models: list(OpenRouter.model_info), - set_model: string, - api_key: string, -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type init_prompt_data = { - init_tutor_chat: chat, - init_composition_chat: chat, - init_suggestion_chat_basic: chat, - init_suggestion_chat_cot: chat, -}; - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = { - current_chats, - chat_history, - external_api_info, - init_prompt_data, - loop: bool, -}; - -// This is important when we need to display the history of chats in chronological order. -let sorted_chats = (chat_map: Id.Map.t(chat)): list(chat) => { - chat_map - |> Id.Map.bindings - |> List.map(((_, chat)) => chat) - |> List.sort((a, b) => int_of_float(b.timestamp -. a.timestamp)); -}; - -let max_collapsed_length: int = 500; - -let mk_mode_prompt = - (~mode: AssistantSettings.mode): option(OpenRouter.message) => { - let prompt = - switch (mode) { - | HazelTutor => Some(InitPrompts.mk_tutor()) - | CodeSuggestion => - Some( - ChatLSP.Completion.mk_const_prompt( - ChatLSP.Options.init, - "code_suggestion", - false, - ), - ) - | TaskCompletion => Some(InitPrompts.mk_composition()) - }; - prompt; -}; - -let parse_blocks = (response: string): list(block_kind) => { - let rec parse_blocks = - (str: string, acc: list(block_kind)): list(block_kind) => { - open Haz3lcore; - let pattern = Str.regexp("```[ \n]*\\([^`]+\\)[ \n]*```"); - switch (Str.search_forward(pattern, str, 0)) { - | exception Not_found => acc - | pos => - let acc = ListUtil.leading(acc); - let code = Str.matched_group(1, str); - let zipper_of_code = Parser.to_zipper(code); - let sketch_z = - switch (zipper_of_code) { - | Some(z) => z - | None => - print_endline("Failed to parse content into segment.\n"); - Zipper.init(); - }; - let sketch = Dump.to_segment(sketch_z); - let before = Str.string_before(str, pos); - let rest_start = pos + String.length(Str.matched_string(str)); - if (rest_start >= String.length(str)) { - acc @ [Text(before), Code(sketch)]; - } else { - let rest = Str.string_after(str, rest_start); - parse_blocks( - rest, - acc @ [Text(before), Code(sketch), Text(rest)], - ); - }; - }; - }; - parse_blocks(response, [Text(response)]); -}; - -let mk_message_display = (~content: string, ~role: role): display => { - { - displayable_content: [Text(content)], - original_content: content, - role, - collapsed: - String.length(content) > max_collapsed_length - || role == System(AssistantPrompt), - }; -}; - -let init_chat = (mode: AssistantSettings.mode): chat => { - let (init_message, init_message_display) = - switch (mk_mode_prompt(~mode)) { - | Some(init_message) => ( - [init_message], - [ - mk_message_display( - ~content=init_message.content, - ~role=System(AssistantPrompt), - ), - ], - ) - | None => ([], []) - }; - - { - outgoing_messages: init_message, - message_displays: init_message_display, - id: Id.mk(), - descriptor: "", - timestamp: JsUtil.timestamp(), - }; -}; - -let new_chat = (model: t, mode: AssistantSettings.mode): chat => { - let (init_message, init_message_display) = - switch (mode) { - | HazelTutor => ( - model.init_prompt_data.init_tutor_chat.outgoing_messages, - model.init_prompt_data.init_tutor_chat.message_displays, - ) - | CodeSuggestion => ( - model.init_prompt_data.init_suggestion_chat_basic.outgoing_messages, - model.init_prompt_data.init_suggestion_chat_basic.message_displays, - ) - | TaskCompletion => ( - model.init_prompt_data.init_composition_chat.outgoing_messages, - model.init_prompt_data.init_composition_chat.message_displays, - ) - }; - { - outgoing_messages: init_message, - message_displays: init_message_display, - id: Id.mk(), - descriptor: "", - timestamp: JsUtil.timestamp(), - }; -}; - -let add_chat_to_history = - (chat: chat, history: Id.Map.t(chat)): Id.Map.t(chat) => - Id.Map.add(chat.id, chat, history); - -let init = (): t => { - let (init_tutor_chat, init_suggestion_chat, init_composition_chat) = ( - init_chat(HazelTutor), - init_chat(CodeSuggestion), - init_chat(TaskCompletion), - ); - { - init_prompt_data: { - init_tutor_chat, - init_composition_chat, - init_suggestion_chat_basic: init_suggestion_chat, - init_suggestion_chat_cot: init_suggestion_chat, - }, - current_chats: { - curr_tutor_chat: init_tutor_chat.id, - curr_suggestion_chat: init_suggestion_chat.id, - curr_composition_chat: init_composition_chat.id, - }, - chat_history: { - past_tutor_chats: add_chat_to_history(init_tutor_chat, Id.Map.empty), - past_suggestion_chats: - add_chat_to_history(init_suggestion_chat, Id.Map.empty), - past_composition_chats: - add_chat_to_history(init_composition_chat, Id.Map.empty), - }, - external_api_info: { - available_models: [], - set_model: "", - api_key: "", - }, - loop: false, - }; -}; - -// We defer true initialization of the assistant model until the user opens the chat interface. -let null_model = (): t => { - let null_chat = { - outgoing_messages: [], - message_displays: [ - mk_message_display( - ~content= - "Please set an API key in the settings to start using the Hazel Assistant.", - ~role=System(InternalError), - ), - ], - id: Id.invalid, - descriptor: "Please set an API key", - timestamp: JsUtil.timestamp(), - }; - { - init_prompt_data: { - init_tutor_chat: null_chat, - init_composition_chat: null_chat, - init_suggestion_chat_basic: null_chat, - init_suggestion_chat_cot: null_chat, - }, - current_chats: { - curr_tutor_chat: null_chat.id, - curr_suggestion_chat: null_chat.id, - curr_composition_chat: null_chat.id, - }, - chat_history: { - past_tutor_chats: add_chat_to_history(null_chat, Id.Map.empty), - past_suggestion_chats: add_chat_to_history(null_chat, Id.Map.empty), - past_composition_chats: add_chat_to_history(null_chat, Id.Map.empty), - }, - external_api_info: { - available_models: [], - set_model: "", - api_key: "", - }, - loop: false, - }; -}; - -[@deriving (show({with_path: false}), yojson, sexp)] -type model = t; - -module Store = - Store.F({ - [@deriving (show({with_path: false}), yojson, sexp)] - type t = model; - let default = () => null_model(); - - let key = Store.Assistant; - }); diff --git a/src/web/app/helpful-assistant/AssistantUpdate.re b/src/web/app/helpful-assistant/AssistantUpdate.re deleted file mode 100644 index 38d53f8113..0000000000 --- a/src/web/app/helpful-assistant/AssistantUpdate.re +++ /dev/null @@ -1,1318 +0,0 @@ -open Haz3lcore; -open Util; -open Util.OptUtil.Syntax; -open API; - -module CodeModel = CodeEditable.Model; -module Model = AssistantModel; - -[@deriving (show({with_path: false}), sexp, yojson)] -type completion = - | Request(Id.t, bool) // When user presses ?? or ?a - | Query(string) // User may followup with a query - | Loop(string, Id.t, int); // Error rounds - -[@deriving (show({with_path: false}), sexp, yojson)] -type composition = - | Request(string) // User-submitted task, question, etc - | Loop(int, OpenRouter.tool_contents); // Iterative tool completion loop - -// Actions to send various kinds of messages to the LLM -[@deriving (show({with_path: false}), sexp, yojson)] -type send_message = - | Tutor(string) - | Completion(completion) - | Composition(composition); - -// Actions to handle certain kinds of LLM responses -[@deriving (show({with_path: false}), sexp, yojson)] -type handle_response = - | Tutor - | CompletionErrorRound(CodeModel.t, int, Id.t) - | CompletionQueryResponse - | CompositionLoopRound(CodeModel.t, int); - -// Actions which actualize actions via LLM responses -[@deriving (show({with_path: false}), sexp, yojson)] -type employ_llm_action = - | RemoveAndSuggest(string, Id.t) - | Describe(string, AssistantSettings.mode, Id.t) - | SetLoop(bool); - -// Future Todo: (Check whether) These might be able to be relocated to AssistantSettings -// Although, arguably, the chat is inherently part of the assistant model, -// serving as a sort of memory. -// Actions that are related to the chat history and/or display of chat messages -[@deriving (show({with_path: false}), sexp, yojson)] -type chat_action = - | NewChat - | DeleteChat(Id.t) - | SwitchChat(Id.t) - | CollapseMessage(int) - | FilterLoadingMessages; - -[@deriving (show({with_path: false}), sexp, yojson)] -type external_api_action = - // Sets the LLM model - | SetLLM(string) - // Sets the API key. - // This will implicitely make a call to OpenRouter to get and set the list of available LLMs. - | SetAPIKey(string) - // Sets the list of available LLMs from OpenRouter - | SetListOfLLMs(list(OpenRouter.model_info)); - -[@deriving (show({with_path: false}), sexp, yojson)] -type t = - | SendMessage(send_message, CodeModel.t, Id.t) - | HandleResponse(handle_response, OpenRouter.reply, Id.t) - | EmployLLMAction(employ_llm_action) - | ChatAction(chat_action) - | InternalError(string, AssistantSettings.mode, Id.t) - | ExternalAPIAction(external_api_action) - | InitializeAssistant; - -let can_undo = (action: t) => { - // TODO: Implement the handling of actions that should be undoable - // I'm thinking none of these actions should be undoable... - // Maybe set API key? - // That could be a good starter project to navigate this assistant part of the codebase. - switch (action) { - | SendMessage(_) => false - | HandleResponse(_) => true /* Necessary to make completion instantiation undoable */ - | EmployLLMAction(_) => false - | ChatAction(_) => false - | InternalError(_) => false - | ExternalAPIAction(_) => false - | InitializeAssistant => false - }; -}; - -let get_mode_info = (mode: AssistantSettings.mode, model: Model.t) => { - switch (mode) { - | HazelTutor => ( - model.chat_history.past_tutor_chats, - Id.Map.find( - model.current_chats.curr_tutor_chat, - model.chat_history.past_tutor_chats, - ), - ) - | CodeSuggestion => ( - model.chat_history.past_suggestion_chats, - Id.Map.find( - model.current_chats.curr_suggestion_chat, - model.chat_history.past_suggestion_chats, - ), - ) - | TaskCompletion => ( - model.chat_history.past_composition_chats, - Id.Map.find( - model.current_chats.curr_composition_chat, - model.chat_history.past_composition_chats, - ), - ) - }; -}; - -/* - let await_llm_response: Model.display = { - displayable_content: [Text("...")], - original_content: "...", - role: Assistant, - collapsed: false, - }; - */ - -let filter_chat_messages = - (message_displays: list(Model.display)): list(Model.display) => { - List.filter( - (message_display: Model.display) => { - !( - message_display.role == Assistant - && message_display.displayable_content == [Text("...")] - && !message_display.collapsed - ) - }, - message_displays, - ); -}; - -let resculpt_model = - ( - ~model: Model.t, - ~mode: AssistantSettings.mode, - ~updated_past_chats: Id.Map.t(Model.chat), - ~chat_id: Id.t, - ) => { - Model.{ - ...model, - chat_history: { - past_tutor_chats: - mode == HazelTutor - ? updated_past_chats : model.chat_history.past_tutor_chats, - past_suggestion_chats: - mode == CodeSuggestion - ? updated_past_chats : model.chat_history.past_suggestion_chats, - past_composition_chats: - mode == TaskCompletion - ? updated_past_chats : model.chat_history.past_composition_chats, - }, - // This is tentative. Keep this if we want the user to be shown the most recent chat. - // Remove this if we want the user to be shown the chat they last/currently interact with. - // This is honestly such an edge case that it probably doesn't matter. - current_chats: { - curr_tutor_chat: - mode == HazelTutor ? chat_id : model.current_chats.curr_tutor_chat, - curr_suggestion_chat: - mode == CodeSuggestion - ? chat_id : model.current_chats.curr_suggestion_chat, - curr_composition_chat: - mode == TaskCompletion - ? chat_id : model.current_chats.curr_composition_chat, - }, - }; -}; - -let update_model_chat_history = - ( - ~model: Model.t, - ~mode: AssistantSettings.mode, - ~updated_chat: Model.chat, - ) - : Model.t => { - let new_chat = - switch (mode) { - | HazelTutor => - Id.Map.update( - updated_chat.id, - maybe_chat => - switch (maybe_chat) { - | Some(_) => Some(updated_chat) - | None => None - }, - model.chat_history.past_tutor_chats, - ) - | CodeSuggestion => - Id.Map.update( - updated_chat.id, - maybe_chat => - switch (maybe_chat) { - | Some(_) => Some(updated_chat) - | None => None - }, - model.chat_history.past_suggestion_chats, - ) - | TaskCompletion => - Id.Map.update( - updated_chat.id, - maybe_chat => - switch (maybe_chat) { - | Some(_) => Some(updated_chat) - | None => None - }, - model.chat_history.past_composition_chats, - ) - }; - let updated_chat_history = - switch (mode) { - | HazelTutor => { - ...model.chat_history, - past_tutor_chats: new_chat, - } - | CodeSuggestion => { - ...model.chat_history, - past_suggestion_chats: new_chat, - } - | TaskCompletion => { - ...model.chat_history, - past_composition_chats: new_chat, - } - }; - { - ...model, - chat_history: updated_chat_history, - }; -}; - -let create_chat_descriptor = - ( - ~model: Model.t, - ~schedule_action, - ~mode: AssistantSettings.mode, - ~chat_id: Id.t, - ) - : unit => { - let (past_chats, _) = get_mode_info(mode, model); - let curr_chat = Id.Map.find(chat_id, past_chats); - - let this_prompt = - String.concat( - "\n", - [ - "You are a helpful assistant that *summarizes* conversations between other assistants and users. ", - "Your summaries should be less than or equal to 7 words, and may include 1 or 2 emojis, if appropriate. ", - "NEVER exceed 7 words. ", - "ONLY provide the summarizing title in your response, do NOT include any other text. ", - "You will be given a conversation between an assistant and a user. ", - "Focus on the giving a summarizing topic title to the conversation between the assistant and the user. ", - "NEVER use first person pronouns in your response. ", - "EVERY response will be displayed as a summarizaing title, so do NOT respond with anything other than a summarizing title. ", - switch (mode) { - | HazelTutor => "This is known to be a chat between a hazel user and an LLM acting as a tutor." - | CodeSuggestion => "This is known to be a chat between a hazel user and an LLM acting as a code suggestion assistant. This means there won't be much dialogue, rather just a prompt, code contexts, and a code suggestion (potentially with a chain of thought), so please do your best to summarize based on the code context and the code suggestion." - | TaskCompletion => "This is known to be a chat between a student and an LLM acting as a task completion assistant." - }, - "With this said, please now provide a summary for the conversation: ", - ], - ); - - let filtered_messages = - List.filter( - (message: Model.display) => { - message.role == User || message.role == Assistant - }, - curr_chat.message_displays, - ); - - let combined_messages = - String.concat( - "\n", - List.map( - (message: Model.display) => { - "<" - ++ Model.string_of_role(message.role) - ++ ">" - ++ message.original_content - ++ "" - }, - filtered_messages, - ), - ); - - let outgoing_messages_for_descriptor = [ - OpenRouter.mk_system_msg(this_prompt), - OpenRouter.mk_user_msg(combined_messages), - ]; - - // Only make descriptor after first few exchanges - List.length(filtered_messages) <= AssistantSettings.make_descriptor_max - ? try({ - let model_id = model.external_api_info.set_model; - let key = model.external_api_info.api_key; - let params: OpenRouter.params = { - ...OpenRouter.default_params, - model_id, - stream: false, - }; - OpenRouter.start_chat( - ~params, - ~key, - ~outgoing_messages=outgoing_messages_for_descriptor, - req => - switch (OpenRouter.handle_chat(req)) { - | Some(Reply({content, _})) => - schedule_action( - EmployLLMAction(Describe(content, mode, chat_id)), - ) - | Some(Error(_)) => - raise( - Invalid_argument( - "Error in receiving response from OpenRouter when creating descriptor", - ), - ) - | None => () - } - ); - }) { - | Invalid_argument(e) => - print_endline("Invalid_argument when creating descriptor: " ++ e); - (); - } - : (); -}; - -let check_req = - ( - ~schedule_action: t => unit, - ~schedule_setting: AssistantSettings.action => unit, - ~editor: CodeEditable.Model.t, - ~chat_id: Id.t, - ) - : unit => { - let z = editor.editor.state.zipper; - let caret = z.caret; - let send_message = (tile_id, advanced_reasoning) => { - schedule_setting(AssistantSettings.SwitchMode(CodeSuggestion)); - schedule_action( - SendMessage( - Completion(Request(tile_id, advanced_reasoning)), - editor, - chat_id, - ), - ); - }; - - // Check if user just typed ?? - switch (caret, Zipper.neighbor_tokens(z)) { - | (Outer, (_, Some("??"))) - | (Outer, (Some("??"), _)) => - let tileId = Option.get(Indicated.index(z)); - let advanced_reasoning = false; - send_message(tileId, advanced_reasoning); - | (Outer, (_, Some("?a"))) - | (Outer, (Some("?a"), _)) => - let tileId = Option.get(Indicated.index(z)); - let advanced_reasoning = true; - send_message(tileId, advanced_reasoning); - | _ => () - }; -}; - -// Sends a request to OpenRouter given outgoing messages. -// Handles the response from OpenRouter. -// Emits internal error if API key or model ID is not set. -let mk_llm_call = - ( - ~mode: AssistantSettings.mode, - ~model: Model.t, - ~schedule_action: t => unit, - ~updated_chat: Model.chat, - ~response_handler: OpenRouter.reply => t, - ) - : unit => { - switch (model.external_api_info.api_key, model.external_api_info.set_model) { - | ("", _) => - let content = "No API key found. Please set an API key in the assistant settings."; - schedule_action(InternalError(content, mode, updated_chat.id)); - | (_, "") => - let content = "No model ID found. Please set a model ID in the assistant settings."; - schedule_action(InternalError(content, mode, updated_chat.id)); - | (key, model_id) => - let tools = - if (mode == TaskCompletion) { - [ - CompositionPrompt.goto_definition, - CompositionPrompt.goto_body, - CompositionPrompt.goto_type_definition, - CompositionPrompt.goto_type_body, - CompositionPrompt.select_all, - CompositionPrompt.paste, - CompositionPrompt.delete, - //CompositionPrompt.submit, - ]; - } else { - []; - }; - let params: OpenRouter.params = { - ...OpenRouter.default_params, - model_id, - tools, - }; - try( - OpenRouter.start_chat( - ~params, ~key, ~outgoing_messages=updated_chat.outgoing_messages, req => - switch (OpenRouter.handle_chat(req)) { - | Some(Reply(response)) => - schedule_action(response_handler(response)) - | Some(Error({message, code})) => - schedule_action( - InternalError( - "Error: " ++ message ++ " (code: " ++ string_of_int(code) ++ ")", - mode, - updated_chat.id, - ), - ) - | None => - /*let str_of_mode = - switch (mode) { - | HazelTutor => "HazelTutor" - | CodeSuggestion => "CodeSuggestion" - | TaskCompletion => "TaskCompletion" - }; - print_endline( - "Assistant: response parse failed (" ++ str_of_mode ++ ")", - );*/ - () - } - ) - ) { - | Invalid_argument(e) => - print_endline( - "Issue when making LLM call. (This is likely from an Option.get during sketch sending): " - ++ e, - ) - | _ => () - }; - }; -}; - -let update = - ( - ~settings: Settings.t, - ~action, - ~model: Model.t, - // todo: Find a way to track unqique editor between concurrent actions - ~editor: CodeModel.t, - ~schedule_action: t => unit, - ~schedule_editor_action: Editors.Update.t => unit, - ) - : Updated.t(Model.t) => { - switch (action) { - | SendMessage(kind, _, chat_id) => - if (model.current_chats.curr_tutor_chat == Id.invalid) { - model |> Updated.return_quiet; - } else { - switch (kind) { - | Tutor(content) => - let mode = AssistantSettings.HazelTutor; - let curr_chat = - Id.Map.find(chat_id, model.chat_history.past_tutor_chats); - let user_message = OpenRouter.mk_user_msg(content); - let ctx = - OpenRouter.mk_user_msg( - String.concat("\n", ChatLSP.get_sketch_and_error_ctx(editor)), - ); - let new_message_displays = [ - Model.mk_message_display(~content=user_message.content, ~role=User), - Model.mk_message_display( - ~content=ctx.content, - ~role=System(AssistantPrompt), - ), - ]; - let new_outgoing_messages = - curr_chat.outgoing_messages @ [user_message, ctx]; - - let updated_chat = { - ...curr_chat, - outgoing_messages: - curr_chat.outgoing_messages @ new_outgoing_messages, - message_displays: curr_chat.message_displays @ new_message_displays, - }; - - mk_llm_call( - ~mode, - ~model, - ~schedule_action, - ~updated_chat, - ~response_handler=response => - HandleResponse(Tutor, response, chat_id) - ); - - update_model_chat_history(~model, ~mode, ~updated_chat) - |> Updated.return_quiet; - - | Composition(kind) => - let mode = AssistantSettings.TaskCompletion; - let curr_chat = - Id.Map.find(chat_id, model.chat_history.past_composition_chats); - switch (kind) { - | Request(content) => - schedule_action(EmployLLMAction(SetLoop(false))); - let user_message = OpenRouter.mk_user_msg(content); - let ctx = - ChatLSP.Composition.mk_ctx_prompt(ChatLSP.Options.init, editor); - let new_message_displays = [ - Model.mk_message_display( - ~content=user_message.content, - ~role=User, - ), - Model.mk_message_display( - ~content=ctx.content, - ~role=System(AssistantPrompt), - ), - ]; - let new_outgoing_messages = [user_message, ctx]; - - let updated_chat = { - ...curr_chat, - outgoing_messages: - curr_chat.outgoing_messages @ new_outgoing_messages, - message_displays: - curr_chat.message_displays @ new_message_displays, - }; - - mk_llm_call( - ~mode, - ~model, - ~schedule_action, - ~updated_chat, - ~response_handler=response => - HandleResponse( - CompositionLoopRound( - editor, - ChatLSP.Composition.max_tool_calls, - ), - response, - chat_id, - ) - ); - - update_model_chat_history(~model, ~mode, ~updated_chat) - |> Updated.return_quiet; - - | Loop(fuel, tool_contents) => - let ctx = - ChatLSP.Composition.mk_ctx_prompt(ChatLSP.Options.init, editor); - - let tool_message = - OpenRouter.mk_tool_msg(ctx.content, tool_contents); - - let new_message_displays = [ - Model.mk_message_display( - ~content=ctx.content, - ~role=System(AssistantPrompt), - ), - ]; - let new_outgoing_messages = [tool_message]; - - let updated_chat = { - ...curr_chat, - outgoing_messages: - curr_chat.outgoing_messages @ new_outgoing_messages, - message_displays: - curr_chat.message_displays @ new_message_displays, - }; - - mk_llm_call( - ~mode, - ~model, - ~schedule_action, - ~updated_chat, - ~response_handler=response => - HandleResponse( - CompositionLoopRound(editor, fuel), - response, - chat_id, - ) - ); - - update_model_chat_history(~model, ~mode, ~updated_chat) - |> Updated.return_quiet; - }; - - | Completion(kind) => - let mode = AssistantSettings.CodeSuggestion; - switch (kind) { - | Request(tile_id, advanced_reasoning) => - let new_chat = Model.new_chat(model, mode); - print_endline("new_chat: " ++ Id.to_string(new_chat.id)); - let updated_past_chats = - Model.add_chat_to_history( - new_chat, - model.chat_history.past_suggestion_chats, - ); - let model_with_new_chat = - resculpt_model( - ~model, - ~mode, - ~updated_past_chats, - ~chat_id=new_chat.id, - ); - let tag = String.sub(Id.to_string(tile_id), 0, 3); - switch ( - { - let* sketch_z_with_tag = - Parser.to_zipper( - ~zipper_init=editor.editor.state.zipper, - tag, - ); - let sketch_seg = Dump.to_segment(sketch_z_with_tag); - let* index = Indicated.index(editor.editor.state.zipper); - let+ ci = Id.Map.find_opt(index, editor.statics.info_map); - ChatLSP.Completion.mk_ctx_prompt( - ChatLSP.Options.init, - ci, - sketch_seg, - (advanced_reasoning ? "?a" : "??") ++ tag, - ); - } - ) { - | None => - print_endline("Suggestion prompt generation failed"); - model_with_new_chat |> Updated.return_quiet; - | Some(ctx_prompt) => - let new_message_display = - Model.mk_message_display( - ~content= - String.concat( - "\n", - List.map( - (msg: OpenRouter.message) => msg.content, - [ctx_prompt], - ), - ), - ~role=User, - ); - let updated_chat = { - ...new_chat, - outgoing_messages: new_chat.outgoing_messages @ [ctx_prompt], - message_displays: - new_chat.message_displays @ [new_message_display], - }; - mk_llm_call( - ~mode, - ~model, - ~schedule_action, - ~updated_chat, - ~response_handler=response => - HandleResponse( - CompletionErrorRound( - editor, - ChatLSP.Options.init.error_rounds_max, - tile_id, - ), - response, - new_chat.id, - ) - ); - update_model_chat_history( - ~model=model_with_new_chat, - ~mode=settings.assistant.mode, - ~updated_chat, - ) - |> Updated.return_quiet; - }; - | Query(content) => - let curr_chat = - Id.Map.find(chat_id, model.chat_history.past_suggestion_chats); - let user_message = OpenRouter.mk_user_msg(content); - let ctx = - OpenRouter.mk_user_msg( - String.concat("\n", ChatLSP.get_sketch_and_error_ctx(editor)), - ); - let new_message_displays = [ - Model.mk_message_display( - ~content=user_message.content, - ~role=User, - ), - Model.mk_message_display( - ~content=ctx.content, - ~role=System(AssistantPrompt), - ), - ]; - let new_outgoing_messages = - curr_chat.outgoing_messages @ [user_message, ctx]; - let updated_chat = { - ...curr_chat, - outgoing_messages: - curr_chat.outgoing_messages @ new_outgoing_messages, - message_displays: - curr_chat.message_displays @ new_message_displays, - }; - - mk_llm_call( - ~mode, - ~model, - ~schedule_action, - ~updated_chat, - ~response_handler=response => - HandleResponse(CompletionQueryResponse, response, chat_id) - ); - - update_model_chat_history(~model, ~mode, ~updated_chat) - |> Updated.return_quiet; - - | Loop(error, tile_id, fuel) => - let curr_chat = - Id.Map.find(chat_id, model.chat_history.past_suggestion_chats); - let error_message = - OpenRouter.mk_user_msg( - "Your previous response caused the following error. Please fix it in your response: " - ++ error, - ); - let new_outgoing_messages = [error_message]; - let new_message_displays = [ - Model.mk_message_display( - ~content=error_message.content, - ~role=System(AssistantPrompt), - ), - ]; - let updated_chat = { - ...curr_chat, - outgoing_messages: - curr_chat.outgoing_messages @ new_outgoing_messages, - message_displays: - curr_chat.message_displays @ new_message_displays, - }; - - // check that fuel is not 0 - if (fuel < 0) { - let content = - "By default we stop the assistant after " - ++ string_of_int(ChatLSP.Options.init.error_rounds_max) - ++ " error rounds."; - schedule_action(EmployLLMAction(SetLoop(false))); - schedule_action(InternalError(content, mode, updated_chat.id)); - } else { - mk_llm_call( - ~mode, - ~model, - ~schedule_action, - ~updated_chat, - ~response_handler=response => - HandleResponse( - CompletionErrorRound(editor, fuel, tile_id), - response, - chat_id, - ) - ); - }; - update_model_chat_history(~model, ~mode, ~updated_chat) - |> Updated.return_quiet; - }; - }; - } - | InternalError(content, mode, chat_id) => - let curr_chat = - switch ( - Id.Map.find_opt(chat_id, model.chat_history.past_composition_chats) - ) { - | Some(chat) => chat - | None => - print_endline("Error: Chat not found"); - get_mode_info(mode, model) |> snd; - }; - - // todo: Should this be a user, assistant, or system message? - // We could make it assistant and put it in the first-person. - let new_outgoing_messages = [OpenRouter.mk_system_msg(content)]; - - let new_message_displays = [ - Model.mk_message_display(~content, ~role=System(InternalError)), - ]; - - // Note: We aren't sending a message here, but we do add it to the chat history. - // for future reference for the LLM so it isn't confused. - // (Eg: Max tool call limit reached, agent should know from history that this - // is why their prior task completion was not successful.) - let updated_chat = { - ...curr_chat, - outgoing_messages: curr_chat.outgoing_messages @ new_outgoing_messages, - message_displays: curr_chat.message_displays @ new_message_displays, - }; - update_model_chat_history(~model, ~mode, ~updated_chat) - |> Updated.return_quiet; - - | HandleResponse(response_kind, response, chat_id) => - let (curr_chat, mode) = - switch (response_kind) { - | Tutor => ( - Id.Map.find(chat_id, model.chat_history.past_tutor_chats), - AssistantSettings.HazelTutor, - ) - | CompositionLoopRound(_) => ( - Id.Map.find(chat_id, model.chat_history.past_composition_chats), - AssistantSettings.TaskCompletion, - ) - | CompletionErrorRound(_) => ( - Id.Map.find(chat_id, model.chat_history.past_suggestion_chats), - AssistantSettings.CodeSuggestion, - ) - | CompletionQueryResponse => ( - Id.Map.find(chat_id, model.chat_history.past_suggestion_chats), - AssistantSettings.CodeSuggestion, - ) - }; - create_chat_descriptor(~model, ~schedule_action, ~mode, ~chat_id); - - let content = response.content; - let tool_call = response.tool_call; - - // If streaming, update the last message display - let (updated_outgoing_messages, updated_message_displays) = { - let last_display = ListUtil.last(curr_chat.message_displays); - if (last_display.role == Assistant) { - let updated_content = last_display.original_content ++ content; - ( - ListUtil.leading(curr_chat.outgoing_messages) - @ [OpenRouter.mk_assistant_msg(updated_content)], - ListUtil.leading(curr_chat.message_displays) - @ [ - Model.mk_message_display( - ~content=updated_content, - ~role=Assistant, - ), - ], - ); - } else { - switch (tool_call) { - | Some(tool_call) => - let updated_message_displays = - Model.mk_message_display( - ~content= - "Agent called \"" - ++ tool_call.name - ++ "\" with the following arguments: " - ++ Json.to_string(tool_call.args), - ~role=Tool, - ); - /* - switch (Json.dot("code", tool_call.args)) { - | Some(`String(arg)) => - let curr_selection = - ErrorPrint.Print.seg( - ~holes=Some("?"), - editor.editor.state.zipper.selection.content, - ); - if (curr_selection == "") { - mk_message_display( - ~content= - "Agent is inserting the following code: " - ++ "```" - ++ arg - ++ "```", - ~role=Tool, - ); - } else { - mk_message_display( - ~content= - "Agent is replacing " - ++ "```" - ++ curr_selection - ++ "``` with " - ++ "```" - ++ arg - ++ "```", - ~role=Tool, - ); - }; - | _ => - mk_message_display( - ~content= - "Agent called \"" - ++ tool_call.name - ++ "\" with the following arguments: " - ++ Json.to_string(tool_call.args), - ~role=Tool, - ) - }; - */ - - switch (content) { - | "" => ( - curr_chat.outgoing_messages, - curr_chat.message_displays @ [updated_message_displays], - ) - | _ => ( - curr_chat.outgoing_messages - @ [OpenRouter.mk_assistant_msg(content)], - curr_chat.message_displays - @ [ - Model.mk_message_display(~content, ~role=Assistant), - updated_message_displays, - ], - ) - }; - | None => ( - curr_chat.outgoing_messages - @ [OpenRouter.mk_assistant_msg(content)], - curr_chat.message_displays - @ [Model.mk_message_display(~content, ~role=Assistant)], - ) - }; - }; - }; - - let updated_chat = { - ...curr_chat, - outgoing_messages: updated_outgoing_messages, - message_displays: updated_message_displays, - }; - - switch (response_kind) { - | Tutor => () - | CompositionLoopRound(_, fuel) => - switch (tool_call, fuel) { - | (None, _) => () - | (_, 0) => - schedule_action( - InternalError( - "By default, we stop the agent after " - ++ string_of_int(ChatLSP.Composition.max_tool_calls) - ++ " tool calls.", - mode, - chat_id, - ), - ) - | (Some(tool_call), _) => - let goto = - ChatLSP.Composition.goto(~schedule_action=schedule_editor_action); - let edit = - ChatLSP.Composition.edit(~schedule_action=schedule_editor_action); - let loop_message = - SendMessage( - Composition( - Loop( - fuel - 1, - { - tool_call_id: tool_call.id, - name: tool_call.name, - }, - ), - ), - editor, - chat_id, - ); - try( - switch (tool_call.name) { - | "goto_definition" => - switch (Json.dot("variable", tool_call.args)) { - | Some(`String(arg)) => - goto( - ~ed=editor, - ~loc=ChatLSP.Composition.Definition, - ~goto_var_of_kind=ChatLSP.Composition.Value, - ~name=arg, - ); - schedule_action(loop_message); - | _ => raise(Failure("Invalid argument for goto_definition")) - } - | "goto_body" => - switch (Json.dot("variable", tool_call.args)) { - | Some(`String(arg)) => - goto( - ~ed=editor, - ~loc=ChatLSP.Composition.Body, - ~goto_var_of_kind=ChatLSP.Composition.Value, - ~name=arg, - ); - schedule_action(loop_message); - | _ => raise(Failure("Invalid argument for goto_body")) - } - | "goto_type_definition" => - switch (Json.dot("variable", tool_call.args)) { - | Some(`String(arg)) => - goto( - ~ed=editor, - ~loc=ChatLSP.Composition.Definition, - ~goto_var_of_kind=ChatLSP.Composition.Type, - ~name=arg, - ); - schedule_action(loop_message); - | _ => - raise(Failure("Invalid argument for goto_type_definition")) - } - | "goto_type_body" => - switch (Json.dot("variable", tool_call.args)) { - | Some(`String(arg)) => - goto( - ~ed=editor, - ~loc=ChatLSP.Composition.Body, - ~goto_var_of_kind=ChatLSP.Composition.Type, - ~name=arg, - ); - schedule_action(loop_message); - | _ => raise(Failure("Invalid argument for goto_type_body")) - } - | "select_all" => - goto( - ~ed=editor, - ~loc=ChatLSP.Composition.All, - ~goto_var_of_kind=ChatLSP.Composition.Value, - ~name="", - ); - schedule_action(loop_message); - | "paste" => - switch (Json.dot("code", tool_call.args)) { - | Some(`String(arg)) => - edit(~loc=ChatLSP.Composition.Current, ~code=arg); - schedule_action(loop_message); - | _ => raise(Failure("Invalid argument for paste")) - } - | "delete" => - edit(~loc=ChatLSP.Composition.Current, ~code=""); - schedule_action(loop_message); - | "submit" => () - | _ => raise(Failure("Unknown tool call: " ++ tool_call.name)) - } - ) { - | Failure(err) => schedule_action(InternalError(err, mode, chat_id)) - }; - } - | CompletionErrorRound(editor, fuel, tileId) => - // Split response into discussion and completion - let code_pattern = - Str.regexp( - "\\(\\(.\\|\n\\)*\\)```[ \n]*\\([^`]+\\)[ \n]*```\\(\\(.\\|\n\\)*\\)", - ); - let index = Option.get(Indicated.index(editor.editor.state.zipper)); - let ci = Option.get(Id.Map.find_opt(index, editor.statics.info_map)); - let sketch_z = editor.editor.state.zipper; - - let (_, completion) = - if (Str.string_match(code_pattern, content, 0)) { - let before = String.trim(Str.matched_group(1, content)); - let code = String.trim(Str.matched_group(3, content)); - (before, code |> StringUtil.trim_leading); - } else { - print_endline("Regex match failed for: " ++ content); - ("", content |> StringUtil.trim_leading); // Fallback if no code block found - }; - - switch (ChatLSP.ErrorRound.mk_reply(ci, sketch_z, completion)) { - | None => - print_endline("ERROR ROUNDS (Non-error Response): " ++ completion); - schedule_action( - EmployLLMAction(RemoveAndSuggest(completion, tileId)), - ); - | Some(error) => - print_endline("ERROR ROUNDS (Error): " ++ error); - print_endline( - "ERROR ROUNDS (Error-causing Response): " ++ completion, - ); - schedule_action( - SendMessage( - Completion(Loop(error, tileId, fuel - 1)), - editor, - chat_id, - ), - ); - }; - | CompletionQueryResponse => () - }; - update_model_chat_history(~model, ~mode, ~updated_chat) - |> Updated.return_quiet; - | EmployLLMAction(action) => - let add_suggestion = - ChatLSP.Completion.add_suggestion( - ~schedule_action=schedule_editor_action, - ); - switch (action) { - | RemoveAndSuggest(response, tileId) => - // Only side effects in the editor are performed here - add_suggestion(~response, ~tile=tileId); - model |> Updated.return_quiet; - | Describe(content, mode, chat_id) => - let (past_chats, _) = get_mode_info(mode, model); - let updated_past_chats = - Id.Map.update( - chat_id, - opt_chat => - switch (opt_chat) { - | Some(chat: Model.chat) => - Some({ - ...chat, - descriptor: content, - }) - | None => None - }, - past_chats, - ); - let curr_chat_id = - switch (mode) { - | HazelTutor => model.current_chats.curr_tutor_chat - | CodeSuggestion => model.current_chats.curr_suggestion_chat - | TaskCompletion => model.current_chats.curr_composition_chat - }; - resculpt_model( - ~model, - ~mode, - ~updated_past_chats, - ~chat_id=curr_chat_id, - ) - |> Updated.return_quiet; - | SetLoop(loop) => - { - ...model, - loop, - } - |> Updated.return_quiet - }; - - | ChatAction(action) => - switch (action) { - | NewChat => - let mode = settings.assistant.mode; - let (past_chats, _) = get_mode_info(mode, model); - let new_chat: Model.chat = Model.new_chat(model, mode); - let updated_history = Model.add_chat_to_history(new_chat, past_chats); - resculpt_model( - ~model, - ~mode, - ~updated_past_chats=updated_history, - ~chat_id=new_chat.id, - ) - |> Updated.return_quiet; - | DeleteChat(chat_to_be_gone_id) => - let mode = settings.assistant.mode; - // Filter out the chat we're deleting - let (past_chats, curr_chat) = get_mode_info(mode, model); - let filtered_past_chats = - Id.Map.filter((id, _) => id != chat_to_be_gone_id, past_chats); - let chrono_history = Model.sorted_chats(filtered_past_chats); - let updated_model = - curr_chat.id == chat_to_be_gone_id - ? switch (ListUtil.hd_opt(chrono_history)) { - | Some(chat) => - resculpt_model( - ~model, - ~mode, - ~updated_past_chats=filtered_past_chats, - ~chat_id=chat.id, - ) - | None => - resculpt_model( - ~model, - ~mode, - ~updated_past_chats=past_chats, - ~chat_id=curr_chat.id, - ) - } - : resculpt_model( - ~model, - ~mode, - ~updated_past_chats=filtered_past_chats, - ~chat_id=curr_chat.id, - ); - updated_model |> Updated.return_quiet; - - // Concat LS' error message and await_llm_response (... animation) - // This works even if out of fuel, as both Respond and ErrorRespond - // remove await_llm_response - | CollapseMessage(index) => - let mode = settings.assistant.mode; - let (past_chats, curr_chat) = get_mode_info(mode, model); - let is_prompt_display = - try( - List.nth(curr_chat.message_displays, index).role - == System(AssistantPrompt) - ) { - | Invalid_argument(_) => true - }; - let updated_message_displays = - List.mapi( - (i: int, msg: Model.display) => - if (i == index) { - { - ...msg, - collapsed: !msg.collapsed, - }; - } else if (msg.role == System(AssistantPrompt) - && is_prompt_display) { - { - ...msg, - collapsed: true, - }; - } else { - msg; - }, - curr_chat.message_displays, - ); - let updated_past_chats = - Id.Map.update( - curr_chat.id, - opt_chat => - switch (opt_chat) { - | Some(chat: Model.chat) => - Some({ - ...chat, - message_displays: updated_message_displays, - }) - | None => None - }, - past_chats, - ); - resculpt_model( - ~model, - ~mode, - ~updated_past_chats, - ~chat_id=curr_chat.id, - ) - |> Updated.return_quiet; - - | SwitchChat(chat_id) => - let mode = settings.assistant.mode; - let (past_chats, _) = get_mode_info(mode, model); - resculpt_model(~model, ~mode, ~updated_past_chats=past_chats, ~chat_id) - |> Updated.return_quiet; - | FilterLoadingMessages => - Model.{ - ...model, - chat_history: { - past_tutor_chats: - Id.Map.map( - (chat: Model.chat) => { - { - ...chat, - message_displays: - filter_chat_messages(chat.message_displays), - } - }, - model.chat_history.past_tutor_chats, - ), - past_suggestion_chats: - Id.Map.map( - (chat: Model.chat) => { - { - ...chat, - message_displays: - filter_chat_messages(chat.message_displays), - } - }, - model.chat_history.past_suggestion_chats, - ), - past_composition_chats: - Id.Map.map( - (chat: Model.chat) => { - { - ...chat, - message_displays: - filter_chat_messages(chat.message_displays), - } - }, - model.chat_history.past_composition_chats, - ), - }, - } - |> Updated.return_quiet - } - | ExternalAPIAction(external_api_action) => - switch (external_api_action) { - | SetLLM(llm_id) => - { - ...model, - external_api_info: { - ...model.external_api_info, - set_model: llm_id, - }, - } - |> Updated.return_quiet - | SetAPIKey(api_key) => - // Set the available models using the provided API key - OpenRouter.get_models(~key=api_key, ~handler=response => { - switch (response) { - | Some(json) => - switch (OpenRouter.parse_models_response(json)) { - | Some(models_response) => - schedule_action( - ExternalAPIAction(SetListOfLLMs(models_response.data)), - ) - | None => - print_endline("Assistant: failed to parse models response") - } - | None => - print_endline("Assistant: no response received from OpenRouter API") - } - }); - { - ...model, - external_api_info: { - ...model.external_api_info, - api_key, - }, - } - |> Updated.return_quiet; - | SetListOfLLMs(llms) => - { - ...model, - external_api_info: { - ...model.external_api_info, - available_models: llms, - }, - } - |> Updated.return_quiet - } - | InitializeAssistant => AssistantModel.init() |> Updated.return_quiet - }; -}; diff --git a/src/web/app/helpful-assistant/AssistantView.re b/src/web/app/helpful-assistant/AssistantView.re deleted file mode 100644 index ebfe701633..0000000000 --- a/src/web/app/helpful-assistant/AssistantView.re +++ /dev/null @@ -1,1214 +0,0 @@ -open Haz3lcore; -open Virtual_dom.Vdom; -open Node; -open Util.WebUtil; -open Util; -open Js_of_ocaml; - -module Update = AssistantUpdate; - -module Model = AssistantModel; - -type selection = - | MakeActive(Selection.t); - -type event = - | MakeActive(ScratchMode.Selection.t); - -let resume_chat_button = - (~inject_global: Globals.Action.t => Ui_effect.t(unit)): Node.t => { - let tooltip = "Confirm and Chat"; - let resume_chat = _ => - Virtual_dom.Vdom.Effect.Many([ - inject_global(Set(Assistant(UpdateChatStatus))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - div( - ~attrs=[clss(["chat-button"]), Attr.on_click(resume_chat)], - [Widgets.button_named(~tooltip, None, resume_chat)], - ); -}; - -let settings_button = - (~inject_global: Globals.Action.t => Ui_effect.t(unit)): Node.t => { - let tooltip = "Settings"; - let end_chat = _ => - Virtual_dom.Vdom.Effect.Many([ - inject_global(Set(Assistant(UpdateChatStatus))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - div( - ~attrs=[clss(["chat-button"]), Attr.on_click(end_chat)], - [ - Widgets.button_named(~tooltip, None, _ => Virtual_dom.Vdom.Effect.Ignore), - ], - ); -}; - -let new_chat_button = (~inject, ~model: Model.t): Node.t => { - let tooltip = "New Chat"; - let new_chat_action = - model.current_chats.curr_tutor_chat == Id.invalid - ? [] : [inject(Update.ChatAction(NewChat))]; - let new_chat = _ => - Virtual_dom.Vdom.Effect.Many( - new_chat_action @ [Virtual_dom.Vdom.Effect.Stop_propagation], - ); - div( - ~attrs=[clss(["add-button"]), Attr.on_click(new_chat)], - [ - Widgets.button(~tooltip, Icons.add, _ => Virtual_dom.Vdom.Effect.Ignore), - ], - ); -}; - -let history_button = - (~inject, ~inject_global: Globals.Action.t => Ui_effect.t(unit)): Node.t => { - let tooltip = "Past Chats"; - let history = _ => - Virtual_dom.Vdom.Effect.Many([ - inject(Update.ChatAction(CollapseMessage(-1))), // Hacky way to collapse any active prompt display - inject_global(Set(Assistant(ToggleHistory))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - div( - ~attrs=[clss(["history-button"]), Attr.on_click(history)], - [ - Widgets.button(~tooltip, Icons.history, _ => - Virtual_dom.Vdom.Effect.Ignore - ), - ], - ); -}; - -let select_llm = - (~inject: Update.t => Ui_effect.t(unit), ~model: Model.t): Node.t => { - let handle_change = (event, _) => { - let value = Js.to_string(Js.Unsafe.coerce(event)##.target##.value); - Virtual_dom.Vdom.Effect.Many([ - inject(ExternalAPIAction(SetLLM(value))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - }; - - div( - ~attrs=[clss(["llm-selector"])], - [ - label(~attrs=[clss(["llm-label"])], [text("LLM Model")]), - select( - ~attrs=[Attr.on_change(handle_change), clss(["llm-dropdown"])], - List.map( - (open_router_model: OpenRouter.model_info) => - option( - ~attrs=[ - Attr.value(open_router_model.id), - switch (model.external_api_info.set_model) { - | "" => Attr.empty - | current_model => - if (current_model == open_router_model.id) { - Attr.selected; - } else { - Attr.empty; - } - }, - ], - [text(open_router_model.name)], - ), - model.external_api_info.available_models, - ), - ), - ], - ); -}; - -let api_input = - ( - ~inject: Update.t => Ui_effect.t(unit), - ~inject_global: Globals.Action.t => Ui_effect.t(unit), - ~signal, - ~model: Model.t, - ~settings: AssistantSettings.t, - ) - : Node.t => { - let init_assistant_model = - model.current_chats.curr_tutor_chat == Id.invalid - ? [inject(Update.InitializeAssistant)] : []; - let handle_submission = (api_key: string) => { - Virtual_dom.Vdom.Effect.Many( - init_assistant_model - @ [ - inject(ExternalAPIAction(SetAPIKey(api_key))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ], - ); - }; - - let toggle_visibility = _ => - Virtual_dom.Vdom.Effect.Many([ - inject_global(Set(Assistant(ToggleAPIKeyVisibility))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - - let submit_key = _ => { - let message = - Js.Opt.case( - Dom_html.document##getElementById(Js.string("api-input")), - () => "", - el => - switch (Js.Unsafe.coerce(el)) { - | input => Js.to_string(input##.value) - }, - ); - Js.Opt.case( - Dom_html.document##getElementById(Js.string("api-input")), - () => (), - el => Js.Unsafe.coerce(el)##.value := Js.string(""), - ); - handle_submission(message); - }; - - let handle_keydown = event => { - let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch (key) { - | Some("Enter") => submit_key() - | _ => Virtual_dom.Vdom.Effect.Ignore - }; - }; - - div( - ~attrs=[clss(["api-key-container"])], - [ - div(~attrs=[clss(["title"])], [text("API Key")]), - div( - ~attrs=[clss(["assistant-info-container"])], - [ - text("Get an OpenRouter API key "), - a( - ~attrs=[ - Attr.href("https://openrouter.ai/settings/keys"), - Attr.target("_blank"), - ], - [text("here")], - ), - text("."), - ], - ), - div( - ~attrs=[clss(["llm-selector"])], - [ - label(~attrs=[clss(["llm-label"])], [text("New API Key")]), - input( - ~attrs=[ - Attr.id("api-input"), - Attr.placeholder("Click to enter your OpenRouter API key"), - Attr.type_("password"), - Attr.property("autocomplete", Js.Unsafe.inject("off")), - Attr.on_focus(_ => - signal(MakeActive(ScratchMode.Selection.TextBox)) - ), - Attr.on_keydown(handle_keydown), - clss(["api-input"]), - Attr.on_copy(_ => {Effect.Stop_propagation}), - Attr.on_paste(_ => {Effect.Stop_propagation}), - Attr.on_cut(_ => {Effect.Stop_propagation}), - ], - (), - ), - ], - ), - div( - ~attrs=[clss(["chat-button"]), Attr.on_click(submit_key)], - [Widgets.button_named(~tooltip="Update API Key", None, submit_key)], - ), - div( - ~attrs=[clss(["llm-selector"])], - [ - label( - ~attrs=[clss(["llm-label"])], - [ - text("Current API Key\n"), - div( - ~attrs=[clss(["toggle-show-button"])], - [ - Widgets.button( - ~tooltip="Show/Hide Key", - settings.show_api_key ? Icons.visible : Icons.invisible, - toggle_visibility, - ), - ], - ), - ], - ), - div( - ~attrs=[clss(["api-key-row"])], - [ - div( - ~attrs=[ - clss(["api-key-display"]), - Attr.id("api-key-display"), - ], - [ - text( - switch (model.external_api_info.api_key) { - | "" => "No API key set" - | key => - settings.show_api_key - ? key : String.make(String.length(key), '*') - }, - ), - ], - ), - ], - ), - ], - ), - ], - ); -}; - -let llm_model_id_input = - (~inject: Update.t => Ui_effect.t(unit), ~signal, ~model: Model.t) - : Node.t => { - let format_price_per_million = (price: string): string => { - // Convert string to float, multiply by 1000 to get per million tokens - // The API provides price per 1K tokens - switch (float_of_string_opt(price)) { - | Some(p) => - let per_million = p *. 1000000.0; - if (per_million == 0.0) { - "Free"; - } else { - "$" ++ Printf.sprintf("%.4f", per_million); - }; - | None => "Unknown" - }; - }; - - let handle_submission = (llm_model: string) => { - Virtual_dom.Vdom.Effect.Many([ - inject(ExternalAPIAction(SetLLM(llm_model))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - }; - - let submit_key = _ => { - let message = - Js.Opt.case( - Dom_html.document##getElementById(Js.string("llm-model-id-input")), - () => "", - el => - switch (Js.Unsafe.coerce(el)) { - | input => Js.to_string(input##.value) - }, - ); - Js.Opt.case( - Dom_html.document##getElementById(Js.string("llm-model-id-input")), - () => (), - el => Js.Unsafe.coerce(el)##.value := Js.string(""), - ); - handle_submission(message); - }; - - let handle_keydown = event => { - let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch (key) { - | Some("Enter") => submit_key() - | _ => Virtual_dom.Vdom.Effect.Ignore - }; - }; - - div( - ~attrs=[clss(["api-key-container"])], - [ - div(~attrs=[clss(["title"])], [text("Model Selection")]), - div( - ~attrs=[clss(["assistant-info-container"])], - [ - text("See available OpenRouter models "), - a( - ~attrs=[ - Attr.href("https://openrouter.ai/models"), - Attr.target("_blank"), - ], - [text("here")], - ), - text("."), - ], - ), - select_llm(~inject, ~model), - div( - ~attrs=[clss(["llm-selector"])], - [ - div( - ~attrs=[clss(["llm-label"])], - [text("Or Enter Model ID Manually")], - ), - input( - ~attrs=[ - Attr.id("llm-model-id-input"), - Attr.placeholder("Enter the ID of an OpenRouter model"), - Attr.type_("text"), - Attr.property("autocomplete", Js.Unsafe.inject("off")), - Attr.on_focus(_ => - signal(MakeActive(ScratchMode.Selection.TextBox)) - ), - Attr.on_keydown(handle_keydown), - clss(["llm-model-id-input"]), - Attr.on_copy(_ => {Effect.Stop_propagation}), - Attr.on_paste(_ => {Effect.Stop_propagation}), - Attr.on_cut(_ => {Effect.Stop_propagation}), - ], - (), - ), - ], - ), - div( - ~attrs=[clss(["chat-button"]), Attr.on_click(submit_key)], - [Widgets.button_named(~tooltip="Update Model", None, submit_key)], - ), - div( - ~attrs=[clss(["llm-selector"])], - [ - label(~attrs=[clss(["llm-label"])], [text("Current Model\n")]), - div( - ~attrs=[clss(["api-key-display"]), Attr.id("api-key-display")], - [ - text( - switch (model.external_api_info.set_model) { - | "" => "No model ID set" - | model_id => model_id - }, - ), - ], - ), - ], - ), - div( - ~attrs=[clss(["llm-selector"])], - [ - label( - ~attrs=[clss(["llm-label"])], - [text("Model Pricing (per million tokens)\n")], - ), - div( - ~attrs=[clss(["api-key-display"])], - [ - text( - switch (model.external_api_info.set_model) { - | "" => "No model selected" - | model_id => - let selected_model = - List.find_opt( - (model: OpenRouter.model_info) => model.id == model_id, - model.external_api_info.available_models, - ); - switch (selected_model) { - | Some(model) => - "Prompt: " - ++ format_price_per_million(model.pricing.prompt) - ++ " / Completion: " - ++ format_price_per_million(model.pricing.completion) - | None => "Pricing information not available" - }; - }, - ), - ], - ), - ], - ), - ], - ); -}; - -let message_input = - ( - ~signal, - ~inject, - ~model: Model.t, - ~settings: AssistantSettings.t, - ~editor: CodeEditable.Model.t, - ) - : Node.t => { - let mode = settings.mode; - let handle_send = (content: string) => { - Js_of_ocaml.Firebug.console##log( - Js_of_ocaml.Js.string("Message sent: " ++ content), - ); - Virtual_dom.Vdom.Effect.Many([ - switch (mode) { - | HazelTutor => - inject( - Update.SendMessage( - Tutor(content), - editor, - model.current_chats.curr_tutor_chat, - ), - ) - | CodeSuggestion => - inject( - Update.SendMessage( - Completion(Query(content)), - editor, - model.current_chats.curr_suggestion_chat, - ), - ) - | TaskCompletion => - inject( - Update.SendMessage( - Composition(Request(content)), - editor, - model.current_chats.curr_composition_chat, - ), - ) - }, - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - }; - let (past_chats, curr_chat) = Update.get_mode_info(settings.mode, model); - let curr_messages = Id.Map.find(curr_chat.id, past_chats).message_displays; - let send_message = _ => { - let message = - Js.Opt.case( - Dom_html.document##getElementById(Js.string("message-input")), - () => "", - el => - switch (Js.Unsafe.coerce(el)) { - | input => Js.to_string(input##.value) - }, - ); - Js.Opt.case( - Dom_html.document##getElementById(Js.string("message-input")), - () => (), - el => Js.Unsafe.coerce(el)##.value := Js.string(""), - ); - handle_send(message); - }; - let handle_keydown = event => { - let key = Js.Optdef.to_option(Js.Unsafe.get(event, "key")); - switch (key, ListUtil.last_opt(curr_messages)) { - | (Some("Enter"), _) => send_message() - | _ => Virtual_dom.Vdom.Effect.Ignore - }; - }; - div( - ~attrs=[clss(["input-container"])], - [ - input( - ~attrs=[ - Attr.id("message-input"), - Attr.placeholder( - switch (settings.mode) { - | HazelTutor => "Ask a question about Hazel (or anything)..." - | CodeSuggestion => "Followup with a question about the agent's code suggestion..." - | TaskCompletion => "Ask the agent to help clarify, plan, or write code..." - }, - ), - Attr.type_("text"), - Attr.property("autocomplete", Js.Unsafe.inject("off")), - Attr.on_focus(_ => - signal(MakeActive(ScratchMode.Selection.TextBox)) - ), - Attr.on_copy(_ => {Effect.Stop_propagation}), - Attr.on_paste(_ => {Effect.Stop_propagation}), - Attr.on_cut(_ => {Effect.Stop_propagation}), - Attr.on_keydown(handle_keydown), - clss(["message-input"]), - ], - (), - ), - switch (ListUtil.last_opt(curr_messages)) { - // todo: update, since we remove loading dots - | Some( - { - role: Assistant, - displayable_content: [Text("...")], - original_content: "...", - collapsed: false, - }: Model.display, - ) => - div( - ~attrs=[ - clss(["send-button-disabled", "icon"]), - Attr.title("Submitting Message Disabled"), - ], - [Icons.send], - ) - | _ => - div( - ~attrs=[ - clss(["send-button", "icon"]), - Attr.on_click(send_message), - Attr.title("Submit Message"), - ], - [Icons.send], - ) - }, - ], - ); -}; - -/* - // Indicates that the LLM is thinking/responding - let loading_dots = () => { - div( - ~attrs=[clss(["loading-dots"])], - [ - div(~attrs=[clss(["dot", "dot1"])], []), - div(~attrs=[clss(["dot", "dot2"])], []), - div(~attrs=[clss(["dot", "dot3"])], []), - ], - ); - }; - */ - -let form_collapse_toggle = - ( - ~message: Model.display, - ~toggle_collapse, - ~index: int, - ~is_first: bool, - ~is_last: bool, - ) - : Node.t => - if (message.collapsed && is_first) { - div( - ~attrs=[ - clss(["collapse-indicator"]), - Attr.on_click(_ => toggle_collapse(index)), - String.length(message.original_content) >= Model.max_collapsed_length - ? Attr.empty : Attr.hidden, - ], - [text("▼ Show more")], - ); - } else if (!message.collapsed - && String.length(message.original_content) - >= Model.max_collapsed_length - && is_last) { - div( - ~attrs=[ - clss(["collapse-indicator"]), - Attr.on_click(_ => toggle_collapse(index)), - String.length(message.original_content) >= Model.max_collapsed_length - ? Attr.empty : Attr.hidden, - ], - [text("▲ Show less")], - ); - } else { - None; - }; - -let text_block = - ( - ~message: Model.display, - ~content: string, - ~toggle_collapse, - ~index: int, - ~is_first: bool, - ~is_last: bool, - ) - : Node.t => { - div( - ~attrs=[ - clss([ - switch (message.role) { - | User => "user-message" - | Assistant => "llm-message" - | System(AssistantPrompt) => "system-prompt-message" - | System(InternalError) => "system-error-message" - | Tool => "tool-message" - }, - ]), - Attr.on_copy(_ => {Effect.Stop_propagation}), - Attr.on_paste(_ => {Effect.Stop_propagation}), - Attr.on_cut(_ => {Effect.Stop_propagation}), - ], - [ - message.collapsed - && String.length(message.original_content) >= Model.max_collapsed_length - ? text( - String.concat( - "", - [ - String.sub( - content, - 0, - min(String.length(content), Model.max_collapsed_length), - ), - "...", - ], - ), - ) - : text(content), - form_collapse_toggle( - ~message, - ~toggle_collapse, - ~index, - ~is_first, - ~is_last, - ), - ], - ); -}; - -let code_block = - ( - ~message: Model.display, - ~sketch: Segment.t, - ~toggle_collapse, - ~index: int, - ~is_first: bool, - ~is_last: bool, - ~globals: Globals.t, - ) - : Node.t => { - div( - ~attrs=[ - clss([ - "example", - switch (message.role) { - | User => "user" - | Assistant => "llm" - | System(AssistantPrompt) => "system-prompt" - | System(InternalError) => "system-error" - | Tool => "tool" - }, - ]), - ], - [ - CellEditor.View.view( - ~globals, - ~signal=_ => Ui_effect.Ignore, - ~inject=_ => Ui_effect.Ignore, - ~selected=None, - ~caption=None, - ~locked=true, - message.role == Assistant - ? { - sketch |> Zipper.unzip |> Editor.Model.mk |> CellEditor.Model.mk; - } - : { - sketch - |> Zipper.unzip - |> Editor.Model.mk - |> CellEditor.Model.mk - |> CellEditor.Update.calculate( - ~settings=globals.settings.core, - ~is_edited=true, - ~stitch=x => x, - ~queue_worker=None, - ); - }, - ), - form_collapse_toggle( - ~message, - ~toggle_collapse, - ~index, - ~is_first, - ~is_last, - ), - ], - ); -}; - -let form_block = - ( - ~message: Model.display, - ~block: Model.block_kind, - ~toggle_collapse, - ~index: int, - ~is_first: bool, - ~is_last: bool, - ~globals: Globals.t, - ) - : Node.t => - if (!message.collapsed || message.collapsed && is_first) { - switch (block) { - | Text(content) => - text_block( - ~message, - ~content, - ~toggle_collapse, - ~index, - ~is_first, - ~is_last, - ) - | Code(sketch) => - code_block( - ~message, - ~sketch, - ~toggle_collapse, - ~index, - ~is_first, - ~is_last, - ~globals, - ) - }; - } else { - None; - }; - -let initial_display = - (~model: Model.t, ~settings: AssistantSettings.t): Node.t => { - let (past_chats, curr_chat) = Update.get_mode_info(settings.mode, model); - let curr_messages = Id.Map.find(curr_chat.id, past_chats).message_displays; - List.length(curr_messages) <= 1 - ? div( - ~attrs=[clss(["initial-display"])], - [ - Icons.hazelnut_agent, - div( - ~attrs=[clss(["initial-display-text"])], - [ - text( - switch (settings.mode) { - | HazelTutor => "Hi, I'm Hazel's AI Tutor! Ask me anything about Hazel." - | CodeSuggestion => "Hi, I'm Hazel's AI Code Completion Assistant! Ask me for code suggestions." - | TaskCompletion => "Hi, I'm Hazel's AI Coding Agent! Let's work on your task together." - }, - ), - ], - ), - div( - ~attrs=[clss(["disclaimer-display-text"])], - [ - text( - "AI-based technologies, such as the Hazel Assistant, are prone to making mistakes. Always verify critical information independently.", - ), - ], - ), - ], - ) - : None; -}; - -let message_display = - ( - ~globals: Globals.t, - ~inject, - ~model: Model.t, - ~settings: AssistantSettings.t, - ) - : Node.t => { - let toggle_collapse = (is_system_prompt, index) => { - // Create an action to toggle the collapsed state of a specific message - Virtual_dom.Vdom.Effect.Many( - if (is_system_prompt && settings.show_history) { - [globals.inject_global(Set(Assistant(ToggleHistory)))]; - } else { - { - []; - } - @ [ - inject(Update.ChatAction(CollapseMessage(index))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]; - }, - ); - }; - let (past_chats, curr_chat) = Update.get_mode_info(settings.mode, model); - let curr_messages = Id.Map.find(curr_chat.id, past_chats).message_displays; - let message_nodes = - List.flatten( - List.mapi( - (index: int, message: Model.display) => { - [ - div( - ~attrs=[ - clss([ - "message-container", - switch (message.role) { - | User => "user" - | Assistant => "llm" - | System(AssistantPrompt) => "system-prompt" - | System(InternalError) => "system-error" - | Tool => "tool" - }, - ]), - ], - [ - div( - ~attrs=[clss(["message-identifier-row"])], - [ - div( - ~attrs=[clss(["message-identifier"])], - [ - switch (message.role) { - | User => text("User") - | Assistant => - switch (settings.mode) { - | CodeSuggestion => - div( - ~attrs=[clss(["llm-identifier"])], - [Icons.hazelnut_agent, text("Assistant")], - ) - | TaskCompletion => - div( - ~attrs=[clss(["llm-identifier"])], - [Icons.hazelnut_agent, text("Agent")], - ) - | HazelTutor => - div( - ~attrs=[clss(["llm-identifier"])], - [Icons.hazelnut_agent, text("Tutor")], - ) - } - - | System(AssistantPrompt) => - div( - ~attrs=[clss(["system-prompt-identifier"])], - [text("System")], - ) - | System(InternalError) => - div( - ~attrs=[clss(["system-error-identifier"])], - [text("System")], - ) - | Tool => - div( - ~attrs=[clss(["tool-identifier"])], - [text("Tool")], - ) - }, - ], - ), - message.role == System(AssistantPrompt) - ? div( - ~attrs=[clss(["show-prompt-button"])], - [ - Widgets.button( - ~tooltip="Show Prompt", Icons.info, _ => - toggle_collapse(true, index) - ), - ], - ) - : None, - ], - ), - ] - @ { - message.role == System(AssistantPrompt) - ? [None] - : { - let parsed_blocks = message.displayable_content; - List.mapi( - (idx, block: Model.block_kind) => - form_block( - ~message, - ~block, - ~toggle_collapse=toggle_collapse(false), - ~index, - ~is_first=idx == 0, - ~is_last=idx == List.length(parsed_blocks) - 1, - ~globals, - ), - parsed_blocks, - ); - }; - }, - ), - ] - }, - curr_messages, - ), - ); - div( - ~attrs=[clss(["message-display-container"])], - message_nodes @ [initial_display(~model, ~settings)], - ); -}; - -let get_sidebar_width = () => { - let sidebar = - Js.Unsafe.coerce(Dom_html.document)##getElementById("side-bar"); - if (Js.Opt.test(sidebar)) { - let width_str = Js.to_string(sidebar##.style##.width); - if (String.length(width_str) >= 2 - && String.sub(width_str, String.length(width_str) - 2, 2) == "px") { - try( - int_of_string( - String.sub(width_str, 0, String.length(width_str) - 2), - ) - ) { - | Invalid_argument(_) => 400 // default width on error - }; - } else { - 400; // default width if no 'px' suffix - }; - } else { - 400; // default width - }; -}; - -let prompt_display = - (~globals: Globals.t, ~model: Model.t, ~settings: AssistantSettings.t) - : Node.t => { - let (past_chats, curr_chat) = Update.get_mode_info(settings.mode, model); - let curr_messages = Id.Map.find(curr_chat.id, past_chats).message_displays; - let display = - List.find_mapi( - (index: int, message: Model.display) => { - message.role == System(AssistantPrompt) && !message.collapsed - ? Some( - div( - ~attrs=[ - Attr.id("prompt-display-container"), - Attr.create( - "style", - "right: " - ++ string_of_int(get_sidebar_width() + 20) - ++ "px", - ), - ], - { - let parsed_blocks = message.displayable_content; - List.map( - (block: Model.block_kind) => - form_block( - ~message, - ~block, - ~toggle_collapse=_ => {Virtual_dom.Vdom.Effect.Ignore}, - ~index, - ~is_first=false, - ~is_last=false, - ~globals, - ), - parsed_blocks, - ); - }, - ), - ) - : None - }, - curr_messages, - ); - switch (display) { - | Some(node) => node - | None => None - }; -}; - -let mode_buttons = - ( - ~inject_global: Globals.Action.t => Ui_effect.t(unit), - ~settings: AssistantSettings.t, - ) - : Node.t => { - let mode_button = - (mode: AssistantSettings.mode, label: string, ~disabled: bool) => { - let switch_mode = _ => - Virtual_dom.Vdom.Effect.Many([ - inject_global(Set(Assistant(SwitchMode(mode)))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - div( - ~attrs=[ - clss([ - "mode-button", - settings.mode == mode ? "active" : "", - disabled ? "disabled" : "", - ]), - Attr.on_click(disabled ? _ => Effect.Many([]) : switch_mode), - ], - [ - text(label), - disabled - ? div(~attrs=[clss(["hover-view"])], [text("Coming soon!")]) - : None, - ], - ); - }; - - div( - ~attrs=[clss(["mode-buttons"])], - [ - mode_button(HazelTutor, "Tutor", ~disabled=false), - mode_button(CodeSuggestion, "Suggest", ~disabled=false), - mode_button(TaskCompletion, "Compose", ~disabled=true), - ], - ); -}; - -let history_menu = - (~model: Model.t, ~settings: AssistantSettings.t, ~inject): Node.t => { - let (past_chats, curr_chat) = Update.get_mode_info(settings.mode, model); - let chronologically_sorted_past_chats = Model.sorted_chats(past_chats); - - div( - ~attrs=[ - Attr.id("history-menu"), - Attr.create( - "style", - "right: " ++ string_of_int(get_sidebar_width() + 20) ++ "px", - ), - ], - [ - div( - ~attrs=[clss(["history-menu-header"])], - [ - switch (settings.mode) { - | HazelTutor => text("Tutor History") - | CodeSuggestion => text("Suggestion History") - | TaskCompletion => text("Task History") - }, - ], - ), - div( - ~attrs=[clss(["history-menu-list"])], - List.map( - (chat: Model.chat) => - div( - ~attrs=[ - chat.id == curr_chat.id - ? clss(["history-menu-item", "active"]) - : clss(["history-menu-item"]), - Attr.on_click(e => { - let target = Js.Unsafe.coerce(e)##.target; - let contains_button = - Js.to_bool(target##.classList##contains("button")) - || Js.to_bool( - target##.parentElement##.classList##contains( - "button", - ), - ); - if (!contains_button) { - Virtual_dom.Vdom.Effect.Many([ - inject(Update.ChatAction(SwitchChat(chat.id))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]); - } else { - Virtual_dom.Vdom.Effect.Stop_propagation; - }; - }), - ], - [ - div( - ~attrs=[clss(["history-menu-item-content"])], - [ - text(chat.descriptor == "" ? "New chat" : chat.descriptor), - ], - ), - div( - ~attrs=[clss(["history-menu-item-actions"])], - [ - div( - ~attrs=[clss(["history-menu-item-time"])], - [text(TimeUtil.format_time_diff(chat.timestamp))], - ), - div( - ~attrs=[ - clss(["delete-chat-button"]), - Attr.on_click(_ => - Virtual_dom.Vdom.Effect.Many([ - inject(Update.ChatAction(DeleteChat(chat.id))), - Virtual_dom.Vdom.Effect.Stop_propagation, - ]) - ), - ], - [ - Widgets.button(~tooltip="Delete chat", Icons.trash, _ => - Virtual_dom.Vdom.Effect.Ignore - ), - ], - ), - ], - ), - ], - ), - chronologically_sorted_past_chats, - ), - ), - ], - ); -}; - -let view = - ( - ~globals: Globals.t, - ~signal, - ~inject: Update.t => Ui_effect.t(unit), - ~model: Model.t, - ~editor: CodeEditable.Model.t, - ) => { - let settings = globals.settings; - let inject_global = globals.inject_global; - /* For debugging: Uncomment to view chat history - let curr_chat = - Id.Map.find( - model.current_chats.curr_composition_chat, - model.chat_history.past_composition_chats, - ); - let concat_text = - String.concat( - "\n", - List.map( - (message: Model.display) => - "<" - ++ Model.string_of_role(message.role) - ++ ">" - ++ message.original_content - ++ "\n", - curr_chat.message_displays, - ), - ); - print_endline(concat_text); - Js_of_ocaml.Firebug.console##log(Js_of_ocaml.Js.string(concat_text)); - */ - let view = - div( - ~attrs=[Attr.id("assistant")], - [ - div( - ~attrs=[clss(["header"])], - [ - settings.assistant.ongoing_chat - ? mode_buttons(~inject_global, ~settings=settings.assistant) - : div( - ~attrs=[clss(["main-title"])], - [text("Assistant Settings")], - ), - settings.assistant.ongoing_chat - ? history_button(~inject, ~inject_global) : None, - settings.assistant.ongoing_chat - ? new_chat_button(~inject, ~model) : None, - settings.assistant.ongoing_chat - ? settings_button(~inject_global) - : resume_chat_button(~inject_global), - ], - ), - settings.assistant.ongoing_chat - ? message_display( - ~globals, - ~inject, - ~model, - ~settings=settings.assistant, - ) - : None, - settings.assistant.ongoing_chat - ? message_input( - ~signal, - ~inject, - ~model, - ~settings=settings.assistant, - ~editor, - ) - : None, - settings.assistant.ongoing_chat - ? None - : api_input( - ~inject, - ~inject_global, - ~signal, - ~model, - ~settings=settings.assistant, - ), - settings.assistant.ongoing_chat - ? None : llm_model_id_input(~inject, ~model, ~signal), - settings.assistant.ongoing_chat && settings.assistant.show_history - ? history_menu(~model, ~settings=settings.assistant, ~inject) : None, - prompt_display(~globals, ~model, ~settings=settings.assistant), - ], - ); - view; -}; diff --git a/src/web/app/helpful-assistant/ChatLSP.re b/src/web/app/helpful-assistant/ChatLSP.re deleted file mode 100644 index 2d8c3d063f..0000000000 --- a/src/web/app/helpful-assistant/ChatLSP.re +++ /dev/null @@ -1,461 +0,0 @@ -open Util; -open Haz3lcore; -open Language; - -let get_sketch_and_error_ctx = - (editor: CodeWithStatics.Model.t): list(string) => { - let sketch_seg = Dump.to_segment(editor.editor.state.zipper); - let errors = ErrorPrint.all(editor.statics.info_map); - let static_error_arr = - switch (errors) { - | [] => ["No static errors found"] - | _ => errors - }; - let ctx = - [ - "PROGRAM SKETCH: ```" - ++ ErrorPrint.Print.seg(~holes="?", sketch_seg) - ++ "```", - ] - @ ["STATIC ERRORS: "] - @ static_error_arr; - ctx; -}; - -module Options = { - [@deriving (show({with_path: false}), sexp, yojson)] - type t = { - params: OpenRouter.params, - instructions: bool, - syntax_notes: bool, - num_examples: int, - expected_type: bool, - relevant_ctx: bool, - error_rounds_max: int, - }; - - let init: t = { - params: OpenRouter.default_params, - instructions: true, - syntax_notes: true, - num_examples: 9, - expected_type: true, - relevant_ctx: true, - error_rounds_max: 2, - }; -}; - -module SystemPrompt = { - let prelude = ["You are a helpful coding assistant in Hazel. \n"]; - - let normal_suggestion_prompt = (completion_token: string) => - SuggestionPrompt_normal.self(completion_token); - - let cot_suggestion_prompt = (completion_token: string) => - SuggestionPrompt_cot.self(completion_token); - - let hazel_syntax_notes = HazelSyntaxNotes.self; - - let composition_prompt = CompositionPrompt.self; - - let summarized_hazel_docs = SummarizedHazelDocs.self; - - let mk_suggestion_prompt = - ( - {instructions, syntax_notes, _}: Options.t, - completion_token: string, - advanced_reasoning: bool, - ) - : string => - String.concat( - "\n", - ( - instructions - ? prelude - @ ( - advanced_reasoning - ? cot_suggestion_prompt(completion_token) - : normal_suggestion_prompt(completion_token) - ) - : [] - ) - @ (syntax_notes ? hazel_syntax_notes : []), - ); -}; - -module Completion = { - let get_static_context = - ( - expected_type: bool, - relevant_ctx: bool, - ci: Info.t, - hole_label: string, - ) - : list(string) => - switch (ci) { - | InfoExp({ana, ctx, _}) - | InfoPat({ana, ctx, _}) => - let expected = RelevantTypes.get(ctx, ana, hole_label); - let relevant = RelevantValues.get(ctx, ana); - (expected_type ? ["expected_ty: " ++ expected] : []) - @ (relevant_ctx ? ["relevant_ctx:\n " ++ relevant] : []); - | InfoTyp(_) - | InfoTPat(_) - | Secondary(_) => [] - }; - - let mk_const_prompt = - (options: Options.t, hole_label: string, advanced_reasoning: bool) - : OpenRouter.message => { - let prompt = - String.concat( - "\n", - [ - SystemPrompt.mk_suggestion_prompt( - options, - hole_label, - advanced_reasoning, - ), - ] - @ CompletionExamples.get( - options.num_examples, - hole_label, - advanced_reasoning, - ), - ); - OpenRouter.mk_system_msg(prompt); - }; - - let mk_ctx_prompt = - (options: Options.t, ci: Info.t, sketch: Segment.t, hole_label: string) - : OpenRouter.message => - OpenRouter.mk_user_msg( - String.concat( - "\n", - ["sketch: ```" ++ ErrorPrint.Print.seg(~holes="?", sketch) ++ "```"] - @ get_static_context( - options.expected_type, - options.relevant_ctx, - ci, - hole_label, - ), - ), - ); - - let add_suggestion = - ( - ~response: string, - ~tile: Id.t, - ~schedule_action: Editors.Update.t => unit, - ) => { - let actions = [ - Action.Select(Tile(Id(tile, Direction.Left))), - Action.Buffer(Set(LLM(response))), - ]; - // Apply each action in sequence - List.iter( - action => { - let perform_action = CodeEditable.Update.Perform(action); - let cell_action = CellEditor.Update.MainEditor(perform_action); - let scratch_action = Editors.Update.Scratch(CellAction(cell_action)); - schedule_action(scratch_action); - }, - actions, - ); - }; -}; - -module Composition = { - let max_tool_calls = 10; - - let statics_of_exp_seg = - (init_ctx: Ctx.t, sketch: Segment.t): (Info.exp, Statics.Map.t) => - Statics.uexp_to_info_map( - ~ctx=init_ctx, - ~ancestors=[], - MakeTerm.go(sketch).term, - Id.Map.empty, - ~duplicates=[], - ~expected_labels=None, - ~label_sort=false, - ); - - // Prompt with appropriate context for each message - let mk_ctx_prompt = - (options: Options.t, editor: CodeWithStatics.Model.t) - : OpenRouter.message => { - let _ = options; // TODO: Either remove params or update function to use params AnCRask - OpenRouter.mk_user_msg( - String.concat( - "\n", - get_sketch_and_error_ctx(editor) - @ [ - "SELECTED CODE: " - ++ ( - String.length( - ErrorPrint.Print.seg( - ~holes="?", - editor.editor.state.zipper.selection.content, - ), - ) - == 0 - ? "None. Use a goto_* command to select a code segment." - : "```" - ++ ErrorPrint.Print.seg( - ~holes="?", - editor.editor.state.zipper.selection.content, - ) - ++ "```" - ), - ], - ), - ); - }; - - type loc_of_edit = - | Before - | After - | Current; - - type loc_of_goto = - | Body - | Definition - | All; - - type goto_var = - | Value - | Type; - - let get_static_context = (relevant_ctx: bool, ci: Info.t): list(string) => - switch (ci) { - | InfoExp({ana, ctx, _}) - | InfoPat({ana, ctx, _}) => - let relevant = RelevantValues.get(ctx, ana); - relevant_ctx ? ["relevant_ctx:\n " ++ relevant] : []; - | InfoTyp(_) - | InfoTPat(_) - | Secondary(_) => [] - }; - - // Finds the first matching variable as 'name' in the context - // highlights the variable and definition (excluding the body) - let goto = - ( - ~ed: CodeWithStatics.Model.t, - ~loc: loc_of_goto, - ~goto_var_of_kind: goto_var, - ~name: string, - ~schedule_action: Editors.Update.t => unit, - ) - : unit => { - let statics = CodeWithStatics.Model.get_statics(ed); - // Find the first matching variable in the context using fold - // TODO: Handle shadowed variables - let matching_id = - Id.Map.fold( - (_, info, acc) => { - switch (acc) { - | Some(_) => acc // Already found a match - | None => - let ctx = Info.ctx_of(info); - switch (goto_var_of_kind) { - | Value => - switch (Ctx.lookup_var(ctx, name)) { - | Some(entry) => Some(entry.id) - | None => None - } - | Type => - switch (Ctx.lookup_tvar_id(ctx, name)) { - | Some(id) => Some(id) - | None => None - } - }; - } - }, - statics.info_map, - None, - ); - // Return appropriate action based on whether we found a match - let actions = - switch (matching_id) { - | Some(id) => [ - Action.Move(Goal(TileId(id))), - // Moving left by token is essentially a hacky method to get - // off of a variable name (term), and triple/quad click on let binding - // itself (this properly highlights full variable name and - // definition when type annotation exists) - Action.Move(Local(Left, ByToken)), - switch (loc) { - // TODO: Implement structure-based navigation actions - | Definition => - Action.Select(Term(Id(Id.invalid, Direction.Left))) - | Body => Action.Select(Term(Id(Id.invalid, Direction.Left))) - | All => Action.Select(Term(Id(Id.invalid, Direction.Left))) - }, - Action.Copy, - ] - | None => [Action.Select(Term(Id(Id.invalid, Direction.Left)))] - }; - - List.iter( - action => { - let perform_action = CodeEditable.Update.Perform(action); - let cell_action = CellEditor.Update.MainEditor(perform_action); - let scratch_action = Editors.Update.Scratch(CellAction(cell_action)); - schedule_action(scratch_action); - }, - actions, - ); - }; - - let edit = - ( - ~loc: loc_of_edit, - ~code: string, - ~schedule_action: Editors.Update.t => unit, - ) - : unit => { - // TODO: Might be helpful to paste a segment instead of a string - // This may allow for better error handling. - let actions = - switch (loc) { - | Before => [ - // Unselect current definition - Action.Unselect(Some(Left)), - // Paste new code - Action.Paste(String(code ++ "\n")), - ] - | After => [ - // Unselect current definition - Action.Unselect(Some(Direction.Right)), - // Paste new code - Action.Paste(String("\n" ++ code)), - ] - | Current => - String.length(code) == 0 - ? [ - // This implies the calling of the ```delete``` tool - // Replace current definition - Action.Paste(String(code)), - // Destruct left - Action.Destruct(Left), - ] - : [ - // Replace current definition - Action.Paste(String(code)), - ] - // We paste the code edit, then reselect the definition, and copy - // to clipboard shim to give context to assistant. - }; - List.iter( - action => { - let perform_action = CodeEditable.Update.Perform(action); - let cell_action = CellEditor.Update.MainEditor(perform_action); - let scratch_action = Editors.Update.Scratch(CellAction(cell_action)); - schedule_action(scratch_action); - }, - actions, - ); - }; -}; - -module ErrorRound = { - open OptUtil.Syntax; - module StringSet = Set.Make(String); - - let get_parse_errs = - (sketch_z: Zipper.t, completion: string): Result.t(Zipper.t, string) => - //NOTE: This function is pretty basic; reporting approach could be improved - /* For now we required that the completion be complete in-itself: */ - switch (Parser.to_zipper(~zipper_init=Zipper.init(), completion)) { - | None => Error("Undocumented parse error, no feedback available") - | Some(completion_z) => - switch (Zipper.local_backpack(completion_z)) { - | [_, ..._] as orphans => - let orphans = - List.map( - Base.tile_to_string( - ~holes="", - ~concave_holes=" ", - ~projector_to_segment=Triggers.projector_to_invoke, - ), - orphans, - ); - Error( - "The parser has detected unmatched delimiters. (The presence of a '=>' in the list likely indicates that a '->' was mistakingly used in a case expression). Unmatched delimiters: " - ++ String.concat(", ", orphans), - ); - | [] => - let segment = Zipper.zip(completion_z); - switch ( - { - let* sketch_z = Destruct.go(Left, sketch_z); - let+ sketch_z = Destruct.go(Left, sketch_z); - Zipper.insert_segment(sketch_z, segment); - } - ) { - | None => Error("Undocumented parse error, no feedback available") - | Some(completion_z) => Ok(completion_z) - }; - } - }; - - let statics_of_exp_zipper = - (init_ctx: Ctx.t, z: Zipper.t): (Info.exp, Statics.Map.t) => - Statics.uexp_to_info_map( - ~ctx=init_ctx, - ~ancestors=[], - MakeTerm.from_zip_for_sem(z).term, - Id.Map.empty, - ~duplicates=[], - ~expected_labels=None, - ~label_sort=false, - ); - - let mk_report = (ctx: Ctx.t, z: Zipper.t, reply: string): ErrorPrint.t => - // TODO: Currently this only works in expression position - switch (get_parse_errs(z, reply)) { - | Error(err) => ParseError(err) - | Ok(full_z) => - let (_, info_map) = statics_of_exp_zipper(ctx, z); - let static_errs_sketch = ErrorPrint.all(info_map); - let (_, info_map) = statics_of_exp_zipper(ctx, full_z); - let static_errs_full = ErrorPrint.all(info_map); - if (List.length(static_errs_full) == 0) { - NoErrors; - } else { - let sketch_errs = StringSet.of_list(static_errs_sketch); - let new_errs = - List.filter( - err => !StringSet.mem(err, sketch_errs), - static_errs_full, - ); - if (List.length(new_errs) == 0) { - NoErrors; - } else { - StaticErrors(new_errs); - }; - }; - }; - - let mk_reply = - (ci: Info.t, sketch_z: Zipper.t, reply: string): option(string) => { - // TODO: Currently this only works in expression position - let wrap = (intro, errs) => - [intro] - @ errs - @ [ - "Please try to address the error(s) by updating your previous code suggestion", - "Please respond ONLY with the update suggestion", - ] - |> String.concat("\n"); - let error_report = mk_report(Info.ctx_of(ci), sketch_z, reply); - switch (error_report) { - | NoErrors => None - | ParseError(err) => - Some(wrap("The following parse error occured:", [err])) - | StaticErrors(errs) => - Some(wrap("The following static errors were discovered:", errs)) - }; - }; -}; diff --git a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Comparator.re b/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Comparator.re deleted file mode 100644 index 68eab59074..0000000000 --- a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Comparator.re +++ /dev/null @@ -1,116 +0,0 @@ -let self = {| - -please fix the issue when im trying to test - -PROGRAM SKETCH: ```type Option = - + Some(Int) - + None -in - -let map_option: (Int -> Int, Option) -> Option = - fun (f, opt) -> - case opt - | Some(n) => Some(f(n)) - | None => None - end -in - -test map_option((fun x -> x + 1), Some(2)) == Some(3) end``` -STATIC ERRORS: -Error in term: - Some(3) -Nature of error: Expecting type Int but got inconsistent type Option -Error in term: - map_option(((fun x -> x + 1)), Some(2)) -Nature of error: Expecting type Int but got inconsistent type Option -SELECTED CODE: None. Use a goto_* command to select a code segment. - - -Ah I see the issue, in Hazel the '==' operator is used explicitly to compare values of the integer type, -this is why we see the error "Expecting type Int but got inconsistent type Option". -I'll fix this by adding a custom comparator for the Option type. - -~~~{ - "tool": "begin" -}~~~ - -I'll navigate to the definition of "map_option" to repeat its definition while adding the definition for the custom comparator directly after it. - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "map_option" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "let comparator : (Option, Option) -> Bool = - fun (a, b) -> - case (a, b) -| Some(a'), Some(b') => a' == b' -| _ => false -end - in " - } -}~~~ - -Finally, I'll fix the test by using the custom comparator. - -~~~{ - "tool": "goto_body", - "args": { - "variable_name": "comparator" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "test map_option((fun x -> x + 1), Some(2)) == Some(3) end; - ?" - } -}~~~ - -PROGRAM SKETCH: ```type Option = - + Some(Int) - + None -in - -let map_option: (Int -> Int, Option) -> Option = - fun (f, opt) -> - case opt - | Some(n) => Some(f(n)) - | None => None - end -in - -let comparator : (Option, Option) -> Bool = - fun (a, b) -> - case (a, b) - | Some(a'), Some(b') => a' == b' - | _ => false - end -in - -test comparator(map_option((fun x -> x + 1), Some(2)),Some(3)) end; -?``` -STATIC ERRORS: -Error in term: - Some(3) -Nature of error: Expecting type Int but got inconsistent type Option -Error in term: - map_option(((fun x -> x + 1)), Some(2)) -Nature of error: No static errors found -SELECTED CODE: test comparator(map_option((fun x -> x + 1), Some(2)),Some(3)) end; - ? - - -Great! Our custom comparator now compares the two results values in the test. - -~~~{ - "tool": "submit" -}~~~ - -|}; diff --git a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Comparator_2.re b/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Comparator_2.re deleted file mode 100644 index be63942c0e..0000000000 --- a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Comparator_2.re +++ /dev/null @@ -1,118 +0,0 @@ -let self = {| - -what is the issue with my code? - -PROGRAM SKETCH: ```type Option = - + Some(Int) - + None -in - -let map_option: (Int -> Int, Option) -> Option = fun (f, opt) -> - case opt - | Some(n) => Some(f(n)) - | None => None - end -in - -test map_option((fun x -> x + 1), Some(2)) == Some(3) end; -? -``` -STATIC ERRORS: -Error in term: - Some(3) -Nature of error: Expecting type Int but got inconsistent type Option -Error in term: - map_option(((fun x -> x + 1)), Some(2)) -Nature of error: Expecting type Int but got inconsistent type Option -SELECTED CODE: None. Use a goto_* command to select a code segment. - - -I see the problem. The error message "Expecting type Int but got inconsistent type Option" in the `test` assertion indicates that the comparison `== Some(3)` is being misinterpreted as comparing an `Int` with an `Option`. This is a common pitfall in Hazel's type system when dealing with ADTs in test assertions. - -To fix this, we need to compare the `Option` values themselves, not their inner `Int` values. Hazel's `test` construct usually expects an expression to evaluate to a boolean. Therefore, comparing two `Option` types directly using `==` might not work as expected if `Option` does not have a defined equality. - -The simplest way to compare ADT values in a test is to define an equality function for the `Option` type, or in this case, directly compare the results using a pattern match or by manually extracting the inner value if it's `Some`. - -Given that we want to test if `map_option` returns `Some(3)`, we can achieve this by using another `map_option` call with `Some(3)` and a function that returns its input, then comparing the results, or the simplest way is to introduce a helper function `option_equal`. - -I will define an `option_equal` function that correctly compares two `Option` values. Then, we will use this `option_equal` function within the `test` block. - -~~~{ - "tool": "begin" -}~~~ - -First, I will navigate to the body of the `map_option` definition. -~~~{ - "tool": "goto_body", - "args": { - "variable_name": "map_option" - } -}~~~ - -PROGRAM SKETCH: ```type Option = - + Some(Int) - + None -in - -let map_option: (Int -> Int, Option) -> Option = fun (f, opt) -> - case opt - | Some(n) => Some(f(n)) - | None => None - end -in - -test map_option((fun x -> x + 1), Some(2)) == Some(3) end; -? -``` -STATIC ERRORS: -Error in term: - Some(3) -Nature of error: Expecting type Int but got inconsistent type Option -Error in term: - map_option(((fun x -> x + 1)), Some(2)) -Nature of error: Expecting type Int but got inconsistent type Option -SELECTED CODE: test map_option((fun x -> x + 1), Some(2)) == Some(3) end; - -I have selected the `test` expression. Now I will paste in the `option_equal` function and wrap the test expression inside a call to `option_equal`. - -~~~{ - "tool": "paste", - "args": { - "code": "let option_equal: (Option, Option) -> Bool = fun (opt1, opt2) ->\n case (opt1, opt2)\n | (Some(n1), Some(n2)) => n1 == n2\n | (None, None) => true\n | _ => false\n end\nin\n\ntest option_equal(map_option((fun x -> x + 1), Some(2)), Some(3)) end;" - } -}~~~ - -PROGRAM SKETCH: ```type Option = - + Some(Int) - + None -in - -let map_option: (Int -> Int, Option) -> Option = fun (f, opt) -> - case opt - | Some(n) => Some(f(n)) - | None => None - end -in - -let option_equal:(Option,Option) ->Bool =fun (opt1,opt2) -> - case (opt1,opt2) - |(Some(n1),Some(n2)) =>n1 ==n2 - |(None,None) =>true - |_ =>false - end -in - -test option_equal(map_option((fun x ->x +1),Some(2)),Some(3)) end; -? -``` -STATIC ERRORS: -No static errors found -SELECTED CODE: None. Use a goto_* command to select a code segment. - -Great! The static errors are gone. This indicates that the `option_equal` function correctly handles the comparison between `Option` types within the `test` block. -I'm submitting this as complete. -~~~{ - "tool": "submit" -}~~~ - -|}; diff --git a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Simple_1.re b/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Simple_1.re deleted file mode 100644 index 025e5bce09..0000000000 --- a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Simple_1.re +++ /dev/null @@ -1,75 +0,0 @@ -let self = {| - -hi please write a unique and cool hazel program - -PROGRAM SKETCH: ```?``` -STATIC ERRORS: -No static errors found -SELECTED CODE: None. Use a goto_* command to select a code segment. - -I can help with that! I'll create a simple program that calculates the hypotenuse of a right-angled triangle. - -~~~{ - "tool": "begin" -}~~~ - -PROGRAM SKETCH: ```?``` -STATIC ERRORS: -No static errors found -SELECTED CODE: None. Use a goto_* command to select a code segment. - -I'll start by pasting the initial structure of the program using `paste`. - -~~~{ - "tool": "paste", - "args": { - "code": "let a = 3 in\nlet b = 4 in\n0" - } -}~~~ - -PROGRAM SKETCH: ```let a = 3 in -let b = 4 in -0``` -STATIC ERRORS: -No static errors found -SELECTED CODE: None. Use a goto_* command to select a code segment. - -Now I will modify the body to calculate the hypotenuse using the Pythagorean theorem. - -~~~{ - "tool": "goto_body", - "args": { - "variable_name": "b" - } -}~~~ - -PROGRAM SKETCH: ```let a = 3 in -let b = 4 in -0``` -STATIC ERRORS: -No static errors found -SELECTED CODE: 0 - -~~~{ - "tool": "paste", - "args": { - "code": "((a * a) + (b * b))" - } -}~~~ - -PROGRAM SKETCH: ```let a = 3 in -let b = 4 in -((a * a) + (b * b))``` -STATIC ERRORS: -No static errors found -SELECTED CODE: None. Use a goto_* command to select a code segment. - -The program now calculates the square of the hypotenuse. Since Hazel doesn't have a built-in square root function, I'll consider this a "unique" program that doesn't fully solve for the hypotenuse but rather the hypotenuse squared. - -I am satisfied with the current state of the program. - -~~~{ - "tool": "submit" -}~~~ - -|}; diff --git a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Simple_2.re b/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Simple_2.re deleted file mode 100644 index 703d4b871c..0000000000 --- a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Simple_2.re +++ /dev/null @@ -1,278 +0,0 @@ -let self = {| - -could you please implement the functions in this program? - -PROGRAM SKETCH: ```let add = fun x, y -> - x + y -in -let subtract = - ? -in -let mult = - ? -in -let divide = - ? -in -?``` -STATIC ERRORS: -No static errors found -SELECTED CODE: None. Use a goto_* command to select a code segment. - -Sure! Let's implement each function one by one. - -~~~{ - "tool": "begin" -}~~~ - -The program currently has a function for addition. I'll start by navigating to the definition of "subtract" to implement it. - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "subtract" - } -}~~~ - -With the selected code segment at the definition of "subtract", I'll implement it as such... - -~~~{ - "tool": "paste", - "args": { - "code": "let subtract = fun x, y -> - x - y -in " - } -}~~~ - -PROGRAM SKETCH: ```let add = fun x, y -> - x + y -in -let subtract = fun x, y -> - x - y -in -let mult = - ? -in -let divide = - ? -in -?``` -STATIC ERRORS: -No static errors found -SELECTED CODE: let subtract = fun x, y -> - x - y -in - -Great! Seeing that worked, I'll navigate to mult and divide to implement them, respectively. - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "mult" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "let mult = fun x, y -> - x * y -in " - } -}~~~ - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "divide" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "let divide = - fun (x, y) -> -case y - | 0 => nan - | _ => x / y -end -in " - } -}~~~ - -PROGRAM SKETCH: ```let add = fun x, y -> - x + y -in -let subtract = fun x, y -> - x - y -in -let mult = fun x, y -> - x * y -in -let divide = - fun (x, y) -> - case y - | 0 => nan - | _ => x / y - end -in -?``` -STATIC ERRORS: Error in term: case y | 0 => nan | _ => x / y end Nature of error: Expecting branches to have consistent types but got types: Float, Int -SELECTED CODE: let divide = - fun (x, y) -> - case y - | 0 => nan - | _ => x / y - end -in - - -I see the issue, we need to use Hazel's float division operator on values with type float. - -~~~{ - "tool": "paste", - "args": { - "code": "let divide : (Float, Float) -> Float = - fun (x, y) -> -case y - | 0 => nan - | _ => x /. y -end -in " - } -}~~~ - -For consistency, I'll annotate and modify the definitions of the other functions. - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "add" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "let add : (Float, Float) -> Float = fun x, y -> - x +. y -in " - } -}~~~ - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "subtract" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "let subtract : (Float, Float) -> Float = fun x, y -> - x -. y -in " - } -}~~~ - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "mult" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "let mult : (Float, Float) -> Float = fun x, y -> - x *. y -in " - } -}~~~ - - -PROGRAM SKETCH: ```let add : (Float, Float) -> Float = fun x, y -> - x +. y -in -let subtract : (Float, Float) -> Float = fun x, y -> - x -. y -in -let mult : (Float, Float) -> Float = fun x, y -> - x *. y -in -let divide : (Float, Float) -> Float = - fun (x, y) -> - case y - | 0 => nan - | _ => x /. y - end -in -?``` -STATIC ERRORS: -No static errors found -SELECTED CODE: let mult : (Float, Float) -> Float = fun x, y -> - x *. y -in - - -Looking good! I'll add a few tests to make sure the functions are working as expected. - -~~~{ - "tool": "goto_body", - "args": { - "variable_name": "divide" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "test add(3.0, 5.2) ==. 8.2 end; - test subtract(6.0, 2.2) ==. 3.8 end; -test mult(3.1, 2.0) ==. 6.2 end; -test divide(5.4, 0.) ==. nan end; - test divide(5.0, 2.5) ==. 2 end;" - } -}~~~ - - -PROGRAM SKETCH: ```let add : (Float, Float) -> Float = fun x, y -> - x +. y -in -let subtract : (Float, Float) -> Float = fun x, y -> - x -. y -in -let mult : (Float, Float) -> Float = fun x, y -> - x *. y -in -let divide : (Float, Float) -> Float = - fun (x, y) -> - case y - | 0. => nan - | _ => x /. y - end -in -test add(3.0, 5.2) ==. 8.2 end; -test subtract(6.0, 2.2) ==. 3.8 end; -test mult(3.1, 2.0) ==. 6.2 end; -test divide(5.4, 0.) ==. nan end; -test divide(5.0, 2.5) ==. 2 end; -?``` -STATIC ERRORS: -No static errors found -SELECTED CODE: let mult : test divide(5.0, 2.5) ==. 2 end; - ? - - -Great! The functions are implement and being thoroughly tested. - -~~~{ - "tool": "submit", -}~~~ - - -|}; diff --git a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Tally.re b/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Tally.re deleted file mode 100644 index a222c6f498..0000000000 --- a/src/web/app/helpful-assistant/prompts/CompositionExamples/Ex_Tally.re +++ /dev/null @@ -1,106 +0,0 @@ -let self = {| - -Define a non-recursive function tally that folds (right) over the provided list of pairs of integers, representing breakfast orders involving a given number of eggs and cups of coffee, and returns a single pair tallying up the total numbers of eggs and cups of coffee consumed by the kitchen in the morning. We have provided an implementation of fold_right with its type specialized as needed. - -PROGRAM SKETCH: ```type Total = (Int, Int) in -type Order = (Int, Int) in -let fold_right : ( - [Order], - Total, - (Order, Total) -> Total -) -> Total = - fun (xs, b, f) -> - case xs - | [] => b - | hd::tl => f(hd, fold_right(tl, b, f)) -end in -let total_eq : (Total, Total) -> Bool = - fun ((a1, b1), (a2, b2)) -> (a1 == a2) && (b1 == b2) -in -let tally: [Order] -> Total = - fun bill -> fold_right(?, ?, ?) -in ?``` -STATIC ERRORS: -No static errors found -SELECTED CODE: let tally: [Order] -> Total = - fun bill -> fold_right(?, ?, ?) -in - -I need to implement the `tally` function using `fold_right`. Let me analyze what's needed: - -1. The base case should be `(0, 0)` - no eggs or coffee initially -2. The folding function should add each order's eggs and coffee to the running total -3. The arguments to `fold_right` are: the list, base value, and combining function - -~~~{ - "tool": "begin" -}~~~ - -~~~{ - "tool": "goto_definition", - "args": { - "variable_name": "tally" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "let tally: [Order] -> Total =\n fun bill -> fold_right(bill, (0, 0), fun (order, total) -> \n let (eggs, coffee) = order in\n let (total_eggs, total_coffee) = total in\n (eggs + total_eggs, coffee + total_coffee)\n )\nin" - } -}~~~ - -Now let me add a simple test to verify the implementation works correctly: - -~~~{ - "tool": "goto_body", - "args": { - "variable_name": "tally" - } -}~~~ - -~~~{ - "tool": "paste", - "args": { - "code": "test total_eq(tally([(2, 1), (3, 2), (1, 1)]), (6, 4)) end;\ntally([(5, 2), (2, 3)])" - } -}~~~ - - -PROGRAM SKETCH: ```type Total = (Int, Int) in -type Order = (Int, Int) in -let fold_right : ( - [Order], - Total, - (Order, Total) -> Total -) -> Total = - fun (xs, b, f) -> - case xs - | [] => b - | hd::tl => f(hd, fold_right(tl, b, f)) -end in -let total_eq : (Total, Total) -> Bool = - fun ((a1, b1), (a2, b2)) -> (a1 == a2) && (b1 == b2) -in -let tally: [Order] -> Total = - fun bill -> fold_right(bill, (0, 0), fun (order, total) -> - let (eggs, coffee) = order in - let (total_eggs, total_coffee) = total in - (eggs + total_eggs, coffee + total_coffee) - ) -in -test total_eq(tally([(2, 1), (3, 2), (1, 1)]), (6, 4)) end; -tally([(5, 2), (2, 3)])``` - -STATIC ERRORS: No static errors found -SELECTED CODE: tally([(5, 2), (2, 3)]) - - - -Great! The tally function has been implemented and tested without any errors. - -~~~{ - "tool": "submit" -}~~~ - -|}; diff --git a/src/web/app/helpful-assistant/prompts/CompositionPrompt.re b/src/web/app/helpful-assistant/prompts/CompositionPrompt.re deleted file mode 100644 index 9f1ad02474..0000000000 --- a/src/web/app/helpful-assistant/prompts/CompositionPrompt.re +++ /dev/null @@ -1,373 +0,0 @@ -open Util; - -let hazel_syntax_notes = HazelSyntaxNotes.self; - -let summarized_hazel_docs = SummarizedHazelDocs.self; - -let instructions = [ - "", - "You are an expert AI programming agent operating in the Hazel programming language.", - "You are working with a user to accomplish a programming task in a paired programming setting.", - "The user will ask you a question or to perform a task (implement a feature, fix an issue, etc).", - "You are a professional coding agent, meaning it is your duty to complete the user's task or attempt to complete their task until you decide", - "the task is complete or it is absolutely infeasible to complete.", - "To reiterate, you are operating in the Hazel programming language. This is a known to be a low-resource language,", - "meaning you will be provided with relevant syntax and semantic information about the programming language", - "that you can carefully study and review when generating your responses.", - "NEVER try to write code from another programming language other than Hazel.", - "You may explain and reason about the program/task/user query, but aim to keep your thinking and explanations concise and to the point.", - "If the user wants you to implement a feature that is quite complex, you should break it down into smaller tasks to work through step by step.", - "You do not need to repeat code in your response. You can simply call the tool to insert the code.", - "After calling a tool, you should pick up immediately from where you left off—No need to repeat or summarize what you've been doing.", - "You should avoid explicitly mentioning tool calls to the user. Your conversation with the user should be natural, as if you were their human pair programming partner.", - "We will now provide you with the following:\n", - "1. A toolkit along with a specification on how to call these tools throughout the attempted completion of the task.\n", - "2. Hazel syntax notes.\n", - "3. A brief summary of Hazel documentation.\n", - "4. A series of GOLDEN EXAMPLES from agents who successfully implemented user-requested features.\n", - "You should frequently come back and reference each of the toolkit, syntax notes, documentation, and golden standard examples.", - "Keep your chats brief and concise, briefly communicating with the user your plan-of-action.", - "After making a tool call, pick up immediately from where you left off.", - "That is, do not repeat yourself or try to summarize what you've been doing.", - "", -]; - -let toolkit = [ - "", - "You are to complete user-specified tasks using only the tools provided.", - "This toolkit contains specific action commands to navigate the sketch and modify code,", - "essentially giving you a sort of cursor to work with and control.", - "All actions commands interact with the high-level, definition-based structure of the program.", - "In a sense, these allow you to navigate and alter meaningful semantic chunks of the program, akin to a structure editor (but with higher-level control).", - "", - "", - "You are an LLM placed in an environment where you are equipped with TOOLS.", - "Every tool call will perform an action on the program and give you updated feedback on the current sketch, any errors present, and your currently selected code.", - "A strong recommendation is to break a complex task into smaller, more manageable steps,", - "where once broken into smaller steps, you can implement each step in as few responses as possible.", - "If you do NOT make a tool call in your response, you are effectively submitting the task to the user.", - "You need NOT make a tool call if the user asks a question that does not require any editing of their code.", - "", -]; - -// idea: allow for multiple variables to be selected at once -// or rather, allow for beginning and end of selection to be specified (based on variable definitons) -let goto_definition: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("goto_definition")), - ( - "description", - `String( - "Selects the definition of the given variable name. Eg. goto_definition x will select ```let x = 1 in``` given a program ```let y = 0 in\nlet x = 1 in\nx + y```.", - ), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ( - "properties", - `Assoc([ - ( - "variable", - `Assoc([ - ("type", `String("string")), - ( - "description", - `String( - "The name of the variable whose definition associated with its let binding is to be selected.", - ), - ), - ]), - ), - ]), - ), - ("required", `List([`String("variable")])), - ]), - ), - ]), - ), - ]); - -let goto_body: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("goto_body")), - ( - "description", - `String( - "Selects the body of the given variable name. Eg. goto_body x will select ```x + y``` given a program ```let y = 0 in\nlet x = 1 in\nx + y```.", - ), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ( - "properties", - `Assoc([ - ( - "variable", - `Assoc([ - ("type", `String("string")), - ( - "description", - `String( - "The name of the variable whose body associated with its let bindingis to be selected.", - ), - ), - ]), - ), - ]), - ), - ("required", `List([`String("variable")])), - ]), - ), - ]), - ), - ]); - -let goto_type_definition: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("goto_type_definition")), - ( - "description", - `String( - {|Selects the definition of the given type name. Eg. goto_type_definition t will select ```type t = - + A(Bool) - + B(Int, Bool) -in``` given a program ```type t = - + A(Bool) - + B(Int, Bool) -in -let f = fun x: t -> - case x - | A(_) => "Argument has constructor A" - | B(_) => "Argument has constructor B" - end -in f(A(false))```.|}, - ), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ( - "properties", - `Assoc([ - ( - "variable", - `Assoc([ - ("type", `String("string")), - ( - "description", - `String( - "The name of the variable whose definition associated with its let binding is to be selected.", - ), - ), - ]), - ), - ]), - ), - ("required", `List([`String("variable")])), - ]), - ), - ]), - ), - ]); - -let goto_type_body: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("goto_type_body")), - ( - "description", - `String( - {|Selects the body of the given type name. Eg. goto_type_body t will select ```let f = fun x: t -> - case x - | A(_) => "Argument has constructor A" - | B(_) => "Argument has constructor B" - end -in f(A(false))``` given a program ```type t = - + A(Bool) - + B(Int, Bool) -in -let f = fun x: t -> - case x - | A(_) => "Argument has constructor A" - | B(_) => "Argument has constructor B" - end -in f(A(false))```.|}, - ), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ( - "properties", - `Assoc([ - ( - "variable", - `Assoc([ - ("type", `String("string")), - ( - "description", - `String( - "The name of the variable whose body associated with its let bindingis to be selected.", - ), - ), - ]), - ), - ]), - ), - ("required", `List([`String("variable")])), - ]), - ), - ]), - ), - ]); - -// todo: remove select_all by properly implementing goto navigators -let select_all: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("select_all")), - ( - "description", - `String( - "Selects the entire sketch. Used in rare case where goto tools are not working.", - ), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ("properties", `Assoc([])), - ("required", `List([])), - ]), - ), - ]), - ), - ]); - -let paste: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("paste")), - ( - "description", - `String( - "Pastes the given code over whatever you currently have selected/highlighted.", - ), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ( - "properties", - `Assoc([ - ( - "code", - `Assoc([ - ("type", `String("string")), - ( - "description", - `String( - "The code to paste over whatever you currently have selected/highlighted.", - ), - ), - ]), - ), - ]), - ), - ("required", `List([`String("code")])), - ]), - ), - ]), - ), - ]); - -let delete: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("delete")), - ( - "description", - `String("Deletes all of the currently selected text."), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ("properties", `Assoc([])), - ("required", `List([])), - ]), - ), - ]), - ), - ]); - -let submit: API.Json.t = - `Assoc([ - ("type", `String("function")), - ( - "function", - `Assoc([ - ("name", `String("submit")), - ( - "description", - `String( - "Submits the task once you believe it to be complete, ending the iterative tool call and task completion process.", - ), - ), - ( - "parameters", - `Assoc([ - ("type", `String("object")), - ("properties", `Assoc([])), - ("required", `List([])), - ]), - ), - ]), - ), - ]); - -let get_few_shot_comp_examples = () => { - "The following are GOLDEN EXAMPLES from agents who successfully implemented user-requested features." - ++ "Oh how you ASPIRE to be as elegant and efficient as they are! " - ++ "In fact, YOU CAN BE! As long as you study what they've done oh-so-well!\n" - ++ Ex_Simple_1.self - ++ Ex_Simple_2.self - ++ Ex_Tally.self - ++ Ex_Comparator.self - ++ Ex_Comparator_2.self - ++ ""; -}; - -let self = instructions @ hazel_syntax_notes @ summarized_hazel_docs; -// @ [get_few_shot_comp_examples()]; diff --git a/src/web/app/helpful-assistant/prompts/SummarizedHazelDocs.re b/src/web/app/helpful-assistant/prompts/SummarizedHazelDocs.re deleted file mode 100644 index 0131c6320e..0000000000 --- a/src/web/app/helpful-assistant/prompts/SummarizedHazelDocs.re +++ /dev/null @@ -1,85 +0,0 @@ -let self = [ - "", - "- Hazel is a live functional programming environment that supports incomplete programs via typed holes.", - "- Typed holes, denoted as `?`, can stand for missing expressions or types.", - "- Programs with holes still carry meaningful static and dynamic semantics, unlike traditional languages.", - "- Hazel uses a structure editor that maintains well-formed syntax trees with holes instead of raw text.", - "- Example: Define a function with an unknown type and evaluate it.", - "```", - "let f: ? -> ? = fun b -> b && true in f(true);", - "```", - "- This function runs successfully even though the type is unknown.", - "- Hazel uses **bidirectional typing**, where types can be either synthesized or analyzed.", - "- Example: Bidirectional typing for boolean functions.", - "```", - "let f = fun b: Bool -> b && true in f(true);", - "```", - "- Holes can be used in types, parameters, and patterns.", - "```", - "let f: (Int, ?) -> Int = fun a, b -> a + 1 in f(1, 2);", - "```", - "- Hazel also supports polymorphic functions.", - "```", - "let poly_id: poly a -> a -> a = typfun a -> fun x: a -> x;", - "```", - "- Polymorphic functions can be applied with type arguments using `@` syntax.", - "```", - "poly_id@(5);", - "```", - "- Hazel supports defining algebraic data types (ADTs) and using them in pattern matches.", - "```", - "type Exp =", - " + Var(String)", - " + Lam(String, Exp)", - " + Ap(Exp, Exp) in", - "", - "let subst: (Exp, String, Exp) -> Exp =", - " fun v, name, e ->", - " case e", - " | Var(n) => if n$==name then v else e", - " | Lam(x, body) => Lam(x, subst(v, name, body))", - " | Ap(e1, e2) => Ap(subst(v, name, e1), subst(v, name, e2))", - " end;", - "```", - "- Hazel permits incomplete pattern matches and treats them semantically.", - "```", - "let f = fun x -> case x | 0 => \"zero\" | ? => \"non-zero\" end;", - "```", - "- Evaluation proceeds until it hits a hole, then stops gracefully.", - "- Example of evaluation with error halted inside a hole:", - "```", - "let f: Int -> Int = fun x -> x + true in f(1);", - "```", - "- The erroneous addition will be wrapped in a non-empty hole, not crash.", - "- Hazel's recursive types allow expressive structures.", - "```", - "type MyList = rec A -> (Nil + Cons(Int, A)) in", - "let x: MyList = Cons(1, Cons(2, Cons(3, Nil)));", - "```", - "- Hazel supports list processing via pattern matching.", - "```", - "let length: [Int] -> Int =", - " fun xs ->", - " case xs", - " | [] => 0", - " | hd::tl => 1 + length(tl)", - " end;", - "```", - "- Hazel allows mutual recursion via let-bindings of tuples.", - "```", - "let (even: Int -> Bool, odd: Int -> Bool) =", - " (fun n -> if n == 0 then true else odd(n - 1),", - " fun n -> if n == 0 then false else even(n - 1))", - "in even(4);", - "```", - "- Projectors and livelits provide visual editing tools in Hazel's GUI.", - "- Hazel allows tests to be included inline.", - "```", - "test 2 + 2 == 4 end;", - "```", - "- Hazel supports explaining sub-expressions via cursor-based documentation.", - "- Hazel is ideal for education, as no edit state is meaningless.", - "- All code edits are type-preserving and guided by Hazel's edit action model.", - "- An important note is that comments in Hazel are enclosed by `#` symbols. Eg `# This is a comment #`. The program WILL be erroneous if you do not include an ending '#' symbol for a comment.", - "", -]; diff --git a/src/web/app/input/Shortcut.re b/src/web/app/input/Shortcut.re index 9f1ea2362e..a7815fd727 100644 --- a/src/web/app/input/Shortcut.re +++ b/src/web/app/input/Shortcut.re @@ -75,6 +75,8 @@ let shortcuts = (sys: Util.Key.sys): list(t) => Globals(ActiveEditor(Move(Goal(Hole(Right))))), // Tab is overloaded so not setting it here ), + ] + @ [ mk_shortcut( ~hotkey=Keyboard.meta(sys) ++ "+d", ~mdIcon="select_all", diff --git a/src/web/app/sidebar/Sidebar.re b/src/web/app/sidebar/Sidebar.re index d7d333d759..477d91b876 100644 --- a/src/web/app/sidebar/Sidebar.re +++ b/src/web/app/sidebar/Sidebar.re @@ -3,6 +3,7 @@ open Node; open Util.WebUtil; open Js_of_ocaml; open Util.JsUtil; +open Haz3lcore; let tab = (~tooltip="", icon, action, isActive) => { let classes = ["tab"] @ (isActive ? ["active"] : []); @@ -44,7 +45,7 @@ let assistant_tab = (~globals: Globals.t): Node.t => { ~attrs=[clss(["assistant-button"])], [ tab( - Icons.hazelnut_agent, + Icons.corylus, ~tooltip="Switch to Helpful Assistant", switch_assistant, globals.settings.sidebar.panel == HelpfulAssistant @@ -174,7 +175,6 @@ let view = ~signal, ~explainThisModel: ExplainThisModel.t, ~assistantModel: AssistantModel.t, - ~editor, info: option(Language.Info.t), ) => { let sub = @@ -197,7 +197,6 @@ let view = ~signal, ~inject=assistant_inject, ~model=assistantModel, - ~editor, ) }, ], diff --git a/src/web/debug/DebugConsole.re b/src/web/debug/DebugConsole.re index 0e87b99f17..f7efe77436 100644 --- a/src/web/debug/DebugConsole.re +++ b/src/web/debug/DebugConsole.re @@ -1,21 +1,18 @@ open Haz3lcore; - -/* This is a place to add ad-hoc debugging print actions. - It was originally directly in Keyboard, but that added a handler - dependency on the model, which is technically against architecture */ +open Util; let print = (~settings: Settings.t, editor: CodeWithStatics.Model.t, key: string) : unit => { let {editor: {state: {zipper, _}, _}, statics, _}: CodeWithStatics.Model.t = editor; let term = statics.term; - let map = statics.info_map; + let info = statics.info_map; let print = print_endline; switch (key) { | "F1" => zipper |> Zipper.show |> print | "F2" => zipper |> Zipper.unselect_and_zip |> Segment.show |> print | "F3" => term |> Language.Exp.show |> print - | "F4" => map |> Language.Statics.Map.show |> print + | "F4" => info |> Language.Statics.Map.show |> print | "F5" when settings.core.dynamics => let env_init = Language.Builtins.env_init; statics.elaborated @@ -29,12 +26,41 @@ let print = switch (index) { | Some(index) => print("id:" ++ Id.to_string(index)); - switch (Id.Map.find_opt(index, map)) { + switch (Id.Map.find_opt(index, info)) { | Some(ci) => print(Language.Info.show(ci)) | None => print("DEBUG: No CI found for index") }; | None => print("DEBUG: No indicated index") }; + | "F7" => () + | "F8" => () + | "F9" => + open HighLevelNodeMap.Public; + let node_map = build(zipper, info); + // Print all nodes and their paths + switch (node_map) { + | Some(node_map) => + node_map + |> Id.Map.bindings + |> List.iter(((id: Id.t, node: HighLevelNodeMap.node)) => { + let path_str = + node.path + |> List.map((path_id: Id.t) => id_to_name(node_map, path_id)) + |> String.concat("/"); + print( + "Node: " + ++ node.name + ++ " (id: " + ++ Id.to_string(id) + ++ ", path: " + ++ path_str + ++ ")", + ); + }) + | None => print("DEBUG: No node map found") + }; + | "F10" => () + | "F11" => () | _ => print("DEBUG: No action for key: " ++ key) }; }; diff --git a/src/web/exercises/examples/BlankExercise.ml b/src/web/exercises/examples/BlankExercise.ml index 191503634b..70ea696612 100644 --- a/src/web/exercises/examples/BlankExercise.ml +++ b/src/web/exercises/examples/BlankExercise.ml @@ -1,3 +1,5 @@ +open Haz3lcore + let exercise : Exercise.spec = { id = @@ -27,6 +29,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; correct_impl = { @@ -48,6 +51,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; your_tests = { @@ -71,6 +75,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; required = 0; provided = 0; @@ -95,6 +100,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_bugs = []; hidden_tests = @@ -119,6 +125,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = []; }; diff --git a/src/web/exercises/examples/Ex_OddlyRecursive.ml b/src/web/exercises/examples/Ex_OddlyRecursive.ml index 399f30fc1e..4c170bfe34 100644 --- a/src/web/exercises/examples/Ex_OddlyRecursive.ml +++ b/src/web/exercises/examples/Ex_OddlyRecursive.ml @@ -251,6 +251,7 @@ let exercise : Exercise.spec = ]; }; caret = Outer; + agent_view = AgentState.init; }; correct_impl = { @@ -778,6 +779,7 @@ let exercise : Exercise.spec = ]; }; caret = Outer; + agent_view = AgentState.init; }; your_tests = { @@ -1028,6 +1030,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; required = 6; provided = 2; @@ -1205,6 +1208,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_bugs = [ @@ -1404,6 +1408,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hint = "always returns false"; }; @@ -1603,6 +1608,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hint = "always returns true"; }; @@ -2392,6 +2398,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hint = "incorrect base case"; }; @@ -3097,6 +3104,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "zero" ]; }; diff --git a/src/web/exercises/examples/Ex_RecursiveFibonacci.ml b/src/web/exercises/examples/Ex_RecursiveFibonacci.ml index 24f056f15e..4d93f72199 100644 --- a/src/web/exercises/examples/Ex_RecursiveFibonacci.ml +++ b/src/web/exercises/examples/Ex_RecursiveFibonacci.ml @@ -20,6 +20,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; correct_impl = { @@ -465,6 +466,7 @@ let exercise : Exercise.spec = ]; }; caret = Outer; + agent_view = AgentState.init; }; your_tests = { @@ -477,6 +479,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; required = 5; provided = 0; @@ -657,6 +660,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_bugs = [ @@ -1367,6 +1371,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hint = "incorrect base cases"; }; @@ -1937,6 +1942,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hint = "incorrect recursion"; }; @@ -3107,6 +3113,7 @@ let exercise : Exercise.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = []; }; diff --git a/src/web/exercises/examples/Tu_ComposingArithmetic_and_Scope.ml b/src/web/exercises/examples/Tu_ComposingArithmetic_and_Scope.ml index 2dec1bd91a..36fe8d7025 100644 --- a/src/web/exercises/examples/Tu_ComposingArithmetic_and_Scope.ml +++ b/src/web/exercises/examples/Tu_ComposingArithmetic_and_Scope.ml @@ -28,6 +28,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -131,6 +132,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "Check the expression in the editor!" ]; }; @@ -164,6 +166,7 @@ let scope_exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -267,6 +270,7 @@ let scope_exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "Make sure you use x as your variable" ]; }; diff --git a/src/web/exercises/examples/Tu_ComputingEquationally_Shadow.ml b/src/web/exercises/examples/Tu_ComputingEquationally_Shadow.ml index c516c8d600..d61732cda7 100644 --- a/src/web/exercises/examples/Tu_ComputingEquationally_Shadow.ml +++ b/src/web/exercises/examples/Tu_ComputingEquationally_Shadow.ml @@ -26,6 +26,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -129,6 +130,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "Did you type out the given expression correctly?" ]; }; @@ -166,6 +168,7 @@ let shadow_exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -269,6 +272,7 @@ let shadow_exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "Have you shadowed x to 7?" ]; }; diff --git a/src/web/exercises/examples/Tu_ExpressiveProgramming.ml b/src/web/exercises/examples/Tu_ExpressiveProgramming.ml index 249a165695..a570c89e07 100644 --- a/src/web/exercises/examples/Tu_ExpressiveProgramming.ml +++ b/src/web/exercises/examples/Tu_ExpressiveProgramming.ml @@ -25,6 +25,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -128,6 +129,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "Reread the question!" ]; }; diff --git a/src/web/exercises/examples/Tu_More.ml b/src/web/exercises/examples/Tu_More.ml index 65c8f852f5..d7a6f8a088 100644 --- a/src/web/exercises/examples/Tu_More.ml +++ b/src/web/exercises/examples/Tu_More.ml @@ -38,6 +38,7 @@ let bools_ex : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -299,6 +300,7 @@ let bools_ex : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ @@ -334,6 +336,7 @@ let cond_ex : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -437,6 +440,7 @@ let cond_ex : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ @@ -474,6 +478,7 @@ let func_ex : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -577,6 +582,7 @@ let func_ex : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "Did you copy in the given expression correctly?" ]; }; diff --git a/src/web/exercises/examples/Tu_Variables_and_Compositionality.ml b/src/web/exercises/examples/Tu_Variables_and_Compositionality.ml index 443969200b..0de004fad2 100644 --- a/src/web/exercises/examples/Tu_Variables_and_Compositionality.ml +++ b/src/web/exercises/examples/Tu_Variables_and_Compositionality.ml @@ -28,6 +28,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -219,6 +220,7 @@ let exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ @@ -257,6 +259,7 @@ let comp_exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hidden_tests = { @@ -360,6 +363,7 @@ let comp_exercise : Tutorial.spec = ancestors = []; }; caret = Outer; + agent_view = AgentState.init; }; hints = [ "Make sure to embed a let expression as shown in the description!" ]; diff --git a/src/web/www/style.css b/src/web/www/style.css index 472164dafc..64c858ec06 100644 --- a/src/web/www/style.css +++ b/src/web/www/style.css @@ -105,7 +105,7 @@ select { background-color: var(--T1); position: relative; min-width: 300px; - width: 400px; + width: 450px; scrollbar-color: var(--main-scroll-thumb) var(--NONE); scrollbar-width: thin; } diff --git a/src/web/www/style/assistant.css b/src/web/www/style/assistant.css index f8521655bb..c7c0181407 100644 --- a/src/web/www/style/assistant.css +++ b/src/web/www/style/assistant.css @@ -27,10 +27,13 @@ #assistant .header { display: flex; - width: 100%; - justify-content: space-between; + flex-direction: row; align-items: center; - font-weight: bold; + padding: 8px 12px; + border-bottom: 1px solid var(--BR3); + background-color: var(--ui-bkg); + justify-content: space-between; + flex-shrink: 0; } #assistant .settings-box { @@ -73,10 +76,23 @@ /* transition: all 0.3s ease; */ } +#assistant .context-usage-display { + font-size: 0.75em; + color: var(--STONE); + opacity: 0.6; + font-weight: bold; + min-width: 35px; + text-align: center; + padding: 4px 6px; + background-color: var(--SAND); + border-radius: 3px; + border: 0.5px solid var(--BR3); +} + #assistant .message-input { flex: 1; background-color: var(--SAND); - padding: 14px; + padding: 8px 12px; border: 0; outline: 0.75px solid var(--BR3); border-radius: 4px; @@ -84,6 +100,13 @@ max-height: 200px; resize: none; overflow-y: auto; + box-sizing: border-box; + color: var(--STONE); +} + +#assistant .message-input::placeholder { + color: var(--STONE); + opacity: 0.5; } #assistant .message-input:focus { @@ -117,7 +140,10 @@ border-radius: 4px; font-weight: bold; cursor: pointer; - /* transition: all 0.3s ease;*/ + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; } #assistant .send-button:hover { @@ -233,6 +259,7 @@ font-size: 1em; max-width: 100%; word-wrap: break-word; + white-space: pre-wrap; } #assistant .system-prompt-message { @@ -242,6 +269,7 @@ font-size: 1em; max-width: 100%; word-wrap: break-word; + white-space: pre-wrap; } #assistant .system-error-message { @@ -251,15 +279,114 @@ font-size: 1em; max-width: 100%; word-wrap: break-word; + white-space: pre-wrap; } #assistant .user-message { padding: 8px 12px; - background-color: var(--T1); + background-color: rgba(0, 0, 0, 0.03); /* Very subtle background */ + border: 0.75px solid var(--BR3); border-radius: 3px; font-size: 1em; max-width: 100%; word-wrap: break-word; + white-space: pre-wrap; +} + +#assistant .user-message:hover { + cursor: pointer; + background-color: rgba(0, 0, 0, 0.02); /* Very subtle background */ +} + +/* User message input container styling */ +#assistant .user-message-input-container { + position: relative; + display: flex; + align-items: flex-end; + width: 100%; + margin-bottom: 10px; +} + +#assistant .user-message-input { + --SAND-30: oklch(99% 0.012 90 / 0.3); + flex: 1; + background-color: var(--SAND-30); + padding: 8px 12px; + padding-right: 44px; /* Space for the send button */ + border: 0.75px solid var(--BR3); + border-radius: 3px; + min-height: 40px; + max-height: 200px; + resize: none; + overflow-y: auto; + font-family: inherit; + font-size: inherit; + font-weight: normal; + color: var(--STONE); + line-height: 1.; + box-sizing: border-box; +} + +#assistant .user-message-input::placeholder { + color: var(--STONE); + opacity: 0.5; +} + +#assistant .user-message-input:hover { + --SAND-50: oklch(99% 0.012 90 / 0.7); + cursor: pointer; + background-color: var(--SAND-50); + border-color: var(--BR2); +} + +#assistant .user-message-input:focus { + outline: 0.75px solid var(--BLACK); + background-color: white; + border-color: var(--BLACK); + cursor: text; +} + +#assistant .user-send-button { + position: absolute; + bottom: 6px; + right: 6px; + color: var(--STONE); + width: 21px; + height: 21px; + border: 0.75px solid var(--BR3); + background-color: var(--T1); + border-radius: 50%; + font-weight: bold; + cursor: pointer; + line-height: 0; + z-index: 1; + opacity: 0; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + pointer-events: none; +} + +#assistant .user-send-button svg { + width: 14px; + height: 14px; +} + +#assistant .user-message-input-container:focus-within .user-send-button { + opacity: 0.7; + background-color: var(--T2); + pointer-events: auto; +} + +#assistant .user-send-button:hover { + background-color: var(--Y1); + opacity: 1; +} + +#assistant .user-send-button:active { + background-color: var(--Y2); } #assistant .loading-dots { @@ -327,11 +454,12 @@ #assistant .api-input, #assistant .llm-model-id-input { background-color: var(--SAND); - padding: 14px; + padding: 8px 12px; outline: none; border-radius: 3.5px; border: none; padding-right: 40px; /* Add padding to prevent text from going under the button */ + box-sizing: border-box; } #assistant .api-input:hover, @@ -828,6 +956,343 @@ font-size: 0.75em; max-width: 100%; word-wrap: break-word; + white-space: pre-wrap; + opacity: 0.7; + text-align: center; +} + +#assistant .system-todo-list { + align-items: center; + justify-content: center; +} + +#assistant .system-todo-list-message { + padding: 16px 20px; + background-color: var(--T1); + border: 0.75px solid var(--BR3); + border-radius: 8px; + font-size: 0.9em; + max-width: 600px; + margin: 0 auto; + word-wrap: break-word; + white-space: pre-wrap; + text-align: left; + color: var(--STONE); +} + +#assistant .system-todo-list-identifier { + display: flex; + align-items: center; + gap: 0.5em; + justify-content: center; +} + +/* Agent looping theme - applies dreamy blue/purple colors when agent is actively editing */ +.agent-looping { + --caret-color: var(--agent-caret-color); + --shard-selected: var(--agent-shard-selected); + --shadow-selected: var(--agent-shadow-selected); + --cell-selected-accent: var(--agent-cell-selected-accent); +} + +/* Quit button styling */ +#assistant .quit-button { + color: var(--STONE); + padding: 0.75em; + border: 0.75px solid var(--BR3); + background-color: var(--T1); + border-radius: 4px; + font-weight: bold; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +#assistant .quit-button:hover { + background-color: var(--Y1); +} + +#assistant .quit-button:active { + background-color: var(--Y2); +} + +/* New styles for header layout */ +#assistant .header-left { + display: flex; + align-items: center; + gap: 1em; +} + +#assistant .header-right { + display: flex; + align-items: center; + gap: 0.5em; +} + +#assistant .header-left-group, +#assistant .header-right-group { + display: flex; + align-items: center; + gap: 12px; +} + +/* View Buttons (Chat / Todos) */ +#assistant .view-buttons { + display: flex; + gap: 20px; +} + +#assistant .view-button { + cursor: pointer; + font-size: 0.9em; + color: var(--BR3); + padding-top: 4px; + padding-bottom: 2px; + border-bottom: 2px solid transparent; + transition: all 0.2s ease; +} + +#assistant .view-button:hover { + color: var(--Y3); +} + +#assistant .view-button.active { + color: var(--BR4); + border-bottom-color: var(--BR4); + font-weight: 500; +} + +/* Mode Dropdown */ +#assistant .mode-dropdown-container { + position: relative; + display: inline-block; + cursor: pointer; +} + +#assistant .mode-dropdown-label { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + border-radius: 4px; + font-size: 0.9em; + color: var(--BR4); + background-color: transparent; + transition: background-color 0.2s; +} + +#assistant .mode-dropdown-container:hover .mode-dropdown-label { + color: var(--Y3); + transition: all 0.2s ease; +} + +#assistant .dropdown-arrow { + font-size: 0.7em; + opacity: 0.7; + padding-top: 0.1em; +} + +#assistant .mode-dropdown-select { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; + appearance: none; /* Remove default arrow */ +} + +/* Todos Placeholder */ +#assistant .todos-placeholder { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + color: var(--BR3); + font-style: italic; +} + +/* Todo List Styling */ +#assistant .todo-list-container { + margin: 12px; + padding: 28px; + border: 1px solid var(--BR3); + border-radius: 8px; + background-color: var(--ui-bkg); +} + +#assistant .todo-list-title { + margin-bottom: 10px; + /* switch to flex so we can show the title and the elapsed time on one line */ + display: flex; + align-items: center; + justify-content: space-between; + color: var(--STONE); +} + +#assistant .todo-list-title-left { + font-weight: 600; + font-size: 1.05em; +} + +#assistant .todo-list-title-time { + font-size: 0.85em; + color: var(--STONE); opacity: 0.7; + margin-left: 12px; +} + +#assistant .todo-items { + display: flex; + flex-direction: column; + gap: 20px; +} + +#assistant .todo-item { + display: flex; + align-items: flex-start; + gap: 16px; + cursor: pointer; + padding: 4px 0; +} + +#assistant .todo-item:hover .todo-item-title { + text-decoration: underline; +} + +#assistant .todo-item-content { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; +} + +#assistant .todo-item-details { + display: flex; + flex-direction: column; + gap: 4px; + padding-top: 4px; + padding-bottom: 4px; + font-size: 0.9em; + color: var(--STONE); +} + +#assistant .todo-detail-header { + font-weight: bold; + font-size: 0.85em; + text-transform: uppercase; + color: var(--BR4); + margin-top: 4px; +} + +#assistant .todo-detail-header-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +#assistant .todo-detail-elapsed { + font-size: 0.85em; + color: var(--STONE); + opacity: 0.75; +} + +#assistant .todo-detail-text { + white-space: pre-wrap; + line-height: 1.4; +} + +#assistant .todo-detail-section { + display: flex; + flex-direction: column; + gap: 4px; + margin-top: 8px; +} + +#assistant .todo-status-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + flex-shrink: 0; +} + +#assistant .todo-status-icon.completed svg { + fill: var(--D0); +} + +#assistant .todo-status-icon.incomplete svg { + fill: var(--GB0); +} + +#assistant .todo-status-icon.active-todo svg { + fill: var(--agent-shadow-selected); + filter: drop-shadow(0 0 3px var(--agent-shadow-selected)); +} + +#assistant .todo-item-title { + font-size: 0.9em; + color: var(--STONE); +} + +#assistant .todo-item-title-row { + display: flex; + align-items: center; + justify-content: space-between; +} + +#assistant .todo-item-title-time { + font-size: 0.85em; + color: var(--STONE); + opacity: 0.75; + margin-left: 8px; +} + +#assistant .no-todo-list { + padding: 20px; text-align: center; + color: var(--STONE); + font-style: italic; +} + +/* Todo Archive Menu */ +#assistant #todo-archive-menu { + position: absolute; + top: 3.5em; + width: 350px; + border-radius: 2px; + box-shadow: 0 4px 12px var(--SHADOW); + z-index: 1000; + overflow: hidden; + animation: slideIn 0.2s ease-out; +} + +/* Todo List View Container */ +.todo-list-view { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + position: relative; +} + +/* Todo Archive Button */ +.todo-archive-button-container { + padding: 8px; + display: flex; + justify-content: flex-start; +} + +.todo-archive-button { + fill: var(--BR4); +} + +.todo-archive-button:hover { + cursor: pointer; + fill: var(--Y3); + transition: all 0.2s ease; } diff --git a/src/web/www/style/projectors/proj-base.css b/src/web/www/style/projectors/proj-base.css index b70e6a08f9..25f981261d 100644 --- a/src/web/www/style/projectors/proj-base.css +++ b/src/web/www/style/projectors/proj-base.css @@ -5,6 +5,7 @@ @import "proj-type.css"; @import "proj-cards.css"; @import "proj-text.css"; +@import "proj-comp.css"; @import "proj-fold.css"; @import "proj-checkbox.css"; @import "proj-slider.css"; diff --git a/src/web/www/style/projectors/proj-comp.css b/src/web/www/style/projectors/proj-comp.css new file mode 100644 index 0000000000..e6e7545a51 --- /dev/null +++ b/src/web/www/style/projectors/proj-comp.css @@ -0,0 +1,58 @@ +/* PROJECTOR: FOLD */ +.projector.comp { + font-family: var(--code-font); + justify-content: unset !important; /* override projectorbase.css */ +} + +.projector.comp.indicated { + font-family: var(--code-font); + justify-content: unset !important; /* override projectorbase.css */ + z-index: var(--projector-indicated-z); +} + +/* Show green backing decoration when comp projector is hovered */ +.projector.comp:hover > svg.shard > path { + fill: #00ff0040; + stroke: #00ff0040; +} + +.projector.comp .comp-view { + filter: drop-shadow(3px 3px 2px #00000010); +} + +.projector.comp .comp-view svg { + --shadow: var(--BR); + filter: drop-shadow(0.7px 0.7px 0px var(--shadow)); +} + +.projector.comp .comp-view path { + --fill: #00ff0020; + fill: var(--fill); + stroke: var(--fill); +} + +.projector.comp .comp-view :hover path { + --fill: #00ff0040; + fill: var(--fill); + stroke: var(--fill); +} + +.projector.comp .comp-view .incoming path { + --fill: #00ff0020; + fill: var(--fill); + stroke: var(--fill); +} + +.projector.comp .comp-view .previous path { + --fill: #ff000020; + fill: var(--fill); + stroke: var(--fill); +} + +/* Force comp projector backing to be transparent, even when indicated */ +.projector.comp.indicated > svg, +.projector.comp.indicated > svg.shard > path, +.projector.comp > svg, +.projector.comp > svg.shard > path { + display: none; +} \ No newline at end of file diff --git a/src/web/www/style/variables.css b/src/web/www/style/variables.css index 9730efa0db..2ac08a97d7 100644 --- a/src/web/www/style/variables.css +++ b/src/web/www/style/variables.css @@ -55,6 +55,11 @@ --GB0: oklch(70% 0.05 120); /* ! nut menu active */ --GB1: oklch(45% 0.05 120); /* ! nut menu fill */ + /* DREAMY - agent looping theme */ + --D0: oklch(75% 0.2 200); /* light blue caret */ + --D1: oklch(88% 0.15 240); /* soft blue selection */ + --D2: oklch(70% 0.25 280); /* purple shadow */ + /* FREE PARKING - ??? */ --textarea-v-stripe: oklch(78% 0.14 6 / 55%); @@ -78,6 +83,12 @@ --caret-color: var(--R1); --error-hole-stroke: var(--R1); + /* AGENT LOOPING THEME - Dreamy blue/purple theme */ + --agent-caret-color: var(--D0); /* Light blue caret */ + --agent-shard-selected: var(--D1); /* Soft blue selection */ + --agent-shadow-selected: var(--D2); /* Purple shadow for dreamy effect */ + --agent-cell-selected-accent: var(--D0); /* Light blue accent */ + /* CODE TOKENS */ --token-exp: var(--STONE); @@ -226,3 +237,4 @@ --modal-back-z: 100; --modal-z: 101; } + diff --git a/test/assistant_tests/Test_Assistant.re b/test/assistant_tests/Test_Assistant.re new file mode 100644 index 0000000000..dd59c37466 --- /dev/null +++ b/test/assistant_tests/Test_Assistant.re @@ -0,0 +1,14 @@ +module Fresh = Language.IdTagged.FreshGrammar; + +/* =============================== */ +/* |||| JOINED TESTS |||| */ +/* =============================== */ + +let tests = []; // ("Composition.Navigation", Test_NavigationActions.nav_tests), +// ("Composition.Editing", Test_EditActions.edit_tests), +// ( +// "HighLevelNodeMap.ViewDefinition", +// Test_ModifiedDefinitionView.view_definition_tests, +// ), + +// ("HighLevelNodeMap.ViewRefs", Test_ReferencesIn.view_refs_tests), diff --git a/test/assistant_tests/Utils_AssistantTests.re b/test/assistant_tests/Utils_AssistantTests.re new file mode 100644 index 0000000000..97452790ef --- /dev/null +++ b/test/assistant_tests/Utils_AssistantTests.re @@ -0,0 +1,15 @@ +open Haz3lcore; +open Test_Editing; +module Fresh = Language.IdTagged.FreshGrammar; + +let mk_statics = (z: Zipper.t) => + Language.( + Statics.mk( + CoreSettings.on, + Builtins.ctx_init(Some(Operators.default_mode)), + MakeTerm.from_zip_for_sem(z).term, + ) + ); + +let apply_actions = (init: string, actions: list(Action.t)): Zipper.t => + perform(perform(Zipper.init(), mk(init)), actions); diff --git a/test/assistant_tests/tests/Test_EditActions.re b/test/assistant_tests/tests/Test_EditActions.re new file mode 100644 index 0000000000..1f2ceb5810 --- /dev/null +++ b/test/assistant_tests/tests/Test_EditActions.re @@ -0,0 +1,241 @@ +// open Alcotest; + // open Haz3lcore; + // open Test_Editing; + // module Fresh = Language.IdTagged.FreshGrammar; + // open Utils_AssistantTests; + // let test = + // (~name, ~init: string, ~acts: list(CompositionGo.action), ~goal) + // : test_case(_) => { + // let acts = + // List.map(a => Action.Composition(CompositionActions.default(a)), acts); + // test_case(name, `Quick, () => + // check( + // testable(Fmt.string, String.equal), + // goal, + // goal, + // acts |> apply_actions(init) |> printer, + // ) + // ); + // }; + // /* =============================== */ + // /* |||| EDIT TOOL TESTS |||| */ + // /* =============================== */ + // // - UpdateDefinition(string) + // // - UpdateBody(string) + // // - UpdatePattern(string) + // // - UpdateBindingClause(string) + // // - DeleteExpression + // // - DeleteBody + // // - InsertAfter(string) + // // - InsertBefore(string) + // let update_definition_tests = { + // [ + // test( + // ~name="Update Definition (\"Simplest\" Case)", + // ~init="let x = 1 in x", + // ~acts=[Edit(UpdateDefinition("/x", "2"))], + // ~goal="let x = 2 in x", + // ), + // test( + // ~name="Update Definition (Simple - V2)", + // ~init={|let x = (1, 2) in x|}, + // ~acts=[Edit(UpdateDefinition("/x", "(0, 2, 4)"))], + // ~goal={|let x = (0, 2, 4) in x|}, + // ), + // ]; + // }; + // let update_body_tests = { + // [ + // test( + // ~name="Update Body (\"Simplest\" Case)", + // ~init="let x = 1 in¦ x + 2", + // ~acts=[Edit(UpdateBody(LLM("x * 2")))], + // ~goal="§let x = 1 in¦ x * 2", + // ), + // ]; + // }; + // let update_pattern_tests = { + // [ + // // little note: + // // Would be nice to handle on our backend, the renaming of all use sites of the pattern as well + // test( + // ~name="Update Pattern (\"Simplest\" Case)", + // ~init="let x = 1 in¦ x", + // ~acts=[Edit(UpdatePattern(LLM("a")))], + // ~goal="§let a = 1 in¦ a", + // ), + // test( + // ~name= + // "Update Pattern (Variable Renaming Case (Cursor should relocate back to original pattern))", + // ~init="let x = 1 in¦ let y = 2 in x + y", + // ~acts=[Edit(UpdatePattern(LLM("a")))], + // ~goal="§let a = 1 in¦ let y = 2 in a + y", + // ), + // test( + // ~name="Update Pattern (Tuple)", + // ~init="let (a, b) = ? in¦ ?", + // ~acts=[Edit(UpdatePattern(LLM("(x, y)")))], + // ~goal="§let (x, y) = ? in¦ ?", + // ), + // test( + // ~name="Update Pattern (Annotated Atomic Type)", + // ~init="let u : Int = ? in¦ ?", + // ~acts=[Edit(UpdatePattern(LLM("u : Float")))], + // ~goal="§let u : Float = ? in¦ ?", + // ), + // test( + // ~name="Update Pattern (Annotated Arrow Type)", + // ~init="let u : Int -> Int = ? in¦ ?", + // ~acts=[Edit(UpdatePattern(LLM("u : Float -> Float")))], + // ~goal="§let u : Float -> Float = ? in¦ ?", + // ), + // test( + // ~name="Update Pattern (Annotated Arrow Type 2 (Remove Annotation))", + // ~init="let u : Int -> Int = ? in¦ ?", + // ~acts=[Edit(UpdatePattern(LLM("u")))], + // ~goal="§let u = ? in¦ ?", + // ), + // test( + // ~name="Update Pattern (Annotated Arrow Type 3 (Introduce Annotation))", + // ~init="let u = ? in¦ ?", + // ~acts=[Edit(UpdatePattern(LLM("u : Int -> Int")))], + // ~goal="§let u : Int -> Int = ? in¦ ?", + // ), + // ]; + // }; + // let update_binding_clause_tests = { + // [ + // test( + // ~name="Update Binding Clause (\"Simplest\" Case)", + // ~init="let x = 1¦ in x", + // ~acts=[Edit(UpdateBindingClause(LLM("let x = 2 in")))], + // ~goal="§let x = 2 in¦ x", + // ), + // test( + // ~name= + // "Update Binding Clause (Multiple Bindings Case (Cursor should go to tail binding))", + // ~init="let x = 1¦ in x", + // ~acts=[Edit(UpdateBindingClause(LLM("let x = 2 in let y = 3 in")))], + // ~goal="let x = 2 in §let y = 3 in¦ x", + // ), + // ]; + // }; + // let delete_binding_clause_tests = { + // [ + // test( + // ~name="Delete Binding Clause (\"Simplest\" Case)", + // ~init="let a = 1¦ in let b = 2 in a + b", + // ~acts=[Edit(DeleteBindingClause)], + // ~goal=" §let b = 2 in¦ a + b" // todo: Update this action to handle the extra space that is now present + // ), + // ]; + // }; + // let delete_body_tests = { + // [ + // test( + // ~name="Delete Body (\"Simplest\" Case)", + // ~init="let a = 1¦ in a * 5", + // ~acts=[Edit(DeleteBody)], + // ~goal="§let a = 1 in¦ ?", + // ), + // // ================================ + // // Special Cases: Final Body and Empty Hole(s) + // test( + // ~name="Delete Body (Two Bindings (Final Body Empty Hole)))", + // ~init="let a = 0¦ in let b = 1 in ", + // ~acts=[Edit(DeleteBody)], + // ~goal="§let a = 0 in¦ ?", + // ), + // test( + // ~name="Delete Body (Two Bindings (Final Body Non-Empty)))", + // ~init="let a = 1¦ in let b = 2 in a + b", + // ~acts=[Edit(DeleteBody)], + // ~goal="§let a = 1 in¦ ?", + // ), + // test( + // ~name= + // "Delete Body (Multiple Bindings (Final Body's Expression Contains an Empty Hole)))", + // ~init="let a = -1¦ in let b = 0 in let c = 1 in a + ", + // ~acts=[Edit(DeleteBody)], + // ~goal="§let a = -1 in¦ ?", + // ), + // // Below is failing as select with defs_exclude_bodies=false is not working when + // // final body token is an implicit hole. + // test( + // ~name= + // "Delete Body (Multiple Bindings (Final Body Expression is an Explicit Hole)))", + // ~init="let a = 10¦ in let b = 11 in let c = 12 in ?", + // ~acts=[Edit(DeleteBody)], + // ~goal="§let a = 10 in¦ ?", + // ), + // test( + // ~name= + // "Delete Body (Multiple Bindings (Final Body Expression is an Implicit Empty Hole)))", + // ~init="let a = 0¦ in let b = 1 in let c = 2 in ", + // ~acts=[Edit(DeleteBody)], + // ~goal="§let a = 0 in¦ ?", + // ), + // test( + // ~name= + // "Delete Body (Multiple Bindings (Final Body Completely Non-Empty)))", + // ~init="let a = 1¦ in let b = 2 in let c = 3 in a + b + c", + // ~acts=[Edit(DeleteBody)], + // ~goal="§let a = 1 in¦ ?", + // ), + // // End Special Cases: Final Body and Empty Hole(s) + // // ================================ + // ]; + // }; + // let insert_before_tests = { + // [ + // test( + // ~name="Insert Before (\"Simplest\" Case)", + // ~init="let b = 2¦ in a * b", + // ~acts=[Edit(InsertBefore(LLM("let a = 1 in")))], + // ~goal="§let a = 1 in¦ let b = 2 in a * b", + // ), + // test( + // ~name="Insert Before (Between Two Bindings Case)", + // ~init="let a = 1 in let c = 3 in¦ a * c", + // ~acts=[Edit(InsertBefore(LLM("let b = 2 in")))], + // ~goal="let a = 1 in §let b = 2 in¦ let c = 3 in a * c", + // ), + // test( + // ~name="Insert Before (Multiple Bindings Case)", + // ~init="let b = 2¦ in a * b", + // ~acts=[Edit(InsertBefore(LLM("let a = 1 in let c = 3 in")))], + // ~goal="let a = 1 in §let c = 3 in¦ let b = 2 in a * b", + // ), + // ]; + // }; + // let insert_after_tests = { + // [ + // test( + // ~name="Insert After (\"Simplest\" Case)", + // ~init="let a = 1¦ in a", + // ~acts=[Edit(InsertAfter(LLM("let b = 2 in")))], + // ~goal="let a = 1 in §let b = 2 in¦ a", + // ), + // test( + // ~name="Insert After (Between Two Bindings)", + // ~init="let a = 10¦ in let c = 30 in a + c", + // ~acts=[Edit(InsertAfter(LLM("let b = 20 in")))], + // ~goal="let a = 10 in §let b = 20 in¦ let c = 30 in a + c", + // ), + // test( + // ~name="Insert After (Multiple Bindings Case)", + // ~init="let a = 0¦ in a", + // ~acts=[Edit(InsertAfter(LLM("let b = 10 in let c = 100 in")))], + // ~goal="let a = 0 in let b = 10 in §let c = 100 in¦ a", + // ), + // ]; + // }; + // let edit_tests = + // update_definition_tests + // @ update_body_tests + // @ update_pattern_tests // todo: (fix) relocate cursor back to pattern after variable renaming + // @ update_binding_clause_tests + // @ delete_binding_clause_tests + // @ delete_body_tests + // @ insert_before_tests + // @ insert_after_tests; diff --git a/test/assistant_tests/tests/Test_NavigationActions.re b/test/assistant_tests/tests/Test_NavigationActions.re new file mode 100644 index 0000000000..45e58f3f13 --- /dev/null +++ b/test/assistant_tests/tests/Test_NavigationActions.re @@ -0,0 +1,115 @@ +// open Alcotest; + // open Haz3lcore; + // open Test_Editing; + // module Fresh = Language.IdTagged.FreshGrammar; + // open Utils_AssistantTests; + // let test = + // (~name, ~init: string, ~acts: list(CompositionGo.action), ~goal) + // : test_case(_) => { + // let acts = + // List.map(a => Action.Composition(CompositionActions.default(a)), acts); + // test_case(name, `Quick, () => + // check( + // testable(Fmt.string, String.equal), + // goal, + // goal, + // acts |> apply_actions(init) |> printer, + // ) + // ); + // }; + // /* =============================== */ + // /* |||| NAVIGATION TOOL TESTS |||| */ + // /* =============================== */ + // // - GoToParent + // // - GoToChild(string, option(int)) + // // - GoToSibling(via) ('via' being either (string, option(int)) or Direction.t) + // let goto_parent_tests = { + // [ + // test( + // ~name="Goto Parent (Simple - V1)", + // ~init={|let x = let y = let z = 7¦ in z in y in x|}, + // ~acts=[Nav(GoToParent)], + // // Goes from 'z' to its parent, 'y' + // ~goal={|let x = §let y = let z = 7 in z in¦ y in x|}, + // ), + // test( + // ~name="Goto Parent (Double go to, Simple - V2)", + // ~init={|let x = let y = let z = 7¦ in z in y in x|}, + // ~acts=[Nav(GoToParent), Nav(GoToParent)], + // // Goes from 'z' to its parent, 'y', and then to its parent, 'x' + // ~goal={|§let x = let y = let z = 7 in z in y in¦ x|}, + // ), + // ]; + // }; + // let goto_child_tests = { + // [ + // test( + // ~name="Goto Child (Name-Identified - Simple - V1)", + // ~init={|let x = let a = 3 in let b = 4 in a + b in¦ x|}, + // ~acts=[Nav(GoToChild("b", None))], + // // Goes from 'x' to its child, 'b' + // ~goal={|let x = let a = 3 in §let b = 4 in¦ a + b in x|}, + // ), + // test( + // ~name="Goto Child (Index-Identified - Shadowed-Case - V1)", + // ~init={|let x = let b = 3 in let b = 4 in b in¦ x|}, + // ~acts=[Nav(GoToChild("b", Some(0)))], + // // Goes from 'x' to its first child, 'b' + // ~goal={|let x = §let b = 3 in¦ let b = 4 in b in x|}, + // ), + // test( + // ~name="Goto Child (Index-Identified - Shadowed-Case - V2)", + // ~init={|let x = let b = 3 in let b = 4 in b in¦ x|}, + // ~acts=[Nav(GoToChild("b", Some(1)))], + // // Goes from 'x' to its second child, 'b' + // ~goal={|let x = let b = 3 in §let b = 4 in¦ b in x|}, + // ), + // test( + // ~name="Goto Child (Index-Identified - Shadowed-Case - V3)", + // ~init= + // {| + // let x = let b = 3 in let h = 10 in let b = 4 in b + h in¦ x + // |}, + // ~acts=[Nav(GoToChild("b", Some(2)))], + // // Goes from 'x' to its third child, 'b' (the second 'b' in this case) + // ~goal= + // {| + // let x = let b = 3 in let h = 10 in §let b = 4 in¦ b + h in x + // |}, + // ), + // ]; + // }; + // let goto_sibling_tests = { + // [ + // test( + // ~name="Goto Sibling (Simple - V1)", + // ~init={|let x = 4 in let y = 5¦ in x + y|}, + // ~acts=[Nav(GoToSibling(NameAndIdx("x", None)))], + // ~goal={|§let x = 4 in¦ let y = 5 in x + y|}, + // ), + // test( + // ~name="Goto Sibling (Simple - V2)", + // ~init={|let x = 4 in let y = 6 in let z = 7¦ in x + y + z|}, + // ~acts=[Nav(GoToSibling(NameAndIdx("x", None)))], + // ~goal={|§let x = 4 in¦ let y = 6 in let z = 7 in x + y + z|}, + // ), + // test( + // ~name="Goto Sibling (Replication V1)", + // ~init= + // "type MyOption = + Some(?) + None in let unwrap : Option -> ? = fun x -> case x | Some(v) => v | None => ? end in¦ ?", + // ~acts=[Nav(GoToSibling(NameAndIdx("let", Some(0))))], + // ~goal= + // "§type MyOption = + Some(?) + None in¦ let unwrap : Option -> ? = fun x -> case x | Some(v) => v | None => ? end in ?", + // ), + // // test( + // // ~name="Goto Sibling (Replication V2)", + // // ~init="type t = + T in let u : old_t -> ? = ? in¦ ?", + // // ~acts=[ + // // Edit(UpdateBindingClause("let u : t -> ? = ? in")), + // // Nav(GoToSibling("t", Some(0))), + // // ], + // // ~goal="§type t = + T in¦ let u : t -> ? = ? in ?", + // // ), + // ]; + // }; + // let nav_tests = goto_parent_tests @ goto_child_tests @ goto_sibling_tests; diff --git a/test/assistant_tests/tests/Test_ReferencesIn.re b/test/assistant_tests/tests/Test_ReferencesIn.re new file mode 100644 index 0000000000..f6e30a4d5f --- /dev/null +++ b/test/assistant_tests/tests/Test_ReferencesIn.re @@ -0,0 +1,117 @@ +// open Alcotest; + // open Haz3lcore; + // open Test_Editing; + // module Fresh = Language.IdTagged.FreshGrammar; + // open Utils_AssistantTests; + // /* =============================== */ + // /* |||| VIEW REFS TESTS |||| */ + // /* =============================== */ + // open Language; + // let refs_list_to_str_for_testing_only = (refs: list(Binding.t)): string => { + // List.map((binding: Binding.t) => binding.name, refs) |> String.concat(" "); + // }; + // let test = + // ( + // ~exclude_rec_refs: bool, + // ~exclude_body_refs: bool, + // ~name, + // ~init: string, + // ~goal, + // ) + // : test_case(_) => { + // let z = perform(Zipper.init(), mk(init)); + // let info_map = mk_statics(z); + // let curr_node_info = + // HighLevelNodeMap.HighLevelNode.build(z, info_map) + // |> HighLevelNodeMap.HighLevelNode.unwrap; + // let refs_in = + // refs_list_to_str_for_testing_only( + // CompositionView.refs_in( + // ~exclude_rec_refs, + // ~exclude_body_refs, + // curr_node_info, + // info_map, + // ), + // ); + // test_case(name, `Quick, () => + // check(testable(Fmt.string, String.equal), goal, goal, refs_in) + // ); + // }; + // let view_refs_tests = [ + // test( + // ~name="View Refs (Include Rec Refs)", + // ~exclude_rec_refs=false, + // ~exclude_body_refs=false, + // ~init= + // {| + // let p1 = + // let c0 = 8 in + // let c1 = 5 in + // let f1¦ = fun x -> + // if true then x + c0 + // else f1(x-1) + // in + // let c2 = 6 + c1 in + // c1 + c2 + c0 + // in + // |}, + // ~goal="c0 f1 c1", + // ), + // test( + // ~name="View Refs (Exclude Rec Refs)", + // ~exclude_rec_refs=true, + // ~exclude_body_refs=false, + // ~init= + // {| + // let p1 = + // let c0 = 8 in + // let c1 = 5 in + // let f1¦ = fun x -> + // if true then x + c0 + // else f1(x-1) + // in + // let c2 = 6 + c1 in + // c1 + c2 + c0 + // in + // |}, + // ~goal="c0 c1", + // ), + // test( + // ~name="View Refs (Include Body Refs)", + // ~exclude_rec_refs=false, + // ~exclude_body_refs=true, + // ~init= + // {| + // let p1 = + // let c0 = 8 in + // let c1 = 5 in + // let f1¦ = fun x -> + // if true then x + c0 + // else f1(x-1) + // in + // let c2 = 6 + c1 in + // c1 + c2 + c0 + // in + // |}, + // ~goal="c0 f1", + // ), + // test( + // ~name="View Refs (Include All)", + // ~exclude_rec_refs=true, + // ~exclude_body_refs=true, + // ~init= + // {| + // let p1 = + // let c0 = 8 in + // let c1 = 5 in + // let f1¦ = fun x -> + // if true then x + c0 + // else f1(x-1) + // in + // let c2 = 6 + c1 in + // c1 + c2 + c0 + // in + // |}, + // ~goal="c0", + // ), + // ];