├── .github ├── CODEOWNERS └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE ├── README.md ├── assets ├── assembllm_banner.png ├── assembllm_logo.webp ├── basic_demo.gif ├── choose_model_demo.gif ├── demo.gif └── piping_curl_demo.gif ├── completions.go ├── completions_test.go ├── config.go ├── config.yaml ├── go.mod ├── go.sum ├── main.go ├── plugins └── README.md ├── scripts.go ├── update_plugins.go ├── workflow.go └── workflows ├── README.md ├── algorithm_time_complexity.yaml ├── article_summarizer.yaml ├── email.yaml ├── eol ├── README.md ├── eol.gif ├── eol.tape └── tools_eol.yaml ├── extism_example_task.yaml ├── generate_presentation.yaml ├── git_diff.yaml ├── product_requirements_stories.yaml ├── research_example.sh ├── research_example_task.yaml ├── rss ├── README.md ├── rss.gif ├── rss.tape └── rss.yaml ├── scrape-html-prescript.yaml ├── scrape_then_summarize ├── README.md ├── scrape.yaml ├── scrape_then_summarize.gif ├── scrape_then_summarize.tape ├── scrape_then_summarize.yaml └── summarize.yaml ├── socratic_challenge.yaml ├── tool_sentiment_analysis.yaml ├── weather ├── README.md ├── tools_weather.yaml ├── weather.gif └── weather.tape └── workflow_chaining.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @bradyjoslin 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Build CLI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | paths: 10 | - '*.go' 11 | - 'go.mod' 12 | - 'go.sum' 13 | pull_request: 14 | branches: [ "main" ] 15 | paths: 16 | - '*.go' 17 | - 'go.mod' 18 | - 'go.sum' 19 | 20 | jobs: 21 | 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Set up Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version: '1.22.3' 31 | 32 | - name: Build 33 | run: go build -v . 34 | 35 | - name: Test 36 | run: | 37 | export SKIP_CHAT_RESPONSE_TESTS=true 38 | go test -v . 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yml 2 | name: goreleaser 3 | 4 | on: 5 | push: 6 | # run only against tags 7 | tags: 8 | - "*" 9 | 10 | permissions: 11 | contents: write 12 | # packages: write 13 | # issues: write 14 | 15 | jobs: 16 | goreleaser: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: stable 27 | # More assembly might be required: Docker logins, GPG, etc. 28 | # It all depends on your needs. 29 | - name: Run GoReleaser 30 | uses: goreleaser/goreleaser-action@v6 31 | with: 32 | # either 'goreleaser' (default) or 'goreleaser-pro' 33 | distribution: goreleaser 34 | # 'latest', 'nightly', or a semver 35 | version: "~> v2" 36 | args: release --clean 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.PUBLISHER_TOKEN }} 39 | # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution 40 | # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | wasi-sdk* 3 | target/ 4 | assembllm 5 | 6 | ## Ignore Visual Studio temporary files, build results, and 7 | ## files generated by popular Visual Studio add-ons. 8 | ## 9 | ## Get latest from `dotnet new gitignore` 10 | 11 | # dotenv files 12 | .env 13 | 14 | # User-specific files 15 | *.rsuser 16 | *.suo 17 | *.user 18 | *.userosscache 19 | *.sln.docstates 20 | 21 | # User-specific files (MonoDevelop/Xamarin Studio) 22 | *.userprefs 23 | 24 | # Mono auto generated files 25 | mono_crash.* 26 | 27 | # Build results 28 | dist/ 29 | [Dd]ebug/ 30 | [Dd]ebugPublic/ 31 | [Rr]elease/ 32 | [Rr]eleases/ 33 | x64/ 34 | x86/ 35 | [Ww][Ii][Nn]32/ 36 | [Aa][Rr][Mm]/ 37 | [Aa][Rr][Mm]64/ 38 | bld/ 39 | [Bb]in/ 40 | [Oo]bj/ 41 | [Ll]og/ 42 | [Ll]ogs/ 43 | 44 | # Visual Studio 2015/2017 cache/options directory 45 | .vs/ 46 | # Uncomment if you have tasks that create the project's static files in wwwroot 47 | #wwwroot/ 48 | 49 | # Visual Studio 2017 auto generated files 50 | Generated\ Files/ 51 | 52 | # MSTest test Results 53 | [Tt]est[Rr]esult*/ 54 | [Bb]uild[Ll]og.* 55 | 56 | # NUnit 57 | *.VisualState.xml 58 | TestResult.xml 59 | nunit-*.xml 60 | 61 | # Build Results of an ATL Project 62 | [Dd]ebugPS/ 63 | [Rr]eleasePS/ 64 | dlldata.c 65 | 66 | # Benchmark Results 67 | BenchmarkDotNet.Artifacts/ 68 | 69 | # .NET 70 | project.lock.json 71 | project.fragment.lock.json 72 | artifacts/ 73 | 74 | # Tye 75 | .tye/ 76 | 77 | # ASP.NET Scaffolding 78 | ScaffoldingReadMe.txt 79 | 80 | # StyleCop 81 | StyleCopReport.xml 82 | 83 | # Files built by Visual Studio 84 | *_i.c 85 | *_p.c 86 | *_h.h 87 | *.ilk 88 | *.meta 89 | *.obj 90 | *.iobj 91 | *.pch 92 | *.pdb 93 | *.ipdb 94 | *.pgc 95 | *.pgd 96 | *.rsp 97 | *.sbr 98 | *.tlb 99 | *.tli 100 | *.tlh 101 | *.tmp 102 | *.tmp_proj 103 | *_wpftmp.csproj 104 | *.log 105 | *.tlog 106 | *.vspscc 107 | *.vssscc 108 | .builds 109 | *.pidb 110 | *.svclog 111 | *.scc 112 | 113 | # Chutzpah Test files 114 | _Chutzpah* 115 | 116 | # Visual C++ cache files 117 | ipch/ 118 | *.aps 119 | *.ncb 120 | *.opendb 121 | *.opensdf 122 | *.sdf 123 | *.cachefile 124 | *.VC.db 125 | *.VC.VC.opendb 126 | 127 | # Visual Studio profiler 128 | *.psess 129 | *.vsp 130 | *.vspx 131 | *.sap 132 | 133 | # Visual Studio Trace Files 134 | *.e2e 135 | 136 | # TFS 2012 Local Workspace 137 | $tf/ 138 | 139 | # Guidance Automation Toolkit 140 | *.gpState 141 | 142 | # ReSharper is a .NET coding add-in 143 | _ReSharper*/ 144 | *.[Rr]e[Ss]harper 145 | *.DotSettings.user 146 | 147 | # TeamCity is a build add-in 148 | _TeamCity* 149 | 150 | # DotCover is a Code Coverage Tool 151 | *.dotCover 152 | 153 | # AxoCover is a Code Coverage Tool 154 | .axoCover/* 155 | !.axoCover/settings.json 156 | 157 | # Coverlet is a free, cross platform Code Coverage Tool 158 | coverage*.json 159 | coverage*.xml 160 | coverage*.info 161 | 162 | # Visual Studio code coverage results 163 | *.coverage 164 | *.coveragexml 165 | 166 | # NCrunch 167 | _NCrunch_* 168 | .*crunch*.local.xml 169 | nCrunchTemp_* 170 | 171 | # MightyMoose 172 | *.mm.* 173 | AutoTest.Net/ 174 | 175 | # Web workbench (sass) 176 | .sass-cache/ 177 | 178 | # Installshield output folder 179 | [Ee]xpress/ 180 | 181 | # DocProject is a documentation generator add-in 182 | DocProject/buildhelp/ 183 | DocProject/Help/*.HxT 184 | DocProject/Help/*.HxC 185 | DocProject/Help/*.hhc 186 | DocProject/Help/*.hhk 187 | DocProject/Help/*.hhp 188 | DocProject/Help/Html2 189 | DocProject/Help/html 190 | 191 | # Click-Once directory 192 | publish/ 193 | 194 | # Publish Web Output 195 | *.[Pp]ublish.xml 196 | *.azurePubxml 197 | # Note: Comment the next line if you want to checkin your web deploy settings, 198 | # but database connection strings (with potential passwords) will be unencrypted 199 | *.pubxml 200 | *.publishproj 201 | 202 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 203 | # checkin your Azure Web App publish settings, but sensitive information contained 204 | # in these scripts will be unencrypted 205 | PublishScripts/ 206 | 207 | # NuGet Packages 208 | *.nupkg 209 | # NuGet Symbol Packages 210 | *.snupkg 211 | # The packages folder can be ignored because of Package Restore 212 | **/[Pp]ackages/* 213 | # except build/, which is used as an MSBuild target. 214 | !**/[Pp]ackages/build/ 215 | # Uncomment if necessary however generally it will be regenerated when needed 216 | #!**/[Pp]ackages/repositories.config 217 | # NuGet v3's project.json files produces more ignorable files 218 | *.nuget.props 219 | *.nuget.targets 220 | 221 | # Microsoft Azure Build Output 222 | csx/ 223 | *.build.csdef 224 | 225 | # Microsoft Azure Emulator 226 | ecf/ 227 | rcf/ 228 | 229 | # Windows Store app package directories and files 230 | AppPackages/ 231 | BundleArtifacts/ 232 | Package.StoreAssociation.xml 233 | _pkginfo.txt 234 | *.appx 235 | *.appxbundle 236 | *.appxupload 237 | 238 | # Visual Studio cache files 239 | # files ending in .cache can be ignored 240 | *.[Cc]ache 241 | # but keep track of directories ending in .cache 242 | !?*.[Cc]ache/ 243 | 244 | # Others 245 | ClientBin/ 246 | ~$* 247 | *~ 248 | *.dbmdl 249 | *.dbproj.schemaview 250 | *.jfm 251 | *.pfx 252 | *.publishsettings 253 | orleans.codegen.cs 254 | 255 | # Including strong name files can present a security risk 256 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 257 | #*.snk 258 | 259 | # Since there are multiple workflows, uncomment next line to ignore bower_components 260 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 261 | #bower_components/ 262 | 263 | # RIA/Silverlight projects 264 | Generated_Code/ 265 | 266 | # Backup & report files from converting an old project file 267 | # to a newer Visual Studio version. Backup files are not needed, 268 | # because we have git ;-) 269 | _UpgradeReport_Files/ 270 | Backup*/ 271 | UpgradeLog*.XML 272 | UpgradeLog*.htm 273 | ServiceFabricBackup/ 274 | *.rptproj.bak 275 | 276 | # SQL Server files 277 | *.mdf 278 | *.ldf 279 | *.ndf 280 | 281 | # Business Intelligence projects 282 | *.rdl.data 283 | *.bim.layout 284 | *.bim_*.settings 285 | *.rptproj.rsuser 286 | *- [Bb]ackup.rdl 287 | *- [Bb]ackup ([0-9]).rdl 288 | *- [Bb]ackup ([0-9][0-9]).rdl 289 | 290 | # Microsoft Fakes 291 | FakesAssemblies/ 292 | 293 | # GhostDoc plugin setting file 294 | *.GhostDoc.xml 295 | 296 | # Node.js Tools for Visual Studio 297 | .ntvs_analysis.dat 298 | node_modules/ 299 | 300 | # Visual Studio 6 build log 301 | *.plg 302 | 303 | # Visual Studio 6 workspace options file 304 | *.opt 305 | 306 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 307 | *.vbw 308 | 309 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 310 | *.vbp 311 | 312 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 313 | *.dsw 314 | *.dsp 315 | 316 | # Visual Studio 6 technical files 317 | *.ncb 318 | *.aps 319 | 320 | # Visual Studio LightSwitch build output 321 | **/*.HTMLClient/GeneratedArtifacts 322 | **/*.DesktopClient/GeneratedArtifacts 323 | **/*.DesktopClient/ModelManifest.xml 324 | **/*.Server/GeneratedArtifacts 325 | **/*.Server/ModelManifest.xml 326 | _Pvt_Extensions 327 | 328 | # Paket dependency manager 329 | .paket/paket.exe 330 | paket-files/ 331 | 332 | # FAKE - F# Make 333 | .fake/ 334 | 335 | # CodeRush personal settings 336 | .cr/personal 337 | 338 | # Python Tools for Visual Studio (PTVS) 339 | __pycache__/ 340 | *.pyc 341 | 342 | # Cake - Uncomment if you are using it 343 | # tools/** 344 | # !tools/packages.config 345 | 346 | # Tabs Studio 347 | *.tss 348 | 349 | # Telerik's JustMock configuration file 350 | *.jmconfig 351 | 352 | # BizTalk build output 353 | *.btp.cs 354 | *.btm.cs 355 | *.odx.cs 356 | *.xsd.cs 357 | 358 | # OpenCover UI analysis results 359 | OpenCover/ 360 | 361 | # Azure Stream Analytics local run output 362 | ASALocalRun/ 363 | 364 | # MSBuild Binary and Structured Log 365 | *.binlog 366 | 367 | # NVidia Nsight GPU debugger configuration file 368 | *.nvuser 369 | 370 | # MFractors (Xamarin productivity tool) working folder 371 | .mfractor/ 372 | 373 | # Local History for Visual Studio 374 | .localhistory/ 375 | 376 | # Visual Studio History (VSHistory) files 377 | .vshistory/ 378 | 379 | # BeatPulse healthcheck temp database 380 | healthchecksdb 381 | 382 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 383 | MigrationBackup/ 384 | 385 | # Ionide (cross platform F# VS Code tools) working folder 386 | .ionide/ 387 | 388 | # Fody - auto-generated XML schema 389 | FodyWeavers.xsd 390 | 391 | # VS Code files for those working on multiple tools 392 | .vscode/* 393 | !.vscode/settings.json 394 | !.vscode/tasks.json 395 | !.vscode/launch.json 396 | !.vscode/extensions.json 397 | *.code-workspace 398 | 399 | # Local History for Visual Studio Code 400 | .history/ 401 | 402 | # Windows Installer files from build outputs 403 | *.cab 404 | *.msi 405 | *.msix 406 | *.msm 407 | *.msp 408 | 409 | # JetBrains Rider 410 | *.sln.iml 411 | .idea 412 | 413 | ## 414 | ## Visual studio for Mac 415 | ## 416 | 417 | 418 | # globs 419 | Makefile.in 420 | *.userprefs 421 | *.usertasks 422 | config.make 423 | config.status 424 | aclocal.m4 425 | install-sh 426 | autom4te.cache/ 427 | *.tar.gz 428 | tarballs/ 429 | test-results/ 430 | 431 | # Mac bundle stuff 432 | *.dmg 433 | *.app 434 | 435 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 436 | # General 437 | .DS_Store 438 | .AppleDouble 439 | .LSOverride 440 | 441 | # Icon must end with two \r 442 | Icon 443 | 444 | 445 | # Thumbnails 446 | ._* 447 | 448 | # Files that might appear in the root of a volume 449 | .DocumentRevisions-V100 450 | .fseventsd 451 | .Spotlight-V100 452 | .TemporaryItems 453 | .Trashes 454 | .VolumeIcon.icns 455 | .com.apple.timemachine.donotpresent 456 | 457 | # Directories potentially created on remote AFP share 458 | .AppleDB 459 | .AppleDesktop 460 | Network Trash Folder 461 | Temporary Items 462 | .apdisk 463 | 464 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 465 | # Windows thumbnail cache files 466 | Thumbs.db 467 | ehthumbs.db 468 | ehthumbs_vista.db 469 | 470 | # Dump file 471 | *.stackdump 472 | 473 | # Folder config file 474 | [Dd]esktop.ini 475 | 476 | # Recycle Bin used on file shares 477 | $RECYCLE.BIN/ 478 | 479 | # Windows Installer files 480 | *.cab 481 | *.msi 482 | *.msix 483 | *.msm 484 | *.msp 485 | 486 | # Windows shortcuts 487 | *.lnk 488 | 489 | # Vim temporary swap files 490 | *.swp 491 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | version: 2 10 | 11 | before: 12 | hooks: 13 | # You may remove this if you don't use go modules. 14 | - go mod tidy 15 | 16 | builds: 17 | - env: 18 | - CGO_ENABLED=0 19 | ignore: 20 | - goos: windows 21 | goarch: 386 22 | goos: 23 | - linux 24 | - windows 25 | - darwin 26 | 27 | archives: 28 | - format: tar.gz 29 | # this name template makes the OS and Arch compatible with the results of `uname`. 30 | name_template: >- 31 | {{ .ProjectName }}_ 32 | {{- title .Os }}_ 33 | {{- if eq .Arch "amd64" }}x86_64 34 | {{- else if eq .Arch "386" }}i386 35 | {{- else }}{{ .Arch }}{{ end }} 36 | {{- if .Arm }}v{{ .Arm }}{{ end }} 37 | # use zip for windows archives 38 | format_overrides: 39 | - goos: windows 40 | format: zip 41 | 42 | changelog: 43 | sort: asc 44 | filters: 45 | exclude: 46 | - "^docs:" 47 | - "^test:" 48 | 49 | brews: 50 | - 51 | name: assembllm 52 | homepage: https://github.com/bradyjoslin 53 | repository: 54 | owner: bradyjoslin 55 | name: homebrew-assembllm 56 | commit_author: 57 | name: bradyjoslin 58 | email: brady@bradyjoslin.com 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Brady Joslin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # assembllm 2 | 3 |

