├── .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 |

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 | 
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 | 
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