├── src ├── util.ts ├── index.ts ├── enum.ts ├── tool.ts ├── overleaf │ ├── retriever.ts │ └── action.ts ├── retriever.ts └── action.ts ├── tsconfig.json ├── .github └── workflows │ └── lint.yml ├── eslint.config.mjs ├── package.json ├── webpack.config.cjs ├── README.md └── .gitignore /src/util.ts: -------------------------------------------------------------------------------- 1 | export const isOverleafDocument = () => 2 | /^https:\/\/www\.overleaf\.com\/project\/[a-f0-9]{24}$/.test( 3 | window.location.href 4 | ); 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { retrievers } from "./retriever"; 2 | export { actions } from "./action"; 3 | export { tool, toolName, ToolName } from "./tool"; 4 | export { Scope, ToolType, CallerType } from "./enum"; 5 | export * as retriever from "./retriever"; 6 | export * as action from "./action"; 7 | -------------------------------------------------------------------------------- /src/enum.ts: -------------------------------------------------------------------------------- 1 | /* Which domain should a tool be availale */ 2 | export enum Scope { 3 | Any = "Any", 4 | Overleaf = "Overleaf", 5 | GoogleDoc = "Google Doc", 6 | } 7 | 8 | export enum ToolType { 9 | Action = "action", 10 | Retriever = "retriever", 11 | } 12 | 13 | /* Who should call this function */ 14 | export enum CallerType { 15 | Any = "any", 16 | ContentScript = "content script", 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2021", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./lib", 11 | "rootDir": "./src", 12 | "declaration": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "**/*.spec.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: "16" 22 | 23 | - name: Install dependencies 24 | run: npm install 25 | 26 | - name: Run linter check 27 | run: npm run lint 28 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "eslint"; 2 | 3 | const { ESLint } = eslint; 4 | 5 | export default new ESLint({ 6 | baseConfig: { 7 | env: { 8 | browser: true, 9 | es2021: true, 10 | node: true, 11 | }, 12 | extends: ["eslint:recommended"], 13 | parserOptions: { 14 | ecmaVersion: 2021, 15 | sourceType: "module", 16 | }, 17 | rules: { 18 | "no-unused-vars": "warn", 19 | "no-console": "off", 20 | indent: ["error", 2], 21 | quotes: ["error", "single"], 22 | semi: ["error", "always"], 23 | }, 24 | }, 25 | ignorePatterns: ["node_modules/", "lib/"], 26 | }); 27 | -------------------------------------------------------------------------------- /src/tool.ts: -------------------------------------------------------------------------------- 1 | import { actions } from "./action"; 2 | import { retrievers } from "./retriever"; 3 | import { CallerType, Scope, ToolType } from "./enum"; 4 | 5 | export const tool: Record = { 6 | ...retrievers, 7 | ...actions, 8 | }; 9 | 10 | export const toolName = Object.keys(tool); 11 | export type ToolName = keyof typeof tool; 12 | 13 | export interface Tool { 14 | name: ToolName; 15 | displayName: string; 16 | description: string; 17 | schema: { 18 | type: "function"; 19 | function: { 20 | name: string; 21 | description: string; 22 | parameters: { 23 | type: "object"; 24 | properties: Record; 25 | required: Array< 26 | keyof Tool["schema"]["function"]["parameters"]["properties"] 27 | >; 28 | }; 29 | }; 30 | }; 31 | type: ToolType; 32 | scope: Scope.Any | Scope[]; 33 | caller: CallerType; 34 | implementation: (parameters: any) => void; 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mlc-ai/web-agent-interface", 3 | "version": "0.0.3", 4 | "description": "A tool library for LLM agent in browsers to interact with the web.", 5 | "main": "lib/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "build": "tsc && webpack --config webpack.config.cjs", 9 | "lint": "eslint . && prettier --check \"./**/*.{ts,tsx,json}\"", 10 | "test": "jest", 11 | "format": "prettier --write \"./**/*.{ts,tsx,json}\"" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/mlc-ai/web-agent-interface.git" 16 | }, 17 | "license": "Apache-2.0", 18 | "bugs": { 19 | "url": "https://github.com/mlc-ai/web-agent-interface/issues" 20 | }, 21 | "homepage": "https://github.com/mlc-ai/web-agent-interface#readme", 22 | "devDependencies": { 23 | "@babel/preset-env": "^7.25.4", 24 | "@babel/preset-typescript": "^7.24.7", 25 | "@types/chrome": "^0.0.278", 26 | "@types/jest": "^29.5.12", 27 | "@types/node": "^22.5.4", 28 | "@typescript-eslint/eslint-plugin": "^8.5.0", 29 | "babel-loader": "^9.1.3", 30 | "eslint": "^9.9.1", 31 | "jest": "^29.7.0", 32 | "prettier": "^3.3.3", 33 | "typescript": "^5.6.2", 34 | "typescript-eslint": "^8.5.0", 35 | "webpack": "^5.94.0", 36 | "webpack-cli": "^5.1.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { IgnorePlugin } = require("webpack"); 3 | 4 | module.exports = { 5 | entry: "./src/index.ts", 6 | output: { 7 | path: path.resolve(__dirname, "lib"), 8 | filename: "index.js", 9 | library: { 10 | type: "commonjs2", 11 | }, 12 | clean: { 13 | keep: /\.d\.ts$/, 14 | }, 15 | sourceMapFilename: "[file].map", 16 | }, 17 | target: "web", 18 | resolve: { 19 | extensions: [".ts", ".js", ".json"], 20 | fallback: { 21 | fs: false, 22 | path: false, 23 | crypto: false, 24 | }, 25 | }, 26 | module: { 27 | rules: [ 28 | { 29 | test: /\.(ts|js)x?$/, 30 | exclude: /node_modules/, 31 | use: { 32 | loader: "babel-loader", 33 | options: { 34 | presets: ["@babel/preset-env", "@babel/preset-typescript"], 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.(png|jpe?g|gif|svg)$/i, 40 | type: "asset/resource", 41 | }, 42 | ], 43 | }, 44 | plugins: [ 45 | new IgnorePlugin({ 46 | resourceRegExp: /^fs$|^path$|^crypto$/, 47 | }), 48 | ], 49 | devtool: "source-map", 50 | mode: "production", 51 | externals: { 52 | // Prevent bundling of certain imported packages 53 | // (e.g., libraries already available as external scripts) 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web-Agent-Interface Library 2 | 3 | ## Overview 4 | 5 | The `web-agent-interface` library provides tools to LLM agents in browsers to interact with different websites. 6 | 7 | **Note**: The project is still in development phase. It has limited coverage and its APIs may change. 8 | 9 | ## Supported Tools 10 | 11 | - **General DOM Operations**: Get page content and user selection. 12 | - **Overleaf**: Edit Overleaf documents. 13 | - **Google Calendar (GCal)**: Read and create events on Google Calendar. 14 | 15 | 16 | ## Installation 17 | 18 | To install and build the library, follow these steps: 19 | 20 | 1. Clone the repository: 21 | 22 | ```bash 23 | git clone https://github.com/mlc-ai/web-agent-interface.git 24 | cd web-agent-interface 25 | ``` 26 | 27 | 2. Install dependencies and build the project: 28 | 29 | ```bash 30 | npm install 31 | npm run build 32 | ``` 33 | 34 | ## Usage 35 | 36 | ### 1. Initialize State 37 | 38 | ```javascript 39 | import { State } from '@mlc-ai/web-agent-interface'; 40 | 41 | const state = new State(); 42 | ``` 43 | 44 | 45 | ### 2. Import Tools 46 | 47 | ```javascript 48 | import { tool, retriever, action } from '@mlc-ai/web-agent-interface'; 49 | ``` 50 | 51 | ### 3. Give Tool Description to Prompt 52 | 53 | ```javascript 54 | const system_prompt = ` 55 | You are a helpful AI agent. 56 | 57 | You have the following tools to use: 58 | 59 | ${tools.map((t) => JSON.stringify(t.schema)).join(",\n")} 60 | ` 61 | ``` 62 | 63 | ### 3. Call Tool Function to Get Observation 64 | 65 | ```javascript 66 | const { tool_name, parameters } = extractToolCall(llm_response); 67 | const observation = tool[tool_name].implementation(state, parameters); 68 | console.log("Got observation:", observation); 69 | ``` 70 | -------------------------------------------------------------------------------- /src/overleaf/retriever.ts: -------------------------------------------------------------------------------- 1 | export function getEditorContext() { 2 | // Identify the contenteditable container 3 | const contentEditableElement = document.querySelector('.cm-content'); 4 | 5 | if (!contentEditableElement) { 6 | console.error('Editable area not found.'); 7 | return null; 8 | } 9 | 10 | // Get the selection object 11 | const selection = window.getSelection(); 12 | 13 | if (!selection?.rangeCount) { 14 | console.warn('No selection or cursor found.'); 15 | return null; 16 | } 17 | 18 | // Get the active range (selection or cursor position) 19 | const range = selection.getRangeAt(0); 20 | 21 | // Check if the selection is within the editable area 22 | if (!contentEditableElement.contains(range.startContainer)) { 23 | console.warn('Selection is outside the editable area.'); 24 | return null; 25 | } 26 | 27 | // Get the selected text (if any) 28 | const selectedText = selection.toString(); 29 | 30 | // Get text content before the cursor/selection 31 | const beforeCursorRange = document.createRange(); 32 | beforeCursorRange.setStart(contentEditableElement, 0); 33 | beforeCursorRange.setEnd(range.startContainer, range.startOffset); 34 | const textBeforeCursor = beforeCursorRange.toString(); 35 | 36 | // Get text content after the cursor/selection 37 | const afterCursorRange = document.createRange(); 38 | afterCursorRange.setStart(range.endContainer, range.endOffset); 39 | afterCursorRange.setEnd(contentEditableElement, contentEditableElement.childNodes.length); 40 | const textAfterCursor = afterCursorRange.toString(); 41 | 42 | return { 43 | selectedText: selectedText, 44 | textBeforeCursor: textBeforeCursor, 45 | textAfterCursor: textAfterCursor, 46 | cursorPosition: range.startOffset // Cursor offset in the start container 47 | }; 48 | } -------------------------------------------------------------------------------- /src/overleaf/action.ts: -------------------------------------------------------------------------------- 1 | export function insertText(parameters: { 2 | textToInsert: string; 3 | position: 'beginning' | 'end' | 'cursor', 4 | }) { 5 | const { textToInsert, position = 'cursor' } = parameters; 6 | 7 | if (position === "beginning") { 8 | const editorElement = document.querySelector(".cm-content"); 9 | if (editorElement) { 10 | const textNode = document.createTextNode(textToInsert); 11 | editorElement.prepend(textNode); 12 | 13 | // Scroll to bottom 14 | const scroller = document.querySelector(".cm-scroller"); 15 | if (scroller) { 16 | scroller.scrollTo({ top: 0, behavior: "smooth" }); 17 | } 18 | } 19 | } 20 | else if (position === 'end') { 21 | const editorElement = document.querySelector(".cm-content"); 22 | if (editorElement) { 23 | const textNode = document.createTextNode(textToInsert); 24 | editorElement.appendChild(textNode); 25 | 26 | // Scroll to start 27 | const scroller = document.querySelector(".cm-scroller"); 28 | if (scroller) { 29 | scroller.scrollTo({ top: scroller.scrollHeight, behavior: "smooth" }); 30 | } 31 | } 32 | } else if (position === "cursor") { 33 | const selection = window.getSelection(); 34 | 35 | if (!selection?.rangeCount) { 36 | console.error("No cursor location available"); 37 | return; 38 | } 39 | 40 | // Get the range of the current selection or cursor position 41 | const range = selection.getRangeAt(0); 42 | 43 | // Extract the currently selected content (if any) 44 | const selectedContent = range.cloneContents(); 45 | 46 | // Create a document fragment to hold the new content 47 | const fragment = document.createDocumentFragment(); 48 | 49 | // Create a text node for the text to insert before the selection 50 | if (textToInsert) { 51 | fragment.appendChild(document.createTextNode(textToInsert)); 52 | } 53 | 54 | // Append the selected content to the fragment 55 | if (selectedContent) { 56 | fragment.appendChild(selectedContent); 57 | } 58 | 59 | // Insert the fragment into the range 60 | range.deleteContents(); // Remove the current selection 61 | range.insertNode(fragment); 62 | 63 | // Move the cursor to the end of the inserted content 64 | range.collapse(false); // Collapse the range to its end 65 | 66 | // Clear the selection and set the updated range 67 | selection.removeAllRanges(); 68 | selection.addRange(range); 69 | } 70 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | params/ 3 | *.bak 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | *.S 10 | # C extensions 11 | *.so 12 | 13 | 14 | *.ll 15 | .npm 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | build-*/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | .conda/ 40 | # PyInstaller 41 | # Usually these files are written by a python script from a template 42 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 43 | *.manifest 44 | *.spec 45 | 46 | # Generated by python/gen_requirements.py 47 | python/requirements/*.txt 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | *.py,cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | docs/_staging/ 87 | 88 | # PyBuilder 89 | target/ 90 | /target/ 91 | 92 | # Jupyter Notebook 93 | .ipynb_checkpoints 94 | 95 | # IPython 96 | profile_default/ 97 | ipython_config.py 98 | 99 | # pyenv 100 | .python-version 101 | 102 | # pipenv 103 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 104 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 105 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 106 | # install all needed dependencies. 107 | #Pipfile.lock 108 | 109 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 110 | __pypackages__/ 111 | 112 | # Celery stuff 113 | celerybeat-schedule 114 | celerybeat.pid 115 | 116 | # SageMath parsed files 117 | *.sage.py 118 | 119 | # Environments 120 | .env 121 | .venv 122 | env/ 123 | venv/ 124 | ENV/ 125 | env.bak/ 126 | venv.bak/ 127 | 128 | # Spyder project settings 129 | .spyderproject 130 | .spyproject 131 | 132 | # Rope project settings 133 | .ropeproject 134 | *~ 135 | *.pyc 136 | *~ 137 | config.mk 138 | config.cmake 139 | Win32 140 | *.dir 141 | perf 142 | *.wasm 143 | .emscripten 144 | 145 | ## IOS 146 | DerivedData/ 147 | 148 | ## Java 149 | *.class 150 | jvm/*/target/ 151 | jvm/*/*/target/ 152 | jvm/native/*/generated 153 | jvm/native/src/main/native/org_apache_tvm_native_c_api.h 154 | *.worksheet 155 | *.idea 156 | *.iml 157 | *.classpath 158 | *.project 159 | *.settings 160 | */node_modules/ 161 | 162 | ## Various settings 163 | *.pbxuser 164 | !default.pbxuser 165 | *.mode1v3 166 | !default.mode1v3 167 | *.mode2v3 168 | !default.mode2v3 169 | *.perspectivev3 170 | !default.perspectivev3 171 | xcuserdata/ 172 | .pkl_memoize_* 173 | 174 | .emscripten* 175 | .m2 176 | 177 | # Compiled Dynamic libraries 178 | *.so 179 | *.dylib 180 | *.dll 181 | 182 | # Compiled Object files 183 | *.slo 184 | *.lo 185 | *.o 186 | *.obj 187 | 188 | # Precompiled Headers 189 | *.gch 190 | *.pch 191 | 192 | # Compiled Static libraries 193 | *.lai 194 | *.la 195 | *.a 196 | *.lib 197 | 198 | # Executables 199 | *.exe 200 | *.out 201 | *.app 202 | 203 | ## Other 204 | *.moved-aside 205 | *.xccheckout 206 | *.xcscmblueprint 207 | .DS_Store 208 | tags 209 | cscope* 210 | *.lock 211 | 212 | # vim temporary files 213 | *.swp 214 | *.swo 215 | 216 | # TVM generated code 217 | perf 218 | .bash_history 219 | # *.json 220 | *.params 221 | *.ro 222 | *.onnx 223 | *.h5 224 | synset.txt 225 | cat.jpg 226 | cat.png 227 | docs.tgz 228 | cat.png 229 | *.mlmodel 230 | tvm_u.* 231 | tvm_t.* 232 | # Mac OS X 233 | .DS_Store 234 | 235 | # Jetbrain 236 | .idea 237 | .ipython 238 | .jupyter 239 | .nv 240 | .pylint.d 241 | .python_history 242 | .pytest_cache 243 | .local 244 | cmake-build-debug 245 | 246 | # Visual Studio 247 | .vs 248 | 249 | # Visual Studio Code 250 | .vscode 251 | 252 | # tmp file 253 | .nfs* 254 | 255 | # keys 256 | *.pem 257 | *.p12 258 | *.pfx 259 | *.cer 260 | *.crt 261 | *.der 262 | 263 | # patch sentinel 264 | patched.txt 265 | 266 | # Python type checking 267 | .mypy_cache/ 268 | .pyre/ 269 | 270 | # pipenv files 271 | Pipfile 272 | Pipfile.lock 273 | 274 | # conda package artifacts 275 | conda/Dockerfile.cuda* 276 | conda/pkg 277 | .node_repl_history 278 | # nix files 279 | .envrc 280 | *.nix 281 | 282 | # Docker files 283 | .sudo_as_admin_successful 284 | 285 | # Downloaded models/datasets 286 | .tvm_test_data 287 | .dgl 288 | .caffe2 289 | 290 | # Local docs build 291 | _docs/ 292 | jvm/target 293 | .config/configstore/ 294 | .ci-py-scripts/ 295 | 296 | # Generated Hexagon files 297 | src/runtime/hexagon/rpc/hexagon_rpc.h 298 | src/runtime/hexagon/rpc/hexagon_rpc_skel.c 299 | src/runtime/hexagon/rpc/hexagon_rpc_stub.c 300 | 301 | # Local tvm-site checkout 302 | tvm-site/ 303 | 304 | # Generated docs files 305 | gallery/how_to/work_with_microtvm/micro_tvmc.py 306 | 307 | # Test sample data files 308 | !tests/python/ci/sample_prs/*.json 309 | 310 | # Used in CI to communicate between Python and Jenkins 311 | .docker-image-names/ 312 | 313 | # Printed TIR code on disk 314 | *.tir 315 | 316 | # GDB history file 317 | .gdb_history 318 | 319 | 3rdparty 320 | dist 321 | tvm_home 322 | node_modules 323 | lib 324 | .parcel-cache 325 | /test-results/ 326 | /playwright-report/ 327 | /blob-report/ 328 | /playwright/.cache/ 329 | -------------------------------------------------------------------------------- /src/retriever.ts: -------------------------------------------------------------------------------- 1 | import * as Overleaf from "./overleaf/retriever"; 2 | import { CallerType, Scope, ToolType } from "./enum"; 3 | import { Tool } from "./tool"; 4 | import { isOverleafDocument } from "./util"; 5 | 6 | export * as Overleaf from "./overleaf/retriever"; 7 | 8 | export const getSelectedText = (parameters: {}): string => { 9 | const selection = window.getSelection(); 10 | if (!selection) { 11 | return ""; 12 | } 13 | return selection.toString(); 14 | }; 15 | 16 | export const getPageContext = (): string => { 17 | const pageTitle = document.title; 18 | const metaDescription = document.querySelector("meta[name='description']"); 19 | const pageDescription = metaDescription ? metaDescription.getAttribute("content") : "No description available"; 20 | 21 | let context: any = { 22 | "url": window.location.href, 23 | "title": pageTitle, 24 | "description": pageDescription, 25 | } 26 | if (isOverleafDocument()) { 27 | context = { 28 | ...context, 29 | ...Overleaf.getEditorContext() 30 | } 31 | } 32 | if (document) { 33 | context = { 34 | ...context, 35 | content: document.body.innerText || "", 36 | } 37 | } 38 | return JSON.stringify(context); 39 | }; 40 | 41 | export async function getCalendarEvents(parameters: { token?: string }) { 42 | let { token } = parameters; 43 | 44 | if (!token) { 45 | // try to get token by using Chrome Identity API 46 | try { 47 | const authResult = await chrome.identity.getAuthToken({ 48 | interactive: true, 49 | scopes: ["https://www.googleapis.com/auth/calendar.events.readonly"], 50 | }); 51 | token = authResult.token; 52 | } catch (e) { 53 | throw new Error( 54 | "getCalendarEvents: `token` must be specified in parameters or `identity` permission must be added to the extension manifest.\n" + 55 | e 56 | ); 57 | } 58 | } 59 | 60 | try { 61 | // API URL to fetch calendar events 62 | const url = 63 | "https://www.googleapis.com/calendar/v3/calendars/primary/events?maxResults=10&orderBy=startTime&singleEvents=true"; 64 | 65 | // Fetch the events from the user's primary Google Calendar 66 | const response = await fetch(url, { 67 | method: "GET", 68 | headers: { 69 | Authorization: `Bearer ${token}`, 70 | "Content-Type": "application/json", 71 | }, 72 | }); 73 | 74 | if (!response.ok) { 75 | throw new Error("Failed to fetch calendar events"); 76 | } 77 | 78 | const events = await response.json(); 79 | 80 | // Process and display events in popup (this is a basic example) 81 | if (events.items) { 82 | events.items.forEach((event: any) => { 83 | const eventElement = document.createElement("div"); 84 | eventElement.textContent = `${event.summary} - ${event.start.dateTime || event.start.date}`; 85 | document.body.appendChild(eventElement); 86 | }); 87 | } else { 88 | document.body.textContent = "No upcoming events found."; 89 | } 90 | } catch (error) { 91 | console.error("Error fetching calendar events:", error); 92 | document.body.textContent = "Error fetching events"; 93 | } 94 | } 95 | 96 | export const retrievers: Record = { 97 | getSelectedText: { 98 | name: "getSelectedText", 99 | displayName: "Get Selected Text", 100 | description: 101 | "Get the user's current selected text content on the document.", 102 | schema: { 103 | type: "function", 104 | function: { 105 | name: "getSelectedText", 106 | description: 107 | "getSelectedText() -> str - Get the user's current selected text content on the document, no parameter is needed.\\n\\n Returns:\\n str: The user's current selected text content on the document.", 108 | parameters: { type: "object", properties: {}, required: [] }, 109 | }, 110 | }, 111 | type: ToolType.Retriever, 112 | scope: Scope.Any, 113 | caller: CallerType.Any, 114 | implementation: getSelectedText, 115 | }, 116 | getPageContext: { 117 | name: "getPageContext", 118 | displayName: "Get Page Context", 119 | description: "Get context information regarding the current page which the user is currently viewing.", 120 | schema: { 121 | type: "function", 122 | function: { 123 | name: "getPageContext", 124 | description: 125 | "getPageContext() -> str - Get context information regarding the current page which the user is currently viewing.\n\n Returns:\n str: The entire text content of the webpage.", 126 | parameters: { type: "object", properties: {}, required: [] }, 127 | }, 128 | }, 129 | type: ToolType.Retriever, 130 | scope: Scope.Any, 131 | caller: CallerType.ContentScript, 132 | implementation: getPageContext, 133 | }, 134 | getGoogleCalendarEvents: { 135 | name: "getGoogleCalendarEvents", 136 | displayName: "Get Google Calendar Events", 137 | description: 138 | "Fetch the user's upcoming events from their primary Google Calendar.", 139 | schema: { 140 | type: "function", 141 | function: { 142 | name: "getGoogleCalendarEvents", 143 | description: 144 | "getGoogleCalendarEvents(token: string) - Fetches up to 10 upcoming events from the user's Google Calendar.\n\n Returns:\n Array: List of upcoming events with event details.", 145 | parameters: { 146 | type: "object", 147 | properties: {}, 148 | required: [], 149 | }, 150 | }, 151 | }, 152 | type: ToolType.Retriever, 153 | scope: Scope.Any, 154 | caller: CallerType.Any, 155 | implementation: getCalendarEvents, 156 | }, 157 | }; 158 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import { CallerType, Scope, ToolType } from "./enum"; 2 | import { Tool } from "./tool"; 3 | import { isOverleafDocument } from "./util"; 4 | 5 | import * as Overleaf from "./overleaf/action"; 6 | 7 | export * as Overleaf from "./overleaf/action"; 8 | 9 | export const replaceSelectedText = (parameters: { 10 | newText: string | string[]; 11 | }): void => { 12 | const { newText } = parameters; 13 | const selection = window.getSelection(); 14 | if (!newText || !selection) { 15 | return; 16 | } 17 | if (selection.rangeCount > 0) { 18 | const range = selection.getRangeAt(0); 19 | range.deleteContents(); 20 | if (Array.isArray(newText)) { 21 | const fragment = document.createDocumentFragment(); 22 | newText.forEach((text) => 23 | fragment.appendChild(document.createTextNode(text)) 24 | ); 25 | range.insertNode(fragment); 26 | } else { 27 | range.insertNode(document.createTextNode(newText)); 28 | } 29 | selection.removeAllRanges(); 30 | } 31 | }; 32 | 33 | export const insertText = (parameters: { 34 | textToInsert: string; 35 | position: "beginning" | "end" | "cursor"; 36 | }): void => { 37 | if (isOverleafDocument()) { 38 | return Overleaf.insertText(parameters); 39 | } else { 40 | throw new Error("Action is not implemented"); 41 | } 42 | }; 43 | 44 | export async function createGoogleCalendarEvent(parameters: { 45 | token?: string; 46 | summary: string; 47 | location?: string; 48 | description?: string; 49 | startDateTime: string; 50 | endDateTime: string; 51 | timeZone?: string; 52 | }) { 53 | let { token } = parameters; 54 | 55 | if (!token) { 56 | // try to get token by using Chrome Identity API 57 | console.log( 58 | "`token` not specified, trying retrieving through Google identity API OAuth flow..." 59 | ); 60 | try { 61 | const authResult = await chrome.identity.getAuthToken({ 62 | interactive: true, 63 | scopes: ["https://www.googleapis.com/auth/calendar.events"], 64 | }); 65 | token = authResult.token; 66 | } catch (e) { 67 | throw new Error( 68 | "createGoogleCalendarEvent: `token` must be specified in parameters or `identity` permission must be added to the extension manifest.\n" + 69 | e 70 | ); 71 | } 72 | } 73 | 74 | try { 75 | const timeZone = 76 | parameters.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone; 77 | 78 | // Define the event payload (this structure follows Google Calendar API requirements) 79 | const event = { 80 | summary: parameters.summary, 81 | location: parameters.location || "", 82 | description: parameters.description || "", 83 | start: { 84 | dateTime: parameters.startDateTime, 85 | timeZone, 86 | }, 87 | end: { 88 | dateTime: parameters.endDateTime, 89 | timeZone, 90 | }, 91 | }; 92 | 93 | // Make a POST request to insert the event 94 | const response = await fetch( 95 | "https://www.googleapis.com/calendar/v3/calendars/primary/events", 96 | { 97 | method: "POST", 98 | headers: { 99 | Authorization: `Bearer ${token}`, 100 | "Content-Type": "application/json", 101 | }, 102 | body: JSON.stringify(event), 103 | } 104 | ); 105 | 106 | if (!response.ok) { 107 | throw new Error("Failed to create calendar event"); 108 | } 109 | 110 | const newEvent = await response.json(); 111 | console.log("Event created:", newEvent); 112 | 113 | return { status: "success", event: newEvent }; 114 | } catch (error) { 115 | console.error("Error creating calendar event:", error); 116 | } 117 | } 118 | 119 | export const actions: Record = { 120 | replaceSelectedText: { 121 | name: "replaceSelectedText", 122 | displayName: "Replace Selected Text", 123 | description: 124 | "Replace the user's current selected text content on the document with new text content.", 125 | schema: { 126 | type: "function", 127 | function: { 128 | name: "replaceSelectedText", 129 | description: 130 | "replaceSelectedText(newText: str) - Replace the user's current selected text content on the document with new text content.\\n\\n Args:\\n newText (str): New text content to replace the user's current selected text content.", 131 | parameters: { 132 | type: "object", 133 | properties: { 134 | newText: { type: "string" }, 135 | }, 136 | required: ["newText"], 137 | }, 138 | }, 139 | }, 140 | type: ToolType.Action, 141 | scope: [Scope.Overleaf], 142 | caller: CallerType.ContentScript, 143 | implementation: replaceSelectedText, 144 | }, 145 | insertText: { 146 | name: "insertText", 147 | displayName: "Insert Text", 148 | description: 149 | "Insert the specified text at a given position relative to the document: at the beginning, end, or cursor position.", 150 | schema: { 151 | type: "function", 152 | function: { 153 | name: "insertText", 154 | description: 155 | "insertText(parameters: { textToInsert: str, position: 'beginning' | 'end' | 'cursor' }) - Insert text into the document at the specified position.\\n\\n Args:\\n textToInsert (str): The text content to be inserted.\\n position ('beginning' | 'end' | 'cursor'): Where to insert the text (beginning of the document, end of the document, or at the cursor position).", 156 | parameters: { 157 | type: "object", 158 | properties: { 159 | textToInsert: { type: "string" }, 160 | position: { 161 | type: "string", 162 | enum: ["beginning", "end", "cursor"], 163 | }, 164 | }, 165 | required: ["textToInsert", "position"], 166 | }, 167 | }, 168 | }, 169 | type: ToolType.Action, 170 | scope: [Scope.Overleaf], 171 | caller: CallerType.ContentScript, 172 | implementation: insertText, 173 | }, 174 | createGoogleCalendarEvent: { 175 | name: "createGoogleCalendarEvent", 176 | displayName: "Create Google Calendar Event", 177 | description: "Create a new event in the user's primary Google Calendar.", 178 | schema: { 179 | type: "function", 180 | function: { 181 | name: "createGoogleCalendarEvent", 182 | description: 183 | "createGoogleCalendarEvent(summary: string, startDateTime: string, endDateTime: string, location?: string, description?: string, timeZone?: string) - Creates a new event in the user's Google Calendar.\n\n Args:\n summary (str): Title of the event.\n startDateTime (str): Start date and time of the event (ISO 8601 format).\n endDateTime (str): End date and time of the event (ISO 8601 format).\n location (str, optional): Location of the event.\n description (str, optional): Description of the event.\n timeZone (str, optional): The timezone of the event.", 184 | parameters: { 185 | type: "object", 186 | properties: { 187 | summary: { type: "string" }, 188 | location: { type: "string", nullable: true }, 189 | description: { type: "string", nullable: true }, 190 | startDateTime: { type: "string" }, 191 | endDateTime: { type: "string" }, 192 | timeZone: { type: "string", nullable: true }, 193 | }, 194 | required: ["summary", "startDateTime", "endDateTime"], 195 | }, 196 | }, 197 | }, 198 | type: ToolType.Action, 199 | scope: Scope.Any, 200 | caller: CallerType.Any, 201 | implementation: createGoogleCalendarEvent, 202 | }, 203 | }; 204 | --------------------------------------------------------------------------------