4 | Banner Image 5 |

6 | 7 | A versatile CLI tool designed to combine multiple Large Language Models (LLMs) using a flexible task-based system. With a unique WebAssembly-based plugin architecture, it supports seamless integration of various AI models and custom scripts. 8 | 9 | ### Key Features: 10 | 11 | - **Flexible Scripting**: Use pre- and post-scripts for data transformation and integration. 12 | - **Prompt Iteration**: Provide an array of prompts that execute sequentially. 13 | - **Task Chaining**: Chaining multiple tasks into workflows, where outputs of each task feed into the next. 14 | - **Workflow Chaining**: Workflows can call other workflows, allowing you to break down complex operations into smaller, reusable chunks and dynamically link them together. 15 | - **Function / Tool Calling**: Convert unstructured prompts to structured data with OpenAI, Anthropic, and Cloudflare. 16 | - **Multi-Model Support**: Available plugins for [OpenAI](https://platform.openai.com/docs/guides/text-generation/chat-completions-api), [Perplexity](https://docs.perplexity.ai/), [Cloudflare AI](https://developers.cloudflare.com/workers-ai/models/#text-generation), and [Anthropic](https://docs.anthropic.com/en/docs/intro-to-claude). 17 | - **Plugin Architecture**: Extensible via WebAssembly plugins written in a variety of languages, including JavaScript, Rust, Go, and C#, using [Extism](https://extism.org/). 18 | 19 | ## Installing 20 | 21 | ```bash 22 | # install with brew 23 | brew tap bradyjoslin/assembllm 24 | brew install bradyjoslin/assembllm/assembllm 25 | 26 | # install with Go 27 | go install github.com/bradyjoslin/assembllm 28 | ``` 29 | 30 | Or grab a pre-built binary from [releases](https://github.com/bradyjoslin/assembllm/releases). 31 | 32 | ## Basic Usage 33 | 34 | ```text 35 | A WASM plug-in based CLI for AI chat completions 36 | 37 | Usage: 38 | assembllm [prompt] [flags] 39 | 40 | Flags: 41 | -p, --plugin string The name of the plugin to use (default "openai") 42 | -P, --choose-plugin Choose the plugin to use 43 | -m, --model string The name of the model to use 44 | -M, --choose-model Choose the model to use 45 | -t, --temperature string The temperature to use 46 | -r, --role string The role to use 47 | --raw Raw output without formatting 48 | -v, --version Print the version 49 | -w, --workflow string The path to a workflow file 50 | -W, --choose-workflow Choose a workflow to run 51 | -i, --iterator String array of prompts ['prompt1', 'prompt2'] 52 | -f, --feedback Optionally provide feedback and rerun workflow 53 | -h, --help help for assembllm 54 | ``` 55 | 56 | ### Simple Commands 57 | 58 | You can quickly utilize the power of LLMs with simple commands and integrate assembllm with other tools via bash pipes for enhanced functionality: 59 | 60 | ![Demo](./assets/basic_demo.gif) 61 | 62 | ## Advanced Prompting with Workflows 63 | 64 | For more complex prompts, including the ability to create prompt pipelines, define and chain tasks together with workflows. We have a [library of workflows](https://github.com/bradyjoslin/assembllm/tree/main/workflows) you can use as examples and templates, let's walk through one together here. 65 | 66 | ### Example Workflow Configuration 67 | 68 | This example demonstrates how to chain multiple tasks together to generate, analyze, compose, and summarize content: 69 | 70 | - **Generate Topic Ideas**: Use Perplexity to generate initial ideas. 71 | - **Conduct Research**: Augment the generated ideas with data from Extism's GitHub repositories using a pre-script. 72 | - **Write Blog Post**: Compose a blog post based on the research, write it to a file, and send as an email. 73 | - **Summarize Blog Post**: Read the blog post from a file and generate a summary. 74 | 75 | ```yaml 76 | tasks: 77 | 78 | - name: topic 79 | plugin: perplexity 80 | prompt: "ten bullets summarizing extism plug-in systems with wasm" 81 | 82 | - name: researcher 83 | plugin: openai 84 | pre_script: > 85 | (Get('https://api.github.com/orgs/extism/repos') | fromJSON()) 86 | | map([ 87 | { 88 | 'name': .name, 89 | 'description': .description, 90 | 'stars': .stargazers_count, 91 | 'url': .html_url 92 | } 93 | ]) 94 | | toJSON() 95 | role: "you are a technical research assistant" 96 | prompt: "analyze these capabilities against the broader backdrop of webassembly." 97 | 98 | - name: writer 99 | plugin: openai 100 | role: "you are a technical writer" 101 | prompt: "write a blog post on the provided research, avoid bullets, use prose and include section headers" 102 | temperature: 0.5 103 | model: 4o 104 | post_script: | 105 | let _ = AppendFile(input, "research_example_output.md"); 106 | Resend("example@example.com", "info@notifications.example.com", "Extism Research", input) 107 | 108 | - name: reader 109 | plugin: openai 110 | pre_script: | 111 | ReadFile("research_example_output.md") 112 | role: "you are a technical reader" 113 | prompt: "summarize the blog post in 5 bullets" 114 | ``` 115 | 116 | Run this workflow with: 117 | 118 | ```sh 119 | assembllm --workflow research_example_task.yaml 120 | ``` 121 | 122 | After running the above workflow, you can expect as outputs a detailed blog post saved to a file and sent as an email, and a concise summary printed to stdout. 123 | 124 | ### Workflow Architecture 125 | 126 | This is a high level overview of the flow of prompt and response data through the various components available within a workflow. 127 | 128 | - An IterationScript is optional, and if included defines an array of prompt data where each value is looped through the task chain. 129 | - A workflow can have one or more tasks. 130 | - A task can optionally include a PreScript, LLM Call, and/or a PostScript. 131 | - A task can call a separate workflow in its PreScript or PostScript for modularity. 132 | 133 | ```mermaid 134 | stateDiagram-v2 135 | direction LR 136 | [*] --> Workflow 137 | state Workflow { 138 | direction LR 139 | IterationScript --> Tasks 140 | state Tasks { 141 | direction LR 142 | state Task(1) { 143 | LLMCall : LLM Call 144 | PreScript --> LLMCall 145 | LLMCall --> PostScript 146 | } 147 | Task(1) --> Task(2) 148 | state Task(2) { 149 | PreScript2 : PreScript 150 | LLMCall2 : LLM Call 151 | PostScript2 : PostScript 152 | PreScript2 --> LLMCall2 153 | LLMCall2 --> PostScript2 154 | } 155 | Task(2) --> Task(n...) 156 | state Task(n...) { 157 | PreScriptn : PreScript 158 | LLMCalln : LLM Call 159 | PostScriptn : PostScript 160 | PreScriptn --> LLMCalln 161 | LLMCalln --> PostScriptn 162 | } 163 | } 164 | Tasks --> IterationScript 165 | } 166 | Workflow --> [*] 167 | ``` 168 | 169 | ### Workflow Prompts 170 | 171 | Workflows in `assembllm` can optionally take a prompt from either standard input (stdin) or as an argument. The provided input is integrated into the prompt defined in the first task of the workflow. 172 | 173 | **Key Points**: 174 | 175 | - **Optional Input**: You can run workflows without providing a prompt as input. 176 | - **Optional First Task Prompt**: The prompt in the first task of a workflow is also optional. 177 | - **Combining Prompts**: If both are provided, they are combined to form a unified prompt for the first task. 178 | 179 | This flexibility allows workflows to be dynamic and adaptable based on user input. 180 | 181 | ### Pre-Scripts and Post-Scripts 182 | 183 | assembllm allows the use of pre-scripts and post-scripts for data transformation and integration, providing flexibility in how data is handled before and after LLM processing. These scripts can utilize various functions to fetch, read, append, and transform data. 184 | 185 | Expressions are powered by [Expr](https://expr-lang.org/), a Go-centric expression language designed to deliver dynamic configurations. All expressions result in a single value. See the full language definition [here](https://expr-lang.org/docs/language-definition). 186 | 187 | In addition to all of the functionality provided by Expr, these functions are available in expressions: 188 | 189 | - **Get**: perform http GET calls within functions 190 | - **Signature**: Get(url: str) -> str 191 | - **Parameters**: url (str): The URL to fetch data from. 192 | - **Returns**: Response data as a string 193 | 194 | - **ReadFile**: read files from your local filesystem 195 | - **Signature**: ReadFile(filepath: str) -> str 196 | - **Parameters**: filepath (str): The path to the file to read. 197 | - **Returns**: File content as a string. 198 | 199 | - **AppendFile**: appends content to file, creating if it doesn't exist 200 | - **Signature**: AppendFile(content: str, filepath: str) -> (int64, error) 201 | - **Parameters**: 202 | - content (str): The content to append. 203 | - filepath (str): The path to the file to append to. 204 | - **Returns**: Number of bytes written as int64 or an error 205 | 206 | - **Resend**: sends content as email using [Resend](https://resend.com/) 207 | - **Signature**: Resend(to: str, from: str, subject: str, body: str) -> Error 208 | - **Parameters**: 209 | - to (str): Email to field 210 | - from (str): Email from field 211 | - subject (str): Email subject 212 | - body (str): Email body, automatically converted from markdown to HTML 213 | - **Returns**: Error, if one occured 214 | - **Requires**: [Resend API key](https://resend.com/docs/dashboard/api-keys/introduction) set to `RESEND_API_KEY` environment variable 215 | 216 | - **Extism**: calls a wasm function, source can be a file or url 217 | - **Signature**: Extism(source: str, function_name: str, args: list) -> str 218 | - **Parameters**: 219 | - source (str): The source of the WebAssembly function (file or URL). 220 | - function_name (str): The name of the function to call. 221 | - args (list): A list of arguments to pass to the function. 222 | - **Returns**: Result of the WebAssembly function call as a string. 223 | 224 | In addition to these functions, an `input` variable is provided with the contents of the prompt at that stage of the chain. 225 | 226 | A `pre_script` is used to manipulate the provided prompt input prior to the LLM call. The prompt value in a `pre-script` can be referenced with using `input` variable. The output of a `pre_script` is appended to the prompt and sent to the LLM. 227 | 228 | A `post_script` is run after sending the prompt to the LLM, and is used to manipulate the results from the LLM plugin. Therefore the `input` value availabe is the LLM's response. Unlike a `pre_script`, a `post_script`'s output *replaces* instead of appends to the prompt at that stage of the chain, so if you would like to pass the prompt along from a `post_script`, you must do so explicitly. For example, if you'd like to write the current LLM results to a file and also pass those results to the next LLM: 229 | 230 | ```yml 231 | ... 232 | - name: file_writer 233 | post_script: | 234 | let _ = AppendFile(input, "research_example_output.md"); 235 | input 236 | ... 237 | ``` 238 | 239 | Here's an example of calling wasm using the `Extism` function within expressions: 240 | 241 | ```sh 242 | tasks: 243 | - name: topic 244 | plugin: openai 245 | prompt: "tell me a joke" 246 | post_script: | 247 | let wasm = "https://github.com/extism/plugins/releases/latest/download/count_vowels.wasm"; 248 | let vowelCount = Extism(wasm, "count_vowels", input); 249 | let vowels = (vowelCount | fromJSON()).count | string(); 250 | input + "\n\n vowels: " + vowels 251 | ``` 252 | 253 | Example results: 254 | 255 | ```txt 256 | Sure, here's a light-hearted joke for you: 257 | 258 | Why don't skeletons fight each other? 259 | 260 | They don't have the guts. 261 | 262 | vowels: 29 263 | ``` 264 | 265 | ### Chaining with Bash Scripts 266 | 267 | While assembllm provides a powerful built-in workflow feature, you can also chain LLM responses directly within Bash scripts for simpler automation. Here’s an example: 268 | 269 | ```sh 270 | #!/bin/bash 271 | 272 | TOPIC="ten bullets summarizing extism plug-in systems with wasm" 273 | RESEARCHER="you are a technical research assistant" 274 | ANALYSIS="analyze these capabilities against the broader backdrop of webassembly." 275 | WRITER="you are a technical writer specializing in trends, skilled at helping developers understand the practical use of new technology described from first principles" 276 | BLOG_POST="write a blog post on the provided research, avoid bullets, use prose and include section headers" 277 | 278 | assembllm -p perplexity "$TOPIC" \ 279 | | assembllm -r "$RESEARCHER" "$ANALYSIS" \ 280 | | assembllm --raw -r "$WRITER" "$BLOG_POST" \ 281 | | tee research_example_output.md 282 | ``` 283 | 284 | **Explanation**: 285 | 286 | - **TOPIC**: Generate initial topic ideas. 287 | - **RESEARCHER**: Analyze the generated ideas. 288 | - **ANALYSIS**: Provide a deeper understanding of the topic. 289 | - **WRITER**: Compose a detailed blog post based on the research. 290 | 291 | This script demonstrates how you can chain multiple LLM commands together, leveraging `assembllm` to process and transform data through each stage. This approach offers an alternative to the built-in workflow feature for those who prefer using Bash scripts. 292 | 293 | ## Plugins 294 | 295 | Plug-ins are powered by [Extism](https://extism.org), a cross-language framework for building web-assembly based plug-in systems. `assembllm` acts as a [host application](https://extism.org/docs/concepts/host-sdk) that uses the Extism SDK to and is responsible for handling the user experience and interacting with the LLM chat completion plug-ins which use Extism's [Plug-in Development Kits (PDKs)](https://extism.org/docs/concepts/pdk). 296 | 297 | ### Sample Plugins 298 | 299 | Sample plugins are provided in the `/plugins` directory implemented using Rust, TypeScript, Go, and C#. These samples are implemented in the default configuration on install. 300 | 301 | ### Plug-in Configuration 302 | 303 | Plugins are defined in `config.yaml`, stored in `~/.assembllm`. The first plugin in the configuration file will be used as the default. 304 | 305 | The provided plugin configuration defines an [Extism manifest](https://extism.org/docs/concepts/manifest/) that `assembllm` uses to load the Wasm module, grant it relevant permissions, and provide configuration data. By default, Wasm is sandboxed, unable to access the filesystem, make network calls, or access system information like environment variables unless explicitly granted by the host. 306 | 307 | Let's walk through a sample configuration. We're importing a plugin named openai whose Wasm source is loaded from a remote URL. A hash is provided to confirm the integrity of the Wasm source. The `apiKey` for the plugin will be loaded from an environment variable named `OPENAI_API_KEY` and passed as a configuration value to the plugin. The base URL the plugin will use to make API calls to the OpenAI API is provided, granting the plugin permission to call that resource as an allowed host. Lastly, we set a default model, which is passed as a configuration value to the plugin. 308 | 309 | ```yml 310 | completion-plugins: 311 | - name: openai 312 | source: https://cdn.modsurfer.dylibso.com/api/v1/module/114e1e892c43baefb4d50cc8b0e9f66df2b2e3177de9293ffdd83898c77e04c7.wasm 313 | hash: 114e1e892c43baefb4d50cc8b0e9f66df2b2e3177de9293ffdd83898c77e04c7 314 | apiKey: OPENAI_API_KEY 315 | url: api.openai.com 316 | model: 4o 317 | ... 318 | ``` 319 | 320 | Here is the full list of available plug-in configuration values: 321 | 322 | - `name`: unique name for the plugin. 323 | - `source`: wasm file location, can be a file path or http location. 324 | - `hash`: sha 256-based hash of the wasm file for validation. Optional, but recommended. 325 | - `apiKey`: environment variable name containing the API Key for the service the plug-in uses 326 | - `accountId`: environment variable name containing the AccountID for the plugin's service. Optional, used by some services like [Cloudflare](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-api-token-and-account-id). 327 | - `url`: the base url for the service used by the plug-in. 328 | - `model`: default model to use. 329 | - `wasi`: whether or not the plugin requires WASI. 330 | 331 | ### Plug-in Architecture 332 | 333 | To be compatible with `assembllm`, each plugin must expose two functions via the PDK: 334 | 335 | - **Models**: provides a list of models supported by the plug-in 336 | - **Completion**: takes a string prompt and returns a completions response 337 | 338 | Optionally, a plugin that supports tool / function calling can export: 339 | 340 | - **completionWithTools**: takes JSON input defining one or many tools and a prompt and returns structured data 341 | 342 | ### models Function 343 | 344 | A `models` function should be exported by the plug-in and return an array of models supported by the LLM. Each object has the following properties: 345 | 346 | - `name` (string): The name of the model. 347 | - `aliases` (array): An array of strings, each string is an alias for the model. 348 | 349 | Sample response: 350 | 351 | ```json 352 | [ 353 | { 354 | "name": "gpt-4o", 355 | "aliases": ["4o"] 356 | }, 357 | { 358 | "name": "gpt-4", 359 | "aliases": ["4"] 360 | }, 361 | { 362 | "name": "gpt-3.5", 363 | "aliases": ["35"] 364 | } 365 | ] 366 | ``` 367 | 368 | Here's a JSON Schema for the objects in the `models` array: 369 | 370 | ```json 371 | { 372 | "$schema": "http://json-schema.org/draft-07/schema#", 373 | "type": "array", 374 | "items": { 375 | "type": "object", 376 | "properties": { 377 | "name": { 378 | "type": "string" 379 | }, 380 | "aliases": { 381 | "type": "array", 382 | "items": { 383 | "type": "string" 384 | } 385 | } 386 | }, 387 | "required": ["name", "aliases"] 388 | } 389 | } 390 | ``` 391 | 392 | ### completion Function 393 | 394 | A `completion` function should be exported by the plug-in that takes the prompt and configuration as input and provides the chat completion response as output. 395 | 396 | The plug-in is also provided configuration data from the `assembllm` host: 397 | 398 | - `api_key`: user's API Key to use for the API service call 399 | - `accountId`: Account ID for the plug-in service. Used by some services like [Cloudflare](https://developers.cloudflare.com/workers-ai/get-started/rest-api/#1-get-api-token-and-account-id). 400 | - `model`: LLM model to use for completions response 401 | - `temperature`: temperature value for the completion response 402 | - `role`: prompt to use as the system message for the prompt 403 | 404 | ### completionWithTools Function 405 | 406 | A `completionWithTools` function can be exported by the plug-in that takes tools definitions and a message with a prompt. 407 | 408 | The structure of the JSON looks like: 409 | 410 | ```json 411 | { 412 | "tools": [ 413 | { 414 | "name": "tool_name", 415 | "description": "A brief description of what the tool does", 416 | "input_schema": { 417 | "type": "object", 418 | "properties": { 419 | "property_name_1": { 420 | "type": "data_type", 421 | "description": "Description of property_name_1" 422 | }, 423 | "property_name_2": { 424 | "type": "data_type", 425 | "description": "Description of property_name_2" 426 | } 427 | // Additional properties as needed 428 | }, 429 | "required": [ 430 | "property_name_1", 431 | "property_name_2" 432 | // Additional required properties as needed 433 | ] 434 | } 435 | } 436 | // Additional tools as needed 437 | ], 438 | "messages": [ 439 | { 440 | "role": "user", 441 | "content": "prompt" 442 | } 443 | ] 444 | } 445 | ``` 446 | 447 | Example: 448 | 449 | ```json 450 | { 451 | "tools": [ 452 | { 453 | "name": "get_weather", 454 | "description": "Get the current weather in a given location", 455 | "input_schema": { 456 | "type": "object", 457 | "properties": { 458 | "location": { 459 | "type": "string", 460 | "description": "The city and state, e.g. San Francisco, CA" 461 | }, 462 | "unit": { 463 | "type": "string", 464 | "description": "The unit of temperature, always celsius" 465 | } 466 | }, 467 | "required": ["location", "unit"] 468 | } 469 | } 470 | ], 471 | "messages": [{"role": "user","content": "What is the weather like in San Francisco?"}] 472 | } 473 | -------------------------------------------------------------------------------- /assets/assembllm_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradyjoslin/assembllm/826f6533313556459d9a8c902f68d1a5bd613d07/assets/assembllm_banner.png -------------------------------------------------------------------------------- /assets/assembllm_logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradyjoslin/assembllm/826f6533313556459d9a8c902f68d1a5bd613d07/assets/assembllm_logo.webp -------------------------------------------------------------------------------- /assets/basic_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradyjoslin/assembllm/826f6533313556459d9a8c902f68d1a5bd613d07/assets/basic_demo.gif -------------------------------------------------------------------------------- /assets/choose_model_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradyjoslin/assembllm/826f6533313556459d9a8c902f68d1a5bd613d07/assets/choose_model_demo.gif -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradyjoslin/assembllm/826f6533313556459d9a8c902f68d1a5bd613d07/assets/demo.gif -------------------------------------------------------------------------------- /assets/piping_curl_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradyjoslin/assembllm/826f6533313556459d9a8c902f68d1a5bd613d07/assets/piping_curl_demo.gif -------------------------------------------------------------------------------- /completions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/charmbracelet/glamour" 11 | extism "github.com/extism/go-sdk" 12 | "gopkg.in/yaml.v3" 13 | ) 14 | 15 | type Model struct { 16 | Name string `json:"name"` 17 | Aliases []string `json:"aliases"` 18 | } 19 | 20 | type CompletionsPlugin struct { 21 | Plugin extism.Plugin 22 | } 23 | 24 | type Property struct { 25 | Type string `json:"type" yaml:"type"` 26 | Description string `json:"description" yaml:"description"` 27 | Enum []string `json:"enum,omitempty" yaml:"enum,omitempty"` 28 | } 29 | 30 | type Schema struct { 31 | Type string `json:"type" yaml:"type"` 32 | Properties map[string]Property `json:"properties" yaml:"properties"` 33 | Required []string `json:"required" yaml:"required"` 34 | } 35 | 36 | type Tool struct { 37 | Name string `json:"name" yaml:"name"` 38 | Description string `json:"description" yaml:"description"` 39 | InputSchema Schema `json:"input_schema" yaml:"input_schema"` 40 | } 41 | 42 | type Message struct { 43 | Role string `json:"role" yaml:"role"` 44 | Content string `json:"content" yaml:"content"` 45 | } 46 | 47 | type Request struct { 48 | Tools []Tool `json:"tools" yaml:"tools"` 49 | Messages []Message `json:"messages" yaml:"messages"` 50 | } 51 | 52 | // Get the available models from the completions plugin 53 | func (pluginCfg CompletionPluginConfig) getModels() ([]string, error) { 54 | plugin, err := pluginCfg.createPlugin() 55 | if err != nil { 56 | return nil, fmt.Errorf("failed to initialize plugin: %v", err) 57 | } 58 | 59 | modelNames, err := plugin.getModelNames() 60 | if err != nil { 61 | return nil, fmt.Errorf("failed to get models: %v", err) 62 | } 63 | 64 | return modelNames, nil 65 | } 66 | 67 | func (pluginInfo CompletionPluginConfig) generateResponseWithTools(prompt string, tools []Tool) (string, error) { 68 | plugin, err := pluginInfo.createPlugin() 69 | if err != nil { 70 | return "", fmt.Errorf("failed to initialize plugin: %v", err) 71 | } 72 | 73 | _, out, err := plugin.completionWithTools(prompt, tools) 74 | if err != nil { 75 | return "", fmt.Errorf("failed to get completion: %v", err) 76 | } 77 | 78 | return string(out), nil 79 | } 80 | 81 | // Get completions response for the prompt from the completions plugin 82 | func (pluginInfo CompletionPluginConfig) generateResponse(prompt string, raw bool) (string, error) { 83 | plugin, err := pluginInfo.createPlugin() 84 | if err != nil { 85 | return "", fmt.Errorf("failed to initialize plugin: %v", err) 86 | } 87 | 88 | _, out, err := plugin.completion(prompt) 89 | if err != nil { 90 | return "", fmt.Errorf("failed to get completion: %v", err) 91 | } 92 | 93 | response := string(out) 94 | 95 | if raw { 96 | return response, nil 97 | } else { 98 | formattedResponse, _ := glamour.Render(response, "dark") 99 | return formattedResponse, nil 100 | } 101 | } 102 | 103 | // Call an exposed Extism function on the completions plugin 104 | func (p *CompletionsPlugin) Call(method string, payload []byte) (uint32, []byte, error) { 105 | return p.Plugin.Call(method, payload) 106 | } 107 | 108 | // Create a new completions extism plugin from the configuration 109 | func (p CompletionPluginConfig) createPlugin() (CompletionsPlugin, error) { 110 | var wasm extism.Wasm 111 | 112 | if strings.HasPrefix(p.Source, "https://") { 113 | wasm = extism.WasmUrl{ 114 | Url: p.Source, 115 | Hash: p.Hash, 116 | } 117 | } else { 118 | homeDir, _ := os.UserHomeDir() 119 | p.Source = strings.Replace(p.Source, "~", homeDir, 1) 120 | 121 | if !isFilePath(p.Source) { 122 | return CompletionsPlugin{}, fmt.Errorf("file not found: %s", p.Source) 123 | } 124 | 125 | wasm = extism.WasmFile{ 126 | Path: p.Source, 127 | Hash: p.Hash, 128 | } 129 | } 130 | 131 | manifest := extism.Manifest{ 132 | Wasm: []extism.Wasm{ 133 | wasm, 134 | }, 135 | } 136 | 137 | plugin, err := extism.NewPlugin( 138 | context.Background(), 139 | manifest, 140 | extism.PluginConfig{ 141 | EnableWasi: p.Wasi, 142 | }, 143 | []extism.HostFunction{}, 144 | ) 145 | if err != nil { 146 | return CompletionsPlugin{}, err 147 | } 148 | if plugin == nil { 149 | return CompletionsPlugin{}, fmt.Errorf("plugin is nil") 150 | } 151 | 152 | plugin.AllowedHosts = []string{p.URL} 153 | plugin.Config = map[string]string{"api_key": p.APIKey, "model": p.Model, "temperature": p.Temperature, "role": p.Role, "account_id": p.AccountId} 154 | 155 | plugin.SetLogLevel(p.LogLevel) 156 | plugin.SetLogger(func(level extism.LogLevel, message string) { 157 | fmt.Printf("[%s] %s\n", level, message) 158 | }) 159 | return CompletionsPlugin{*plugin}, nil 160 | } 161 | 162 | // Get list of supported models 163 | func (plugin *CompletionsPlugin) models() (uint32, []byte, error) { 164 | return plugin.Call("models", []byte{}) 165 | } 166 | 167 | // Get completions for the prompt 168 | func (plugin *CompletionsPlugin) completion(prompt string) (uint32, []byte, error) { 169 | return plugin.Call("completion", []byte(prompt)) 170 | } 171 | 172 | func (plugin *CompletionsPlugin) completionWithTools(prompt string, tools []Tool) (uint32, []byte, error) { 173 | request := Request{ 174 | Tools: tools, 175 | Messages: []Message{ 176 | { 177 | Role: "user", 178 | Content: prompt, 179 | }, 180 | }, 181 | } 182 | 183 | data, err := json.Marshal(request) 184 | if err != nil { 185 | return 0, nil, err 186 | } 187 | 188 | return plugin.Call("completionWithTools", data) 189 | } 190 | 191 | // Get a plugin configuration from the available plugins 192 | func (plugins CompletionPluginConfigs) getPlugin(pluginName string) (CompletionPluginConfig, error) { 193 | var pluginInfo CompletionPluginConfig 194 | for _, p := range plugins.Plugins { 195 | if p.Name == pluginName { 196 | pluginInfo = p 197 | break 198 | } 199 | } 200 | if pluginInfo.Name == "" { 201 | return CompletionPluginConfig{}, fmt.Errorf("plugin not found: %s", pluginName) 202 | } 203 | return pluginInfo, nil 204 | } 205 | 206 | // Get the available models from the completions plugin 207 | func (plugin CompletionsPlugin) getModelNames() ([]string, error) { 208 | _, jsonMs, err := plugin.models() 209 | if err != nil { 210 | return nil, fmt.Errorf("failed to get models: %v", err) 211 | } 212 | var models []Model 213 | err = json.Unmarshal([]byte(jsonMs), &models) 214 | if err != nil { 215 | return nil, fmt.Errorf("failed to unmarshal models: %v", err) 216 | } 217 | 218 | var modelNames []string 219 | for _, model := range models { 220 | modelNames = append(modelNames, model.Name) 221 | } 222 | return modelNames, nil 223 | } 224 | 225 | // Unmarshal the plugin configuration from the yaml file 226 | func (p *CompletionPluginConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { 227 | type rawPlugin CompletionPluginConfig 228 | raw := rawPlugin{} 229 | 230 | if err := unmarshal(&raw); err != nil { 231 | return err 232 | } 233 | 234 | raw.APIKey = os.Getenv(raw.APIKey) 235 | raw.AccountId = os.Getenv(raw.AccountId) 236 | raw.LogLevel = logLevel 237 | 238 | *p = CompletionPluginConfig(raw) 239 | 240 | return nil 241 | } 242 | 243 | // Check if the path is a file 244 | func isFilePath(s string) bool { 245 | info, err := os.Stat(s) 246 | return !os.IsNotExist(err) && !info.IsDir() 247 | } 248 | 249 | // Loads the available chat completion plugins from a yaml file 250 | func getAvailablePlugins(filename string) (CompletionPluginConfigs, error) { 251 | file, err := os.ReadFile(filename) 252 | if err != nil { 253 | return CompletionPluginConfigs{}, err 254 | } 255 | 256 | var completionPluginConfigs CompletionPluginConfigs 257 | err = yaml.Unmarshal(file, &completionPluginConfigs) 258 | if err != nil { 259 | return CompletionPluginConfigs{}, err 260 | } 261 | 262 | return completionPluginConfigs, nil 263 | } 264 | 265 | // Gets the available plugins from the yaml file, then gets the plugin config for the specified plugin 266 | func getPluginConfig(pluginName string, configPath string) (CompletionPluginConfig, error) { 267 | pluginConfigs, err := getAvailablePlugins(configPath) 268 | if err != nil { 269 | return CompletionPluginConfig{}, fmt.Errorf("failed to get config from yaml: %v", err) 270 | } 271 | 272 | pluginCfg, err := pluginConfigs.getPlugin(pluginName) 273 | if err != nil { 274 | return CompletionPluginConfig{}, fmt.Errorf("failed to get plugin info: %v", err) 275 | } 276 | 277 | return pluginCfg, nil 278 | } 279 | -------------------------------------------------------------------------------- /completions_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func shouldSkip() bool { 9 | return os.Getenv("SKIP_CHAT_RESPONSE_TESTS") == "true" 10 | } 11 | 12 | func TestBadConfigFilePath(t *testing.T) { 13 | t.Parallel() 14 | 15 | _, err := getPluginConfig("openai", "badpath") 16 | if err == nil { 17 | t.Fatalf("expected error, got nil") 18 | } 19 | } 20 | 21 | func TestGetNonExistentPluginConfig(t *testing.T) { 22 | t.Parallel() 23 | 24 | _, err := getPluginConfig("", "config.yaml") 25 | if err == nil { 26 | t.Fatalf("expected error, got nil") 27 | } 28 | } 29 | 30 | func TestGetPluginConfig(t *testing.T) { 31 | t.Parallel() 32 | 33 | want := "openai" 34 | 35 | pluginCfg, err := getPluginConfig(want, "config.yaml") 36 | if err != nil { 37 | t.Fatalf("expected nil, got %v", err) 38 | } 39 | 40 | got := pluginCfg.Name 41 | 42 | if got != want { 43 | t.Fatalf("want %s, got %s", want, got) 44 | } 45 | } 46 | 47 | func TestGetModels(t *testing.T) { 48 | t.Parallel() 49 | 50 | pluginCfg, err := getPluginConfig("openai", "config.yaml") 51 | if err != nil { 52 | t.Fatalf("expected nil, got %v", err) 53 | } 54 | 55 | models, err := pluginCfg.getModels() 56 | if err != nil { 57 | t.Fatalf("expected nil, got %v", err) 58 | } 59 | 60 | if len(models) == 0 { 61 | t.Fatalf("expected models, got none") 62 | } 63 | } 64 | 65 | func TestGetResponse(t *testing.T) { 66 | t.Parallel() 67 | 68 | if shouldSkip() { 69 | t.Skip("Skipping this test") 70 | } 71 | 72 | pluginCfg, err := getPluginConfig("openai", "config.yaml") 73 | if err != nil { 74 | t.Fatalf("expected nil, got %v", err) 75 | } 76 | 77 | _, err = pluginCfg.generateResponse("hello", false) 78 | if err != nil { 79 | t.Fatalf("expected nil, got %v", err) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | 9 | extism "github.com/extism/go-sdk" 10 | ) 11 | 12 | const configFileName = "config.yaml" 13 | 14 | var ( 15 | //go:embed config.yaml 16 | defaultConfig []byte 17 | ) 18 | 19 | type CompletionPluginConfig struct { 20 | Name string `yaml:"name"` 21 | Source string `yaml:"source"` 22 | Hash string `yaml:"hash"` 23 | APIKey string `yaml:"apiKey"` 24 | AccountId string `yaml:"accountId"` 25 | URL string `yaml:"url"` 26 | Model string `yaml:"model"` 27 | Temperature string `yaml:"temperature"` 28 | Role string `yaml:"role"` 29 | Wasi bool `yaml:"wasi"` 30 | LogLevel extism.LogLevel 31 | } 32 | 33 | type CompletionPluginConfigs struct { 34 | Plugins []CompletionPluginConfig `yaml:"completion-plugins"` 35 | } 36 | 37 | type Config struct { 38 | CompletionPluginConfigs CompletionPluginConfigs `yaml:"completion-plugins"` 39 | } 40 | 41 | func createConfig(configPath string) { 42 | // Ensure the directory exists 43 | if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { 44 | log.Fatalf("Unable to create directory for config file: %v", err) 45 | } 46 | 47 | // Write the default configuration to the new configuration file 48 | err := os.WriteFile(configPath, defaultConfig, 0600) 49 | if err != nil { 50 | log.Fatalf("Unable to write default config to file: %v", err) 51 | } 52 | } 53 | 54 | func readConfig(configPath string) []byte { 55 | // Read the existing config file 56 | configData, err := os.ReadFile(configPath) 57 | if err != nil { 58 | log.Fatalf("Unable to read config file: %v", err) 59 | } 60 | return configData 61 | } 62 | 63 | func writeConfig(configDataUpdates []byte, configPath string) { 64 | // Write the updated config back to the file 65 | err := os.WriteFile(configPath, configDataUpdates, 0600) 66 | if err != nil { 67 | log.Fatalf("Unable to write updated config to file: %v", err) 68 | } 69 | } 70 | 71 | func getConfigPath() string { 72 | homeDir, err := os.UserHomeDir() 73 | if err != nil { 74 | log.Fatalf("Unable to get user's home directory: %v", err) 75 | } 76 | 77 | configPath := filepath.Join(homeDir, "."+appName, configFileName) 78 | return configPath 79 | } 80 | 81 | func setupConfig() { 82 | configPath := getConfigPath() 83 | 84 | // Check if the configuration file exists 85 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 86 | createConfig(configPath) 87 | } else { 88 | configData := readConfig(configPath) 89 | 90 | configDataUpdates := updatePlugins(configData) 91 | 92 | writeConfig(configDataUpdates, configPath) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | completion-plugins: 2 | - name: openai 3 | source: https://github.com/bradyjoslin/assembllm-openai/releases/latest/download/assembllm_openai.wasm 4 | hash: 5 | apiKey: OPENAI_API_KEY 6 | url: api.openai.com 7 | model: 8 | - name: perplexity 9 | source: https://github.com/bradyjoslin/assembllm-perplexity/releases/latest/download/assembllm_perplexity.wasm 10 | hash: 11 | apiKey: PERPLEXITY_API_KEY 12 | url: api.perplexity.ai 13 | model: 14 | - name: cloudflare 15 | source: https://github.com/bradyjoslin/assembllm-cloudflare/releases/latest/download/assembllm_cloudflare.wasm 16 | hash: 17 | apiKey: WORKERS_AI_TOKEN 18 | accountId: CF_ACCOUNT_ID 19 | url: api.cloudflare.com 20 | model: 21 | - name: anthropic 22 | source: https://github.com/bradyjoslin/assembllm-anthropic-go/releases/latest/download/assembllm-anthropic-go.wasm 23 | hash: 24 | apiKey: ANTHROPIC_API_KEY 25 | url: api.anthropic.com 26 | model: 27 | wasi: true 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bradyjoslin/assembllm 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/charmbracelet/lipgloss v0.11.0 7 | github.com/extism/go-sdk v1.2.0 8 | github.com/yuin/goldmark v1.5.4 9 | ) 10 | 11 | require ( 12 | github.com/alecthomas/chroma/v2 v2.8.0 // indirect 13 | github.com/atotto/clipboard v0.1.4 // indirect 14 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 15 | github.com/aymerick/douceur v0.2.0 // indirect 16 | github.com/catppuccin/go v0.2.0 // indirect 17 | github.com/charmbracelet/bubbles v0.18.0 // indirect 18 | github.com/charmbracelet/bubbletea v0.26.3 // indirect 19 | github.com/charmbracelet/x/ansi v0.1.1 // indirect 20 | github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect 21 | github.com/charmbracelet/x/input v0.1.1 // indirect 22 | github.com/charmbracelet/x/term v0.1.1 // indirect 23 | github.com/charmbracelet/x/windows v0.1.2 // indirect 24 | github.com/dlclark/regexp2 v1.4.0 // indirect 25 | github.com/dustin/go-humanize v1.0.1 // indirect 26 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 27 | github.com/gorilla/css v1.0.0 // indirect 28 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 29 | github.com/itchyny/gojq v0.12.13 // indirect 30 | github.com/itchyny/timefmt-go v0.1.5 // indirect 31 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 32 | github.com/mattn/go-isatty v0.0.20 // indirect 33 | github.com/mattn/go-localereader v0.0.1 // indirect 34 | github.com/mattn/go-runewidth v0.0.15 // indirect 35 | github.com/microcosm-cc/bluemonday v1.0.25 // indirect 36 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 37 | github.com/muesli/cancelreader v0.2.2 // indirect 38 | github.com/muesli/reflow v0.3.0 // indirect 39 | github.com/muesli/termenv v0.15.2 // indirect 40 | github.com/olekukonko/tablewriter v0.0.5 // indirect 41 | github.com/rivo/uniseg v0.4.7 // indirect 42 | github.com/spf13/pflag v1.0.5 // indirect 43 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 44 | github.com/yuin/goldmark-emoji v1.0.2 // indirect 45 | golang.org/x/net v0.23.0 // indirect 46 | golang.org/x/sync v0.7.0 // indirect 47 | golang.org/x/sys v0.20.0 // indirect 48 | golang.org/x/text v0.15.0 // indirect 49 | mvdan.cc/sh/v3 v3.7.0 // indirect 50 | ) 51 | 52 | require ( 53 | github.com/bitfield/script v0.22.1 54 | github.com/charmbracelet/glamour v0.7.0 55 | github.com/charmbracelet/huh v0.4.2 56 | github.com/charmbracelet/huh/spinner v0.0.0-20240529143420-2ae64435bd5d 57 | github.com/expr-lang/expr v1.16.9 58 | github.com/gobwas/glob v0.2.3 // indirect 59 | github.com/spf13/cobra v1.8.0 60 | github.com/tetratelabs/wazero v1.3.0 // indirect 61 | gopkg.in/yaml.v3 v3.0.1 62 | ) 63 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= 2 | github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= 3 | github.com/alecthomas/chroma/v2 v2.8.0 h1:w9WJUjFFmHHB2e8mRpL9jjy3alYDlU0QLDezj1xE264= 4 | github.com/alecthomas/chroma/v2 v2.8.0/go.mod h1:yrkMI9807G1ROx13fhe1v6PN2DDeaR73L3d+1nmYQtw= 5 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 6 | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 7 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 8 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 9 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 10 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 11 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 12 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 13 | github.com/bitfield/script v0.22.1 h1:DphxoC5ssYciwd0ZS+N0Xae46geAD/0mVWh6a2NUxM4= 14 | github.com/bitfield/script v0.22.1/go.mod h1:fv+6x4OzVsRs6qAlc7wiGq8fq1b5orhtQdtW0dwjUHI= 15 | github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA= 16 | github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= 17 | github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= 18 | github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= 19 | github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg= 20 | github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q= 21 | github.com/charmbracelet/glamour v0.7.0 h1:2BtKGZ4iVJCDfMF229EzbeR1QRKLWztO9dMtjmqZSng= 22 | github.com/charmbracelet/glamour v0.7.0/go.mod h1:jUMh5MeihljJPQbJ/wf4ldw2+yBP59+ctV36jASy7ps= 23 | github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM= 24 | github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0= 25 | github.com/charmbracelet/huh/spinner v0.0.0-20240529143420-2ae64435bd5d h1:Rz6VB8MuncqGbY6kub0uQ2DJzMruKfmtMdpoSigbT1U= 26 | github.com/charmbracelet/huh/spinner v0.0.0-20240529143420-2ae64435bd5d/go.mod h1:1cf8ar2//4C/JYpK4/EHprHIKQDanErotkH8enQWG6g= 27 | github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g= 28 | github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8= 29 | github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk= 30 | github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= 31 | github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a h1:lOpqe2UvPmlln41DGoii7wlSZ/q8qGIon5JJ8Biu46I= 32 | github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= 33 | github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE= 34 | github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU= 35 | github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4= 36 | github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0= 37 | github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= 38 | github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= 39 | github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= 40 | github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= 41 | github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 42 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 43 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 44 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= 45 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 46 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 47 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 48 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 49 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 50 | github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= 51 | github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= 52 | github.com/extism/go-sdk v1.2.0 h1:A0DnIMthdP8h6K9NbRpRs1PIXHOUlb/t/TZWk5eUzx4= 53 | github.com/extism/go-sdk v1.2.0/go.mod h1:xUfKSEQndAvHBc1Ohdre0e+UdnRzUpVfbA8QLcx4fbY= 54 | github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 55 | github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 56 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 57 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 58 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 59 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 60 | github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= 61 | github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= 62 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 63 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 64 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 65 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 66 | github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= 67 | github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= 68 | github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= 69 | github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= 70 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 71 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 72 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 73 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 74 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 75 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 76 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 77 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 78 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 79 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 80 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 81 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 82 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 83 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 84 | github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= 85 | github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= 86 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 87 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 88 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 89 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 90 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 91 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 92 | github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= 93 | github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= 94 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 95 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 96 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 97 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 98 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 99 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 100 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 101 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 102 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 103 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 104 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 105 | github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= 106 | github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 107 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 108 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 109 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 110 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 111 | github.com/tetratelabs/wazero v1.3.0 h1:nqw7zCldxE06B8zSZAY0ACrR9OH5QCcPwYmYlwtcwtE= 112 | github.com/tetratelabs/wazero v1.3.0/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= 113 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 114 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 115 | github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 116 | github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= 117 | github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 118 | github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= 119 | github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= 120 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 121 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 122 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 123 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 124 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 125 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 126 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= 129 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 130 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= 131 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 132 | golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= 133 | golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= 134 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 135 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 136 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 137 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 138 | mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= 139 | mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= 140 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/charmbracelet/huh" 12 | "github.com/charmbracelet/huh/spinner" 13 | "github.com/charmbracelet/lipgloss" 14 | extism "github.com/extism/go-sdk" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | type App struct { 19 | Config AppConfig 20 | RootCmd *cobra.Command 21 | } 22 | 23 | type AppConfig struct { 24 | Name string 25 | Model string 26 | ChooseAIModel bool 27 | ChoosePlugin bool 28 | ChooseWorkflow bool 29 | Temperature string 30 | Role string 31 | Raw bool 32 | Version bool 33 | WorkflowPath string 34 | IteratorPrompt bool 35 | CurrentIterationValue interface{} 36 | Feedback bool 37 | } 38 | 39 | const ( 40 | version = "0.7.0" 41 | ) 42 | 43 | var ( 44 | appCfg AppConfig 45 | logLevel = extism.LogLevelOff 46 | appName = filepath.Base(os.Args[0]) 47 | ) 48 | 49 | // Gets the available models from the completions plugin and prompts the user to choose one 50 | func chooseModel(pluginCfg CompletionPluginConfig) (string, error) { 51 | modelNames, err := pluginCfg.getModels() 52 | if err != nil { 53 | return "", fmt.Errorf("failed to get models: %v", err) 54 | } 55 | 56 | var opts []huh.Option[string] 57 | for _, model := range modelNames { 58 | opts = append(opts, huh.Option[string]{ 59 | Key: model, 60 | Value: model, 61 | }) 62 | } 63 | 64 | var model string 65 | huh.NewSelect[string](). 66 | Title("Choose a model:"). 67 | Options(opts...). 68 | Value(&model). 69 | WithTheme(huh.ThemeCharm()). 70 | Run() 71 | 72 | return model, nil 73 | } 74 | 75 | // Initializes the flags for the root command 76 | func initializeFlags(app *App) { 77 | app.RootCmd.CompletionOptions.HiddenDefaultCmd = true 78 | app.RootCmd.SetHelpCommand(&cobra.Command{Hidden: true}) 79 | 80 | flags := app.RootCmd.Flags() 81 | flags.StringVarP(&appCfg.Name, "plugin", "p", "openai", "The name of the plugin to use") 82 | flags.BoolVarP(&appCfg.ChoosePlugin, "choose-plugin", "P", false, "Choose the plugin to use") 83 | flags.StringVarP(&appCfg.Model, "model", "m", "", "The name of the model to use") 84 | flags.BoolVarP(&appCfg.ChooseAIModel, "choose-model", "M", false, "Choose the model to use") 85 | flags.BoolVarP(&appCfg.ChooseAIModel, "choose-model(deprecated)", "c", false, "Choose the model to use") 86 | flags.MarkHidden("choose-model(deprecated)") 87 | flags.MarkShorthandDeprecated("choose-model(deprecated)", "use -M instead") 88 | flags.StringVarP(&appCfg.Temperature, "temperature", "t", "", "The temperature to use") 89 | flags.StringVarP(&appCfg.Role, "role", "r", "", "The role to use") 90 | flags.BoolVarP(&appCfg.Raw, "raw", "", false, "Raw output without formatting") 91 | flags.BoolVarP(&appCfg.Version, "version", "v", false, "Print the version") 92 | flags.StringVarP(&appCfg.WorkflowPath, "workflow", "w", "", "The path to a workflow file") 93 | flags.BoolVarP(&appCfg.ChooseWorkflow, "choose-workflow", "W", false, "Choose a workflow to run") 94 | flags.BoolVarP(&appCfg.IteratorPrompt, "iterator", "i", false, "String array of prompts ['prompt1', 'prompt2']") 95 | flags.BoolVarP(&appCfg.Feedback, "feedback", "f", false, "Optionally provide feedback and rerun workflow") 96 | flags.SortFlags = false 97 | } 98 | 99 | // Generates a prompt for the chat completions 100 | // If there is input from stdin, it will be included in the prompt 101 | // If the user specified a prompt as an argument, it will be included in the prompt 102 | // If there is no prompt, the user will be prompted to enter one 103 | func generatePrompt(args []string, ask bool) string { 104 | var prompt string 105 | stdInStats, err := os.Stdin.Stat() 106 | if err != nil { 107 | fmt.Println("error getting stdin stats:", err) 108 | os.Exit(1) 109 | } 110 | 111 | if (stdInStats.Mode() & os.ModeCharDevice) == 0 { 112 | reader := bufio.NewReader(os.Stdin) 113 | s, err := io.ReadAll(reader) 114 | if err != nil { 115 | fmt.Println("error reading from stdin:", err) 116 | os.Exit(1) 117 | } 118 | prompt += string(s) 119 | } 120 | 121 | if len(args) == 1 { 122 | prompt += args[0] 123 | } 124 | 125 | if prompt == "" && ask { 126 | err := huh.NewInput(). 127 | Title("What would you like to ask or discuss?"). 128 | Value(&prompt). 129 | WithTheme(huh.ThemeCharm()). 130 | Run() 131 | if err != nil { 132 | fmt.Println("error getting input:", err) 133 | os.Exit(1) 134 | } 135 | } 136 | 137 | return prompt 138 | } 139 | 140 | // Overrides the plugin config with the user flags 141 | func overridePluginConfigWithUserFlags(appConfig AppConfig, pluginConfig CompletionPluginConfig) CompletionPluginConfig { 142 | if appConfig.Model != "" { 143 | pluginConfig.Model = appConfig.Model 144 | } 145 | 146 | if appConfig.Temperature != "" { 147 | pluginConfig.Temperature = appConfig.Temperature 148 | } 149 | 150 | if appConfig.Role != "" { 151 | pluginConfig.Role = appConfig.Role 152 | } 153 | 154 | return pluginConfig 155 | } 156 | 157 | func choosePlugin() (string, error) { 158 | pluginCfgs, err := getAvailablePlugins(getConfigPath()) 159 | if err != nil { 160 | return "", err 161 | } 162 | 163 | var opts []huh.Option[string] 164 | for _, plugin := range pluginCfgs.Plugins { 165 | opts = append(opts, huh.Option[string]{ 166 | Key: plugin.Name, 167 | Value: plugin.Name, 168 | }) 169 | } 170 | 171 | var plugin string 172 | huh.NewSelect[string](). 173 | Title("Choose a plugin:"). 174 | Options(opts...). 175 | Value(&plugin). 176 | WithTheme(huh.ThemeCharm()). 177 | Run() 178 | 179 | return plugin, nil 180 | } 181 | 182 | func createSpinner(action func()) error { 183 | return spinner.New(). 184 | Title("Generating..."). 185 | TitleStyle(lipgloss.NewStyle().Faint(true)). 186 | Action(action). 187 | Run() 188 | } 189 | 190 | func printVersion() { 191 | fmt.Println(appName + " " + version) 192 | } 193 | 194 | func chooseWorkflow() (string, error) { 195 | var workflowPath string 196 | err := huh.NewFilePicker(). 197 | Title("Select a workflow file:"). 198 | AllowedTypes([]string{".yaml", "yml"}). 199 | Value(&workflowPath). 200 | Picking(true). 201 | Height(10). 202 | Run() 203 | if err != nil { 204 | return "", fmt.Errorf("error choosing workflow: %v", err) 205 | } 206 | 207 | return workflowPath, nil 208 | } 209 | 210 | func buildIteratorPrompts(args []string) []string { 211 | var prompt string 212 | if len(args) > 0 { 213 | prompt = args[0] 214 | } else { 215 | huh.NewInput(). 216 | Title("Enter the prompts in brackets separated by commas:"). 217 | Value(&prompt). 218 | Placeholder("[prompt1, prompt2]"). 219 | WithTheme(huh.ThemeCharm()). 220 | Run() 221 | } 222 | var prompts []string 223 | ps := strings.Trim(prompt, "[]") 224 | prompts = strings.Split(ps, ",") 225 | return prompts 226 | } 227 | 228 | func executeCompletion(pc CompletionPluginConfig, prompt string, spin bool) (string, error) { 229 | var res string 230 | var err error 231 | 232 | if spin { 233 | err = createSpinner( 234 | func() { 235 | res, err = pc.generateResponse(prompt, appCfg.Raw) 236 | }, 237 | ) 238 | } else { 239 | res, err = pc.generateResponse(prompt, appCfg.Raw) 240 | } 241 | if err != nil { 242 | return "", err 243 | } 244 | 245 | return res, nil 246 | } 247 | 248 | func runCommand(cmd *cobra.Command, args []string) error { 249 | if appCfg.Version { 250 | printVersion() 251 | return nil 252 | } 253 | 254 | if appCfg.ChooseWorkflow { 255 | wp, err := chooseWorkflow() 256 | 257 | appCfg.WorkflowPath = wp 258 | if err != nil { 259 | return err 260 | } 261 | } 262 | 263 | if appCfg.WorkflowPath != "" { 264 | return executeWorkflow(args) 265 | } 266 | 267 | if appCfg.ChoosePlugin { 268 | pluginName, err := choosePlugin() 269 | if err != nil { 270 | return err 271 | } 272 | appCfg.Name = pluginName 273 | } 274 | 275 | pluginCfg, err := getPluginConfig(appCfg.Name, getConfigPath()) 276 | if err != nil { 277 | return err 278 | } 279 | 280 | pluginCfg = overridePluginConfigWithUserFlags(appCfg, pluginCfg) 281 | 282 | if appCfg.ChooseAIModel { 283 | pluginCfg.Model, err = chooseModel(pluginCfg) 284 | if err != nil { 285 | return err 286 | } 287 | } 288 | 289 | if appCfg.IteratorPrompt { 290 | prompts := buildIteratorPrompts(args) 291 | 292 | for _, p := range prompts { 293 | res, err := executeCompletion(pluginCfg, p, true) 294 | if err != nil { 295 | return err 296 | } 297 | 298 | fmt.Println(res) 299 | } 300 | return nil 301 | } 302 | 303 | prompt := generatePrompt(args, true) 304 | res, err := executeCompletion(pluginCfg, prompt, true) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | fmt.Print(res) 310 | return nil 311 | } 312 | 313 | func main() { 314 | app := &App{ 315 | Config: AppConfig{}, 316 | RootCmd: &cobra.Command{ 317 | Use: appName + " [prompt]", 318 | Short: "A WASM plug-in based CLI for AI chat completions", 319 | Args: cobra.ArbitraryArgs, 320 | RunE: runCommand, 321 | SilenceUsage: true, 322 | SilenceErrors: true, 323 | }, 324 | } 325 | 326 | initializeFlags(app) 327 | setupConfig() 328 | 329 | if err := app.RootCmd.Execute(); err != nil { 330 | fmt.Println(err) 331 | os.Exit(1) 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # Assembllm Plugins 2 | 3 | ## LLM plugins: 4 | 5 | | Service | Language | Source | 6 | | ---------- | ---------- | --------------------------------------------------------------------------------- | 7 | | OpenAI | Rust | [assembllm-openai](https://github.com/bradyjoslin/assembllm-openai) | 8 | | Perplexity | Rust | [assembllm-perplexity](https://github.com/bradyjoslin/assembllm-perplexity) | 9 | | Cloudflare | Rust | [assembllm-cloudflare](https://github.com/bradyjoslin/assembllm-cloudflare) | 10 | | Anthropic | Go | [assembllm-anthropic-go](https://github.com/bradyjoslin/assembllm-anthropic-go) | 11 | | OpenAI | Go | [assembllm-openai-go](https://github.com/bradyjoslin/assembllm-openai-go) | 12 | | OpenAI | TypeScript | [assembllm-openai-ts](https://github.com/bradyjoslin/assembllm-openai-ts) | 13 | | OpenAI | CSharp | [assembllm-openai-csharp](https://github.com/bradyjoslin/assembllm-openai-csharp) | 14 | 15 | ## Script Plug-ins 16 | 17 | ### Assembllm HTML Tools 18 | 19 | Source: https://github.com/bradyjoslin/assembllm-htmltools 20 | 21 | ### HTML Scraper 22 | 23 | **Input** 24 | 25 | The `scraper` function expects a JSON input with the following structure: 26 | 27 | - `html`: The HTML content as a string. 28 | - `selector`: A CSS selector to identify the elements to extract text from. 29 | 30 | **Output** 31 | 32 | The function outputs the text content of the matched elements. 33 | 34 | 35 | ### HTML Rewriter 36 | 37 | The `htmlrewrite` function expects a JSON input with the following structure: 38 | 39 | ```json 40 | { 41 | "html": "", 42 | "rules": [ 43 | { 44 | "selector": "", 45 | "html_content": "" 46 | }, 47 | ] 48 | } 49 | ``` 50 | 51 | **Output** 52 | 53 | The function outputs the modified HTML content as a string. 54 | -------------------------------------------------------------------------------- /scripts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/bitfield/script" 15 | "github.com/expr-lang/expr" 16 | extism "github.com/extism/go-sdk" 17 | "github.com/yuin/goldmark" 18 | "github.com/yuin/goldmark/extension" 19 | ) 20 | 21 | func resend(to string, from string, subject string, body string) error { 22 | var html bytes.Buffer 23 | 24 | gm := goldmark.New( 25 | goldmark.WithExtensions( 26 | extension.Linkify, 27 | extension.Strikethrough, 28 | extension.Table, 29 | ), 30 | ) 31 | _ = gm.Convert([]byte(body), &html) 32 | 33 | escapedHTML := strconv.QuoteToASCII(html.String()) 34 | escapedHTML = escapedHTML[1 : len(escapedHTML)-1] // Remove the extra double quotes 35 | 36 | payload := []byte(fmt.Sprintf(`{ 37 | "from": "%s", 38 | "to": "%s", 39 | "subject": "%s", 40 | "html": "%s" 41 | }`, from, to, subject, escapedHTML)) 42 | 43 | client := &http.Client{} 44 | req, err := http.NewRequest("POST", "https://api.resend.com/emails", bytes.NewBuffer(payload)) 45 | 46 | if err != nil { 47 | return err 48 | } 49 | 50 | apiKey := os.Getenv("RESEND_API_KEY") 51 | if apiKey == "" { 52 | return errors.New("RESEND_API_KEY is not set") 53 | } 54 | 55 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey)) 56 | req.Header.Add("Content-Type", "application/json") 57 | 58 | res, err := client.Do(req) 59 | if err != nil { 60 | return err 61 | } 62 | defer res.Body.Close() 63 | 64 | bodyBytes, err := io.ReadAll(res.Body) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if res.StatusCode != http.StatusOK { 70 | return fmt.Errorf("error sending email \n status code: %d\n%s", res.StatusCode, string(bodyBytes)) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func httpGet(url string) (string, error) { 77 | res, err := http.Get(url) 78 | if err != nil { 79 | return "", err 80 | } 81 | defer res.Body.Close() 82 | 83 | body, err := io.ReadAll(res.Body) 84 | if err != nil { 85 | return "", err 86 | } 87 | 88 | return string(body), nil 89 | } 90 | 91 | func appendFile(content string, path string) (int64, error) { 92 | b, err := script.Echo(content).AppendFile(path) 93 | if err != nil { 94 | return 0, err 95 | } 96 | return b, nil 97 | } 98 | 99 | func readfile(path string) (string, error) { 100 | content, err := os.ReadFile(path) 101 | if err != nil { 102 | return "", err 103 | } 104 | return string(content), nil 105 | } 106 | 107 | func callExtismPlugin(source string, function string, input string) (string, error) { 108 | var wasm extism.Wasm 109 | 110 | if strings.HasPrefix(source, "https://") { 111 | wasm = extism.WasmUrl{ 112 | Url: source, 113 | } 114 | } else { 115 | if !isFilePath(source) { 116 | return "", fmt.Errorf("file not found: %s", source) 117 | } 118 | 119 | wasm = extism.WasmFile{ 120 | Path: source, 121 | } 122 | } 123 | 124 | manifest := extism.Manifest{ 125 | Wasm: []extism.Wasm{ 126 | wasm, 127 | }, 128 | } 129 | 130 | plugin, err := extism.NewPlugin( 131 | context.Background(), 132 | manifest, 133 | extism.PluginConfig{ 134 | EnableWasi: true, 135 | }, 136 | []extism.HostFunction{}, 137 | ) 138 | if err != nil { 139 | return "", err 140 | } 141 | if plugin == nil { 142 | return "", fmt.Errorf("plugin is nil") 143 | } 144 | 145 | _, out, err := plugin.Call(function, []byte(input)) 146 | if err != nil { 147 | return "", err 148 | 149 | } 150 | response := string(out) 151 | 152 | return response, nil 153 | } 154 | 155 | func runExpr(input string, expression string) (string, error) { 156 | env := map[string]interface{}{ 157 | "input": input, 158 | "Get": httpGet, 159 | "AppendFile": appendFile, 160 | "ReadFile": readfile, 161 | "Extism": callExtismPlugin, 162 | "Resend": resend, 163 | "iterValue": appCfg.CurrentIterationValue, 164 | "Workflow": workflowChain, 165 | } 166 | 167 | program, err := expr.Compile(expression, expr.Env(env)) 168 | if err != nil { 169 | return "", err 170 | } 171 | 172 | output, err := expr.Run(program, env) 173 | if err != nil { 174 | return "", err 175 | } 176 | 177 | return fmt.Sprintf("%v", output), nil 178 | } 179 | -------------------------------------------------------------------------------- /update_plugins.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "strings" 4 | 5 | func updatePlugins(configData []byte) []byte { 6 | currentConfig := string(configData) 7 | configUpdates := currentConfig 8 | // Mapping of old plug-in hashes to latest 9 | updates := []struct { 10 | Old []struct { 11 | Source, Hash string 12 | } 13 | New struct { 14 | Source, Hash string 15 | } 16 | }{ 17 | { 18 | Old: []struct{ Source, Hash string }{ 19 | { 20 | Source: "https://cdn.modsurfer.dylibso.com/api/v1/module/114e1e892c43baefb4d50cc8b0e9f66df2b2e3177de9293ffdd83898c77e04c7.wasm", 21 | Hash: "114e1e892c43baefb4d50cc8b0e9f66df2b2e3177de9293ffdd83898c77e04c7", 22 | }, 23 | { 24 | Source: "https://cdn.modsurfer.dylibso.com/api/v1/module/e5768c2835a01ee1a5f10702020a82e0ba2166ba114733e2215b2c2ef423985f.wasm", 25 | Hash: "e5768c2835a01ee1a5f10702020a82e0ba2166ba114733e2215b2c2ef423985f", 26 | }, 27 | }, 28 | New: struct{ Source, Hash string }{ 29 | Source: "https://github.com/bradyjoslin/assembllm-openai/releases/latest/download/assembllm_openai.wasm", 30 | Hash: "", 31 | }, 32 | }, 33 | { 34 | Old: []struct{ Source, Hash string }{ 35 | { 36 | Source: "https://cdn.modsurfer.dylibso.com/api/v1/module/dd58ff133011b296ff5ba00cc3b0b4df34c1a176e5aebff9643d1ac83b88c72b.wasm", 37 | Hash: "dd58ff133011b296ff5ba00cc3b0b4df34c1a176e5aebff9643d1ac83b88c72b", 38 | }, 39 | }, 40 | New: struct{ Source, Hash string }{ 41 | Source: "https://github.com/bradyjoslin/assembllm-cloudflare/releases/latest/download/assembllm_cloudflare.wasm", 42 | Hash: "", 43 | }, 44 | }, 45 | { 46 | Old: []struct{ Source, Hash string }{ 47 | { 48 | Source: "https://cdn.modsurfer.dylibso.com/api/v1/module/9c1a87483040d5033866fc5b8581cc8aa7bc18abd9a601a14a4dec998a5a75f9.wasm", 49 | Hash: "9c1a87483040d5033866fc5b8581cc8aa7bc18abd9a601a14a4dec998a5a75f9", 50 | }, 51 | }, 52 | New: struct{ Source, Hash string }{ 53 | Source: "https://github.com/bradyjoslin/assembllm-perplexity/releases/latest/download/assembllm_perplexity.wasm", 54 | Hash: "", 55 | }, 56 | }, 57 | { 58 | Old: []struct{ Source, Hash string }{ 59 | { 60 | Source: "https://cdn.modsurfer.dylibso.com/api/v1/module/93f3517589bd44dfde3a0406ab2d574f239aca10378996bb6c63e8d73a510e2b.wasm", 61 | Hash: "93f3517589bd44dfde3a0406ab2d574f239aca10378996bb6c63e8d73a510e2b", 62 | }, 63 | }, 64 | New: struct{ Source, Hash string }{ 65 | Source: "https://github.com/bradyjoslin/assembllm-openai-go/releases/latest/download/assembllm-openai-go.wasm", 66 | Hash: "", 67 | }, 68 | }, 69 | { 70 | Old: []struct{ Source, Hash string }{ 71 | { 72 | Source: "https://cdn.modsurfer.dylibso.com/api/v1/module/6d2e458bf3eea4925503bc7803c0d01366430a8e2779bd088b8f9887745b4e00.wasm", 73 | Hash: "6d2e458bf3eea4925503bc7803c0d01366430a8e2779bd088b8f9887745b4e00", 74 | }, 75 | }, 76 | New: struct{ Source, Hash string }{ 77 | Source: "https://github.com/bradyjoslin/assembllm-openai-csharp/releases/latest/download/assembllm-openai-csharp.wasm", 78 | Hash: "", 79 | }, 80 | }, 81 | { 82 | Old: []struct{ Source, Hash string }{ 83 | { 84 | Source: "https://cdn.modsurfer.dylibso.com/api/v1/module/a9110e703ff5c68cbf028c725851fd287ac1ef0b909b1d97c600f881e272fa8c.wasm", 85 | Hash: "a9110e703ff5c68cbf028c725851fd287ac1ef0b909b1d97c600f881e272fa8c", 86 | }, 87 | }, 88 | New: struct{ Source, Hash string }{ 89 | Source: "https://github.com/bradyjoslin/assembllm-openai-ts/releases/latest/download/assembllm-openai-ts.wasm", 90 | Hash: "", 91 | }, 92 | }, 93 | } 94 | 95 | // Check if updates are needed 96 | for _, update := range updates { 97 | for _, old := range update.Old { 98 | // Replace the old source and hash with the new ones 99 | configUpdates = strings.Replace(configUpdates, old.Source, update.New.Source, -1) 100 | configUpdates = strings.Replace(configUpdates, old.Hash, update.New.Hash, -1) 101 | } 102 | } 103 | 104 | return []byte(configUpdates) 105 | } 106 | -------------------------------------------------------------------------------- /workflow.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "reflect" 9 | 10 | "github.com/charmbracelet/glamour" 11 | "github.com/charmbracelet/huh" 12 | "github.com/charmbracelet/huh/spinner" 13 | "github.com/charmbracelet/lipgloss" 14 | "github.com/expr-lang/expr" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | type Tasks struct { 19 | IterationValuesIn string `yaml:"iterator_script"` 20 | IterationValues []interface{} 21 | Tasks []Task `yaml:"tasks"` 22 | } 23 | 24 | type Task struct { 25 | Name string `yaml:"name"` 26 | Prompt string `yaml:"prompt"` 27 | Role string `yaml:"role"` 28 | Plugin string `yaml:"plugin"` 29 | Model string `yaml:"model"` 30 | Temperature string `yaml:"temperature"` 31 | PreScript string `yaml:"pre_script"` 32 | PostScript string `yaml:"post_script"` 33 | Tools []Tool `yaml:"tools,omitempty"` 34 | } 35 | 36 | func getAbsolutePath(path string) (string, error) { 37 | workflowDir := filepath.Dir(appCfg.WorkflowPath) 38 | joinedPath := filepath.Join(workflowDir, path) 39 | return filepath.Abs(joinedPath) 40 | } 41 | 42 | func workflowChain(path string, p string) (string, error) { 43 | absPath, err := getAbsolutePath(path) 44 | if err != nil { 45 | return "", fmt.Errorf("error loading workflow, check filepath: %v", err) 46 | } 47 | 48 | res, err := exec.Command("assembllm", "--raw", "-w", absPath, p).Output() 49 | if err != nil { 50 | return "", fmt.Errorf("error loading workflow: %v\n%v\n%v", absPath, string(res), err) 51 | } 52 | return string(res), nil 53 | } 54 | 55 | func generateResponseForTasks(tasks Tasks) (string, error) { 56 | var out string 57 | 58 | for _, task := range tasks.Tasks { 59 | 60 | if task.PreScript != "" { 61 | s, err := runExpr(task.Prompt, task.PreScript) 62 | if err != nil { 63 | return "", err 64 | } 65 | task.Prompt = task.Prompt + s 66 | } 67 | 68 | var res string 69 | if task.Plugin != "" { 70 | pluginCfg, err := getPluginConfig(task.Plugin, getConfigPath()) 71 | if err != nil { 72 | return "", err 73 | } 74 | if task.Temperature != "" { 75 | pluginCfg.Temperature = task.Temperature 76 | } 77 | 78 | pluginCfg.Role = task.Role 79 | pluginCfg.Model = task.Model 80 | prompt := out + task.Prompt 81 | 82 | if task.Tools != nil { 83 | res, err = pluginCfg.generateResponseWithTools(prompt, task.Tools) 84 | if err != nil { 85 | return "", err 86 | } 87 | } else { 88 | 89 | res, err = pluginCfg.generateResponse(prompt, true) 90 | if err != nil { 91 | return "", err 92 | } 93 | } 94 | } 95 | 96 | if task.PostScript != "" { 97 | s, err := runExpr(res, task.PostScript) 98 | if err != nil { 99 | return "", err 100 | } 101 | res = s 102 | } 103 | 104 | out = res 105 | } 106 | 107 | if !appCfg.Raw { 108 | return glamour.Render(out, "dark") 109 | } 110 | return out, nil 111 | } 112 | 113 | func handleTasks(prompt string) error { 114 | tasksCfg, err := os.ReadFile(appCfg.WorkflowPath) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | var tasks Tasks 120 | err = yaml.Unmarshal(tasksCfg, &tasks) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | if tasks.IterationValuesIn == "" { 126 | tasks.IterationValues = []interface{}{nil} 127 | } else { 128 | env := map[string]interface{}{ 129 | "input": prompt, 130 | "Get": httpGet, 131 | "AppendFile": appendFile, 132 | "ReadFile": readfile, 133 | "Extism": callExtismPlugin, 134 | "Resend": resend, 135 | } 136 | 137 | program, err := expr.Compile(tasks.IterationValuesIn, expr.Env(env), expr.AsKind(reflect.Slice)) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | output, err := expr.Run(program, env) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | tasks.IterationValues = output.([]interface{}) 148 | } 149 | 150 | var res string 151 | for i := range tasks.IterationValues { 152 | appCfg.CurrentIterationValue = tasks.IterationValues[i] 153 | 154 | if len(tasks.Tasks) > 0 { 155 | if prompt != "" { 156 | tasks.Tasks[0].Prompt = prompt + " " + tasks.Tasks[0].Prompt 157 | } 158 | } 159 | 160 | action := func(tasks Tasks) { 161 | res, err = generateResponseForTasks(tasks) 162 | if err != nil { 163 | fmt.Println(err) 164 | os.Exit(1) 165 | } 166 | } 167 | 168 | _ = spinner.New(). 169 | Title("Generating..."). 170 | TitleStyle(lipgloss.NewStyle().Faint(true)). 171 | Action(func() { action(tasks) }). 172 | Run() 173 | 174 | fmt.Print(res) 175 | } 176 | 177 | if appCfg.Feedback { 178 | var rerun bool 179 | huh.NewConfirm().Title("Would you like to provide feedback and rerun the workflow?").Value(&rerun).Run() 180 | 181 | if rerun { 182 | var feedback string 183 | huh.NewInput().Title("Provide your feedback or follow-up question:").Value(&feedback).Run() 184 | return handleTasks("you were prompted with " + prompt + "and responded with " + res + " the user provided this feedback: " + feedback) 185 | } 186 | } 187 | 188 | return nil 189 | } 190 | 191 | func executeWorkflow(args []string) error { 192 | prompt := generatePrompt(args, false) 193 | return handleTasks(prompt) 194 | } 195 | -------------------------------------------------------------------------------- /workflows/README.md: -------------------------------------------------------------------------------- 1 | # Example Workflows 2 | 3 | These samples are intended to be useful on their own while also serving as cookbooks that can be used as references to build your own workflows. Contributions welcome! 4 | 5 | ## End of Life 6 | 7 | Provides end of life information on software products provided within a prompt. Explains how to use LLM function calling to get structured data from an unstructured prompt and how to perform API calls in a workflow script. ([details](./eol/)) 8 | 9 | 10 | 11 | ## RSS 12 | 13 | Takes a URL to an RSS feed and summarizes the first 5 articles in the feed. Shows how to make API calls and parse data in a script, and call one workflow from another (workflow chaining). 14 | ([details](./rss/README.md)) 15 | 16 | 17 | 18 | ## Scrape and Summarize Web content 19 | 20 | Takes a URL and a CSS selector and scrapes web content then provides a concise summary and analysis of the text. Shows how to use [assembllm HTML Tools wasm plug-ins](https://github.com/bradyjoslin/assembllm-htmltools) for web scraping to get clean and concise web content used in a prompt, and demonsrates a workflow built entirely using workflow chaining. ([details](./scrape_then_summarize/README.md)): 21 | 22 | 23 | 24 | ## Weather 25 | 26 | Uses tools / function calling capability that takes unstructured prompt text and returns structured data, which we use to call an api to get the weather forecast for a given location. We then feed the API response into a new task, providing the original prompt and API response for context, getting a response contextualized with weather data.([details](./weather/)): 27 | 28 | 29 | -------------------------------------------------------------------------------- /workflows/algorithm_time_complexity.yaml: -------------------------------------------------------------------------------- 1 | name: Algorithm Time Complexity Workflow 2 | description: | 3 | This workflow is designed to help users analyze the time complexity of algorithms and functions. 4 | The AI will calculate its time complexity using Big O notation. 5 | 6 | tasks: 7 | - name: algorithm_time_complexity 8 | plugin: cloudflare 9 | model: "@hf/thebloke/deepseek-coder-6.7b-instruct-awq" 10 | role: | 11 | Your task is to analyze the provided function or algorithm and calculate its time 12 | complexity using Big O notation. Explain your reasoning step by step, describing 13 | how you arrived at the final time complexity. Consider the worst-case scenario when 14 | determining the time complexity. If the function or algorithm contains multiple steps 15 | or nested loops, provide the time complexity for each step and then give the overall 16 | time complexity for the entire function or algorithm. Assume any built-in functions or 17 | operations used have a time complexity of O(1) unless otherwise specified. 18 | 19 | prompt: | 20 | ```python 21 | def example_function(n): 22 | total = 0 23 | for i in range(n): 24 | for j in range(n): 25 | total += i * j 26 | return total 27 | ``` 28 | -------------------------------------------------------------------------------- /workflows/article_summarizer.yaml: -------------------------------------------------------------------------------- 1 | name: article summary and analysis 2 | description: | 3 | Optimized for taking an article's title and URL as a prompt and providing an 4 | effective summary using Perplexity's Online model, which contains up to date 5 | information. 6 | 7 | tasks: 8 | - plugin: perplexity 9 | prompt: | 10 | # Task Objective 11 | 12 | Give a detailed analysis of the topic supported by a URL source provided. 13 | Provide references, if available. Include the URL provided at the end of 14 | the summary. Response must be 250 words or less. 15 | 16 | ## Task Details 17 | 18 | Be insightful. Think about the author's claims deeply and express the strengths 19 | and weaknesses of their argument. Where practical, think about how the topic 20 | can impact a person's daily life, or an industry long-term, or is it just 21 | passing information with little impact. 22 | -------------------------------------------------------------------------------- /workflows/email.yaml: -------------------------------------------------------------------------------- 1 | 2 | name: Email Workflow 3 | description: | 4 | This workflow is designed to help users rewrite provided text as an effectively written email. 5 | The AI will rewrite the text in a concise and clear manner, ensuring that the email is well-structured 6 | and easy to read. The AI will also provide a subject line, bottom line, and background information if 7 | necessary. 8 | 9 | Reference: https://hbr.org/2016/11/how-to-write-email-with-military-precision 10 | 11 | tasks: 12 | - name: email 13 | role: 14 | Rewrite the provided text into an effective email. Do not add any new details; use only the information given. 15 | Short emails are more impactful than long ones, so aim to fit all content within one screen to avoid the need 16 | for scrolling. Avoid passive voice as it tends to make sentences longer and less clear. As the Air Force manual 17 | states, "Besides lengthening and twisting sentences, passive verbs often muddy them." Instead, use active voice, 18 | which places nouns before verbs, making it clear who is performing the action. By using active voice, you let the 19 | "verbs do the work for you." For example, instead of saying, "The factory was buzzed by an F18," say, "An F18 20 | buzzed the factory." 21 | 22 | Format in this way 23 | 24 | Subject - specify one of the following classifiers based on email content. ACTION - Compulsory for the 25 | recipient to take some action, SIGN - Requires the signature of the recipient, INFO - For informational 26 | purposes only, and there is no response or action required DECISION - Requires a decision by the recipient 27 | REQUEST - Seeks permission or approval by the recipient COORD - Coordination by or with the recipient is 28 | needed. Subject, no longer than 5 word summary 29 | 30 | Bottom Line - helps readers quickly digest the announcement, decision, and when the new procedures go into 31 | effect. The reader doesn’t necessarily want to know all the background information that led to the decision. 32 | He or she likely wants to know 'how does this email affect me?' and the BLUF should answer this question every 33 | time.} 34 | 35 | Background - Include this section only if there is information not included in the bottom line that should be 36 | mentioned. Bulleted list of background details, concise. 37 | plugin: openai 38 | -------------------------------------------------------------------------------- /workflows/eol/README.md: -------------------------------------------------------------------------------- 1 | # End of Life Workflow Example 2 | 3 | **Overview** 4 | 5 | [This workflow](./tools_eol.yaml) uses tools / function calling capability that takes unstructured prompt text and returns structured data, which we use to call an api to get product lifecycle / end of life information. We then feed the API response into a new task, providing the original prompt and API response for context to provide an answer. 6 | 7 | **Sample Output**: 8 | 9 | ![eol gif](eol.gif) 10 | 11 | **Usage** 12 | 13 | ```sh 14 | assembllm -w tools_eol.yaml 15 | ``` 16 | 17 | Example: 18 | 19 | ``` 20 | assembllm -w tools_eol.yaml 'we are using debian 9, should we think about upgrading?' 21 | ``` 22 | 23 | ## Step by Step Guide 24 | 25 | ### Step 1: Fetch and Parse RSS Feed 26 | 27 | Define an iterator script that builds a single value array with the input prompt provided. Because an iterator's `iterValue` is retained across workflow tasks, we're using this as a means to hold the initial state of the prompt, which we'll use in our second task. More on that below. 28 | 29 | ```yaml 30 | iterator_script: | 31 | [input] 32 | ``` 33 | 34 | ### Step 2: 35 | 36 | We define a task that uses OpenAI and define a tool for that plug-in which instructs OpenAI to provide specific structured data from a given prompt, in this case find digital products from within the prompt. 37 | 38 | ```yaml 39 | tasks: 40 | - name: eol 41 | plugin: anthropic 42 | tools: 43 | - name: eol 44 | description: End-of-life (EOL) and support product information 45 | input_schema: 46 | type: object 47 | properties: 48 | product: 49 | type: string 50 | description: | 51 | The name of a digital product. Product may be one or more of: "akeneo-pim","alibaba-dragonwell","almalinux","alpine",... 52 | required: 53 | - product 54 | ``` 55 | 56 | At this stage a reponse to 'we are using debian 9, should we think about upgrading?' would look something like: 57 | 58 | ```json 59 | [ 60 | { 61 | "name": "eol", 62 | "input": { 63 | "product": "debian" 64 | } 65 | } 66 | ] 67 | ``` 68 | 69 | ### Step 3 70 | 71 | A post script is defined immediately after the tools call which parses the structured data response, then calls the endoflife.date api for each product found. We limit the API data to the last 20 records to save on input tokens. If no product is found, we return a string "no product found". 72 | 73 | ```yaml 74 | post_script: | 75 | let jsonIn = input | fromJSON(); 76 | map(jsonIn, { 77 | let product = .input.product; 78 | product != nil ? (Get("https://endoflife.date/api/" + product + ".json") | fromJSON() | take(20)) : "no product found" 79 | }) 80 | ``` 81 | 82 | ### Step 4 83 | 84 | Lastly, we define another task which we'll execute a callback to OpenAI, building a prompt to let the LLM know the initial prompt from the user and the response from our tool. 85 | 86 | ```yaml 87 | - name: eol_response 88 | pre_script: | 89 | let primaryAsk = "The user asked: " + iterValue + ", we used a tool to find data to help answer, provide a summary response. Here is the authoritative data: " + input; 90 | let noproductfound = "If no product found, just reply with only 'no product found"; 91 | let dateContext = "First realize that today is: " + string(now()) + ", all dates should be compared against today, dates before are in the past, future hasn't happened."; 92 | dateContext + "\n" + primaryAsk + "\n" + noproductfound 93 | plugin: anthropic 94 | ``` 95 | -------------------------------------------------------------------------------- /workflows/eol/eol.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradyjoslin/assembllm/826f6533313556459d9a8c902f68d1a5bd613d07/workflows/eol/eol.gif -------------------------------------------------------------------------------- /workflows/eol/eol.tape: -------------------------------------------------------------------------------- 1 | # VHS documentation 2 | # 3 | # Output: 4 | # Output .gif Create a GIF output at the given 5 | # Output .mp4 Create an MP4 output at the given 6 | # Output .webm Create a WebM output at the given 7 | # 8 | # Require: 9 | # Require Ensure a program is on the $PATH to proceed 10 | # 11 | # Settings: 12 | # Set FontSize Set the font size of the terminal 13 | # Set FontFamily Set the font family of the terminal 14 | # Set Height Set the height of the terminal 15 | # Set Width Set the width of the terminal 16 | # Set LetterSpacing Set the font letter spacing (tracking) 17 | # Set LineHeight Set the font line height 18 | # Set LoopOffset % Set the starting frame offset for the GIF loop 19 | # Set Theme Set the theme of the terminal 20 | # Set Padding Set the padding of the terminal 21 | # Set Framerate Set the framerate of the recording 22 | # Set PlaybackSpeed Set the playback speed of the recording 23 | # Set MarginFill Set the file or color the margin will be filled with. 24 | # Set Margin Set the size of the margin. Has no effect if MarginFill isn't set. 25 | # Set BorderRadius Set terminal border radius, in pixels. 26 | # Set WindowBar Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight) 27 | # Set WindowBarSize Set window bar size, in pixels. Default is 40. 28 | # Set TypingSpeed