├── .github ├── FUNDING.yml ├── requirements-test.txt ├── local.sh ├── requirements-docs.txt ├── .markdown-link-check.json └── workflows │ ├── pyright.yml │ ├── test.yml │ └── docs.yml ├── .docs ├── tsconfig.json ├── src │ ├── assets │ │ └── talon.png │ ├── env.d.ts │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── guides │ │ │ ├── quickstart.mdx │ │ │ └── customizing.mdx │ │ │ ├── index.md │ │ │ └── reference │ │ │ ├── gpt.mdx │ │ │ └── copilot.mdx │ └── components │ │ └── LocalFile.astro ├── video_thumbnail.jpg ├── usage-examples │ ├── bullets.gif │ ├── table.gif │ └── examples.md ├── .gitignore ├── package.json ├── public │ └── favicon.svg ├── astro.config.mjs └── README.md ├── GPT ├── lists │ ├── modelThread.talon-list │ ├── model.talon-list │ ├── customPrompt.talon-list.example │ ├── modelSource.talon-list │ ├── modelDestination.talon-list │ └── staticPrompt.talon-list ├── gpt-confirmation-gui.talon ├── gpt-shell.talon ├── gpt-with-context.talon ├── gpt-cursorless.talon ├── gpt.talon ├── readme.md └── gpt.py ├── Images ├── ai-images.talon └── ai-images.py ├── .gitignore ├── .vscode ├── settings.json └── extensions.json ├── .editorconfig ├── lib ├── modelTypes.py ├── styles.css ├── pureHelpers.py ├── modelState.py ├── a11yHelpers.py ├── modelConfirmationGUI.py ├── talonSettings.py ├── HTMLBuilder.py └── modelHelpers.py ├── .test └── unit_test.py ├── pyproject.toml ├── LICENSE ├── talon-ai-settings.talon.example ├── copilot ├── codeium.talon ├── continue.talon ├── copilot.py ├── copilot.talon └── README.md ├── models.json.example ├── .pre-commit-config.yaml ├── CONTRIBUTING.md └── readme.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [c-loftus] 2 | -------------------------------------------------------------------------------- /.github/requirements-test.txt: -------------------------------------------------------------------------------- 1 | pytest==8.2.1 2 | -------------------------------------------------------------------------------- /.docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strictest" 3 | } 4 | -------------------------------------------------------------------------------- /GPT/lists/modelThread.talon-list: -------------------------------------------------------------------------------- 1 | list: user.modelThread 2 | - 3 | and: continueLast 4 | -------------------------------------------------------------------------------- /.docs/src/assets/talon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Loftus/talon-ai-tools/HEAD/.docs/src/assets/talon.png -------------------------------------------------------------------------------- /.docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /.docs/video_thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Loftus/talon-ai-tools/HEAD/.docs/video_thumbnail.jpg -------------------------------------------------------------------------------- /.github/local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | gh extension install https://github.com/nektos/gh-act 3 | cd .. 4 | gh act 5 | -------------------------------------------------------------------------------- /.docs/usage-examples/bullets.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Loftus/talon-ai-tools/HEAD/.docs/usage-examples/bullets.gif -------------------------------------------------------------------------------- /.docs/usage-examples/table.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/C-Loftus/talon-ai-tools/HEAD/.docs/usage-examples/table.gif -------------------------------------------------------------------------------- /.github/requirements-docs.txt: -------------------------------------------------------------------------------- 1 | myst_parser >=1,<2 2 | sphinx >=5,<8 3 | sphinx_rtd_theme >=1.2,<2 4 | talondoc==1.0.0 5 | -------------------------------------------------------------------------------- /Images/ai-images.talon: -------------------------------------------------------------------------------- 1 | # Generate an image using the openai API 2 | image generate $: user.image_generate(text) 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | gpt-settings.talon 2 | # TalonDoc 3 | /docs/_build 4 | .ruff_cache/ 5 | .mypy_cache/ 6 | .pytest_cache/ 7 | __pycache__/ 8 | 9 | node_modules/ 10 | models.json 11 | -------------------------------------------------------------------------------- /.github/.markdown-link-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": "10s", 3 | "retryOn429": true, 4 | "retryCount": 5, 5 | "fallbackRetryDelay": "30s", 6 | "aliveStatusCodes": [0, 200, 206, 403] 7 | } 8 | -------------------------------------------------------------------------------- /.docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from "astro:content"; 2 | import { docsSchema } from "@astrojs/starlight/schema"; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /.docs/usage-examples/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | ## Format Text Into a Markdown Table 4 | 5 | ![Format raw text into a markdown table](./table.gif) 6 | 7 | ## Format Text Into a Bullet Point List 8 | 9 | ![Format raw text into bullet points](./bullets.gif) 10 | -------------------------------------------------------------------------------- /GPT/lists/model.talon-list: -------------------------------------------------------------------------------- 1 | list: user.model 2 | - 3 | 4 | # The default model name 5 | model: model 6 | # OpenAI models 7 | four o mini: gpt-4o-mini 8 | four o: gpt-4o 9 | 10 | # If using user.model_endpoint = "llm", run `llm models` and add any models you want to use here. 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.formatOnSave": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": "always" 6 | }, 7 | "editor.defaultFormatter": "charliermarsh.ruff" 8 | }, 9 | "python.analysis.typeCheckingMode": "standard" 10 | } 11 | -------------------------------------------------------------------------------- /.docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.github/workflows/pyright.yml: -------------------------------------------------------------------------------- 1 | name: Pyright Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | pyright: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: jakebailey/pyright-action@v2 14 | with: 15 | version: 1.1.398 16 | python-version: 3.11 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_size = 4 9 | indent_style = space 10 | max_line_length = 88 11 | trim_trailing_whitespace = true 12 | 13 | [*.{md,yaml,yml}] 14 | indent_size = 2 15 | 16 | [Makefile] 17 | indent_style = tab 18 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python", 4 | "ms-python.vscode-pylance", 5 | "charliermarsh.ruff", 6 | "astro-build.astro-vscode", 7 | "unifiedjs.vscode-mdx", 8 | "mrob95.vscode-talonscript", 9 | "AndreasArvidsson.andreas-talon", 10 | "pokey.command-server" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/modelTypes.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, NotRequired, TypedDict 2 | 3 | 4 | class GPTMessageItem(TypedDict): 5 | type: Literal["text", "image_url"] 6 | text: NotRequired[str] 7 | image_url: NotRequired[dict[Literal["url"], str]] 8 | 9 | 10 | class GPTMessage(TypedDict): 11 | role: Literal["user", "system", "assistant"] 12 | content: list[GPTMessageItem] 13 | -------------------------------------------------------------------------------- /lib/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #2e3440; 3 | margin: 0; 4 | padding: 0; 5 | font-family: "Ubuntu Mono", monospace; 6 | } 7 | .container { 8 | max-width: 800px; 9 | margin: 0 auto; 10 | padding: 20px; 11 | box-sizing: border-box; 12 | } 13 | h1, 14 | p, 15 | h2, 16 | h3, 17 | ul, 18 | ol, 19 | table, 20 | a { 21 | color: #eceff4; 22 | margin: 20px 0; 23 | } 24 | img { 25 | max-width: 100%; 26 | height: auto; 27 | display: block; 28 | margin: 20px auto; 29 | } 30 | -------------------------------------------------------------------------------- /.docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro check && astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/check": "^0.8.2", 14 | "@astrojs/starlight": "^0.25.1", 15 | "astro": "^4.10.2", 16 | "sharp": "^0.32.6", 17 | "typescript": "^5.5.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GPT/lists/customPrompt.talon-list.example: -------------------------------------------------------------------------------- 1 | # Copy this file into your user directory and add your own custom prompts 2 | # Any prompts in this list are automatically added into the capture 3 | # and thus can be used like normal alongside all of the other model commands 4 | 5 | list: user.customPrompt 6 | - 7 | 8 | # Example of a custom prompt that is unique to a user's personal workflow 9 | 10 | check language: I am learning a new foreign language. Check the grammar of what I have written and return feedback in English with references to what I wrote. 11 | -------------------------------------------------------------------------------- /GPT/lists/modelSource.talon-list: -------------------------------------------------------------------------------- 1 | list: user.modelSource 2 | - 3 | 4 | # A list of sources from which the model can take in data to process 5 | 6 | # Apply the prompt to the text on the clipboard 7 | clip: clipboard 8 | 9 | # Apply the prompt to the text in the context 10 | context: context 11 | 12 | # Apply the prompt to the currently selected text 13 | this: selection 14 | 15 | # Apply the prompt to the previously returned GPT response 16 | response: gptResponse 17 | 18 | # Apply the prompt to the last phrase typed by Talon via a user's dictation 19 | last: lastTalonDictation 20 | -------------------------------------------------------------------------------- /GPT/gpt-confirmation-gui.talon: -------------------------------------------------------------------------------- 1 | tag: user.model_window_open 2 | - 3 | 4 | # Confirm and paste the output of the model 5 | ^paste response$: user.confirmation_gui_paste() 6 | 7 | # Confirm and paste the output of the model selected 8 | ^chain response$: 9 | user.confirmation_gui_paste() 10 | user.gpt_select_last() 11 | 12 | ^copy response$: user.confirmation_gui_copy() 13 | ^pass response to context$: user.confirmation_gui_pass_context() 14 | 15 | # Deny the output of the model and discard it 16 | ^discard response$: user.confirmation_gui_close() 17 | 18 | ^{user.model} toggle window$: user.confirmation_gui_close() 19 | -------------------------------------------------------------------------------- /.docs/src/components/LocalFile.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { readFile } from "fs/promises"; 3 | import { Code } from "@astrojs/starlight/components"; 4 | const { data } = Astro.props; // Path to a file in this repo relative to this file, e.g. "../../../GPT/lists/customPrompt.talon-list.example" 5 | const fileContent = await readFile(new URL(data, import.meta.url), "utf-8"); 6 | // Function to extract the basename from a path 7 | const getBasename = (path: string) => { 8 | return path.split("/").filter(Boolean).pop() || "Index"; 9 | }; 10 | const basename = getBasename(data); 11 | --- 12 | 13 | 14 | -------------------------------------------------------------------------------- /GPT/gpt-shell.talon: -------------------------------------------------------------------------------- 1 | mode: command 2 | - 3 | 4 | # Generate a shell command by specifying what you want it to do 5 | # You have to explicitly confirm the output of the model before 6 | # it is pasted so nothing is accidentally executed 7 | {user.model} [{user.modelThread}] shell $: 8 | result = user.gpt_generate_shell(user.text, model, modelThread or "") 9 | user.confirmation_gui_append(result) 10 | 11 | # Generate a SQL command by specifying what you want it to return 12 | {user.model} [{user.modelThread}] (sequel | sql) $: 13 | result = user.gpt_generate_sql(user.text, model, modelThread or "") 14 | user.confirmation_gui_append(result) 15 | -------------------------------------------------------------------------------- /.docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GPT/gpt-with-context.talon: -------------------------------------------------------------------------------- 1 | # All of the following commands allow the user to store or manipulate context 2 | # `context` represents additional info that can persist across conversations 3 | 4 | # Passes a model source to a model destination unchanged; useful for debugging, 5 | # passing around context, or chaining the responses of previous prompts 6 | # If the source is omitted, default to selected text; If the destination is omitted default to paste 7 | # Example: `model pass this to context` 8 | # Example: `model pass context to clip` 9 | # Example: `model pass clip to this` 10 | {user.model} pass ({user.modelSource} | {user.modelDestination} | {user.modelSource} {user.modelDestination})$: 11 | user.gpt_pass(modelSource or "", modelDestination or "") 12 | 13 | # Clear the context stored in the model 14 | {user.model} clear context: user.gpt_clear_context() 15 | -------------------------------------------------------------------------------- /.docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import starlight from "@astrojs/starlight"; 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | site: "https://colton.place/talon-ai-tools", 7 | base: "talon-ai-tools", 8 | integrations: [ 9 | starlight({ 10 | title: "talon-ai-tools Docs", 11 | social: { 12 | github: "https://github.com/C-Loftus/talon-ai-tools", 13 | }, 14 | sidebar: [ 15 | { 16 | label: "Guides", 17 | autogenerate: { directory: "guides" }, 18 | }, 19 | { 20 | label: "Reference", 21 | autogenerate: { directory: "reference" }, 22 | }, 23 | ], 24 | }), 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python 3.11 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.11" 22 | cache: "pip" 23 | cache-dependency-path: | 24 | ./.tests/requirements-dev.txt 25 | 26 | # - name: Install system dependencies 27 | # run: | 28 | # sudo apt-get update 29 | 30 | - name: Install Python dependencies 31 | run: /usr/bin/python3 -m pip install -r ./.github/requirements-test.txt 32 | 33 | - name: Run tests 34 | run: /usr/bin/python3 -m pytest ./.test/ -vvvv -rP 35 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Astro Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout your repository using git 22 | uses: actions/checkout@v4 23 | - name: Install, build, and upload your site 24 | uses: withastro/action@v2 25 | with: 26 | path: .docs/ # The root location of your Astro project inside the repository. (optional) 27 | 28 | deploy: 29 | needs: build 30 | runs-on: ubuntu-latest 31 | if: github.ref == 'refs/heads/main' 32 | environment: 33 | name: github-pages 34 | url: ${{ steps.deployment.outputs.page_url }} 35 | steps: 36 | - name: Deploy to GitHub Pages 37 | id: deployment 38 | uses: actions/deploy-pages@v4 39 | -------------------------------------------------------------------------------- /.test/unit_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # We are using a separate Python interpreter to test. Talon's Python env sets up packages differently 4 | # so we want to just manually add the library to the path. 5 | 6 | sys.path.append(".") 7 | 8 | from lib.pureHelpers import strip_markdown 9 | 10 | 11 | def test_strip_markdown(): 12 | markdown = """ 13 | ```python 14 | print("hello") 15 | ``` 16 | """ 17 | assert strip_markdown(markdown) == 'print("hello")' 18 | 19 | markdown = """ 20 | ```sh 21 | echo "hello" 22 | ``` 23 | """ 24 | assert strip_markdown(markdown) == 'echo "hello"' 25 | 26 | markdown = """ 27 | ```bash 28 | echo "hello" 29 | ``` 30 | """ 31 | assert strip_markdown(markdown) == 'echo "hello"' 32 | 33 | # Unclear if this is a case we even care about 34 | # markdown = """ 35 | # ```markdown 36 | # # Test 37 | # ```rust 38 | # println!("hello"); 39 | # ``` 40 | # ``` 41 | # """ 42 | # assert strip_markdown(markdown) == '# Test\n```rust\nprintln!("hello");```' 43 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | 2 | [project] 3 | name = "talon-ai-tools" 4 | version = "1.0.0" 5 | description = "Use Talon with AI tools to speed up your development." 6 | license = { file = 'LICENSE' } 7 | authors = [ 8 | { name = "Colton Loftus", email = "c-loftus@users.noreply.github.com" }, 9 | ] 10 | readme = "README.md" 11 | keywords = ["talon", "GPT", "OpenAI"] 12 | classifiers = [ 13 | "License :: OSI Approved :: MIT License", 14 | "Programming Language :: Python :: 3.11", 15 | ] 16 | 17 | [tool.black] 18 | target-version = ['py311'] 19 | 20 | [tool.isort] 21 | profile = 'black' 22 | 23 | [tool.pytest.ini_options] 24 | pythonpath = [".", "test/stubs"] 25 | 26 | [tool.pyright] 27 | pythonVersion = "3.11" 28 | # Talon classes don't use self so ignore the associated errors 29 | reportSelfClsParameterName = false 30 | reportGeneralTypeIssues = false 31 | # Imgui functions return an object that can't be statically resolved 32 | reportFunctionMemberAccess = false 33 | # Talon can't be installed in CI so ignore source errors 34 | reportMissingModuleSource = false 35 | reportMissingImports = false 36 | -------------------------------------------------------------------------------- /GPT/lists/modelDestination.talon-list: -------------------------------------------------------------------------------- 1 | list: user.modelDestination 2 | - 3 | 4 | # A list of adjectives/adverbs describing how the result of the GPT query should be returned 5 | 6 | # paste in place 7 | to this: paste 8 | 9 | # Paste above the current selection 10 | above: above 11 | 12 | # Past below the current selection 13 | below: below 14 | 15 | # Instead of pasting, return the result to the clipboard 16 | to clip: clipboard 17 | 18 | # Instead of pasting, add the result to the context 19 | to context: context 20 | 21 | # Instead of pasting, add the result to a new context 22 | to new context: newContext 23 | 24 | # Instead of pasting, append the result to the clipboard 25 | to after clip: appendClipboard 26 | 27 | # Open the result in the browser 28 | to browser: browser 29 | 30 | # Speak the result with TTS 31 | to speech: textToSpeech 32 | 33 | # Output the result in a talon imgui window 34 | to window: window 35 | 36 | # Select the response after insertion so you can apply subsequent prompts on it 37 | chain: chain 38 | 39 | # Insert the response as a snippet with placeholders (only works in vscode) 40 | snip: snip 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colton Loftus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.docs/src/content/docs/guides/quickstart.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quickstart 3 | description: Set up talon-ai-tools in under 5 min 4 | sidebar: 5 | order: 1 6 | --- 7 | 8 | :::note 9 | 10 | This assumes you have Talon installed. If you are new to Talon check out the Quickstart on [https://talon.wiki/](https://talon.wiki/) 11 | 12 | ::: 13 | 14 | import { Steps } from "@astrojs/starlight/components"; 15 | 16 | 17 | 18 | 1. Download or `git clone` this repo into your Talon user directory. 19 | 1. [Obtain an OpenAI API key](https://platform.openai.com/signup). 20 | 21 | 1. Create a Python file anywhere in your Talon user directory. 22 | 1. Set the key environment variable within the Python file 23 | 24 | :::caution 25 | 26 | Make sure you do not push the key to a public repo! 27 | 28 | ::: 29 | 30 | ```python 31 | # Example of setting the environment variable 32 | import os 33 | 34 | os.environ["OPENAI_API_KEY"] = "YOUR-KEY-HERE" 35 | ``` 36 | 37 | 38 | 39 | ### Quickstart Video 40 | 41 | [![Talon-AI-Tools Quickstart](../../../../video_thumbnail.jpg)](https://www.youtube.com/watch?v=FctiTs6D2tM "Talon-AI-Tools Quickstart") 42 | -------------------------------------------------------------------------------- /talon-ai-settings.talon.example: -------------------------------------------------------------------------------- 1 | # This is an example settings file. 2 | # To make changes, copy this into your user directory and remove the .example extension 3 | 4 | settings(): 5 | # Set the endpoint that the model requests should go to. 6 | # Works with any API with the same schema as OpenAI's (i.e. Azure, llamafiles, etc.) 7 | # Set to "llm" to use the local llm cli tool as a helper for routing all your model requests 8 | # user.model_endpoint = "https://api.openai.com/v1/chat/completions" 9 | 10 | # If using user.model_endpoint = "llm" and the llm binary is not found on Talon's PATH, you can 11 | # specify it directly: 12 | # user.model_llm_path = "/path/to/llm" 13 | 14 | # user.model_system_prompt = "You are an assistant helping an office worker to be more productive." 15 | 16 | # Change to the model of your choice 17 | # user.model_default = 'gpt-4o' 18 | 19 | # Increase the window width. 20 | # user.model_window_char_width = 120 21 | 22 | # Disable notifications for nominal behavior. Useful on Windows where notifications are 23 | # throttled. 24 | # user.model_verbose_notifications = false 25 | 26 | # Use codeium instead of Github Copilot 27 | # tag(): user.codeium 28 | -------------------------------------------------------------------------------- /lib/pureHelpers.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import re 3 | 4 | """ 5 | Everything in this file are functions which do not interact with the 6 | system via talon or the model via API calls 7 | """ 8 | 9 | 10 | def remove_wrapper(text: str): 11 | """Remove the string wrapper from the str representation of a command""" 12 | # different command wrapper for Linux. 13 | if platform.system() == "Linux": 14 | regex = r"^.*?'(.*?)'.*?$" 15 | else: 16 | # TODO condense these regexes. Hard to test between platforms 17 | # since the wrapper is slightly different 18 | regex = r'[^"]+"([^"]+)"' 19 | match = re.search(regex, text) 20 | return match.group(1) if match else text 21 | 22 | 23 | def strip_markdown(text: str) -> str: 24 | """Remove markdown from the text""" 25 | # Define the regex pattern to match the markdown code block syntax 26 | 27 | # Don't allow spaces after language identifier? 28 | # strict_pattern = r"```[a-zA-Z]*\n([\s\S]*?)```" 29 | 30 | pattern = r"```[a-zA-Z]*\s*\n([\s\S]*?)```" 31 | 32 | # Use re.sub to replace the matched pattern with the code inside the block 33 | stripped_code = re.sub(pattern, r"\1", text) 34 | 35 | return stripped_code.strip() 36 | -------------------------------------------------------------------------------- /copilot/codeium.talon: -------------------------------------------------------------------------------- 1 | app: vscode 2 | # You must manually enable this. By default we use Github Copilot for pilot commands 3 | tag: user.codeium 4 | - 5 | 6 | ## Commands for https://codeium.com/ extension 7 | 8 | pilot (previous | last): user.vscode("editor.action.inlineSuggest.showPrevious") 9 | pilot next: user.vscode("editor.action.inlineSuggest.showNext") 10 | 11 | pilot yes: user.vscode("editor.action.inlineSuggest.commit") 12 | pilot nope: user.vscode("editor.action.inlineSuggest.undo") 13 | 14 | pilot chat []: 15 | user.vscode("codeium.openChatView") 16 | sleep(2) 17 | user.paste(user.prose or "") 18 | 19 | pilot toggle: user.vscode("codeium.toggleEnabledForCurrentLanguage") 20 | 21 | # Submit the request from within a codeium request window 22 | pilot submit: key(ctrl-shift-enter) 23 | 24 | pilot make []: 25 | user.vscode("codeium.openCodeiumCommand") 26 | sleep(0.7) 27 | user.paste(user.prose or "") 28 | 29 | pilot search: user.vscode("codeium.openSearchView") 30 | pilot explain: user.vscode("codeium.explainCodeBlock") 31 | pilot debug: user.vscode("codeium.explainProblem") 32 | pilot editor: user.vscode("codeium.openChatInPane") 33 | 34 | pilot cancel: user.vscode("editor.action.inlineSuggest.hide") 35 | pilot refactor: user.vscode("codeium.refactorCodeBlock") 36 | -------------------------------------------------------------------------------- /.docs/src/content/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: talon-ai-tools 3 | description: Control your favorite large language model across your entire desktop 4 | template: splash 5 | hero: 6 | tagline: Control your favorite large language models across your entire desktop 7 | image: 8 | file: ../../assets/talon.png 9 | actions: 10 | - text: Quickstart 11 | link: /talon-ai-tools/guides/quickstart/ 12 | icon: right-arrow 13 | variant: primary 14 | - text: View on Github 15 | link: https://github.com/C-Loftus/talon-ai-tools 16 | icon: external 17 | --- 18 | 19 | 20 | 37 | -------------------------------------------------------------------------------- /models.json.example: -------------------------------------------------------------------------------- 1 | // This is an example model configuration file. 2 | // To use: copy settings into models.json in the same directory. Remove any comments. 3 | [ 4 | { 5 | // The name used in user.model_default or the right-hand-side of model.talon-list. 6 | "name": "gpt-4o-mini", 7 | // Additional JSON merged into the OpenAI Chat Completions API request if user.model_endpoint is not "llm". 8 | "api_options": { 9 | // The temperature of the model. Higher values make the model more creative. 10 | "temperature": 0.7 11 | } 12 | }, 13 | { 14 | "name": "gemini-2.0-flash-search", 15 | // The model ID used in the LLM CLI tool or the API. If unspecified, defaults to the name. 16 | "model_id": "gemini-2.0-flash", 17 | // Model-specific system prompt (overrides user.model_system_prompt). 18 | "system_prompt": "You are a sassy but helpful assistant.", 19 | // Options passed to the LLM CLI tool if user.model_endpoint = "llm". Run `llm models --options` to see all options for each model. 20 | "llm_options": { 21 | // Enables a model-specific setting, namely, the Gemini search feature, which allows the model to search the web for information. 22 | "google_search": true 23 | } 24 | } 25 | ] 26 | -------------------------------------------------------------------------------- /Images/ai-images.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | 3 | import requests 4 | from talon import Module 5 | 6 | from ..lib.modelHelpers import get_token, notify 7 | 8 | mod = Module() 9 | 10 | 11 | @mod.action_class 12 | class Actions: 13 | def image_generate(prompt: str): 14 | """Generate an image from the provided text""" 15 | 16 | url = "https://api.openai.com/v1/images/generations" 17 | TOKEN = get_token() 18 | headers = {"Content-Type": "application/json"} 19 | # If the model endpoint is Azure, we need to use a different header 20 | if "azure.com" in url: 21 | headers["api-key"] = TOKEN 22 | # otherwise default to the standard header format for openai 23 | else: 24 | headers["Authorization"] = f"Bearer {TOKEN}" 25 | data = { 26 | "model": "dall-e-3", 27 | "prompt": prompt, 28 | "n": 1, 29 | "size": "1024x1024", 30 | } 31 | 32 | response = requests.post(url, headers=headers, json=data) 33 | 34 | match response.status_code: 35 | case 200: 36 | response_dict = response.json() 37 | image_url = response_dict["data"][0]["url"] 38 | # TODO choose whether to save the image, save the url, or paste the image into the current window 39 | webbrowser.open(image_url) 40 | case _: 41 | print(response.json()) 42 | notify("Error generating image") 43 | raise Exception("Error generating image") 44 | -------------------------------------------------------------------------------- /lib/modelState.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar 2 | 3 | from talon import actions 4 | 5 | from .modelTypes import GPTMessageItem 6 | 7 | 8 | class GPTState: 9 | text_to_confirm: ClassVar[str] = "" 10 | last_response: ClassVar[str] = "" 11 | last_was_pasted: ClassVar[bool] = False 12 | context: ClassVar[list[GPTMessageItem]] = [] 13 | debug_enabled: ClassVar[bool] = False 14 | 15 | @classmethod 16 | def start_debug(cls): 17 | """Enable debug printing""" 18 | GPTState.debug_enabled = True 19 | actions.app.notify("Enabled debug logging") 20 | 21 | @classmethod 22 | def stop_debug(cls): 23 | """Disable debug printing""" 24 | GPTState.debug_enabled = False 25 | actions.app.notify("Disabled debug logging") 26 | 27 | @classmethod 28 | def clear_context(cls): 29 | """Reset the stored context""" 30 | cls.context = [] 31 | actions.app.notify("Cleared user context") 32 | 33 | @classmethod 34 | def push_context(cls, context: GPTMessageItem): 35 | """Add the selected text to the stored context""" 36 | if context.get("type") != "text": 37 | actions.app.notify( 38 | "Only text can be added to context. To add images, try using a prompt to summarize or otherwise describe the image to the context." 39 | ) 40 | return 41 | cls.context += [context] 42 | actions.app.notify("Appended user context") 43 | 44 | @classmethod 45 | def reset_all(cls): 46 | cls.text_to_confirm = "" 47 | cls.last_response = "" 48 | cls.last_was_pasted = False 49 | cls.context = [] 50 | -------------------------------------------------------------------------------- /copilot/continue.talon: -------------------------------------------------------------------------------- 1 | app: vscode 2 | - 3 | 4 | ## Commands to interact with https://continue.dev/ extension 5 | 6 | tin you new: user.vscode("continue.newSession") 7 | tin you file select: user.vscode("continue.selectFilesAsContext") 8 | tin you history: user.vscode("continue.viewHistory") 9 | tin you (accept | yes): user.vscode("continue.acceptDiff") 10 | tin you reject: user.vscode("continue.rejectDiff") 11 | tin you toggle fullscreen: user.vscode("continue.toggleFullScreen") 12 | tin you cancel: key("escape") 13 | tin you debug terminal: user.vscode("continue.debugTerminal") 14 | tin you share: user.vscode("continue.shareSession") 15 | tin you select files: user.vscode("continue.selectFilesAsContext") 16 | tin you add : 17 | user.cursorless_ide_command("continue.focusContinueInput", cursorless_target) 18 | tin you edit : 19 | user.cursorless_ide_command("continue.quickEdit", cursorless_target) 20 | 21 | tin you dock : 22 | user.cursorless_ide_command("continue.writeDocstringForCode", cursorless_target) 23 | 24 | tin you comment : 25 | user.cursorless_ide_command("continue.writeCommentsForCode", cursorless_target) 26 | 27 | tin you optimize : 28 | user.cursorless_ide_command("continue.optimizeCode", cursorless_target) 29 | 30 | tin you fix code : 31 | user.cursorless_ide_command("continue.fixCode", cursorless_target) 32 | 33 | tin you fix grammar : 34 | user.cursorless_ide_command("continue.fixGrammar", cursorless_target) 35 | 36 | bar tin you: user.vscode("continue.continueGUIView.focus") 37 | -------------------------------------------------------------------------------- /GPT/gpt-cursorless.talon: -------------------------------------------------------------------------------- 1 | mode: command 2 | tag: user.cursorless 3 | - 4 | 5 | # Apply a prompt to any text, and output it any target 6 | # Paste it to the cursor if no target is specified 7 | {user.model} [{user.modelThread}] []$: 8 | text = user.cursorless_get_text_list(cursorless_target) 9 | result = user.gpt_apply_prompt_for_cursorless(user.modelSimplePrompt, model, modelThread or "", text) 10 | default_destination = user.cursorless_create_destination(cursorless_target) 11 | user.cursorless_insert(cursorless_destination or default_destination, result) 12 | 13 | # Add the text from a cursorless target to your context 14 | {user.model} pass to context$: 15 | text = user.cursorless_get_text_list(cursorless_target) 16 | user.gpt_push_context(text) 17 | 18 | # Add the text from a cursorless target to a new context 19 | {user.model} pass to new context$: 20 | text = user.cursorless_get_text_list(cursorless_target) 21 | user.gpt_clear_context() 22 | user.gpt_push_context(text) 23 | 24 | # Applies an arbitrary prompt from the clipboard to a cursorless target. 25 | # Useful for applying complex/custom prompts that need to be drafted in a text editor. 26 | {user.model} [{user.modelThread}] apply [from] clip $: 27 | prompt = clip.text() 28 | text = user.cursorless_get_text_list(cursorless_target) 29 | result = user.gpt_apply_prompt_for_cursorless(prompt, model, modelThread or "", text) 30 | default_destination = user.cursorless_create_destination(cursorless_target) 31 | user.cursorless_insert(default_destination, result) 32 | -------------------------------------------------------------------------------- /GPT/gpt.talon: -------------------------------------------------------------------------------- 1 | # Shows the list of available prompts 2 | {user.model} help$: user.gpt_help() 3 | 4 | # Runs a model prompt on the selected text; inserts with paste by default 5 | # Example: `model fix grammar below` -> Fixes the grammar of the selected text and pastes below 6 | # Example: `model explain this` -> Explains the selected text and pastes in place 7 | # Example: `model fix grammar clip to browser` -> Fixes the grammar of the text on the clipboard and opens in browser` 8 | # Example: `four o mini explain this` -> Uses gpt-4o-mini model to explain the selected text 9 | # Example: `model and explain this` -> Explains the selected text and pastes in place, continuing the most recent conversation thread 10 | {user.model} [{user.modelThread}] [{user.modelSource}] [{user.modelDestination}]$: 11 | user.gpt_apply_prompt(modelPrompt, model, modelThread or "", modelSource or "", modelDestination or "") 12 | 13 | # Select the last GPT response so you can edit it further 14 | {user.model} take response: user.gpt_select_last() 15 | 16 | # Applies an arbitrary prompt from the clipboard to selected text and pastes the result. 17 | # Useful for applying complex/custom prompts that need to be drafted in a text editor. 18 | {user.model} [{user.modelThread}] apply [from] clip$: 19 | prompt = clip.text() 20 | text = edit.selected_text() 21 | result = user.gpt_apply_prompt(prompt, model, modelThread or "", text) 22 | user.paste(result) 23 | 24 | # Reformat the last dictation with additional context or formatting instructions 25 | {user.model} [{user.modelThread}] [nope] that was $: 26 | result = user.gpt_reformat_last(text, model, modelThread or "") 27 | user.paste(result) 28 | 29 | # Enable debug logging so you can more details about messages being sent 30 | {user.model} start debug: user.gpt_start_debug() 31 | 32 | # Disable debug logging 33 | {user.model} stop debug: user.gpt_stop_debug() 34 | -------------------------------------------------------------------------------- /lib/a11yHelpers.py: -------------------------------------------------------------------------------- 1 | # Adapted from MIT licensed code here: 2 | # https://github.com/phillco/talon-axkit/blob/main/dictation/dictation_context.py 3 | 4 | from talon import Context, Module, ui 5 | 6 | ctx = Context() 7 | ctx.matches = r""" 8 | os: mac 9 | app: code 10 | """ 11 | mod = Module() 12 | 13 | 14 | @mod.action_class 15 | class GenericActions: 16 | def a11y_get_context_of_editor(selection: str) -> str: 17 | """Creates a `AccessibilityContext` representing the state of the document""" 18 | # If we aren't in a valid editor on a valid platform, just return an empty string 19 | return "" 20 | 21 | 22 | @ctx.action_class("user") 23 | class Actions: 24 | def a11y_get_context_of_editor(selection: str) -> str: 25 | """Creates a `AccessibilityContext` representing the state of the document""" 26 | 27 | try: 28 | el = ui.focused_element() 29 | # Weird edge case where certain MacOS systems do not have a focused element. If this is the case 30 | # just return an empty string as the context. We can't get anything better 31 | except RuntimeError: 32 | return "" 33 | 34 | if not el or not el.attrs: # type: ignore Talon doesn't properly type hint this 35 | return "" 36 | 37 | # Only return extra context if we are in an editor 38 | if not el.get("AXRoleDescription") == "editor": # type: ignore Talon doesn't properly type hint this 39 | return "" 40 | 41 | context = el.get("AXValue") # type: ignore 42 | 43 | if context is None: 44 | print( 45 | "GPT Warning: Tried to get a11y info but no accessibility information present in the focused element" 46 | ) 47 | # This probably means that a11y support is not enabled (we can't get more than just the current 48 | # selection)or that we selected the entire document 49 | if not context or context == selection: 50 | return "" 51 | 52 | return context 53 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | minimum_pre_commit_version: "3.2.0" 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v6.0.0 5 | hooks: 6 | - id: check-added-large-files 7 | - id: check-case-conflict 8 | - id: check-executables-have-shebangs 9 | - id: check-merge-conflict 10 | - id: check-shebang-scripts-are-executable 11 | - id: check-symlinks 12 | - id: destroyed-symlinks 13 | - id: detect-private-key 14 | - id: fix-byte-order-marker 15 | # NB. To avoid sometimes needing multiple runs, we need: 16 | # - trailing-whitespace BEFORE end-of-file-fixer, 17 | # otherwise trailing newline followed by whitespace, "\n ", 18 | # will need multiple runs. 19 | # - end-of-file-fixer BEFORE mixed-line-ending, 20 | # otherwise a file with CRLF line endings but missing a trailing 21 | # newline will need multiple runs. 22 | - id: trailing-whitespace 23 | - id: end-of-file-fixer 24 | - id: mixed-line-ending 25 | - repo: https://github.com/pre-commit/mirrors-prettier 26 | rev: "v4.0.0-alpha.8" 27 | hooks: 28 | - id: prettier 29 | - repo: https://github.com/ikamensh/flynt/ 30 | rev: "1.0.6" 31 | hooks: 32 | - id: flynt 33 | - repo: https://github.com/pycqa/isort 34 | rev: 7.0.0 35 | hooks: 36 | - id: isort 37 | - repo: https://github.com/psf/black-pre-commit-mirror 38 | rev: 25.11.0 39 | hooks: 40 | - id: black 41 | - repo: https://github.com/Lucas-C/pre-commit-hooks 42 | rev: v1.5.5 43 | hooks: 44 | - id: remove-tabs 45 | types: [file] 46 | files: \.talon$ 47 | args: ["--whitespaces-count=4"] 48 | - repo: https://github.com/wenkokke/talonfmt 49 | rev: 1.10.2 50 | hooks: 51 | - id: talonfmt 52 | args: ["--in-place"] 53 | 54 | - repo: https://github.com/tcort/markdown-link-check 55 | rev: v3.14.2 56 | hooks: 57 | - id: markdown-link-check 58 | # We have to treat 403 as success, because some sites have ddos protection. So we just need to manually check those 59 | args: [-q, -c .github/.markdown-link-check.json] 60 | -------------------------------------------------------------------------------- /copilot/copilot.py: -------------------------------------------------------------------------------- 1 | from talon import Context, Module, actions 2 | 3 | mod = Module() 4 | ctx = Context() 5 | 6 | ctx.matches = r""" 7 | app: vscode 8 | """ 9 | 10 | mod.list( 11 | "copilot_slash_command", "Slash commands that can be used with copilot, e.g. /test" 12 | ) 13 | ctx.lists["user.copilot_slash_command"] = { 14 | "test": "tests", 15 | "dock": "doc", 16 | "fix": "fix", 17 | "explain": "explain", 18 | "change": "", 19 | } 20 | 21 | mod.list("makeshift_destination", "Cursorless makeshift destination") 22 | ctx.lists["user.makeshift_destination"] = { 23 | "to": "clearAndSetSelection", 24 | "after": "editNewLineAfter", 25 | "before": "editNewLineBefore", 26 | } 27 | 28 | mod.tag("codeium", desc="Enable codeium for copilot integration") 29 | 30 | 31 | @mod.action_class 32 | class Actions: 33 | def copilot_inline_chat(copilot_slash_command: str = "", prose: str = ""): 34 | """Initiate copilot inline chat session""" 35 | actions.user.vscode("inlineChat.start") 36 | has_content = copilot_slash_command or prose 37 | if has_content: 38 | actions.sleep("50ms") 39 | if copilot_slash_command: 40 | actions.insert(f"/{copilot_slash_command} ") 41 | if prose: 42 | actions.insert(prose) 43 | if has_content: 44 | actions.key("enter") 45 | 46 | def copilot_chat(prose: str): 47 | """Initiate copilot chat session""" 48 | actions.user.vscode("workbench.panel.chat.view.copilot.focus") 49 | if prose: 50 | actions.sleep("50ms") 51 | actions.insert(prose) 52 | actions.key("enter") 53 | 54 | def copilot_focus_code_block(index: int): 55 | """Bring a copilot chat suggestion to the cursor""" 56 | actions.user.vscode("workbench.panel.chat.view.copilot.focus") 57 | action = ( 58 | "workbench.action.chat.previousCodeBlock" 59 | if index < 0 60 | else "workbench.action.chat.nextCodeBlock" 61 | ) 62 | count = index + 1 if index >= 0 else abs(index) 63 | for _ in range(count): 64 | actions.user.vscode(action) 65 | 66 | def copilot_bring_code_block(index: int) -> None: 67 | """Bring a copilot chat suggestion to the cursor""" 68 | actions.user.copilot_focus_code_block(index) 69 | actions.user.vscode("workbench.action.chat.insertCodeBlock") 70 | -------------------------------------------------------------------------------- /lib/modelConfirmationGUI.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | 3 | from talon import Context, Module, actions, clip, imgui, settings 4 | 5 | from .modelHelpers import GPTState, notify 6 | 7 | mod = Module() 8 | ctx = Context() 9 | 10 | 11 | @imgui.open() 12 | def confirmation_gui(gui: imgui.GUI): 13 | gui.text("Confirm model output before pasting") 14 | gui.line() 15 | gui.spacer() 16 | 17 | for paragraph in GPTState.text_to_confirm.split("\n"): 18 | for line in textwrap.wrap( 19 | paragraph, settings.get("user.model_window_char_width") 20 | ): 21 | gui.text(line) 22 | 23 | gui.spacer() 24 | if gui.button("Copy response"): 25 | actions.user.confirmation_gui_copy() 26 | 27 | gui.spacer() 28 | if gui.button("Paste response"): 29 | actions.user.confirmation_gui_paste() 30 | 31 | gui.spacer() 32 | if gui.button("Discard response"): 33 | actions.user.confirmation_gui_close() 34 | 35 | 36 | @mod.action_class 37 | class UserActions: 38 | def confirmation_gui_append(model_output: str): 39 | """Add text to the confirmation gui""" 40 | ctx.tags = ["user.model_window_open"] 41 | GPTState.text_to_confirm = model_output 42 | confirmation_gui.show() 43 | 44 | def confirmation_gui_close(): 45 | """Close the model output without pasting it""" 46 | GPTState.text_to_confirm = "" 47 | confirmation_gui.hide() 48 | ctx.tags = [] 49 | 50 | def confirmation_gui_pass_context(): 51 | """Add the model output to the context""" 52 | actions.user.gpt_push_context(GPTState.text_to_confirm) 53 | GPTState.text_to_confirm = "" 54 | actions.user.confirmation_gui_close() 55 | 56 | def confirmation_gui_copy(): 57 | """Copy the model output to the clipboard""" 58 | clip.set_text(GPTState.text_to_confirm) 59 | GPTState.text_to_confirm = "" 60 | 61 | actions.user.confirmation_gui_close() 62 | 63 | def confirmation_gui_paste(): 64 | """Paste the model output""" 65 | 66 | if not GPTState.text_to_confirm: 67 | notify("GPT error: No text in confirmation GUI to paste") 68 | else: 69 | actions.user.paste(GPTState.text_to_confirm) 70 | GPTState.last_response = GPTState.text_to_confirm 71 | GPTState.last_was_pasted = True 72 | GPTState.text_to_confirm = "" 73 | actions.user.confirmation_gui_close() 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in contributing to `talon-ai-tools`. We are happy to receive new contributions. If you have any questions about contributing, feel free to ask. 4 | 5 | ## Contributing Python 6 | 7 | - For all PRs please describe the purpose of the PR and give an example of any non-trivial functionality. 8 | - Do not use external Python dependencies with `pip install` 9 | - This pollutes the Talon Python interpreter and can have unintended side effects 10 | - Prefix all actions exposed from a module with `gpt_` or another appropriate prefix to distinguish it in the global namespace 11 | - i.e. Write a function like `actions.user.gpt_query()` not `actions.user.query()` 12 | - Do not expose any actions globally with talon action classes if they don't need to be called from `.talon` files 13 | - Comment all functions with doctrings and type annotations for arguments 14 | - Any code that is intended to be reused within the repo should be placed within the `lib/` folder 15 | 16 | ## Contributing Talonscript 17 | 18 | - Talon commands should follow a grammar that is intuitive for users or mimics the grammar of existing tools like Cursorless 19 | - Prefix all commands with an appropriate prefix like `model` or `pilot` to make sure they aren't accidentally run 20 | - Duplicate commands following a similar syntax should be condensed into captures or lists _only_ if it improves readability for users or significantly reduces duplicate code for maintainers 21 | - It is better to duplicate code to make a command explicit and readable rather than overly condense the command within a fancy talon capture. 22 | - The commands inside a `.talon` file, to the extent possible, should be intuitive for a new user to understand. Numerous talon lists chained together requires a user to introspect code and adds friction to discoverability. 23 | - It is best to use talon commands that are very expliciti, even if overly verbose, and let the user customize them if desired. Brevity risks confusion. 24 | - Any command that requires a capture or list should include a brief inline comment above it showing an example of the command and a one line description of what it does 25 | - If a list is needed, `.talon-list` files should be used instead of defining lists inside Python 26 | - This makes it easier to customize list items by overriding the `.talon-list` and not needing to fork the Python. 27 | - Avoid any configuration pattern that requires a user to fork this repository 28 | -------------------------------------------------------------------------------- /copilot/copilot.talon: -------------------------------------------------------------------------------- 1 | app: vscode 2 | not tag: user.codeium 3 | - 4 | pilot jest: user.vscode("editor.action.inlineSuggest.trigger") 5 | pilot jest next: user.vscode("editor.action.inlineSuggest.showNext") 6 | pilot jest (previous | last): user.vscode("editor.action.inlineSuggest.showPrevious") 7 | pilot yes: user.vscode("editor.action.inlineSuggest.commit") 8 | pilot yes word: user.vscode("editor.action.inlineSuggest.acceptNextWord") 9 | pilot nope: user.vscode("editor.action.inlineSuggest.undo") 10 | pilot cancel: user.vscode("editor.action.inlineSuggest.hide") 11 | pilot block last: user.vscode("workbench.action.chat.previousCodeBlock") 12 | pilot block next: user.vscode("workbench.action.chat.nextCodeBlock") 13 | pilot new file : 14 | user.copilot_focus_code_block(cursorless_ordinal_or_last) 15 | user.vscode("workbench.action.chat.insertIntoNewFile") 16 | pilot copy : 17 | user.copilot_focus_code_block(cursorless_ordinal_or_last) 18 | edit.copy() 19 | pilot bring : 20 | user.copilot_bring_code_block(cursorless_ordinal_or_last) 21 | pilot bring {user.makeshift_destination} : 22 | user.cursorless_command(makeshift_destination, cursorless_target) 23 | user.copilot_bring_code_block(cursorless_ordinal_or_last) 24 | pilot chat []$: user.copilot_chat(prose or "") 25 | pilot {user.copilot_slash_command} [to ]$: 26 | user.cursorless_command("setSelection", cursorless_target) 27 | user.copilot_inline_chat(copilot_slash_command or "", prose or "") 28 | pilot make []: user.copilot_inline_chat("", prose or "") 29 | pilot chat new: user.vscode("workbench.action.chat.newChat") 30 | pilot chat open: user.vscode("workbench.action.chat.open") 31 | pilot attach: user.vscode("workbench.action.chat.attachFile") 32 | pilot [hunk] next: user.vscode("chatEditor.action.navigateNext") 33 | pilot [hunk] last: user.vscode("chatEditor.action.navigatePrevious") 34 | pilot hunk (accept | keep): user.vscode("chatEditor.action.acceptHunk") 35 | pilot file (accept | keep): user.vscode("chatEditor.action.accept") 36 | pilot all files (accept | keep): user.vscode("chatEditing.acceptAllFiles") 37 | pilot hunk (undo | reject): user.vscode("chatEditor.action.undoHunk") 38 | pilot file (undo | reject): user.vscode("chatEditor.action.reject") 39 | pilot all files (undo | reject): user.vscode("chatEditing.discardAllFiles") 40 | pilot chat undo: user.vscode("workbench.action.chat.undoEdit") 41 | -------------------------------------------------------------------------------- /.docs/src/content/docs/guides/customizing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Customizing behavior 3 | description: Customizing the model behavior 4 | --- 5 | 6 | talon-ai-tools can be configured by changing settings in any `.talon` file. You can copy any of the following settings, uncomment them, and change the values to customize which model you use or its runtime behavior. 7 | 8 | import LocalFile from "../../../components/LocalFile.astro"; 9 | 10 | 11 | 12 | ## Adding custom prompts 13 | 14 | You do not need to fork the repository to add your own custom prompts. Copy the file below, place it anywhere inside your talon `user/` directory and follow the pattern of the key value mapping. 15 | 16 | 17 | 18 | ## Advanced Customization 19 | 20 | ### Model-Specific Configuration with models.json 21 | 22 | For advanced configuration of specific models, you can create a `models.json` file in the root directory of the repository. Here's an example of what this file can contain: 23 | 24 | 25 | 26 | The configuration is automatically reloaded when the file changes, so you don't need to restart Talon after making changes. 27 | 28 | ### Configuring Model Name 29 | 30 | The word `model` is the default prefix before all LLM commands to prevent collisions with other Talon commands. However, you can change or override it. To do so just create another talon list with the same name and a higher specificity. Here is an example that you can copy and past into your own configuration files 31 | 32 | ```yml title="myCustomModelName.talon-list" 33 | list: user.model 34 | - 35 | 36 | # Whatever you say that matches the value on the left will be mapped to the word `model` 37 | # and thus allow any model commands to work as normal, but with a new prefix keyword 38 | 39 | my custom model name: model 40 | ``` 41 | 42 | ### Providing Custom User Context 43 | 44 | In case you want to provide additional context to the LLM, there is a hook that you can override in your own python code and anything that is returned will be sent with every request. Here is an example: 45 | 46 | ```py 47 | from talon import Context, Module, actions 48 | 49 | mod = Module() 50 | 51 | ctx = Context() 52 | 53 | 54 | @ctx.action_class("user") 55 | class UserActions: 56 | def gpt_additional_user_context(): 57 | """This is an override function that can be used to add additional context to the prompt""" 58 | result = actions.user.talon_get_active_context() 59 | return [ 60 | f"The following describes the currently focused application:\n\n{result}" 61 | ] 62 | ``` 63 | -------------------------------------------------------------------------------- /.docs/src/content/docs/reference/gpt.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: GPT Commands 3 | description: Run 4 | sidebar: 5 | order: 1 6 | --- 7 | 8 | import LocalFile from "../../../components/LocalFile.astro"; 9 | 10 | GPT commands make requests to an LLM endpoint. In our grammar, each GPT command is prefixed with the word `model`. A model command has 4 parts 11 | 12 | - `model` 13 | - A prompt name 14 | - A source _(optional; defaults to selected text)_ 15 | - A destination _(optional; defaults to paste)_ 16 | 17 | You can mix and match any combination of prompt, source, and destination. 18 | 19 | ## Examples 20 | 21 | | Command | Explanation | 22 | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- | 23 | | `model fix grammar` | Fix typos in the selected text and paste the response. `fix grammar` is the prompt name. | 24 | | `model fix grammar to clip` | Fix typos in the selected text and put the result on the clipboard. `to clip` is a destination. | 25 | | `model explain clip to window` | Explains the text on the clipboard and puts the response in a new browser window. `clip` is the source. `to window` is the destination. | 26 | 27 | ## Other model commands 28 | 29 | There are also a few other special commands like `model please` and `model ask` that don't use a prompt, but instead use the user's arbitrary natural language request. 30 | 31 | | Command | Description | Example | 32 | | --------------------- | ----------------------------------------- | ----------------------------------------- | 33 | | `model help` | Show the help menu with all the prompts | "model help" | 34 | | `model please ` | Say an arbitrary prompt and then apply it | "model please translate this to Japanese" | 35 | | `model ask ` | Ask a question to the model | "model ask what is the meaning of life" | 36 | 37 | ## All model options 38 | 39 | _The left is the spoken word, and the right is how talon interprets it_ 40 | 41 | ### Mappings of prompts to their spoken name 42 | 43 | 44 | 45 | ### Sources that the model can operate on 46 | 47 | 48 | 49 | ### Destinations for model insertions 50 | 51 | 52 | -------------------------------------------------------------------------------- /.docs/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 10 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 11 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics) 12 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) 13 | 14 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 15 | 16 | ## 🚀 Project Structure 17 | 18 | Inside of your Astro + Starlight project, you'll see the following folders and files: 19 | 20 | ``` 21 | . 22 | ├── public/ 23 | ├── src/ 24 | │ ├── assets/ 25 | │ ├── content/ 26 | │ │ ├── docs/ 27 | │ │ └── config.ts 28 | │ └── env.d.ts 29 | ├── astro.config.mjs 30 | ├── package.json 31 | └── tsconfig.json 32 | ``` 33 | 34 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 35 | 36 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 37 | 38 | Static assets, like favicons, can be placed in the `public/` directory. 39 | 40 | ## 🧞 Commands 41 | 42 | All commands are run from the root of the project, from a terminal: 43 | 44 | | Command | Action | 45 | | :------------------------ | :----------------------------------------------- | 46 | | `npm install` | Installs dependencies | 47 | | `npm run dev` | Starts local dev server at `localhost:4321` | 48 | | `npm run build` | Build your production site to `./dist/` | 49 | | `npm run preview` | Preview your build locally, before deploying | 50 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 51 | | `npm run astro -- --help` | Get help using the Astro CLI | 52 | 53 | ## 👀 Want to learn more? 54 | 55 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 56 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Talon-AI-Tools 2 | 3 | **Control large language models and AI tools through voice commands using the [Talon Voice](https://talon.wiki) dictation engine.** 4 | 5 | This functionality is especially helpful for users who: 6 | 7 | - want to quickly edit text and fix dictation errors 8 | - code by voice using tools like [Cursorless](https://www.cursorless.org/) 9 | - have health issues affecting their hands and want to reduce keyboard use 10 | - want to speed up their workflow and use AI commands across the entire desktop 11 | 12 | **Prompts and extends the following tools:** 13 | 14 | - Github Copilot 15 | - OpenAI API (with any GPT model) or [simonw/llm CLI](https://github.com/simonw/llm) for text generation and processing 16 | - Any OpenAI compatible model endpoint can be used (Azure, local llamafiles, etc) 17 | - OpenAI API for image recognition 18 | 19 | ## Setup: 20 | 21 | 1. Download or `git clone` this repo into your Talon user directory. 22 | 2. Choose one of the following three options to configure LLM access (unless you want to exclusively use this with Copilot): 23 | 24 | ### Option 1: Direct OpenAI API Access 25 | 26 | 1. [Obtain an OpenAI API key](https://platform.openai.com/signup). 27 | 2. Create a Python file anywhere in your Talon user directory. 28 | 3. Set the key environment variable within the Python file: 29 | 30 | > [!CAUTION] 31 | > Make sure you do not push the key to a public repo! 32 | 33 | ```python 34 | # Example of setting the environment variable 35 | import os 36 | 37 | os.environ["OPENAI_API_KEY"] = "YOUR-KEY-HERE" 38 | ``` 39 | 40 | ### Option 2 (recommended): simonw/llm CLI 41 | 42 | 1. Install [simonw/llm](https://github.com/simonw/llm#installation) and set up one or more models to use. 43 | 44 | > [!NOTE] 45 | > Run `llm keys set` with the name of the provider you wish to use to set the API key for your requests. i.e. `llm keys set openai`. 46 | 47 | 2. Add the following lines to your settings: 48 | 49 | ```talon 50 | user.model_endpoint = "llm" 51 | # If the llm binary is not found on Talon's PATH, uncomment and set: 52 | # user.model_llm_path = "/path/to/llm" 53 | ``` 54 | 55 | 3. Choose a model in settings: 56 | 57 | ```talon 58 | user.model_default = "claude-3.7-sonnet" # or whichever model you installed 59 | ``` 60 | 61 | 4. By default, all model interactions will be logged locally and viewable on your machine via `llm logs`. If you prefer, you can disable this with `llm logs off`. 62 | 63 | ### Option 3: Custom Endpoint URL 64 | 65 | 1. Add the following line to settings to use your preferred endpoint: 66 | 67 | ``` 68 | user.model_endpoint = "https://your-custom-endpoint.com/v1/chat/completions" 69 | ``` 70 | 71 | This works with any API that follows the OpenAI schema, including: 72 | 73 | - Azure OpenAI Service[] 74 | - Local LLM servers (e.g., llamafiles, ollama) 75 | - Self-hosted models with OpenAI-compatible wrappers 76 | 77 | ## Usage 78 | 79 | See the [GPT](./GPT/readme.md) or [Copilot](./copilot/README.md) folders for usage examples. 80 | 81 | ### Quickstart Video 82 | 83 | [![Talon-AI-Tools Quickstart](./.docs/video_thumbnail.jpg)](https://www.youtube.com/watch?v=FctiTs6D2tM "Talon-AI-Tools Quickstart") 84 | -------------------------------------------------------------------------------- /lib/talonSettings.py: -------------------------------------------------------------------------------- 1 | from talon import Context, Module 2 | 3 | mod = Module() 4 | ctx = Context() 5 | # Stores all our prompts that don't require arguments 6 | # (ie those that just take in the clipboard text) 7 | mod.list("staticPrompt", desc="GPT Prompts Without Dynamic Arguments") 8 | mod.list("customPrompt", desc="Custom user-defined GPT prompts") 9 | mod.list("modelPrompt", desc="GPT Prompts") 10 | mod.list("model", desc="The name of the model") 11 | mod.list("modelDestination", desc="What to do after returning the model response") 12 | mod.list("modelSource", desc="Where to get the text from for the GPT") 13 | mod.list("modelThread", desc="Which conversation thread to continue") 14 | 15 | 16 | # model prompts can be either static and predefined by this repo or custom outside of it 17 | @mod.capture( 18 | rule="{user.staticPrompt} | {user.customPrompt} | (please ) | (ask )" 19 | ) 20 | def modelPrompt(matched_prompt) -> str: 21 | return str(matched_prompt) 22 | 23 | 24 | # model prompts can be either static and predefined by this repo or custom outside of it 25 | @mod.capture(rule="{user.staticPrompt} | {user.customPrompt}") 26 | def modelSimplePrompt(matched_prompt) -> str: 27 | return str(matched_prompt) 28 | 29 | 30 | mod.setting( 31 | "model_default", 32 | type=str, 33 | default="gpt-4o-mini", 34 | desc="The default model to use when no specific model is specified in the command", 35 | ) 36 | 37 | mod.setting( 38 | "openai_model", 39 | type=str, 40 | default="do_not_use", 41 | desc="DEPRECATED: Use model_default instead. This setting is maintained for backward compatibility only.", 42 | ) 43 | 44 | mod.setting( 45 | "model_temperature", 46 | type=float, 47 | default=-1.0, 48 | desc="DEPRECATED: Use llm_options or api_options in models.json instead.", 49 | ) 50 | 51 | mod.setting( 52 | "model_default_destination", 53 | type=str, 54 | default="paste", 55 | desc="The default insertion destination. This can be overridden contextually to provide application level defaults.", 56 | ) 57 | 58 | mod.setting( 59 | "model_endpoint", 60 | type=str, 61 | default="https://api.openai.com/v1/chat/completions", 62 | desc='The endpoint to send the model requests to. If "llm" is specified instead of a url, the llm CLI tool is used when routing all language model requests (see https://github.com/simonw/llm).', 63 | ) 64 | 65 | mod.setting( 66 | "model_llm_path", 67 | type=str, 68 | default="llm", 69 | desc='The path to the executable for the "llm" CLI tool. Only used if model_endpoint is set to "llm", signifying that you want to use "llm" as the manager for all your language model requests', 70 | ) 71 | 72 | mod.setting( 73 | "model_verbose_notifications", 74 | type=bool, 75 | default=True, 76 | desc="If true, show notifications when model starts and completes successfully.", 77 | ) 78 | 79 | mod.setting( 80 | "model_system_prompt", 81 | type=str, 82 | default="You are an assistant helping an office worker to be more productive. Output just the response to the request and no additional content. Do not generate any markdown formatting such as backticks for programming languages unless it is explicitly requested. If the user requests code generation, output just code and not additional natural language explanation.", 83 | desc="The default system prompt that informs the way the model should behave at a high level", 84 | ) 85 | 86 | 87 | mod.setting( 88 | "model_shell_default", 89 | type=str, 90 | default="bash", 91 | desc="The default shell for outputting model shell commands", 92 | ) 93 | 94 | mod.setting( 95 | "model_window_char_width", 96 | type=int, 97 | default=80, 98 | desc="The default window width (in characters) for showing model output", 99 | ) 100 | -------------------------------------------------------------------------------- /.docs/src/content/docs/reference/copilot.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Copilot Commands 3 | description: Interact with AI copilot tools 4 | --- 5 | 6 | Copilot commands interact with GitHub copilot. There are also commands for [Codeium](https://codeium.com/) and [Continue](https://www.continue.dev/) 7 | 8 | ### Inline chat / refactoring / code generation 9 | 10 | There are some commands to interact with the inline chat, leveraging Cursorless targets: 11 | 12 | | Command | Description | Example | 13 | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | 14 | | `"pilot change "` | selects target and opens inline chat to enable typing instructions for how Copilot should change the target. | `"pilot change funk air"` | 15 | | `"pilot change to "` | tells Copilot to apply the instructions in `` to the target. | `"pilot change funk air to remove arguments"`. | 16 | | `"pilot test "` | Asks copilot to generate a test for the target. | `"pilot test funk air"` | 17 | | `"pilot doc "` | Asks copilot to generate documentation for the target. | `"pilot doc funk air"` | 18 | | `"pilot fix "` | Tells copilot to fix the target. | `"pilot fix funk air"` | 19 | | `"pilot fix to "` | Tells copilot to fix the target using the instructions in ``. | `"pilot fix funk air to remove warnings"` | 20 | | `"pilot make "` | Tells copilot to generate code using the instructions in ``, at the current cursor position. | `"pilot make a function that returns the sum of two numbers"` | 21 | 22 | ### Chat sidebar 23 | 24 | There are some commands to interact with the chat sidebar: 25 | 26 | | Command | Description | Example | 27 | | --------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | 28 | | `"pilot chat"` | opens chat to enable typing | | 29 | | `"pilot chat hello world"` | opens chat, types "hello world" and hits enter | | 30 | | `"pilot bring "` | inserts nth code block in most recent chat response into your editor at cursor position | `"pilot bring first"`, `"pilot bring last"`, `"pilot bring second last"` | 31 | | `"pilot copy "` | copies nth code block in most recent chat response | `"pilot copy first"` | 32 | | `"pilot bring "` | inserts nth code block in most recent chat response into your editor at Cursorless destination | `"pilot bring first after state air"`, `"pilot bring first to line air"` | 33 | | `"pilot bring first to funk"` | replaces function containing your cursor with first code block in most recent chat response | 34 | 35 | ## Copilot command source 36 | 37 | import LocalFile from "../../../components/LocalFile.astro"; 38 | 39 | 40 | -------------------------------------------------------------------------------- /copilot/README.md: -------------------------------------------------------------------------------- 1 | # Copilot Interaction in VSCode 2 | 3 | This directory contains commands for interacting with [GitHub Copilot](https://github.com/features/copilot) and/or [Codeium](https://codeium.com/). The former is paid, the latter is a free alternative. 4 | 5 | The only setup step is to install the corresponding extension in VSCode. 6 | 7 | ## GitHub Copilot 8 | 9 | ### Inline chat / refactoring / code generation 10 | 11 | There are some commands to interact with the inline chat, leveraging Cursorless targets: 12 | 13 | | Command | Description | Example | 14 | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | 15 | | `"pilot change "` | selects target and opens inline chat to enable typing instructions for how Copilot should change the target. | `"pilot change funk air"` | 16 | | `"pilot change to "` | tells Copilot to apply the instructions in `` to the target. | `"pilot change funk air to remove arguments"`. | 17 | | `"pilot test "` | Asks copilot to generate a test for the target. | `"pilot test funk air"` | 18 | | `"pilot doc "` | Asks copilot to generate documentation for the target. | `"pilot doc funk air"` | 19 | | `"pilot fix "` | Tells copilot to fix the target. | `"pilot fix funk air"` | 20 | | `"pilot fix to "` | Tells copilot to fix the target using the instructions in ``. | `"pilot fix funk air to remove warnings"` | 21 | | `"pilot make "` | Tells copilot to generate code using the instructions in ``, at the current cursor position. | `"pilot make a function that returns the sum of two numbers"` | 22 | 23 | ### Chat sidebar 24 | 25 | There are some commands to interact with the chat sidebar: 26 | 27 | | Command | Description | Example | 28 | | --------------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | 29 | | `"pilot chat"` | opens chat to enable typing | | 30 | | `"pilot chat hello world"` | opens chat, types "hello world" and hits enter | | 31 | | `"pilot bring "` | inserts nth code block in most recent chat response into your editor at cursor position | `"pilot bring first"`, `"pilot bring last"`, `"pilot bring second last"` | 32 | | `"pilot copy "` | copies nth code block in most recent chat response | `"pilot copy first"` | 33 | | `"pilot bring "` | inserts nth code block in most recent chat response into your editor at Cursorless destination | `"pilot bring first after state air"`, `"pilot bring first to line air"` | 34 | | `"pilot bring first to funk"` | replaces function containing your cursor with first code block in most recent chat response | | 35 | 36 | ## Codeium 37 | 38 | Codeium commands are currently in active development and are likely to change. Please directly see [codeium.talon](./codeium.talon) for the latest commands. 39 | -------------------------------------------------------------------------------- /GPT/readme.md: -------------------------------------------------------------------------------- 1 | # GPT and LLM Interaction 2 | 3 | Query language models with voice commands. Helpful to automatically generate text, fix errors from dictation automatically, and generally speed up your Talon workflow. 4 | 5 | ## Help 6 | 7 | - See [the list of prompts](lists/staticPrompt.talon-list) for all the prompts that can be used with the `model` command. 8 | 9 | - See [the list of available models](lists/model.talon-list) that can be used to specify which model to use directly in the voice command (e.g., "four o mini explain this"). 10 | 11 | - See the [examples file](../.docs/usage-examples/examples.md) for gifs that show how to use the commands. 12 | 13 | - View the [docs](http://localhost:4321/talon-ai-tools/) for more detailed usage and help 14 | 15 | ## OpenAI API Pricing 16 | 17 | The OpenAI API that is used in this repo, through which you make queries to GPT 3.5 (the model used for ChatGPT), is not free. However it is extremely cheap and unless you are frequently processing large amounts of text, it will likely cost less than $1 per month. Most months I have spent less than $0.50 18 | 19 | ## Configuration 20 | 21 | To add additional prompts, copy the [Talon list for custom prompts](lists/customPrompt.talon-list.example) to anywhere in your user directory and add your desired prompts. These prompts will automatically be added into the `` capture. 22 | 23 | If you wish to change any configuration settings, copy the [example configuration file](../talon-ai-settings.talon.example) into your user directory and modify settings that you want to change. 24 | 25 | ### Model-Specific Configuration 26 | 27 | For advanced configuration of specific models, you can create a `models.json` file in the root directory of this repository. Copy `models.json.example` to `models.json` as a starting point, and then customize it to your needs. 28 | 29 | The `models.json` file allows you to define per-model settings including: 30 | 31 | - Model aliases (via `model_id`) 32 | - Model-specific system prompts 33 | - API options (like temperature, top_p, etc.) 34 | - LLM CLI options (used when `user.model_endpoint` is set to "llm") 35 | 36 | The configuration is automatically reloaded when the file changes, so you don't need to restart Talon after making changes. 37 | 38 | ### Global Settings 39 | 40 | | Setting | Default | Notes | 41 | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 42 | | user.model_default | `"gpt-4o-mini"` | The default model to use when no specific model is specified in the command. You can also specify a model directly in the voice command, e.g., "four o mini explain this" | 43 | | user.model_endpoint | `"https://api.openai.com/v1/chat/completions"` | Any OpenAI compatible endpoint address can be used (Azure, local llamafiles, etc) | 44 | | user.model_shell_default | `"bash"` | The default shell for `model shell` commands | 45 | | user.model_system_prompt | `"You are an assistant helping an office worker to be more productive. Output just the response to the request and no additional content. Do not generate any markdown formatting such as backticks for programming languages unless it is explicitly requested."` | The meta-prompt for how to respond to all prompts (can be overridden per-model in models.json) | 46 | -------------------------------------------------------------------------------- /lib/HTMLBuilder.py: -------------------------------------------------------------------------------- 1 | # Talon's imgui gui library is not accessible to screen readers. 2 | # By using HTML we can create temporary web pages that are accessible to screen readers. 3 | 4 | import enum 5 | import os 6 | import platform 7 | import tempfile 8 | import webbrowser 9 | 10 | 11 | def get_style(): 12 | # read in all the styles from a file ./styles.css 13 | style_path = os.path.join(os.path.dirname(__file__), "styles.css") 14 | with open(style_path, "r") as f: 15 | all_styles = f.read() 16 | 17 | return f""" 18 | 21 | """ 22 | 23 | 24 | class ARIARole(enum.Enum): 25 | MAIN = "main" 26 | BANNER = "banner" 27 | NAV = "navigation" 28 | FOOTER = "contentinfo" 29 | # TODO other roles? 30 | 31 | 32 | class Builder: 33 | """ 34 | Easily build HTML pages and add aria roles to elements 35 | in order to make them accessible to screen readers. 36 | """ 37 | 38 | def __init__(self): 39 | self.elements = [] 40 | self.doc_title = "Generated Help Page from Talon" 41 | 42 | def _flat_helper(self, text, tag, role=None): 43 | if role: 44 | self.elements.append(f"<{tag} role='{role.value}'>{text}") 45 | else: 46 | self.elements.append(f"<{tag}>{text}") 47 | 48 | def title(self, text): 49 | self.doc_title = text 50 | 51 | def h1(self, text, role=None): 52 | self._flat_helper(text, "h1", role) 53 | 54 | def h2(self, text, role=None): 55 | self._flat_helper(text, "h2", role) 56 | 57 | def h3(self, text, role=None): 58 | self._flat_helper(text, "h3", role) 59 | 60 | def p(self, text, role=None): 61 | self._flat_helper(text, "p", role) 62 | 63 | def a(self, text, href, role=None): 64 | self.elements.append( 65 | f"{text}" 66 | if role 67 | else f"{text}" 68 | ) 69 | 70 | def _li(self, text): 71 | self._flat_helper(text, "li") 72 | 73 | def ul(self, *text, role=None): 74 | self.elements.append(f"
    " if role else "
      ") 75 | 76 | for item in text: 77 | self._li(item) 78 | self.elements.append("
    ") 79 | 80 | def ol(self, *text, role=None): 81 | self.elements.append(f"
      " if role else "
        ") 82 | for item in text: 83 | self._li(item) 84 | self.elements.append("
      ") 85 | 86 | def base64_img(self, img, alt="", role=None): 87 | self.elements.append( 88 | f"{alt}" 89 | if role 90 | else f"{alt}" 91 | ) 92 | 93 | def start_table(self, headers, role=None): 94 | self.elements.append(f"" if role else "
      ") 95 | self.elements.append("") 96 | for header in headers: 97 | self.elements.append(f"") 98 | self.elements.append("") 99 | 100 | def add_row(self, cells): 101 | self.elements.append("") 102 | for cell in cells: 103 | self.elements.append(f"") 104 | self.elements.append("") 105 | 106 | def end_table(self): 107 | self.elements.append("
      {header}
      {cell}
      ") 108 | 109 | def render(self): 110 | html_content = "\n".join(self.elements) 111 | full_html = f""" 112 | 113 | 114 | 115 | 116 | 117 | {self.doc_title} 118 | {get_style()} 119 | 120 | 121 |
      122 | {html_content} 123 |
      124 | 125 | 126 | """ 127 | 128 | # If you are using a browser through a snap package on Linux you cannot 129 | # open many directories so we just default to the downloads folder since that is one we can use 130 | dir = None 131 | if platform.system() == "Linux": 132 | default = os.path.join(os.path.expanduser("~"), "Downloads") 133 | if os.path.exists(default): 134 | dir = default 135 | 136 | with tempfile.NamedTemporaryFile( 137 | mode="w+", suffix=".html", delete=False, encoding="utf-8", dir=dir 138 | ) as temp_file: 139 | temp_file.write(full_html) 140 | temp_file_path = temp_file.name 141 | webbrowser.open("file://" + os.path.abspath(temp_file_path)) 142 | 143 | 144 | # API Demo 145 | # builder = Builder() 146 | # builder.title("Generated Help Page from Talon") 147 | # builder.h1("Banner Heading", role=ARIARole.BANNER) 148 | # builder.h1("Header 1 for the page") 149 | # builder.h2("Header 2") 150 | # builder.h3("Smaller Header 3") 151 | # builder.ul("Bullet 1", "Bullet number two") 152 | # builder.p("This is a paragraph within the article", role=ARIARole.MAIN) 153 | # builder.h2("Navigation Heading", role=ARIARole.NAV) 154 | # builder.p("This is a paragraph within the article") 155 | # builder.ol("First element: Hello", "Second one: World") 156 | # builder.p("This is labeled as an aria footer within the article", role=ARIARole.FOOTER) 157 | # builder.render() 158 | -------------------------------------------------------------------------------- /GPT/lists/staticPrompt.talon-list: -------------------------------------------------------------------------------- 1 | list: user.staticPrompt 2 | - 3 | 4 | # Use static prompts as aliases for detailed instructions 5 | # Reduce verbosity and prevent the need to say the entire prompt each time time 6 | 7 | ## FIXES 8 | fix grammar formally: Fix any mistakes or irregularities in grammar, spelling, or formatting. Use a professional business tone. The text was created using voice dictation. Thus, there are likely to be issues regarding homophones and other misrecognitions. Do not change the original structure of the text. 9 | fix grammar: Fix any mistakes or irregularities in grammar, spelling, or formatting. The text was created using voice dictation. Thus, there are likely to be issues regarding homophones and other misrecognitions. Do not change the tone. Do not change the original structure of the text. 10 | fix syntax: Fix any syntax errors in this code selection. Do not change any behavior. 11 | 12 | ## FORMATTING 13 | format table: The following markdown text is raw data. There is no index. Return the text in a markdown table format. Each row has a new line in the original data. 14 | format bullets: Convert each paragraph into a heading with a series of bullet points underneath it. Each paragraph is separated by a new line. Separate paragraphs should not have combined bullet points. This should all be done in markdown syntax. If it is a small paragraph, then you can just leave it as a heading and not add bullet points. Do not reduce content, only reduce things that would be redundant. These bullet points should be in a useful format for notes for those who want to quickly look at it. If there is a citation in the markdown original, then keep the citation just at the top and not within every individual bullet point. 15 | format mermaid: Convert the following plain text into the text syntax for a mermaid diagram. 16 | format comment: Format the following text as a comment for the current programming language. Use the proper comment syntax for the current language. Split the comment into multiple lines if the lines are too long. 17 | group: Act as an organizer. The following text consists of various topics all put together. Please group these items into categories and label each category. Return just the results. 18 | join: Act as an editor. The following text is separated into multiple parts. Please group them together into one part maintaining the flow and meaning. Reorder in whatever way makes sense. Remove any redundant information. The result should be only one part with no additional structure. Return just the modified text. 19 | 20 | ## TEXT GENERATION 21 | explain: Explain this text in a way that is easier to understand for a layman without technical knowledge. 22 | summarize: Summarize this text into a format suitable for project notes. 23 | add context: Add additional text to the selected text that would be appropriate to the situation and add useful information. 24 | fit schema: The given text has a series of responses that need to be categorized. Each response has a key that needs to be mapped to a value. Infer the schema from the text unless it is given at the top of the text with prior examples. Return the key-value pairs in a JSON format unless you infer a different format. 25 | answer: Generate text that satisfies the question or request given in the input. 26 | shell: Generate a shell script that performs the following actions. Output only the command. Do not output any comments or explanations. Default to the bash shell unless otherwise specified. 27 | add emoji: Return the same exact text verbatim with the same formatting, but add emoji when appropriate in order to make the text fun and easier to understand. 28 | make softer: Act as an editor. I want you to make the following text softer in tone. Return just the modified text. 29 | make stronger: Act as an editor. I want you to make the following text stronger in tone. Return just the modified text. 30 | 31 | ## FILE CONVERSIONS 32 | convert to jason: Convert the following data into a JSON format. 33 | convert to markdown: Convert the following text into a markdown format. 34 | convert to python: Convert the following key-value pairs into the syntax for a Python dictionary. So you should serialize the key-value pairs into a native Python format. 35 | convert to sheet: Convert the following data into a CSV format. 36 | convert to yam: Convert the following data into a YAML format. 37 | 38 | ## CHECKERS 39 | describe code: Explain what the following code does in natural language at a high level without getting into the specifics of the syntax. 40 | check grammar: Check the grammar and formatting of the following text. Return a list of all potential errors. 41 | check spelling: Check the spelling of the following text. Return a list of all potential errors. 42 | check structure: Skim the structure and layout of the following text. Tell me if the structure and order of my writing are correct. If it is not correct or flows poorly, then tell me what might be wrong with it. If it is all correct, then say it looks good. 43 | 44 | ## TRANSLATIONS 45 | translate to english: Translate the following text into English. 46 | 47 | ## CODE GENERATION 48 | generate code: The following plaintext describes a process in code in the language that is specified by the system prompt. Please output the code necessary to do this. Return just code and not any natural language explanations. 49 | update comments: Act as a software engineer. The following code may be missing comments or the comments could be out of date. Please update the comments. If you are unsure how to comment something, ask a question in a comment instead. Return just the code and not any explanations. 50 | clean code: Act as a software engineer. Reduce any duplication in the selected code and improve it to be more idiomatic and clear for other users. However, do not change the behavior or functionality. Return just the code and not any explanations. 51 | improve semantics: The following is an HTML document. Keep the same structure and layout but if it is needed, change any elements to use proper semantic HTML and make sure it is implementing best practices for user accessibility. Output just the HTML and not any extra explanations. 52 | 53 | ## WRITING HELPERS 54 | add questions: Help me explore this question from multiple perspectives. For each perspective, ask follow-up questions and indicate what perspective is being taken. 55 | format outline: Create an outline that encapsulates the text below. Keep the number of sections between three and five to optimize for human working memory. Return just the outline. 56 | format prose: As an editor, format the following outline or summarization as prose. You can have headings and paragraphs. Avoid using bullet points. Reorder and add transitions as necessary to make the document flow. Return just the text. 57 | -------------------------------------------------------------------------------- /GPT/gpt.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Optional 3 | 4 | from talon import Module, actions, clip, settings 5 | 6 | from ..lib.HTMLBuilder import Builder 7 | from ..lib.modelConfirmationGUI import confirmation_gui 8 | from ..lib.modelHelpers import ( 9 | extract_message, 10 | format_clipboard, 11 | format_message, 12 | messages_to_string, 13 | notify, 14 | send_request, 15 | ) 16 | from ..lib.modelState import GPTState 17 | from ..lib.modelTypes import GPTMessageItem 18 | 19 | mod = Module() 20 | mod.tag( 21 | "model_window_open", 22 | desc="Tag for enabling the model window commands when the window is open", 23 | ) 24 | 25 | 26 | def gpt_query( 27 | prompt: GPTMessageItem, 28 | text_to_process: Optional[GPTMessageItem], 29 | model: str, 30 | thread: str, 31 | destination: str = "", 32 | ): 33 | """Send a prompt to the GPT API and return the response""" 34 | 35 | # Reset state before pasting 36 | GPTState.last_was_pasted = False 37 | 38 | response = send_request(prompt, text_to_process, model, thread, destination) 39 | GPTState.last_response = extract_message(response) 40 | return response 41 | 42 | 43 | @mod.action_class 44 | class UserActions: 45 | def gpt_generate_shell(text_to_process: str, model: str, thread: str) -> str: 46 | """Generate a shell command from a spoken instruction""" 47 | shell_name = settings.get("user.model_shell_default") 48 | if shell_name is None: 49 | raise Exception("GPT Error: Shell name is not set. Set it in the settings.") 50 | 51 | prompt = f""" 52 | Generate a {shell_name} shell command that will perform the given task. 53 | Only include the code. Do not include any comments, backticks, or natural language explanations. Do not output the shell name, only the code that is valid {shell_name}. 54 | Condense the code into a single line such that it can be ran in the terminal. 55 | """ 56 | 57 | result = gpt_query( 58 | format_message(prompt), format_message(text_to_process), model, thread 59 | ) 60 | return extract_message(result) 61 | 62 | def gpt_generate_sql(text_to_process: str, model: str, thread: str) -> str: 63 | """Generate a SQL query from a spoken instruction""" 64 | 65 | prompt = """ 66 | Generate SQL to complete a given request. 67 | Output only the SQL in one line without newlines. 68 | Do not output comments, backticks, or natural language explanations. 69 | Prioritize SQL queries that are database agnostic. 70 | """ 71 | return gpt_query( 72 | format_message(prompt), format_message(text_to_process), model, thread 73 | ).get("text", "") 74 | 75 | def gpt_start_debug(): 76 | """Enable debug logging""" 77 | GPTState.start_debug() 78 | 79 | def gpt_stop_debug(): 80 | """Disable debug logging""" 81 | GPTState.stop_debug() 82 | 83 | def gpt_clear_context(): 84 | """Reset the stored context""" 85 | GPTState.clear_context() 86 | 87 | def gpt_push_context(context: str | list[str]): 88 | """Add the selected text to the stored context""" 89 | if isinstance(context, list): 90 | context = "\n".join(context) 91 | GPTState.push_context(format_message(context)) 92 | 93 | def gpt_additional_user_context() -> list[str]: 94 | """This is an override function that can be used to add additional context to the prompt""" 95 | return [] 96 | 97 | def gpt_select_last() -> None: 98 | """select all the text in the last GPT output""" 99 | if not GPTState.last_was_pasted: 100 | notify("Tried to select GPT output, but it was not pasted in an editor") 101 | return 102 | 103 | lines = GPTState.last_response.split("\n") 104 | for _ in lines[:-1]: 105 | actions.edit.extend_up() 106 | actions.edit.extend_line_end() 107 | for _ in lines[0]: 108 | actions.edit.extend_left() 109 | 110 | def gpt_apply_prompt( 111 | prompt: str, 112 | model: str, 113 | thread: str, 114 | source: str = "", 115 | destination: str = "", 116 | ): 117 | """Apply an arbitrary prompt to arbitrary text""" 118 | 119 | text_to_process: GPTMessageItem = actions.user.gpt_get_source_text(source) 120 | if not text_to_process.get("text") and not text_to_process.get("image_url"): 121 | text_to_process = None # type: ignore 122 | 123 | # Handle special cases in the prompt 124 | ### Ask is a special case, where the text to process is the prompted question, not selected text 125 | if prompt.startswith("ask"): 126 | text_to_process = format_message(prompt.removeprefix("ask")) 127 | prompt = "Generate text that satisfies the question or request given in the input." 128 | 129 | response = gpt_query( 130 | format_message(prompt), text_to_process, model, thread, destination 131 | ) 132 | 133 | actions.user.gpt_insert_response(response, destination) 134 | return response 135 | 136 | def gpt_apply_prompt_for_cursorless( 137 | prompt: str, 138 | model: str, 139 | thread: str, 140 | source: list[str], 141 | ) -> str: 142 | """Apply a prompt to text from Cursorless and return a string result. 143 | This function is specifically designed for Cursorless integration 144 | and does not trigger insertion actions.""" 145 | 146 | # Join the list into a single string 147 | source_text = "\n".join(source) 148 | text_to_process = format_message(source_text) 149 | 150 | # Send the request but don't insert the response (Cursorless will handle insertion) 151 | response = gpt_query(format_message(prompt), text_to_process, model, thread, "") 152 | 153 | # Return just the text string 154 | return extract_message(response) 155 | 156 | def gpt_pass(source: str = "", destination: str = "") -> None: 157 | """Passes a response from source to destination""" 158 | actions.user.gpt_insert_response( 159 | actions.user.gpt_get_source_text(source), destination 160 | ) 161 | 162 | def gpt_help() -> None: 163 | """Open the GPT help file in the web browser""" 164 | # get the text from the file and open it in the web browser 165 | current_dir = os.path.dirname(__file__) 166 | file_path = os.path.join(current_dir, "lists", "staticPrompt.talon-list") 167 | with open(file_path, "r") as f: 168 | lines = f.readlines()[2:] 169 | 170 | builder = Builder() 171 | builder.h1("Talon GPT Prompt List") 172 | for line in lines: 173 | if "##" in line: 174 | builder.h2(line) 175 | else: 176 | builder.p(line) 177 | 178 | builder.render() 179 | 180 | def gpt_reformat_last(how_to_reformat: str, model: str, thread: str) -> str: 181 | """Reformat the last model output""" 182 | PROMPT = f"""The last phrase was written using voice dictation. It has an error with spelling, grammar, or just general misrecognition due to a lack of context. Please reformat the following text to correct the error with the context that it was {how_to_reformat}.""" 183 | last_output = actions.user.get_last_phrase() 184 | if last_output: 185 | actions.user.clear_last_phrase() 186 | return extract_message( 187 | gpt_query( 188 | format_message(PROMPT), format_message(last_output), model, thread 189 | ) 190 | ) 191 | else: 192 | notify("No text to reformat") 193 | raise Exception("No text to reformat") 194 | 195 | def gpt_insert_response( 196 | gpt_message: GPTMessageItem, 197 | method: str = "", 198 | cursorless_destination: Any = None, 199 | ) -> None: 200 | """Insert a GPT result in a specified way""" 201 | # Use a custom default if nothing is provided and the user has set 202 | # a different default destination 203 | if method == "": 204 | method = settings.get("user.model_default_destination") 205 | 206 | if gpt_message.get("type") != "text": 207 | actions.app.notify( 208 | f"Tried to insert an image to {method}, but that is not currently supported. To insert an image to this destination use a prompt to convert it to text." 209 | ) 210 | return 211 | 212 | message_text_no_images = extract_message(gpt_message) 213 | match method: 214 | case "above": 215 | actions.key("left") 216 | actions.edit.line_insert_up() 217 | GPTState.last_was_pasted = True 218 | actions.user.paste(message_text_no_images) 219 | case "below": 220 | actions.key("right") 221 | actions.edit.line_insert_down() 222 | GPTState.last_was_pasted = True 223 | actions.user.paste(message_text_no_images) 224 | case "clipboard": 225 | clip.set_text(message_text_no_images) 226 | case "snip": 227 | actions.user.insert_snippet(message_text_no_images) 228 | case "context": 229 | GPTState.push_context(gpt_message) 230 | case "newContext": 231 | GPTState.clear_context() 232 | GPTState.push_context(gpt_message) 233 | case "appendClipboard": 234 | if clip.text() is not None: 235 | clip.set_text(clip.text() + "\n" + message_text_no_images) # type: ignore Unclear why this is throwing a type error in pylance 236 | else: 237 | clip.set_text(message_text_no_images) 238 | case "browser": 239 | builder = Builder() 240 | builder.h1("Talon GPT Result") 241 | for line in message_text_no_images.split("\n"): 242 | builder.p(line) 243 | builder.render() 244 | case "textToSpeech": 245 | try: 246 | actions.user.tts(message_text_no_images) 247 | except KeyError: 248 | notify("GPT Failure: text to speech is not installed") 249 | 250 | # Although we can insert to a cursorless destination, the cursorless_target capture 251 | # Greatly increases DFA compliation times and should be avoided if possible 252 | case "cursorless": 253 | actions.user.cursorless_insert( 254 | cursorless_destination, message_text_no_images 255 | ) 256 | # Don't add to the window twice if the thread is enabled 257 | case "window": 258 | # If there was prior text in the confirmation GUI and the user 259 | # explicitly passed new text to the gui, clear the old result 260 | GPTState.text_to_confirm = message_text_no_images 261 | actions.user.confirmation_gui_append(message_text_no_images) 262 | case "chain": 263 | GPTState.last_was_pasted = True 264 | actions.user.paste(message_text_no_images) 265 | actions.user.gpt_select_last() 266 | 267 | case "paste": 268 | GPTState.last_was_pasted = True 269 | actions.user.paste(message_text_no_images) 270 | # If the user doesn't specify a method assume they want to paste. 271 | # However if they didn't specify a method when the confirmation gui 272 | # is showing, assume they don't want anything to be inserted 273 | case _ if not confirmation_gui.showing: 274 | GPTState.last_was_pasted = True 275 | actions.user.paste(message_text_no_images) 276 | # Don't do anything if none of the previous conditions were valid 277 | case _: 278 | pass 279 | 280 | def gpt_get_source_text(spoken_text: str) -> GPTMessageItem: 281 | """Get the source text that is will have the prompt applied to it""" 282 | match spoken_text: 283 | case "clipboard": 284 | return format_clipboard() 285 | case "context": 286 | if GPTState.context == []: 287 | notify("GPT Failure: Context is empty") 288 | raise Exception( 289 | "GPT Failure: User applied a prompt to the phrase context, but there was no context stored" 290 | ) 291 | return format_message(messages_to_string(GPTState.context)) 292 | case "gptResponse": 293 | if GPTState.last_response == "": 294 | raise Exception( 295 | "GPT Failure: User applied a prompt to the phrase GPT response, but there was no GPT response stored" 296 | ) 297 | return format_message(GPTState.last_response) 298 | 299 | case "lastTalonDictation": 300 | last_output = actions.user.get_last_phrase() 301 | if last_output: 302 | actions.user.clear_last_phrase() 303 | return format_message(last_output) 304 | else: 305 | notify("GPT Failure: No last dictation to reformat") 306 | raise Exception( 307 | "GPT Failure: User applied a prompt to the phrase last Talon Dictation, but there was no text to reformat" 308 | ) 309 | case "this" | _: 310 | return format_message(actions.edit.selected_text()) 311 | -------------------------------------------------------------------------------- /lib/modelHelpers.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | import os 5 | import platform 6 | import subprocess 7 | from pathlib import Path 8 | from typing import IO, Any, Literal, NotRequired, Optional, TypedDict 9 | 10 | import requests 11 | from talon import actions, app, clip, resource, settings 12 | 13 | from ..lib.pureHelpers import strip_markdown 14 | from .modelState import GPTState 15 | from .modelTypes import GPTMessage, GPTMessageItem 16 | 17 | """" 18 | All functions in this this file have impure dependencies on either the model or the talon APIs 19 | """ 20 | 21 | 22 | # TypedDict definition for model configuration 23 | class ModelConfig(TypedDict): 24 | name: str 25 | model_id: NotRequired[str] 26 | system_prompt: NotRequired[str] 27 | llm_options: NotRequired[dict[str, Any]] 28 | api_options: NotRequired[dict[str, Any]] 29 | 30 | 31 | # Path to the models.json file 32 | MODELS_PATH = Path(__file__).parent.parent / "models.json" 33 | 34 | # Store loaded model configurations 35 | model_configs: dict[str, ModelConfig] = {} 36 | 37 | 38 | def load_model_config(f: IO) -> None: 39 | """ 40 | Load model configurations from models.json 41 | """ 42 | global model_configs 43 | try: 44 | content = f.read() 45 | configs = json.loads(content) 46 | # Convert list to dictionary with name as key 47 | model_configs = {config["name"]: config for config in configs} 48 | except Exception as e: 49 | notify(f"Failed to load models.json: {e!r}") 50 | model_configs = {} 51 | 52 | 53 | def ensure_models_file_exists(): 54 | if not MODELS_PATH.exists(): 55 | with open(MODELS_PATH, "w") as f: 56 | f.write("[]") 57 | 58 | 59 | ensure_models_file_exists() 60 | 61 | 62 | # Set up file watcher to reload configuration when models.json changes 63 | @resource.watch(str(MODELS_PATH)) 64 | def on_update(f: IO): 65 | load_model_config(f) 66 | 67 | 68 | def resolve_model_name(model: str) -> str: 69 | """ 70 | Get the actual model name from the model list value. 71 | """ 72 | if model == "model": 73 | # Check for deprecated setting first for backward compatibility 74 | openai_model: str = settings.get("user.openai_model") # type: ignore 75 | if openai_model != "do_not_use": 76 | logging.warning( 77 | "The setting 'user.openai_model' is deprecated. Please use 'user.model_default' instead." 78 | ) 79 | model = openai_model 80 | else: 81 | model = settings.get("user.model_default") # type: ignore 82 | return model 83 | 84 | 85 | def get_model_config(model_name: str) -> Optional[ModelConfig]: 86 | """ 87 | Get the configuration for a specific model from the loaded configs 88 | """ 89 | return model_configs.get(model_name) 90 | 91 | 92 | def messages_to_string(messages: list[GPTMessageItem]) -> str: 93 | """Format messages as a string""" 94 | formatted_messages = [] 95 | for message in messages: 96 | if message.get("type") == "image_url": 97 | formatted_messages.append("image") 98 | else: 99 | formatted_messages.append(message.get("text", "")) 100 | return "\n\n".join(formatted_messages) 101 | 102 | 103 | def notify(message: str): 104 | """Send a notification to the user. Defaults the Andreas' notification system if you have it installed""" 105 | try: 106 | actions.user.notify(message) 107 | except Exception: 108 | app.notify(message) 109 | # Log in case notifications are disabled 110 | print(message) 111 | 112 | 113 | def get_token() -> str: 114 | """Get the OpenAI API key from the environment""" 115 | try: 116 | return os.environ["OPENAI_API_KEY"] 117 | except KeyError: 118 | message = "GPT Failure: env var OPENAI_API_KEY is not set." 119 | notify(message) 120 | raise Exception(message) 121 | 122 | 123 | def format_messages( 124 | role: Literal["user", "system", "assistant"], messages: list[GPTMessageItem] 125 | ) -> GPTMessage: 126 | return { 127 | "role": role, 128 | "content": messages, 129 | } 130 | 131 | 132 | def format_message(content: str) -> GPTMessageItem: 133 | return {"type": "text", "text": content} 134 | 135 | 136 | def extract_message(content: GPTMessageItem) -> str: 137 | return content.get("text", "") 138 | 139 | 140 | def format_clipboard() -> GPTMessageItem: 141 | clipped_image = clip.image() 142 | if clipped_image: 143 | data = clipped_image.encode().data() 144 | base64_image = base64.b64encode(data).decode("utf-8") 145 | return { 146 | "type": "image_url", 147 | "image_url": {"url": f"data:image/;base64,{base64_image}"}, 148 | } 149 | else: 150 | if not clip.text(): 151 | raise RuntimeError( 152 | "User requested info from the clipboard but there is nothing in it" 153 | ) 154 | 155 | return format_message(clip.text()) # type: ignore Unclear why this is not narrowing the type 156 | 157 | 158 | def send_request( 159 | prompt: GPTMessageItem, 160 | content_to_process: Optional[GPTMessageItem], 161 | model: str, 162 | thread: str, 163 | destination: str = "", 164 | ) -> GPTMessageItem: 165 | """Generate run a GPT request and return the response""" 166 | model = resolve_model_name(model) 167 | 168 | continue_thread = thread == "continueLast" 169 | 170 | notification = "GPT Task Started" 171 | if len(GPTState.context) > 0: 172 | notification += ": Reusing Stored Context" 173 | 174 | # Use specified model if provided 175 | if model: 176 | notification += f", Using model: {model}" 177 | 178 | if settings.get("user.model_verbose_notifications"): 179 | notify(notification) 180 | 181 | # Get model configuration if available 182 | config = get_model_config(model) 183 | 184 | language = actions.code.language() 185 | language_context = ( 186 | f"The user is currently in a code editor for the programming language: {language}." 187 | if language != "" 188 | else None 189 | ) 190 | application_context = f"The following describes the currently focused application:\n\n{actions.user.talon_get_active_context()}" 191 | snippet_context = ( 192 | "\n\nPlease return the response as a snippet with placeholders. A snippet can control cursors and text insertion using constructs like tabstops ($1, $2, etc., with $0 as the final position). Linked tabstops update together. Placeholders, such as ${1:foo}, allow easy changes and can be nested (${1:another ${2:}}). Choices, using ${1|one,two,three|}, prompt user selection." 193 | if destination == "snip" 194 | else None 195 | ) 196 | 197 | system_message = "\n\n".join( 198 | [ 199 | item 200 | for item in [ 201 | ( 202 | config["system_prompt"] 203 | if config and "system_prompt" in config 204 | else settings.get("user.model_system_prompt") 205 | ), 206 | language_context, 207 | application_context, 208 | snippet_context, 209 | ] 210 | + actions.user.gpt_additional_user_context() 211 | + [context.get("text") for context in GPTState.context] 212 | if item 213 | ] 214 | ) 215 | 216 | content: list[GPTMessageItem] = [prompt] 217 | if content_to_process is not None: 218 | if content_to_process["type"] == "image_url": 219 | image = content_to_process 220 | # If we are processing an image, we have 221 | # to add it as a second message 222 | content = [prompt, image] 223 | elif content_to_process["type"] == "text": 224 | # If we are processing text content, just 225 | # add the text on to the same message instead 226 | # of splitting it into multiple messages 227 | prompt["text"] = ( 228 | prompt["text"] + '\n\n"""' + content_to_process["text"] + '"""' # type: ignore a Prompt has to be of type text 229 | ) 230 | content = [prompt] 231 | 232 | request = GPTMessage( 233 | role="user", 234 | content=content, 235 | ) 236 | 237 | model_endpoint: str = settings.get("user.model_endpoint") # type: ignore 238 | if model_endpoint == "llm": 239 | response = send_request_to_llm_cli( 240 | prompt, content_to_process, system_message, model, continue_thread 241 | ) 242 | else: 243 | if continue_thread: 244 | notify( 245 | "Warning: Thread continuation is only supported when using setting user.model_endpoint = 'llm'" 246 | ) 247 | response = send_request_to_api(request, system_message, model) 248 | 249 | return response 250 | 251 | 252 | def send_request_to_api( 253 | request: GPTMessage, system_message: str, model: str 254 | ) -> GPTMessageItem: 255 | """Send a request to the model API endpoint and return the response""" 256 | # Get model configuration if available 257 | config = get_model_config(model) 258 | 259 | # Use model_id from configuration if available 260 | model_id = config["model_id"] if config and "model_id" in config else model 261 | 262 | data = { 263 | "messages": ( 264 | [ 265 | format_messages( 266 | "system", 267 | [GPTMessageItem(type="text", text=system_message)], 268 | ), 269 | ] 270 | if system_message 271 | else [] 272 | ) 273 | + [request], 274 | "max_tokens": 2024, 275 | "n": 1, 276 | "model": model_id, 277 | } 278 | 279 | # Check for deprecated temperature setting 280 | temperature: float = settings.get("user.model_temperature") # type: ignore 281 | if temperature != -1.0: 282 | logging.warning( 283 | "The setting 'user.model_temperature' is deprecated. Please configure temperature in models.json instead." 284 | ) 285 | data["temperature"] = temperature 286 | 287 | # Apply API options from configuration if available 288 | if config and "api_options" in config: 289 | data.update(config["api_options"]) 290 | 291 | if GPTState.debug_enabled: 292 | print(data) 293 | 294 | url: str = settings.get("user.model_endpoint") # type: ignore 295 | headers = {"Content-Type": "application/json"} 296 | token = get_token() 297 | # If the model endpoint is Azure, we need to use a different header 298 | if "azure.com" in url: 299 | headers["api-key"] = token 300 | else: 301 | headers["Authorization"] = f"Bearer {token}" 302 | 303 | raw_response = requests.post(url, headers=headers, data=json.dumps(data)) 304 | 305 | match raw_response.status_code: 306 | case 200: 307 | if settings.get("user.model_verbose_notifications"): 308 | notify("GPT Task Completed") 309 | resp = raw_response.json()["choices"][0]["message"]["content"].strip() 310 | formatted_resp = strip_markdown(resp) 311 | return format_message(formatted_resp) 312 | case _: 313 | notify("GPT Failure: Check the Talon Log") 314 | raise Exception(raw_response.json()) 315 | 316 | 317 | def send_request_to_llm_cli( 318 | prompt: GPTMessageItem, 319 | content_to_process: Optional[GPTMessageItem], 320 | system_message: str, 321 | model: str, 322 | continue_thread: bool, 323 | ) -> GPTMessageItem: 324 | """Send a request to the LLM CLI tool and return the response""" 325 | # Get model configuration if available 326 | config = get_model_config(model) 327 | 328 | # Use model_id from configuration if available 329 | model_id = config["model_id"] if config and "model_id" in config else model 330 | 331 | # Build command 332 | command: list[str] = [settings.get("user.model_llm_path")] # type: ignore 333 | if continue_thread: 334 | command.append("-c") 335 | command.append(prompt["text"]) # type: ignore 336 | cmd_input: bytes | None = None 337 | if content_to_process and content_to_process["type"] == "image_url": 338 | img_url: str = content_to_process["image_url"]["url"] # type: ignore 339 | if img_url.startswith("data:"): 340 | command.extend(["-a", "-"]) 341 | base64_data: str = img_url.split(",", 1)[1] 342 | cmd_input = base64.b64decode(base64_data) 343 | else: 344 | command.extend(["-a", img_url]) 345 | 346 | # Add model option 347 | command.extend(["-m", model_id]) 348 | 349 | # Check for deprecated temperature setting 350 | temperature: float = settings.get("user.model_temperature") # type: ignore 351 | if temperature != -1.0: 352 | logging.warning( 353 | "The setting 'user.model_temperature' is deprecated. Please configure temperature in models.json instead." 354 | ) 355 | command.extend(["-o", "temperature", str(temperature)]) 356 | 357 | # Apply llm_options from configuration if available 358 | if config and "llm_options" in config: 359 | for key, value in config["llm_options"].items(): 360 | if isinstance(value, bool): 361 | if value: 362 | command.extend(["-o", key, "true"]) 363 | else: 364 | command.extend(["-o", key, "false"]) 365 | else: 366 | command.extend(["-o", key, str(value)]) 367 | 368 | # Add system message if available 369 | if system_message: 370 | command.extend(["-s", system_message]) 371 | 372 | if GPTState.debug_enabled: 373 | print(command) 374 | 375 | # Configure output encoding 376 | process_env = os.environ.copy() 377 | if platform.system() == "Windows": 378 | process_env["PYTHONUTF8"] = "1" # For Python 3.7+ to enable UTF-8 mode 379 | # On other platforms, UTF-8 is also the common/expected encoding. 380 | output_encoding = "utf-8" 381 | 382 | # Execute command and capture output. 383 | try: 384 | result = subprocess.run( 385 | command, 386 | input=cmd_input, 387 | capture_output=True, 388 | check=True, 389 | creationflags=( 390 | subprocess.CREATE_NO_WINDOW if platform.system() == "Windows" else 0 # type: ignore 391 | ), 392 | env=process_env if platform.system() == "Windows" else None, 393 | ) 394 | if settings.get("user.model_verbose_notifications"): 395 | notify("GPT Task Completed") 396 | resp = result.stdout.decode(output_encoding).strip() 397 | formatted_resp = strip_markdown(resp) 398 | return format_message(formatted_resp) 399 | except subprocess.CalledProcessError as e: 400 | error_msg = e.stderr.decode(output_encoding).strip() if e.stderr else str(e) 401 | notify(f"GPT Failure: {error_msg}") 402 | raise e 403 | except Exception as e: 404 | notify("GPT Failure: Check the Talon Log") 405 | raise e 406 | --------------------------------------------------------------------------------