├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── archetypes ├── batch │ ├── batch-data.json │ ├── batch.prompt │ └── project.yaml ├── chain │ ├── project.yaml │ ├── properties.json │ ├── story.prompt │ └── user-action.prompt ├── image │ ├── image.prompt │ └── project.yaml ├── plugin-catalog │ ├── defaultConfig │ │ ├── ai-plugin.json │ │ ├── logo.webp │ │ ├── mocks │ │ │ └── catalog.json │ │ ├── openapi.yaml │ │ └── plugin-manifest.prompt │ └── project.yaml ├── plugins-quickstart │ ├── defaultConfig │ │ ├── ai-plugin.json │ │ ├── logo.png │ │ ├── mocks │ │ │ └── todos-global.json │ │ ├── openapi.yaml │ │ └── plugin-manifest.prompt │ ├── pluginFlavors │ │ ├── kaleb │ │ │ └── mocks │ │ │ │ └── todos.json │ │ └── mike │ │ │ └── mocks │ │ │ └── todos.json │ └── project.yaml └── prompt │ ├── project.yaml │ ├── prompt-json.prompt │ └── prompt-text.prompt ├── archive.sh ├── bin ├── air.dart └── archetype_command.dart ├── coverage.sh ├── example ├── README.md ├── api_key ├── batch │ ├── product.json │ ├── product.prompt │ └── project.yaml ├── experiment-chain-multiple │ ├── character-action.prompt │ ├── project.yaml │ ├── properties.json │ └── structured-story.prompt ├── experiment-chain-single │ ├── project.yaml │ └── simple-story.prompt ├── experiment-simple │ ├── project.yaml │ └── simple-story.prompt ├── image │ ├── image.prompt │ └── project.yaml ├── plugins-quickstart │ ├── defaultConfig │ │ ├── ai-plugin.json │ │ ├── logo.png │ │ ├── mocks │ │ │ └── todos-global.json │ │ ├── openapi.yaml │ │ └── plugin-manifest.prompt │ ├── pluginFlavors │ │ ├── kaleb │ │ │ └── mocks │ │ │ │ └── todos.json │ │ └── mike │ │ │ └── mocks │ │ │ └── todos.json │ └── project.yaml ├── reporting │ ├── character-action.prompt │ ├── project.yaml │ ├── properties.json │ └── structured-story.prompt └── system-message.txt ├── lib └── src │ ├── archetypes.dart │ ├── chatgpt │ ├── catalog_parser.dart │ └── plugin_server.dart │ ├── gpt_plugin.dart │ ├── io_helper.dart │ ├── network_client.dart │ ├── plugins │ ├── batch_plugin.dart │ ├── experiment_plugin.dart │ ├── image_plugin.dart │ └── reporting_plugin.dart │ ├── prompts.dart │ └── reporter.dart ├── pubspec.lock ├── pubspec.yaml └── test ├── archeypes_test.dart ├── image_plugin_test.dart ├── io_helper_test.dart ├── mocks.dart ├── plugin_server_test.dart ├── prompts_test.dart └── reporter_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | .dart_tool/ 2 | .packages 3 | .idea/ 4 | *.iml 5 | output/ 6 | data/ 7 | build/ 8 | .DS_Store 9 | api_key 10 | .run 11 | coverage 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.12.0 2 | - Upgrade to Dart 3 [#70](https://github.com/sisbell/stackwire-gpt/issues/70) 3 | - Catalog Plugin Archetype [#71](https://github.com/sisbell/stackwire-gpt/issues/71) 4 | ## 0.11.0 5 | - Allow Plugin Servers to Inherit Properties [#66](https://github.com/sisbell/stackwire-gpt/issues/66) 6 | - Plugin Server Variant Support [#67](https://github.com/sisbell/stackwire-gpt/issues/67) 7 | ## 0.10.0 8 | - ChatGPT Plugin Support [#61](https://github.com/sisbell/stackwire-gpt/issues/61) 9 | - Extracting JSON Response fails for Nested JSON [#55](https://github.com/sisbell/stackwire-gpt/issues/55) 10 | - Minor bug fixes 11 | ## 0.9.1 12 | - Download Archetype Archive for Project Generation [#51](https://github.com/sisbell/stackwire-gpt/issues/51) 13 | ## 0.9.0 14 | - CLI Support for Generating Projects [#11](https://github.com/sisbell/stackwire-gpt/issues/11) 15 | ## 0.8.0 16 | - Reporting Plugin for HTML Output [#48](https://github.com/sisbell/stackwire-gpt/issues/48) 17 | ## 0.7.0 18 | - Move blockRuns out of Configuration Node [#45](https://github.com/sisbell/stackwire-gpt/issues/45) 19 | - Use YAML for EOM files [#44](https://github.com/sisbell/stackwire-gpt/issues/44) 20 | - Get Experiment Input From Data File [#41](https://github.com/sisbell/stackwire-gpt/issues/41) 21 | - Output Metrics for All OpenAI Requests [#39](https://github.com/sisbell/stackwire-gpt/issues/39) 22 | ## 0.6.0 23 | - Add support for dryRun flag [#36](https://github.com/sisbell/stackwire-gpt/issues/36) 24 | - Command for returning number of OpenAI Calls [#35](https://github.com/sisbell/stackwire-gpt/issues/35) 25 | - Clean Command [#24](https://github.com/sisbell/stackwire-gpt/issues/24) 26 | ## 0.5.0 27 | - New plugin architecture. Standardized eom files. Standardized reports. [#23](https://github.com/sisbell/stackwire-gpt/issues/23) 28 | ## 0.4.0 29 | - Image generation command [#20](https://github.com/sisbell/stackwire-gpt/issues/20) 30 | ## 0.3.0 31 | - gpt native command is for GUID partition table. Change command used to "air" to avoid conflict.[#18](https://github.com/sisbell/stackwire-gpt/issues/18) 32 | ## 0.2.0 33 | - Initial version. 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A command line tool for running GPT commands. This tool supports prompt-batching, prompt-chaining and ChatGPT Plugins. 2 | 3 | ## Features 4 | Use this tool to 5 | * Create **batch** processing of GPT requests. Run a set of data against a prompt and record the responses. 6 | * Create **experiments** to see how different config parameters and prompts affect performance and output. 7 | * Create **images** with DALL-E 8 | * Do rapid prototyping of **ChatGPT Plugins** through a mock server 9 | * Discover ChatGPT Plugins with a ChatGPT Plugin 10 | 11 | ## Getting Started 12 | You will need to create an OpenAI API Key. If you have an account, you can create a key here 13 | 14 | https://platform.openai.com/account/api-keys 15 | 16 | > Do not share your API key with others, or expose it in the browser or other client-side code. 17 | > You will incur any charges if someone uses your key. Don't check your key into any public repository. 18 | 19 | Create a file that contains your API Key (the one below is not real). In our example. we name the file **api_key** and add the key. 20 | ``` 21 | sk-gKtTxOumv4orO6cfWlh0ZK 22 | ``` 23 | ## Install This tool 24 | Make sure your have dart installed. Follow the instructions, in the link below. 25 | 26 | https://dart.dev/get-dart 27 | 28 | After installation, you can install the gpt program with the following command 29 | 30 | > dart pub global activate gpt 31 | 32 | ## Usage 33 | The following are the use cases supported 34 | * [Generate ChatGPT Plugin](#generate-chatgpt-plugin) 35 | * [Generate Image Project](#generate-image-project) 36 | * [Generate Batch Project](#generate-batch-project) 37 | * [Generate Prompt Project](#generate-prompt-project) 38 | * [Generate Chain Project](#generate-chain-project) 39 | * [Add Report Plugin](#add-report-plugin) 40 | * [ZapVine Plugin Catalog](https://github.com/sisbell/stackwire-gpt/wiki/ZapVine-Plugin-Catalog) 41 | 42 | ## Creating Projects 43 | Run the generate project command 44 | ``` 45 | air genp 46 | ``` 47 | You will first need to select the archetype 48 | ``` 49 | ? Project Archetype › 50 | ❯ Prompt 51 | Chain 52 | Batch 53 | Image 54 | ChatGPT Plugin 55 | ``` 56 | Enter the projectName and projectVersion 57 | ``` 58 | ✔ Project Archetype · Prompt 59 | ✔ Project Name: · myproject 60 | ✔ Project Version: · 1.0 61 | ``` 62 | 63 | Depending on the project, you may need to enter your API Key. You can skip, use an existing key file or create a new key file 64 | ``` 65 | ? Import Key › 66 | ❯ Skip 67 | Use Existing OpenAI API Key File 68 | Create New OpenAI API Key File 69 | ``` 70 | The following option allows us to enter the api key directly. It will 71 | save the key to a file. If you have trouble copying and pasting the key, 72 | just enter a few characters and then edit the file afterwards. 73 | ``` 74 | ✔ Import Key · Create New OpenAI API Key File 75 | ? API Key: › sk-gKtTxOumv4orO6cfWlh0ZK 76 | ``` 77 | ### Generate ChatGPT Plugin 78 | The ChatGPT Plugin project allows you to do rapid prototyping of a ChatGPT Plugin. Specifically it 79 | allows you to mock responses to ChatGPT. The project is based upon the quickstart project at: https://github.com/openai/plugins-quickstart 80 | 81 | Your project file will look like 82 | 83 | ```yaml 84 | --- 85 | projectName: plugin-quickstart 86 | projectVersion: '1.0' 87 | projectType: plugin 88 | defaultConfig: 89 | properties: 90 | port: 5003 91 | nameForHuman: TODO Plugin (no auth) 92 | nameForModel: todo 93 | descriptionForHuman: Plugin for managing a TODO list, you can add, remove and view your TODOs. 94 | mockedRequests: 95 | - path: "/todos/global" 96 | method: get 97 | mockedResponse: todos-global.json 98 | - path: "/todos/user" 99 | method: get 100 | mockedResponse: todos-global.json 101 | 102 | pluginServers: 103 | - serverId: todo-mike 104 | flavor: mike 105 | # mocked requests 106 | mockedRequests: 107 | - path: "/todos/mike" 108 | method: get 109 | mockedResponse: todos.json # returns the content of this file 110 | 111 | # Adds a different user for testing 112 | - serverId: todo-kaleb 113 | flavor: kaleb 114 | mockedRequests: 115 | - path: "/todos/kaleb" 116 | method: get 117 | mockedResponse: todos.json 118 | properties: 119 | showHttpHeaders: true # Show http headers in logs 120 | ``` 121 | Any configuration in the defaultConfig node will be applied to each pluginServer unless that plugin specifically 122 | overrides the property. 123 | 124 | A sample mocked response (_todos.json_) is given below. This will be returned on a call to **/todos/mike** 125 | 126 | ```json 127 | { 128 | "todos": [ 129 | "Clean out a septic tank", 130 | "Repair a broken sewer pipe", 131 | "Collect roadkill for disposal", 132 | "Assist in bee hive relocation", 133 | "Service a grease trap at a restaurant" 134 | ] 135 | } 136 | ``` 137 | To start a mocked instance of the plugin server 138 | ``` 139 | air plugin 140 | ``` 141 | or to start a specific server add the _serverId_ option 142 | 143 | ``` 144 | air plugin --serverId todo-mike 145 | ``` 146 | For more information about creating and using a 147 | [ChatGPT-Plugin](https://github.com/sisbell/stackwire-gpt/wiki/ChatGPT-Plugin) 148 | 149 | ### Generate Image Project 150 | If you chose to create an image project, you will be asked for a description. 151 | Don't worry you can change it later after generating the project. 152 | ``` 153 | ? Image Description: › A goldfish with big eyes 154 | ``` 155 | Your project file will look like 156 | ```yaml 157 | projectName: image-2 158 | projectVersion: 2.0 159 | apiKeyFile: api_key 160 | blocks: 161 | - blockId: image-block-1 162 | pluginName: ImageGptPlugin 163 | executions: 164 | #First Image 165 | - id: img-1 166 | sizes: 167 | - 256 168 | - 256 169 | - 1024 170 | prompt: image.prompt 171 | properties: 172 | imageDescription: A goldfish with big eyes 173 | ``` 174 | You prompt file will be 175 | ``` 176 | Generate a picture of a ${imageDescription} 177 | ``` 178 | 179 | For more information about [images](https://github.com/sisbell/stackwire-gpt/wiki/Images) 180 | 181 | ![image](https://user-images.githubusercontent.com/64116/235335782-ce438b16-45eb-4413-a12f-ca33136a63b2.png) 182 | 183 | ### Generate Batch Project 184 | The following asks how many times to execute the batch data. If you choose 5 times, 185 | it will run all the batch data calls 5 times each. 186 | 187 | ``` 188 | ? Number of Times to Run The Block: › 5 189 | ``` 190 | You will see a project file like the following 191 | ```yaml 192 | --- 193 | projectName: mybatch 194 | projectVersion: 1.0 195 | apiKeyFile: api_key 196 | blocks: 197 | - blockId: batch-1 198 | pluginName: BatchGptPlugin 199 | blockRuns: 5 200 | configuration: 201 | requestParams: 202 | model: gpt-3.5-turbo 203 | temperature: 0.7 204 | top_p: 1 205 | max_tokens: 500 206 | executions: 207 | - id: batch-1 208 | dataFile: batch-data.json 209 | prompt: batch.prompt 210 | ``` 211 | The prompt is a simple Hello World prompt 212 | ``` 213 | Write me a paragraph about the world I live in 214 | 215 | World: ```${my_world}``` 216 | ``` 217 | The batch-data.json file contains the batch data. 218 | 219 | ```json 220 | { 221 | "my_world" : [ 222 | "Hello World, I live in a magical place with magical creatures", 223 | "Hello World, I live in a futuristic Utopia", 224 | "Hello World, I live in a futuristic Dystopia" 225 | ] 226 | } 227 | ``` 228 | Modify these two files with your own data. 229 | For more information about [batches](https://github.com/sisbell/stackwire-gpt/wiki/Batches) 230 | 231 | ## Generate Prompt Project 232 | The following asks how many times to run the prompt request. If you choose 5 times, 233 | it will run all the prompt request 5 times. 234 | 235 | ``` 236 | ? Number of Times to Run The Block: › 5 237 | ``` 238 | Next choose the output format. Do you just want a straight text response, or do you want it in JSON format. 239 | ``` 240 | ? Response Format › 241 | JSON 242 | ❯ TEXT 243 | ``` 244 | If you choose JSON, you will be asked if you want to enable fixing the JSON response. 245 | This will attempt to parse any extraneous text that the AI assistant may add. 246 | 247 | ``` 248 | ? Attempt to FIX JSON Responses? (y/n) › no 249 | ``` 250 | 251 | The project file will look like 252 | ```yaml 253 | --- 254 | projectName: prompt-1 255 | projectVersion: 1.0 256 | apiKeyFile: api_key 257 | blocks: 258 | - blockId: single-1 259 | pluginName: ExperimentGptPlugin 260 | blockRuns: 5 261 | configuration: 262 | requestParams: 263 | model: gpt-3.5-turbo 264 | temperature: 1.2 265 | top_p: 1 266 | max_tokens: 500 267 | executions: 268 | - id: exp-1 269 | responseFormat: json 270 | fixJson: false 271 | promptChain: 272 | - prompt-json.prompt 273 | properties: 274 | character: Commander in Starfleet 275 | mainCharacterName: '' 276 | - blockId: report-1 277 | pluginName: ReportingGptPlugin 278 | executions: 279 | - id: report-1 280 | blockIds: 281 | - single-1 282 | ``` 283 | The file includes some default values for the OpenAI requests. Change them to suit your needs. 284 | By default, it also adds the reporting plugin which generated HTML output of the user/assistant response. 285 | 286 | The _prompt-json.prompt_ looks like the following. Note how the output specifies 287 | to use JSON. Modify the prompt and properties to your needs. 288 | 289 | ``` 290 | Write me a story about ${character}. The main character is ${mainCharacterName}. 291 | If no main character is given, choose one. Write one sentence only. 292 | 293 | The response should be in JSON using the following structure: 294 | Only use these fields. {"mainCharacterName": "", "story": ""} 295 | ``` 296 | For more information about [prompts](https://github.com/sisbell/stackwire-gpt/wiki/Prompts) 297 | 298 | ## Generate Chain Project 299 | Choose the _Chain_ project archetype. Then go through the options. 300 | 301 | ``` 302 | ? Number of Times to Run The Block: › 1 303 | ``` 304 | ``` 305 | ? Attempt to FIX JSON Responses? (y/n) › yes 306 | ``` 307 | 308 | ``` 309 | ? Number of Times to Run The Prompt Chain: › 2 310 | ``` 311 | 312 | The generated _project.yaml_ file. 313 | 314 | ```yaml 315 | --- 316 | projectName: "chain-project" 317 | projectVersion: "1.0" 318 | apiKeyFile: "api_key" 319 | blocks: 320 | # Block demonstrates the use of importing properties 321 | - blockId: chain-1 322 | pluginName: ExperimentGptPlugin 323 | blockRuns: 1 # Number of Stories 324 | configuration: 325 | requestParams: 326 | model: gpt-3.5-turbo 327 | temperature: 1.2 328 | top_p: 1 329 | max_tokens: 500 330 | executions: 331 | - id: exp-1-import 332 | chainRuns: 2 # Number of times to run the promptChain 333 | promptChain: 334 | - story.prompt 335 | - user-action.prompt # Simulates user input 336 | excludesMessageHistory: 337 | - user-action.prompt 338 | fixJson: true 339 | responseFormat: json 340 | # Import properties from a properties file 341 | import: 342 | propertiesFile: properties.json # predefined values 343 | properties: 344 | planet: 1 # Earth 345 | action: 3 # Lands on the planet 346 | 347 | - blockId: report-1 348 | pluginName: ReportingGptPlugin 349 | executions: 350 | - id: report-1 351 | blockIds: 352 | - chain-1 353 | ``` 354 | 355 | The property fields in the above _project.yaml_ file point to 356 | the index within the _properties.json_ file is below. This file 357 | allows you to easily change test input. 358 | 359 | ```json 360 | { 361 | "planet": [ 362 | "Earth", 363 | "Venus", 364 | "Jupiter" 365 | ], 366 | "action": [ 367 | "Blows up the planet", 368 | "Observes the planet From Orbit", 369 | "Lands on the planet", 370 | "Zips around the planet and hopes no one notices" 371 | ] 372 | } 373 | ``` 374 | The tool will substitute the planet "Earth" and the action "Lands on the planet" into 375 | the story prompt below. Notice that the AI will generate the character's name and 376 | the first paragraph of the story. 377 | 378 | ``` 379 | The response will be in JSON Format. 380 | 381 | Captain ${captainsName} is near ${planet}. . 382 | The last part of the story is: ${story} 383 | Then the captain ${action} 384 | 385 | Tell me a story about what happens next. 386 | Be very descriptive. Write two sentences only. 387 | Give me the captains name, if I haven't given it. 388 | 389 | RESPONSE 390 | The response must only be in JSON using the following structure. 391 | Only use these fields. {"captainsName": "${captainsName}", "story": ""} 392 | ``` 393 | The tool will now pass the returned captain's name and the story from the first 394 | prompt into the _user-action.prompt_. We will get back an action that the 395 | character takes. 396 | 397 | ``` 398 | Give me an action for ${captainsName} for the following story: 399 | ${story} 400 | 401 | The response must be in JSON using the following structure. 402 | Only use these fields. {"action": ""} 403 | ``` 404 | Now we will run the _story.prompt_ again but this time we will have both 405 | the captain's name and the next action he takes. 406 | 407 | The follow is sample output from an actual run 408 | ``` 409 | As Captain John lands on the planet, he feels the trembling beneath his feet and sees the vibrant green flora around him. 410 | He plants the Earth's flag to claim its new discovery and soon finds a thriving alien civilization welcoming him with open arms. 411 | 412 | [user action "plants the flag to claim the new discovery"] 413 | 414 | As Captain John plants the Earth's flag on the newfound planet, he is approached by the leaders of the alien civilization 415 | who speak his language and reveal that they have known about Earth for centuries. They invite him to partake in a feast in 416 | his honor, where he learns about their advanced technology and way of life. 417 | ``` 418 | Notice that chain run is the same as the number of paragraphs we have in the 419 | output. If we wanted another paragraph, we would set _chainRuns_ to 3. If 420 | we had set blockRuns to 5, we would have generated 5 different stories. 421 | 422 | For more information about [chains](https://github.com/sisbell/stackwire-gpt/wiki/Chains) 423 | 424 | ## Add Report Plugin 425 | To generate an HTML report, add the _ReportingGptPlugin_ as the last block. Under the blockIds 426 | add any previous block id that you want to add to the generated report. 427 | 428 | ```yaml 429 | --- 430 | projectName: experiment-reporting 431 | projectVersion: '1.7' 432 | apiKeyFile: "../../api_key" 433 | blocks: 434 | - blockId: chain-1 435 | pluginName: ExperimentGptPlugin 436 | blockRuns: 1 437 | ... 438 | # Generate HTML Report 439 | - blockId: report-1 440 | pluginName: ReportingGptPlugin 441 | executions: 442 | - id: report-execution 443 | blockIds: 444 | - chain-1 445 | ``` 446 | #### Sample Report 447 | The report will display the entire chat for the configured block executions. 448 | 449 | report 450 | 451 | For more information about [reporting](https://github.com/sisbell/stackwire-gpt/wiki/Reporting) 452 | 453 | ## Command Help 454 | > air --help 455 | 456 | ``` 457 | A command line tool for running GPT commands 458 | 459 | Usage: air [arguments] 460 | 461 | Global options: 462 | -h, --help Print this usage information. 463 | 464 | Available commands: 465 | clean Cleans project's output directory 466 | count Returns the number of OpenApiCalls that would be made 467 | genp Generates a new project 468 | plugin Runs local version of ChatGPT Plugin 469 | run Runs a project's blocks 470 | 471 | Run "air help " for more information about a command. 472 | 473 | ``` 474 | ## Additional Commands 475 | ### Clean Project 476 | To clean a project, run the following 477 | 478 | > air clean 479 | 480 | This deletes the _output_ directory for the project. 481 | 482 | ### Count of OpenAI Calls for a Project 483 | Running OpenAI calls with a tool can be costly if you mis-configure it. 484 | To determine how many OpenAI calls a project will create, run the following command 485 | 486 | > air count 487 | 488 | or for the count of a specific block 489 | 490 | > air count -b myblockId 491 | 492 | It will output 493 | 494 | ``` 495 | Project: product-summary-2.8 496 | Total OpenAPI Calls would be 12 497 | ``` 498 | 499 | ### DryRun 500 | If you want to know that your project is doing before incurring costs to OpenAI, use the dryRun flag. 501 | 502 | > air run --dryRun 503 | 504 | ``` 505 | Executing Block 506 | Running Project: image-generation-2.3 507 | BlockId: image-1, PluginName: ImageGptPlugin 508 | ---------- 509 | Starting Block Run: 1 510 | Starting execution: 1 - Requires 1 calls to OpenAI 511 | POST to https://api.openai.com/v1/images/generations 512 | {"prompt":"Generate a picture of a Unicorn with a gold horn and wings","n":1,"size":"256x256","response_format":"url"} 513 | Finished execution: 1 514 | 515 | Starting execution: 2 - Requires 2 calls to OpenAI 516 | POST to https://api.openai.com/v1/images/generations 517 | {"prompt":"Generate a picture of a fish with giant eyes","n":1,"size":"256x256","response_format":"b64_json"} 518 | POST to https://api.openai.com/v1/images/generations 519 | {"prompt":"Generate a picture of a fish with giant eyes","n":1,"size":"512x512","response_format":"b64_json"} 520 | Finished execution: 2 521 | 522 | 523 | -------- 524 | Finished running project: 0 seconds 525 | ``` 526 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /archetypes/batch/batch-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "my_world" : [ 3 | "Hello World, I live in a magical place with magical creatures", 4 | "Hello World, I live in a futuristic Utopia", 5 | "Hello World, I live in a futuristic Dystopia" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /archetypes/batch/batch.prompt: -------------------------------------------------------------------------------- 1 | Write me a paragraph about the world I live in 2 | 3 | World: ```${my_world}``` -------------------------------------------------------------------------------- /archetypes/batch/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: "${projectName}" 3 | projectVersion: "${projectVersion}" 4 | apiKeyFile: "${apiKeyFile}" 5 | blocks: 6 | - blockId: batch-1 7 | pluginName: BatchGptPlugin 8 | blockRuns: ${blockRuns} 9 | configuration: 10 | requestParams: 11 | model: gpt-3.5-turbo 12 | temperature: 0.7 13 | top_p: 1 14 | max_tokens: 500 15 | executions: 16 | - id: batch-1 17 | dataFile: batch-data.json 18 | prompt: batch.prompt -------------------------------------------------------------------------------- /archetypes/chain/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: "${projectName}" 3 | projectVersion: "${projectVersion}" 4 | apiKeyFile: "${apiKeyFile}" 5 | blocks: 6 | # Block demonstrates the use of importing properties 7 | - blockId: chain-1 8 | pluginName: ExperimentGptPlugin 9 | blockRuns: ${blockRuns} 10 | configuration: 11 | requestParams: 12 | model: gpt-3.5-turbo 13 | temperature: 1.2 14 | top_p: 1 15 | max_tokens: 500 16 | executions: 17 | - id: exp-1-import 18 | chainRuns: ${chainRuns} # Number of times to run the promptChain 19 | promptChain: 20 | - story.prompt 21 | - user-action.prompt # Simulates user input 22 | excludesMessageHistory: 23 | - user-action.prompt 24 | fixJson: true 25 | responseFormat: json 26 | # Import properties from a properties file 27 | import: 28 | propertiesFile: properties.json # predefined values 29 | properties: 30 | planet: 1 # Earth 31 | action: 3 # Lands on the planet 32 | 33 | - blockId: report-1 34 | pluginName: ReportingGptPlugin 35 | executions: 36 | - id: report-1 37 | blockIds: 38 | - chain-1 -------------------------------------------------------------------------------- /archetypes/chain/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "planet": [ 3 | "Earth", 4 | "Venus", 5 | "Jupiter" 6 | ], 7 | "action": [ 8 | "Blows up the planet", 9 | "Observes the planet From Orbit", 10 | "Lands on the planet", 11 | "Zips around the planet and hopes no one notices" 12 | ] 13 | } -------------------------------------------------------------------------------- /archetypes/chain/story.prompt: -------------------------------------------------------------------------------- 1 | The response will be in JSON Format. 2 | 3 | Captain ${captainsName} is near ${planet}. . 4 | The last part of the story is: ${story} 5 | Then the captain ${action} 6 | 7 | Tell me a story about what happens next. 8 | Be very descriptive. Write two sentences only. 9 | Give me the captains name, if I haven't given it. 10 | 11 | RESPONSE 12 | The response must only be in JSON using the following structure. 13 | Only use these fields. {"captainsName": "${captainsName}", "story": ""} -------------------------------------------------------------------------------- /archetypes/chain/user-action.prompt: -------------------------------------------------------------------------------- 1 | Give me an action for ${captainsName} for the following story: 2 | ${story} 3 | 4 | The response must be in JSON using the following structure. 5 | Only use these fields. {"action": ""} -------------------------------------------------------------------------------- /archetypes/image/image.prompt: -------------------------------------------------------------------------------- 1 | Generate a picture of a ${imageDescription} -------------------------------------------------------------------------------- /archetypes/image/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: "${projectName}" 3 | projectVersion: "${projectVersion}" 4 | apiKeyFile: "${apiKeyFile}" 5 | blocks: 6 | - blockId: image-block-1 7 | pluginName: ImageGptPlugin 8 | executions: 9 | #First Image 10 | - id: img-1 11 | sizes: 12 | - 256 13 | - 256 14 | - 1024 15 | prompt: image.prompt 16 | properties: 17 | imageDescription: ${imageDescription} 18 | -------------------------------------------------------------------------------- /archetypes/plugin-catalog/defaultConfig/ai-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "v1", 3 | "name_for_human": "${nameForHuman}", 4 | "name_for_model": "${nameForModel}", 5 | "description_for_human": "${descriptionForHuman}", 6 | "description_for_model": "${manifestPrompt}", 7 | "auth": { 8 | "type": "none" 9 | }, 10 | "api": { 11 | "type": "openapi", 12 | "url": "http://localhost:${port}/openapi.yaml", 13 | "is_user_authenticated": false 14 | }, 15 | "logo_url": "http://localhost:${port}/logo.webp", 16 | "contact_email": "legal@example.com", 17 | "legal_info_url": "http://example.com/legal" 18 | } 19 | -------------------------------------------------------------------------------- /archetypes/plugin-catalog/defaultConfig/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sisbell/stackwire-gpt/7fe2dc2d0947dc25deaceca219638ebbe20ec807/archetypes/plugin-catalog/defaultConfig/logo.webp -------------------------------------------------------------------------------- /archetypes/plugin-catalog/defaultConfig/mocks/catalog.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "website-performance-plugin.eidam.dev", 4 | "name": "Website Performance", 5 | "description": "Measure key metrics about your website - performance, accessibility, best practices, SEO, PWA.", 6 | "logo": "https://tabler-icons.io/static/tabler-icons/icons/brand-speedtest.svg" 7 | }, 8 | { 9 | "id": "chatspot.ai", 10 | "name": "ChatSpot", 11 | "description": "Get access to marketing/sales data including domain information, company research and search keyword research.", 12 | "logo": "https://chatspot.ai/plugin/logo.png" 13 | }, 14 | { 15 | "id": "gptnews.uk", 16 | "name": "UK Latest News", 17 | "description": "Get the latest news stories from the UK's top news outlets including BBC News, Sky News, The Independent, and others.", 18 | "logo": "https://gptnews.uk/logo.svg" 19 | }, 20 | { 21 | "id": "xapi.lihaorui.com", 22 | "name": "Xpapers", 23 | "description": "Effortlessly find real academic papers on arXiv. Dive into abstracts, references, and access public PDF URLs.", 24 | "logo": "https://xsearchlogo.oss-us-west-1.aliyuncs.com/logo.png?x-oss-process=image/resize,w_200,l_200" 25 | }, 26 | { 27 | "id": "www.buywisely.com.au", 28 | "name": "BuyWisely", 29 | "description": "Compare Prices & Discover the Latest Offers from thousands of online shops in Australia.", 30 | "logo": "https://buywisely.com.au/assets/logo.png" 31 | }, 32 | { 33 | "id": "imagesearch.jjzhang.repl.co", 34 | "name": "ImageSearch", 35 | "description": "Find and display image from unsplash.", 36 | "logo": "https://imagesearch.jjzhang.repl.co/logo.png" 37 | }, 38 | { 39 | "id": "esne.ai", 40 | "name": "Podcast search", 41 | "description": "This tool explores podcasts from PodcastIndex.org, a platform for decentralized audio content discovery.", 42 | "logo": "https://esne.ai/logo.png" 43 | }, 44 | { 45 | "id": "abcaudio.vynalezce.com", 46 | "name": "ABC Music Notation", 47 | "description": "Plugin for converting ABC music notation to wav, midi and postscript files.", 48 | "logo": "https://abcaudio.vynalezce.com/static/logo.png" 49 | }, 50 | { 51 | "id": "chatgpt.wild.creatuity.net", 52 | "name": "Creatuity Stores", 53 | "description": "We integrate stores so you can search for products in all of them at the same time.", 54 | "logo": "https://chatgpt.wild.creatuity.net/.well-known/logo.png" 55 | }, 56 | { 57 | "id": "iamrich--eviltwinv.repl.co", 58 | "name": "I Am Rich", 59 | "description": "Proudly declare 'I am rich'.", 60 | "logo": "https://iamrich--eviltwinv.repl.co/iamrich.jpg" 61 | }, 62 | { 63 | "id": "theresanaiforthat.com", 64 | "name": "There's An AI For That", 65 | "description": "Find the right AI tools for any use case, from the world's largest database of AI tools.", 66 | "logo": "https://media.theresanaiforthat.com/favicon-dark-large.png" 67 | }, 68 | { 69 | "id": "api.ludum.dev", 70 | "name": "Tic Tac Toe", 71 | "description": "Playing a game of Tic Tac Toe with varying board sizes. You can submit your move and get the AI's response move.", 72 | "logo": "https://api.ludum.dev/logo.png" 73 | }, 74 | { 75 | "id": "portfolioslab.com", 76 | "name": "PortfoliosLab", 77 | "description": "Stocks, ETFs, funds, crypto analysis: historical performance, volatility, risk metrics, Sharpe ratio, drawdowns, etc.", 78 | "logo": "https://portfolioslab.com/logo.png" 79 | }, 80 | { 81 | "id": "plugin.daizy.com", 82 | "name": "DAIZY", 83 | "description": "Deep insights on ETFs, stocks, cryptos. Institutional-grade data: performance, risk, sustainability, research.", 84 | "logo": "https://uploads-ssl.webflow.com/62ea3bc0bbc782948e62e0bb/639313e510990d725bfec083_webclip.png" 85 | }, 86 | { 87 | "id": "creaturegen.vercel.app", 88 | "name": "Creature Generator", 89 | "description": "Creates a random creature and an image it for use in role playing games.", 90 | "logo": "https://creaturegen.vercel.app/logo.png" 91 | }, 92 | { 93 | "id": "app.wolfie.ai", 94 | "name": "Career Copilot", 95 | "description": "A trusted, always on assistant to help software developers find a better job. Built by Commit.dev.", 96 | "logo": "https://app.wolfie.ai/images/chatgpt-logo.png" 97 | }, 98 | { 99 | "id": "simbiss.net", 100 | "name": "World News", 101 | "description": "Summarize news headlines. You can ask for the latest news from various sources around the world.", 102 | "logo": "https://simbiss.net/logo.png" 103 | }, 104 | { 105 | "id": "www.zumper.com", 106 | "name": "Zumper Rental Search", 107 | "description": "Find a rental home in the US and Canada.", 108 | "logo": "https://www.zumper.com/img/favicon.png" 109 | }, 110 | { 111 | "id": "optionspro.io", 112 | "name": "Options Pro", 113 | "description": "Options Pro is your personal options trading assistant to help you navigate market conditions.", 114 | "logo": "https://optionspro.io/assets/Icon/icon.png" 115 | }, 116 | { 117 | "id": "api.gamebase.chat", 118 | "name": "GameBase", 119 | "description": "Chat and get game info, database is based on the latest gaming information in 2023, supports multiple platforms.", 120 | "logo": "https://raw.githubusercontent.com/zichanghu/Nap/main/game-center-chatgptplugin-colorful.png" 121 | }, 122 | { 123 | "id": "voxscript.awt.icu", 124 | "name": "VoxScript", 125 | "description": "Enables searching of YouTube transcripts, financial data sources, and Google Search results, and more!", 126 | "logo": "https://voxscript.awt.icu/images/VoxScript_logo_32x32.png" 127 | }, 128 | { 129 | "id": "wahi.com", 130 | "name": "Wahi", 131 | "description": "Hey Ontario, ask and get so in the know on the latest listings, property insights and more.", 132 | "logo": "https://wahi.com/wp-content/uploads/2022/10/wahi-logo.svg" 133 | }, 134 | { 135 | "id": "giga-do.azurewebsites.net", 136 | "name": "Giga Tutor", 137 | "description": "Giga is your AI powered personalised tutor, it keeps the answers to your questions personalised.", 138 | "logo": "https://giga-do.azurewebsites.net/logo.png" 139 | }, 140 | { 141 | "id": "aiplugin-owljourney.owlting.com", 142 | "name": "OwlJourney", 143 | "description": "Provides lodging and activity suggestions, ensuring an engaging and user-friendly journey.", 144 | "logo": "https://aiplugin-owljourney.owlting.com/logo.svg" 145 | }, 146 | { 147 | "id": "credityelp.com", 148 | "name": "CreditYelp", 149 | "description": "Access various essential financial calculators for a detailed repayment schedule and payoff term.", 150 | "logo": "https://credityelp.com/openai/logo.png" 151 | }, 152 | { 153 | "id": "api.tomorrow.io", 154 | "name": "Tomorrow.io Weather", 155 | "description": "Predicting, planning, and adapting to the weather forecast via contextualized chat-based insights.", 156 | "logo": "https://assets.hypercast2.climacell.co/logo.png" 157 | }, 158 | { 159 | "id": "comicfinder.fly.dev", 160 | "name": "Comic Finder", 161 | "description": "A plugin that finds a relevant comic given a description. Currently supports XKCD and SMBC comics.", 162 | "logo": "https://comicfinder.fly.dev/logo.png" 163 | }, 164 | { 165 | "id": "www.accesslinks.ai", 166 | "name": "Access Link", 167 | "description": "Access any links on the web and get the information you need.", 168 | "logo": "https://www.accesslinks.ai/.well-known/logo.png" 169 | }, 170 | { 171 | "id": "playlistai-plugin.vercel.app", 172 | "name": "PlaylistAI", 173 | "description": "Create Spotify playlists for any prompt.", 174 | "logo": "https://playlistai-plugin.vercel.app/icon.png" 175 | }, 176 | { 177 | "id": "chat.noteable.io", 178 | "name": "Noteable", 179 | "description": "Create notebooks in Python, SQL, and Markdown to explore data, visualize, and share notebooks with everyone.", 180 | "logo": "https://chat.noteable.io/origami/static/images/noteable-logo.png" 181 | }, 182 | { 183 | "id": "plugin.bramework.com", 184 | "name": "Bramework", 185 | "description": "Find keywords, generate content briefs, perform SEO analysis, and extract SEO information.", 186 | "logo": "https://plugin.bramework.com/logo.png" 187 | }, 188 | { 189 | "id": "plugin.ai.vivian.com", 190 | "name": "Vivian Health", 191 | "description": "Take the first step to finding your next healthcare job.", 192 | "logo": "https://plugin.ai.vivian.com/.well-known/logo.png" 193 | }, 194 | { 195 | "id": "smyth.seo.app", 196 | "name": "SEO.app", 197 | "description": "Your personal SEO assistant for content marketing.", 198 | "logo": "https://smyth.seo.app/static/seo-app-icon.png" 199 | }, 200 | { 201 | "id": "yabblezone.net", 202 | "name": "Yabble", 203 | "description": "Create insights instantly. Any audience. Any question. Yabble it.", 204 | "logo": "https://yabblezone.net/.well-known/logo.png" 205 | }, 206 | { 207 | "id": "polygon.io", 208 | "name": "Polygon", 209 | "description": "Market data, news, and fundamentals for stocks, options, forex, and crypto from Polygon.io.", 210 | "logo": "https://polygon.io/imgs/favicon.png" 211 | }, 212 | { 213 | "id": "midjourney-ruddy.vercel.app", 214 | "name": "Photorealistic", 215 | "description": "Generate Photorealistic prompts for Midjourney.", 216 | "logo": "https://midjourney-ruddy.vercel.app/imgs/logo96.png" 217 | }, 218 | { 219 | "id": "trialradar.marketflare.repl.co", 220 | "name": "Clinical Trial Radar", 221 | "description": "Discover current info on global clinical trials, organizations, diseases, and biomarkers from public & private studies.", 222 | "logo": "https://www.marketflare.com/wp-content/uploads/2015/12/mf_icon.png" 223 | }, 224 | { 225 | "id": "showme.redstarplugin.com", 226 | "name": "Show Me", 227 | "description": "Create and edit diagrams directly in chat.", 228 | "logo": "https://showme.redstarplugin.com/logo.svg" 229 | }, 230 | { 231 | "id": "turo.com", 232 | "name": "Turo", 233 | "description": "Search for the perfect Turo vehicle for your next trip.", 234 | "logo": "https://resources.turo.com/next-js/0.0.1/app_icon.png" 235 | }, 236 | { 237 | "id": "xyz-prompt-perfect.uc.r.appspot.com", 238 | "name": "Prompt Perfect", 239 | "description": "Type 'perfect' to craft the perfect prompt, every time.", 240 | "logo": "https://xyz-prompt-perfect.uc.r.appspot.com/static/prompt_perfect_logo.png" 241 | }, 242 | { 243 | "id": "chatgpt-plugin.2u.com", 244 | "name": "edX", 245 | "description": "Find courses and content from leading universities to expand your knowledge at any level.", 246 | "logo": "https://www.edx.org/images/logos/edx_logo_chatgpt_plugin.svg" 247 | }, 248 | { 249 | "id": "api.getchange.io", 250 | "name": "Change", 251 | "description": "Discover impactful nonprofits to support in your community and beyond.", 252 | "logo": "https://api.getchange.io/.well-known/change-logo.png" 253 | }, 254 | { 255 | "id": "api.metaphor.systems", 256 | "name": "Metaphor", 257 | "description": "Access the internet's highest quality content. Recommended by people, powered by neural search.", 258 | "logo": "https://api.metaphor.systems/logo.png" 259 | }, 260 | { 261 | "id": "api.radar.cloudflare.com", 262 | "name": "Cloudflare Radar", 263 | "description": "Get real-time insights into Internet traffic patterns and threats as seen by Cloudflare.", 264 | "logo": "https://api.radar.cloudflare.com/.well-known/logo.svg" 265 | }, 266 | { 267 | "id": "webreader.webpilotai.com", 268 | "name": "WebPilot", 269 | "description": "Browse & QA Webpage/PDF/Data. Generate articles, from one or more URLs.", 270 | "logo": "https://webreader.webpilotai.com/logo.png" 271 | }, 272 | { 273 | "id": "polarr.co", 274 | "name": "Polarr", 275 | "description": "Search Polarr's massive pool of user generated filters to make your photos and videos perfect.", 276 | "logo": "https://www.polarr.com/favicon-256x256.png" 277 | }, 278 | { 279 | "id": "dev.to", 280 | "name": "DEV Community", 281 | "description": "Plugin for recommending articles or users from DEV Community.", 282 | "logo": "https://dev.to/logo.png" 283 | }, 284 | { 285 | "id": "www.rentable.co", 286 | "name": "Rentable Apartments", 287 | "description": "Get apartment options in a city of your choice, scoped to your needs and budget.", 288 | "logo": "https://abodo-assets.s3.amazonaws.com/external/rentable-logo-red.png" 289 | }, 290 | { 291 | "id": "gpt-service-api.hellopublic.com", 292 | "name": "Public", 293 | "description": "Get real-time and historical market data, including asset prices, news, research, and comprehensive financial analysis.", 294 | "logo": "https://universal.hellopublic.com/gpt/public-icon.png" 295 | }, 296 | { 297 | "id": "gochitchat.ai", 298 | "name": "Link Reader", 299 | "description": "Reads the content of all kinds of links, like webpage, PDF, PPT, image, Word & other docs.", 300 | "logo": "https://gochitchat.ai/linkreader/logo.png" 301 | }, 302 | { 303 | "id": "www.coupert.com", 304 | "name": "Coupert", 305 | "description": "Search for the internet’s best coupons from thousands of online stores.", 306 | "logo": "https://www.coupert.com/img/favicon.svg" 307 | }, 308 | { 309 | "id": "www.phloxcorp.io", 310 | "name": "Wishbucket", 311 | "description": "Unified product search across all Korean platforms and brands.", 312 | "logo": "https://www.phloxcorp.io/logo.png" 313 | }, 314 | { 315 | "id": "openai-plugin.yayforms.com", 316 | "name": "Yay! Forms", 317 | "description": "Allows you to create AI-Powered Forms, Surveys, Quizzes, or Questionnaires on Yay! Forms.", 318 | "logo": "https://app.yayforms.com/logo.svg" 319 | }, 320 | { 321 | "id": "stage.glowing.ai", 322 | "name": "Glowing", 323 | "description": "Schedule and send daily SMS messages - reminders, inspiration, helpers and more.", 324 | "logo": "https://stage.glowing.ai/.well-known/glowing.png" 325 | }, 326 | { 327 | "id": "ai.abcmouse.com", 328 | "name": "ABCmouse", 329 | "description": "Provides fun and educational learning activities for children 2-8 years old.", 330 | "logo": "https://ai.abcmouse.com/logo.png" 331 | }, 332 | { 333 | "id": "www.openai.hubbubworld.com", 334 | "name": "Hubbub", 335 | "description": "Local health risk & safety guidance for COVID-19, Flu, RSV and more in the US.", 336 | "logo": "https://cdn.hubbubworld.com/openai/i/hubbub-a-safer-world-256.png" 337 | }, 338 | { 339 | "id": "oneword.domains", 340 | "name": "One Word Domains", 341 | "description": "Check the availability of a domain and compare prices across different registrars.", 342 | "logo": "https://oneword.domains/logo.png" 343 | }, 344 | { 345 | "id": "c3glide-d9g5.boldstratus.com", 346 | "name": "C3 Glide", 347 | "description": "Get live aviation data for pilots. Ask questions about METARs, TAFs, NOTAMs for flight planning.", 348 | "logo": "https://c3glide-d9g5.boldstratus.com/c3glide-api/assets/BoldStratus+Purple+Icon.png" 349 | }, 350 | { 351 | "id": "likewiserecommends.com", 352 | "name": "Likewise", 353 | "description": "Your media and entertainment companion. Get TV, Movies, Books & Podcast Recommendations.", 354 | "logo": "https://likewiserecommends.com/.well-known/logo.png" 355 | }, 356 | { 357 | "id": "plugins.zillow.com", 358 | "name": "Zillow", 359 | "description": "Your real estate assistant is here! Search listings, view property details, and get home with Zillow.", 360 | "logo": "https://delivery.digitalassets.zillowgroup.com/api/public/content/200x200_CMS_Full.png?v=60fab90c" 361 | }, 362 | { 363 | "id": "scholar-ai.net", 364 | "name": "ScholarAI", 365 | "description": "Unlock the power of scientific knowledge with fast, reliable, and peer-reviewed data at your fingertips.", 366 | "logo": "https://scholar-ai.net/logo.png" 367 | }, 368 | { 369 | "id": "llmsearch.endpoint.getyourguide.com", 370 | "name": "GetYourGuide", 371 | "description": "Find tours, excursions and other travel activities you can book on GetYourGuide.", 372 | "logo": "https://code.getyourguide.com/assets/gyg-logo.svg" 373 | }, 374 | { 375 | "id": "stock-advisor.com", 376 | "name": "AITickerChat", 377 | "description": "Retrieve USA stock insights from SEC filings as well as Earnings Call Transcripts.", 378 | "logo": "https://stock-advisor.com/.well-known/logo.png" 379 | }, 380 | { 381 | "id": "trip.com", 382 | "name": "Trip.com", 383 | "description": "Discover the ultimate travel companion - simplify your flight and hotel bookings. Enjoy your effortless trip!", 384 | "logo": "https://ak-s.tripcdn.com/modules/ibu/online-home/ee6a046e4f5b73083c94ac36ec3f81e2.ee6a046e4f5b73083c94ac36ec3f81e2.png" 385 | }, 386 | { 387 | "id": "savvytrader.com", 388 | "name": "Savvy Trader AI", 389 | "description": "Realtime stock, crypto and other investment data.", 390 | "logo": "https://savvytrader.com/android-chrome-192x192.png" 391 | }, 392 | { 393 | "id": "chatgpt-plugin.prod.golden.dev", 394 | "name": "Golden", 395 | "description": "Get current factual data on companies from the Golden knowledge graph.", 396 | "logo": "https://chatgpt-plugin.prod.golden.dev/logo.png" 397 | }, 398 | { 399 | "id": "lexi-shopping-assistant-chatgpt-plugin.iamnazzty.repl.co", 400 | "name": "Lexi Shopper", 401 | "description": "Get product recommendations from your local Amazon store.", 402 | "logo": "https://lexi-shopping-assistant-chatgpt-plugin.iamnazzty.repl.co/logo.png" 403 | }, 404 | { 405 | "id": "keyplays.malcsilberman.repl.co", 406 | "name": "Keyplays Live Soccer", 407 | "description": "Latest live soccer standings, results, commentary, tv stations, keyplays (with and without scores).", 408 | "logo": "https://keyplays.malcsilberman.repl.co/static/img/icon.png" 409 | }, 410 | { 411 | "id": "blockatlas.com", 412 | "name": "BlockAtlas", 413 | "description": "Search the US Census! Find data sets, ask questions, and visualize.", 414 | "logo": "https://blockatlas.com/logo.png" 415 | }, 416 | { 417 | "id": "opentrivia.drengskapur.workers.dev", 418 | "name": "Open Trivia", 419 | "description": "Get trivia questions from various categories and difficulty levels.", 420 | "logo": "https://raw.githubusercontent.com/drengskapur/open-trivia-database-chat-plugin/main/icon.png" 421 | }, 422 | { 423 | "id": "searchweb.keymate.ai", 424 | "name": "KeyMate.AI Search", 425 | "description": "Search the web by using a Custom Search Engine with KeyMate.AI, your AI-powered web search engine.", 426 | "logo": "https://searchweb.keymate.ai/.well-known/icon.png" 427 | }, 428 | { 429 | "id": "portfoliopilot.com", 430 | "name": "PortfolioPilot", 431 | "description": "Your AI investing guide: portfolio assessment, recommendations, answers to all finance questions.", 432 | "logo": "https://portfoliopilot.com/logo.png" 433 | }, 434 | { 435 | "id": "crafty-clues.jeevnayak.repl.co", 436 | "name": "Crafty Clues", 437 | "description": "Guess the words that the AI craftily clues for you. Add restrictions to make the game more interesting!", 438 | "logo": "https://crafty-clues.jeevnayak.repl.co/static/logo.png" 439 | }, 440 | { 441 | "id": "word-sneak.jeevnayak.repl.co", 442 | "name": "Word Sneak", 443 | "description": "The AI has to sneak 3 secret words into your conversation. Guess the words to win the game!", 444 | "logo": "https://word-sneak.jeevnayak.repl.co/static/logo.png" 445 | }, 446 | { 447 | "id": "www.redfin.com", 448 | "name": "Redfin", 449 | "description": "Have questions about the housing market? Find the answers to help you win in today's market.", 450 | "logo": "https://ssl.cdn-redfin.com/vLATEST/images/logos/redfin-logo-square-red-500.png" 451 | }, 452 | { 453 | "id": "klever-chatgpt-plugin-prod.herokuapp.com", 454 | "name": "Kraftful", 455 | "description": "Your product development coach. Ask about best practices. Get top gurus’ product thinking.", 456 | "logo": "https://klever-chatgpt-plugin-prod.herokuapp.com/logo.png" 457 | }, 458 | { 459 | "id": "gptshop.bohita.com", 460 | "name": "Bohita", 461 | "description": "Create apparel with any image you can describe! Get it delivered right to your door.", 462 | "logo": "https://gptshop.bohita.com/logo.png" 463 | }, 464 | { 465 | "id": "www.shimmer.ooo", 466 | "name": "Shimmer: Nutrition Coach", 467 | "description": "Track meals & gain insights for a healthier lifestyle from 1m+ restaurants & grocery stores.", 468 | "logo": "https://shimmer.ooo/logo.svg" 469 | }, 470 | { 471 | "id": "ppc-optimizer.gcs.ai", 472 | "name": "Competitor PPC Ads", 473 | "description": "Discover your competitors' best PPC ads by entering their website address.", 474 | "logo": "https://ppc-optimizer.gcs.ai/PaidAdsOptimizer-logo.png" 475 | }, 476 | { 477 | "id": "remoteambition.com", 478 | "name": "Ambition", 479 | "description": "Search millions of jobs near you.", 480 | "logo": "https://assets.remoteambition.com/ai-plugin-logo.png" 481 | }, 482 | { 483 | "id": "chatgpt.vipmanor.com", 484 | "name": "Manorlead", 485 | "description": "Get a list of listings for rent or sale in cities across Canada and the US based on your search criteria.", 486 | "logo": "https://chatgpt.vipmanor.com/logo.png" 487 | }, 488 | { 489 | "id": "ai.seovendor.co", 490 | "name": "SEO CORE AI", 491 | "description": "Use AI to analyze and improve the SEO of a website. Get advice on websites, keywords and competitors.", 492 | "logo": "https://ai.seovendor.co/seo-analysis-logo.jpg" 493 | }, 494 | { 495 | "id": "kalendar.ai", 496 | "name": "KalendarAI", 497 | "description": "KalendarAI sales agents generate revenue with potential customers from 200+ million companies globally.", 498 | "logo": "https://kalendar.ai/assets/logo-black-50c5284888eeea1d77f877d9a6736f1bf23533f975fae3939824cf429ad95e34.png" 499 | }, 500 | { 501 | "id": "algorithma.ruvnet.repl.co", 502 | "name": "Algorithma", 503 | "description": "Shape your virtual life with in this immersive life simulator game to begin Type /start to begin.", 504 | "logo": "https://algorithma.replit.app/.well-known/logo.png" 505 | }, 506 | { 507 | "id": "openai.creaticode.com", 508 | "name": "CreatiCode Scratch", 509 | "description": "Display Scratch programs as images and write 2D/3D programs using CreatiCode Scratch extensions.", 510 | "logo": "https://play.creaticode.com/tcode-static-files/images/newlogo200.png" 511 | }, 512 | { 513 | "id": "jettel.de", 514 | "name": "Video Insights", 515 | "description": "Interact with online video platforms like Youtube or Daily Motion.", 516 | "logo": "https://jettel.de/logo.png" 517 | }, 518 | { 519 | "id": "plugin-dtwewgpm2a-uc.a.run.app", 520 | "name": "Tutory", 521 | "description": "Access affordable, on-demand tutoring and education right at your fingertips.", 522 | "logo": "https://plugin-dtwewgpm2a-uc.a.run.app/logo.png" 523 | }, 524 | { 525 | "id": "api.tasty.co", 526 | "name": "Tasty Recipes", 527 | "description": "Discover recipe ideas, meal plans and cooking tips from Tasty's millions of users!", 528 | "logo": "https://api.tasty.co/.well-known/logo.png" 529 | }, 530 | { 531 | "id": "www.mbplayer.com", 532 | "name": "MixerBox OnePlayer", 533 | "description": "Unlimited music, podcasts, and videos across various genres. Enjoy endless listening with our rich playlists!", 534 | "logo": "https://www.mbplayer.com/favicon-app_store_icon.png" 535 | }, 536 | { 537 | "id": "chatgpt-plugin.tabelog.com", 538 | "name": "Tabelog", 539 | "description": "Allows you to find restaurants in Japan that have availability for reservations.", 540 | "logo": "https://tblg.k-img.com/images/smartphone/icon/app_icon_tabelog_flat_3x.png" 541 | }, 542 | { 543 | "id": "plugin.speechki.org", 544 | "name": "Speechki", 545 | "description": "The easiest way to convert texts to ready-to-use audio — download link, audio player page, or embed!", 546 | "logo": "https://plugin.speechki.org/icon.svg" 547 | }, 548 | { 549 | "id": "haulingbuddies.com", 550 | "name": "Hauling Buddies", 551 | "description": "Locate dependable animal transporters using recommendations, reviews, and regulatory compliance search features.", 552 | "logo": "https://haulingbuddies.com/assets/icon_68_68-f5783fef14eb6cefa4084be40395b4e7402c395fd5441c0ceffdfe882c70d7f2.png" 553 | }, 554 | { 555 | "id": "api.giftwrap.ai", 556 | "name": "Giftwrap", 557 | "description": "Ask about gift ideas for any occasion and recipient. Get it wrapped and delivered, no address needed.", 558 | "logo": "https://giftwrap.ai/logo.png" 559 | }, 560 | { 561 | "id": "biztoc.com", 562 | "name": "BizToc", 563 | "description": "Search BizToc for business & finance news.", 564 | "logo": "https://biztoc.com/favicon.png" 565 | }, 566 | { 567 | "id": "api.speak.com", 568 | "name": "Speak", 569 | "description": "Learn how to say anything in another language with Speak, your AI-powered language tutor.", 570 | "logo": "https://api.speak.com/ai-plugin-logo.png" 571 | }, 572 | { 573 | "id": "opentable.com", 574 | "name": "OpenTable", 575 | "description": "Allows you to search for restaurants available for booking dining experiences", 576 | "logo": "https://cdn.otstatic.com/third-party/images/opentable-logo-512.png" 577 | }, 578 | { 579 | "id": "server.shop.app", 580 | "name": "Shop", 581 | "description": "Search for millions of products from the world's greatest brands.", 582 | "logo": "https://cdn.shopify.com/shop-assets/static_uploads/shop-logo-white-bg-purple.png" 583 | }, 584 | { 585 | "id": "api.factba.se", 586 | "name": "FiscalNote", 587 | "description": "FiscalNote enables access to select market-leading, real-time data sets for legal, political, and regulatory information", 588 | "logo": "https://api.factba.se/static/fn-logo.png" 589 | }, 590 | { 591 | "id": "wolframalpha.com", 592 | "name": "Wolfram", 593 | "description": "Access computation, math, curated knowledge & real-time data through Wolfram|Alpha and Wolfram Language.", 594 | "logo": "https://www.wolframcdn.com/images/icons/Wolfram.png" 595 | }, 596 | { 597 | "id": "zapier.com", 598 | "name": "Zapier", 599 | "description": "Interact with over 5,000+ apps like Google Sheets, Gmail, HubSpot, Salesforce, and thousands more.", 600 | "logo": "https://cdn.zappy.app/8f853364f9b383d65b44e184e04689ed.png" 601 | }, 602 | { 603 | "id": "apim.expedia.com", 604 | "name": "Expedia", 605 | "description": "Bring your trip plans to life – get there, stay there, find things to see and do.", 606 | "logo": "https://a.travel-assets.com/egds/marks/brands/expedia/onekey__chiclet_square.svg" 607 | }, 608 | { 609 | "id": "instacart.com", 610 | "name": "Instacart", 611 | "description": "What’s cookin'? Ask about recipes, meal plans, & more -- and get ingredients delivered from 40,000+ stores!", 612 | "logo": "https://www.instacart.com/assets/beetstrap/brand/2022/carrotlogo-1286c257354036d178c09e815906198eb7f012b8cdc4f6f8ec86d3e64d799a5b.png" 613 | }, 614 | { 615 | "id": "www.klarna.com", 616 | "name": "Klarna Shopping", 617 | "description": "Search and compare prices from thousands of online shops.", 618 | "logo": "https://www.klarna.com/assets/sites/5/2020/04/27143923/klarna-K-150x150.jpg" 619 | }, 620 | { 621 | "id": "kayak.com", 622 | "name": "KAYAK", 623 | "description": "Search flights, stays & rental cars or get recommendations where you can go on your budget.", 624 | "logo": "https://content.r9cdn.net/images/apple-touch-icons/apple-touch-icon-120x120.png" 625 | } 626 | ] -------------------------------------------------------------------------------- /archetypes/plugin-catalog/defaultConfig/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: My API 4 | version: 1.0.0 5 | description: A Plugin for discovering ChatGPT Plugins. 6 | servers: 7 | - url: http://localhost:${port} 8 | paths: 9 | /catalog: 10 | get: 11 | operationId: getPluginInfo 12 | summary: Get the plugin catalog 13 | responses: 14 | '200': 15 | description: A list of Plugins in the catalog 16 | content: 17 | application/json: 18 | schema: 19 | type: array 20 | items: 21 | type: object 22 | properties: 23 | id: 24 | type: string 25 | name: 26 | description: The name of the plugin 27 | type: string 28 | description: 29 | description: A description of the plugin 30 | type: string 31 | logo: 32 | description: The plugin logo 33 | type: string 34 | -------------------------------------------------------------------------------- /archetypes/plugin-catalog/defaultConfig/plugin-manifest.prompt: -------------------------------------------------------------------------------- 1 | A plugin that allows the user to search for other ChatGPT Plugins. These ChatGPT plugins perform specialized queries 2 | so help the user find the right one for their needs. Keep things informal with conversational tone, like a helpful 3 | assistant. Don't list out information in bullet point form. Prompt user asking them for more information about what 4 | they want to find. -------------------------------------------------------------------------------- /archetypes/plugin-catalog/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: plugin-catalog 3 | projectVersion: '1.0' 4 | projectType: plugin 5 | defaultConfig: 6 | properties: 7 | port: 5003 8 | nameForHuman: ZapVine Plugin Catalog 9 | nameForModel: zapvine 10 | descriptionForHuman: Discover ChatGPT Plugins through a ChatGPT Plugin 11 | 12 | pluginServers: 13 | - serverId: catalog 14 | mockedRequests: 15 | - path: /catalog 16 | method: get 17 | mockedResponse: catalog.json 18 | 19 | -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/defaultConfig/ai-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "v1", 3 | "name_for_human": "${nameForHuman}", 4 | "name_for_model": "${nameForModel}", 5 | "description_for_human": "${descriptionForHuman}", 6 | "description_for_model": "${manifestPrompt}", 7 | "auth": { 8 | "type": "none" 9 | }, 10 | "api": { 11 | "type": "openapi", 12 | "url": "http://localhost:${port}/openapi.yaml", 13 | "is_user_authenticated": false 14 | }, 15 | "logo_url": "http://localhost:${port}/logo.png", 16 | "contact_email": "legal@example.com", 17 | "legal_info_url": "http://example.com/legal" 18 | } 19 | -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/defaultConfig/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sisbell/stackwire-gpt/7fe2dc2d0947dc25deaceca219638ebbe20ec807/archetypes/plugins-quickstart/defaultConfig/logo.png -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/defaultConfig/mocks/todos-global.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | "Finish reading the book", 4 | "Book a haircut appointment", 5 | "Buy birthday gift for friend", 6 | "Complete online course module", 7 | "Organize the garage" 8 | ] 9 | } -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/defaultConfig/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: TODO Plugin 4 | description: A plugin that allows the user to create and manage a TODO list using ChatGPT. 5 | version: 'v1' 6 | servers: 7 | - url: http://localhost:${port} 8 | paths: 9 | /todos/{username}: #Get this value 10 | get: #Get this value 11 | operationId: getTodos #Get this value 12 | summary: Get the list of todos 13 | parameters: 14 | - in: path 15 | name: username 16 | schema: 17 | type: string 18 | required: true 19 | description: The name of the user. 20 | responses: 21 | "200": #Get this value 22 | description: OK 23 | content: 24 | application/json: #Get this value 25 | schema: 26 | $ref: '#/components/schemas/getTodosResponse' 27 | post: #Get this value 28 | operationId: addTodo #Get this value 29 | summary: Add a todo to the list 30 | parameters: 31 | - in: path 32 | name: username 33 | schema: 34 | type: string 35 | required: true 36 | description: The name of the user. 37 | requestBody: 38 | required: true 39 | content: 40 | application/json: #Get this value 41 | schema: 42 | $ref: '#/components/schemas/addTodoRequest' 43 | responses: 44 | "200": #Get this value 45 | description: OK 46 | delete: 47 | operationId: deleteTodo 48 | summary: Delete a todo from the list 49 | parameters: 50 | - in: path 51 | name: username 52 | schema: 53 | type: string 54 | required: true 55 | description: The name of the user. 56 | requestBody: 57 | required: true 58 | content: 59 | application/json: 60 | schema: 61 | $ref: '#/components/schemas/deleteTodoRequest' 62 | responses: 63 | "200": 64 | description: OK 65 | 66 | components: 67 | schemas: 68 | getTodosResponse: 69 | type: object 70 | properties: 71 | todos: 72 | type: array 73 | items: 74 | type: string 75 | description: The list of todos. 76 | addTodoRequest: 77 | type: object 78 | required: 79 | - todo 80 | properties: 81 | todo: 82 | type: string 83 | description: The todo to add to the list. 84 | required: true 85 | deleteTodoRequest: 86 | type: object 87 | required: 88 | - todo_idx 89 | properties: 90 | todo_idx: 91 | type: integer 92 | description: The index of the todo to delete. 93 | required: true -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/defaultConfig/plugin-manifest.prompt: -------------------------------------------------------------------------------- 1 | A plugin that allows the user to create and manage a TODO list using ChatGPT. 2 | If you do not know the user's username, ask them first before making queries to the plugin. 3 | Otherwise, use the username global -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/pluginFlavors/kaleb/mocks/todos.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | "Repair a broken tractor", 4 | "Harvest the wheat field", 5 | "Shear the sheep", 6 | "Install new fencing for livestock", 7 | "Plant new crops for the season", 8 | "Get new haircut" 9 | ] 10 | } -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/pluginFlavors/mike/mocks/todos.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | "Clean out a septic tank", 4 | "Repair a broken sewer pipe", 5 | "Collect roadkill for disposal", 6 | "Assist in bee hive relocation", 7 | "Service a grease trap at a restaurant" 8 | ] 9 | } -------------------------------------------------------------------------------- /archetypes/plugins-quickstart/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: "${projectName}" 3 | projectVersion: "${projectVersion}" 4 | projectType: plugin 5 | defaultConfig: 6 | properties: 7 | port: 5003 8 | nameForHuman: TODO Plugin (no auth) 9 | nameForModel: todo 10 | descriptionForHuman: Plugin for managing a TODO list, you can add, remove and view your TODOs. 11 | mockedRequests: 12 | - path: "/todos/global" 13 | method: get 14 | mockedResponse: todos-global.json 15 | - path: "/todos/user" 16 | method: get 17 | mockedResponse: todos-global.json 18 | 19 | pluginServers: 20 | - serverId: todo-mike 21 | flavor: mike 22 | # mocked requests 23 | mockedRequests: 24 | - path: "/todos/mike" 25 | method: get 26 | mockedResponse: todos.json # returns the content of this file 27 | 28 | # Adds a different user for testing 29 | - serverId: todo-kaleb 30 | flavor: kaleb 31 | mockedRequests: 32 | - path: "/todos/kaleb" 33 | method: get 34 | mockedResponse: todos.json 35 | properties: 36 | showHttpHeaders: true # Show http headers in logs -------------------------------------------------------------------------------- /archetypes/prompt/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: "${projectName}" 3 | projectVersion: "${projectVersion}" 4 | apiKeyFile: "${apiKeyFile}" 5 | blocks: 6 | - blockId: single-1 7 | pluginName: ExperimentGptPlugin 8 | blockRuns: ${blockRuns} 9 | configuration: 10 | requestParams: 11 | model: gpt-3.5-turbo 12 | temperature: 1.2 13 | top_p: 1 14 | max_tokens: 500 15 | executions: 16 | - id: exp-1 17 | responseFormat: ${responseFormat} 18 | fixJson: ${fixJson} 19 | promptChain: 20 | - ${promptName} 21 | properties: 22 | character: Commander in Starfleet 23 | - blockId: report-1 24 | pluginName: ReportingGptPlugin 25 | executions: 26 | - id: report-1 27 | blockIds: 28 | - single-1 -------------------------------------------------------------------------------- /archetypes/prompt/prompt-json.prompt: -------------------------------------------------------------------------------- 1 | Write me a story about ${character}. The main character is ${mainCharacterName}. 2 | If no main character is given, choose one. Write one sentence only. 3 | 4 | The response should be in JSON using the following structure: 5 | Only use these fields. {"mainCharacterName": "", "story": ""} -------------------------------------------------------------------------------- /archetypes/prompt/prompt-text.prompt: -------------------------------------------------------------------------------- 1 | Write me a story about ${character}. The main character is ${mainCharacterName}. 2 | If no main character is given, choose one. Write one sentence only. -------------------------------------------------------------------------------- /archive.sh: -------------------------------------------------------------------------------- 1 | mkdir build 2 | cd archetypes 3 | zip -r ../build/archetypes-4.zip * 4 | -------------------------------------------------------------------------------- /bin/air.dart: -------------------------------------------------------------------------------- 1 | import 'dart:mirrors'; 2 | 3 | import 'package:args/command_runner.dart'; 4 | import 'package:file/file.dart'; 5 | import 'package:file/local.dart'; 6 | import 'package:gpt/src/chatgpt/catalog_parser.dart'; 7 | import 'package:gpt/src/chatgpt/plugin_server.dart'; 8 | import 'package:gpt/src/gpt_plugin.dart'; 9 | import 'package:gpt/src/io_helper.dart'; 10 | 11 | import 'archetype_command.dart'; 12 | 13 | final localFileSystem = LocalFileSystem(); 14 | 15 | final ioHelper = IOHelper(fileSystem: localFileSystem); 16 | 17 | void main(List arguments) async { 18 | CommandRunner("air", "A command line tool for running GPT commands") 19 | ..addCommand(ApiCountCommand()) 20 | ..addCommand(ArchetypeCommand()) 21 | ..addCommand(CatalogCommand()) 22 | ..addCommand(CleanCommand()) 23 | ..addCommand(PluginServerCommand()) 24 | ..addCommand(RunCommand()) 25 | ..run(arguments); 26 | } 27 | 28 | class ApiCountCommand extends ProjectInitializeCommand { 29 | @override 30 | String get description => 31 | "Returns the number of OpenApiCalls that would be made"; 32 | 33 | @override 34 | String get name => "count"; 35 | 36 | ApiCountCommand() { 37 | argParser.addOption('blockId', abbr: 'b'); 38 | } 39 | 40 | @override 41 | Future run() async { 42 | await super.run(); 43 | num result = 0; 44 | final blockId = argResults?['blockId']; 45 | if (blockId != null) { 46 | final block = getBlockById(blocks, blockId)!; 47 | result += await apiCallCountForBlock(block); 48 | } else { 49 | for (var block in blocks) { 50 | result += await apiCallCountForBlock(block); 51 | } 52 | } 53 | final projectName = projectConfig["projectName"]; 54 | final projectVersion = projectConfig["projectVersion"]; 55 | print("Project: $projectName-$projectVersion"); 56 | print("Total OpenAPI Calls would be $result"); 57 | } 58 | 59 | Future apiCallCountForBlock(block) async { 60 | final gptPlugin = getPlugin(block, projectConfig); 61 | return await gptPlugin.apiCallCountForBlock(); 62 | } 63 | 64 | Map? getBlockById(List array, String id) { 65 | for (Map obj in array) { 66 | if (obj["blockId"] == id) { 67 | return obj; 68 | } 69 | } 70 | return null; 71 | } 72 | } 73 | 74 | class CatalogCommand extends Command { 75 | @override 76 | String get description => "Creates a Plugin Catalog File"; 77 | 78 | @override 79 | String get name => "catalog"; 80 | 81 | @override 82 | Future run() async { 83 | final inputFile = localFileSystem.file("manifests.json"); 84 | final outputFile = localFileSystem.file("catalog.json"); 85 | await parseCatalog(inputFile, outputFile); 86 | } 87 | } 88 | 89 | class CleanCommand extends ProjectInitializeCommand { 90 | @override 91 | String get description => "Cleans project's output directory"; 92 | 93 | @override 94 | String get name => "clean"; 95 | 96 | CleanCommand(); 97 | 98 | @override 99 | Future run() async { 100 | await super.run(); 101 | Directory directory = localFileSystem.directory(outputDirName); 102 | if (directory.existsSync()) { 103 | directory.deleteSync(recursive: true); 104 | print( 105 | "Output directory '$outputDirName' and its contents have been deleted."); 106 | } else { 107 | print("Output directory '$outputDirName' does not exist."); 108 | } 109 | } 110 | } 111 | 112 | class RunCommand extends ProjectInitializeCommand { 113 | @override 114 | String get description => "Runs a project's blocks"; 115 | 116 | @override 117 | String get name => "run"; 118 | 119 | RunCommand() { 120 | argParser.addOption('blockId', abbr: 'b'); 121 | argParser.addFlag("dryRun", defaultsTo: false); 122 | } 123 | 124 | @override 125 | Future run() async { 126 | await super.run(); 127 | final blockId = argResults?['blockId']; 128 | final dryRun = argResults?['dryRun']; 129 | if (blockId != null) { 130 | final block = getBlockById(blocks, blockId)!; 131 | await executeBlock(block, dryRun); 132 | } else { 133 | for (var block in blocks) { 134 | print("Executing Block"); 135 | await executeBlock(block, dryRun); 136 | } 137 | } 138 | } 139 | 140 | Future executeBlock(block, dryRun) async { 141 | final gptPlugin = getPlugin(block, projectConfig); 142 | await gptPlugin.execute(dryRun); 143 | } 144 | 145 | Map? getBlockById(List array, String id) { 146 | for (Map obj in array) { 147 | if (obj["blockId"] == id) { 148 | return obj; 149 | } 150 | } 151 | return null; 152 | } 153 | } 154 | 155 | class PluginServerCommand extends Command { 156 | @override 157 | String get description => "Runs local version of ChatGPT Plugin"; 158 | 159 | @override 160 | String get name => "plugin"; 161 | 162 | PluginServerCommand() { 163 | argParser.addOption('serverId', abbr: 's'); 164 | } 165 | 166 | @override 167 | Future run() async { 168 | String projectFile = 'project.yaml'; 169 | final project = await ioHelper.readYamlFile(projectFile); 170 | final defaultConfig = project["defaultConfig"]; 171 | final pluginServers = project["pluginServers"]; 172 | final serverId = argResults?['serverId']; 173 | final serverConfig = getPluginServerConfig(pluginServers, serverId); 174 | 175 | final server = PluginServer(LocalFileSystem()); 176 | await server.setup(defaultConfig, serverConfig); 177 | server.start(); 178 | } 179 | 180 | Map getPluginServerConfig(servers, serverId) { 181 | if (serverId != null) { 182 | final server = getPluginServerById(servers, serverId); 183 | if (server == null) { 184 | throw ArgumentError("server not found: $serverId"); 185 | } 186 | return server; 187 | } else { 188 | return servers[0]; 189 | } 190 | } 191 | 192 | Map? getPluginServerById(List array, String id) { 193 | for (Map obj in array) { 194 | if (obj["serverId"] == id) { 195 | return obj; 196 | } 197 | } 198 | return null; 199 | } 200 | } 201 | 202 | GptPlugin getPlugin(block, projectConfig) { 203 | final pluginName = block["pluginName"]; 204 | LibraryMirror libraryMirror = 205 | currentMirrorSystem().findLibrary(Symbol('gpt_plugins')); 206 | ClassMirror pluginMirror = 207 | libraryMirror.declarations[Symbol(pluginName)] as ClassMirror; 208 | final gptPlugin = pluginMirror 209 | .newInstance(Symbol(''), [projectConfig, block]).reflectee as GptPlugin; 210 | return gptPlugin; 211 | } 212 | 213 | abstract class ProjectInitializeCommand extends Command { 214 | late String outputDirName; 215 | 216 | late Map project; 217 | 218 | late Map projectConfig; 219 | 220 | late String reportDir; 221 | 222 | late List blocks; 223 | 224 | @override 225 | Future run() async { 226 | String projectFile = 'project.yaml'; 227 | project = await ioHelper.readYamlFile(projectFile); 228 | final apiKeyFile = project['apiKeyFile'] ?? "api_key"; 229 | final apiKey = await ioHelper.readFileAsString(apiKeyFile); 230 | outputDirName = project['outputDir'] ?? "output"; 231 | final projectName = project["projectName"]; 232 | final projectVersion = project["projectVersion"]; 233 | reportDir = "$outputDirName/$projectName/$projectVersion"; 234 | final dataDir = "$reportDir/data"; 235 | await ioHelper.createDirectoryIfNotExist(dataDir); 236 | 237 | blocks = project["blocks"]; 238 | projectConfig = { 239 | "apiKey": apiKey, 240 | "dataDir": dataDir, 241 | "outputDir": outputDirName, 242 | "projectName": projectName, 243 | "projectVersion": projectVersion, 244 | "reportDir": reportDir 245 | }; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /bin/archetype_command.dart: -------------------------------------------------------------------------------- 1 | import 'package:args/command_runner.dart'; 2 | import 'package:file/local.dart'; 3 | import 'package:gpt/src/archetypes.dart'; 4 | import 'package:gpt/src/io_helper.dart'; 5 | import 'package:gpt/src/prompts.dart'; 6 | import 'package:interact/interact.dart'; 7 | import 'package:path/path.dart' as path; 8 | 9 | final localFileSystem = LocalFileSystem(); 10 | 11 | final ioHelper = IOHelper(fileSystem: localFileSystem); 12 | 13 | class ArchetypeCommand extends Command { 14 | @override 15 | String get description => "Generates a new project"; 16 | 17 | @override 18 | String get name => "genp"; 19 | 20 | @override 21 | void run() async { 22 | final builder = ArchetypeBuilder(localFileSystem); 23 | final archetypeDirectory = await builder.downloadArchetypeArchive(); 24 | final archetypeDirectories = { 25 | "ChatGPT Plugin": "plugins-quickstart", 26 | "ZapVine Plugin Catalog": "plugin-catalog", 27 | "Chain": "chain", 28 | "Prompt": "prompt", 29 | "Batch": "batch", 30 | "Image": "image" 31 | }; 32 | final projectTypes = [ 33 | 'ChatGPT Plugin', 34 | 'ZapVine Plugin Catalog', 35 | 'Prompt', 36 | 'Chain', 37 | 'Batch', 38 | 'Image' 39 | ]; 40 | final selectedProjectIndex = Select( 41 | prompt: 'Project Archetype', 42 | options: projectTypes, 43 | initialIndex: 0, 44 | ).interact(); 45 | 46 | final projectName = (selectedProjectIndex == 1) 47 | ? "PluginCatalog" 48 | : Input(prompt: 'Project Name: ', defaultValue: "MyProject").interact(); 49 | final projectVersion = (selectedProjectIndex == 1) 50 | ? "1.0" 51 | : Input(prompt: 'Project Version: ', defaultValue: "1.0").interact(); 52 | final projectSelection = projectTypes[selectedProjectIndex]; 53 | final archetypeName = archetypeDirectories[projectSelection]; 54 | final projectDir = localFileSystem.directory(projectName); 55 | final sourceDir = 56 | localFileSystem.directory(path.join(archetypeDirectory, archetypeName)); 57 | await ioHelper.copyDirectoryContents(sourceDir, projectDir); 58 | Map templateProperties = { 59 | "projectName": projectName, 60 | "projectVersion": projectVersion 61 | }; 62 | if (selectedProjectIndex == 0) { 63 | //plugins 64 | } else if (selectedProjectIndex == 1) { 65 | //catalog 66 | } else if (selectedProjectIndex == 2) { 67 | //prompt 68 | askImportKey(templateProperties, projectName); 69 | askBlockRuns(templateProperties); 70 | askResponseFormat(templateProperties); 71 | } else if (selectedProjectIndex == 3) { 72 | //chain 73 | askImportKey(templateProperties, projectName); 74 | askBlockRuns(templateProperties); 75 | askFixJson(templateProperties); 76 | templateProperties.addAll({"responseFormat": "json"}); 77 | askChainRuns(templateProperties); 78 | } else if (selectedProjectIndex == 4) { 79 | //batch 80 | askImportKey(templateProperties, projectName); 81 | askBlockRuns(templateProperties); 82 | } else if (selectedProjectIndex == 5) { 83 | //image 84 | askImportKey(templateProperties, projectName); 85 | askImageDescription(templateProperties); 86 | } 87 | print(templateProperties); 88 | final projectYaml = await builder.readProjectYaml(projectName); 89 | final calculatedProjectYaml = 90 | substituteTemplateProperties(projectYaml, templateProperties); 91 | final isValid = builder.verifyYamlFormat(calculatedProjectYaml); 92 | if (isValid) { 93 | await ioHelper.writeString( 94 | calculatedProjectYaml, "$projectName/project.yaml"); 95 | print("Created Project"); 96 | } else { 97 | print("Invalid yaml file. Project not created"); 98 | } 99 | } 100 | 101 | void askBlockRuns(templateProperties) { 102 | final blockRuns = 103 | Input(prompt: 'Number of Times to Run The Block: ').interact(); 104 | templateProperties.addAll({"blockRuns": blockRuns}); 105 | } 106 | 107 | void askChainRuns(templateProperties) { 108 | final blockRuns = 109 | Input(prompt: 'Number of Times to Run The Prompt Chain: ').interact(); 110 | templateProperties.addAll({"chainRuns": blockRuns}); 111 | } 112 | 113 | void askFixJson(templateProperties) { 114 | final fixJSONConfirmation = Confirm( 115 | prompt: 'Attempt to FIX JSON Responses?', 116 | defaultValue: false, 117 | ).interact(); 118 | templateProperties.addAll({"fixJson": fixJSONConfirmation.toString()}); 119 | } 120 | 121 | void askImageDescription(templateProperties) { 122 | final imageDescription = Input(prompt: 'Image Description: ').interact(); 123 | templateProperties.addAll({"imageDescription": imageDescription}); 124 | } 125 | 126 | void askImportKey(templateProperties, projectName) { 127 | final keyTypes = [ 128 | 'Skip', 129 | 'Use Existing OpenAI API Key File', 130 | 'Create New OpenAI API Key File' 131 | ]; 132 | final selectedKeyIndex = Select( 133 | prompt: 'Import Key', 134 | options: keyTypes, 135 | initialIndex: 0, 136 | ).interact(); 137 | 138 | if (selectedKeyIndex == 1) { 139 | final keyFile = Input(prompt: 'API Key File: ').interact(); 140 | templateProperties.addAll({"apiKeyFile": keyFile}); 141 | } else if (selectedKeyIndex == 2) { 142 | final key = Input(prompt: 'API Key: ').interact(); 143 | ioHelper.writeString(key, "$projectName/api_key"); 144 | templateProperties.addAll({"apiKeyFile": "api_key"}); 145 | } 146 | } 147 | 148 | void askResponseFormat(templateProperties) { 149 | final outputTypes = ['JSON', 'TEXT']; 150 | final selectedOutputIndex = Select( 151 | prompt: 'Response Format', 152 | options: outputTypes, 153 | initialIndex: 1, 154 | ).interact(); 155 | if (selectedOutputIndex == 0) { 156 | templateProperties.addAll({"responseFormat": "json"}); 157 | templateProperties.addAll({"promptName": "prompt-json.prompt"}); 158 | askFixJson(templateProperties); 159 | } else { 160 | templateProperties.addAll({"responseFormat": "text"}); 161 | templateProperties.addAll({"promptName": "prompt-text.prompt"}); 162 | templateProperties.addAll({"fixJson": false.toString()}); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /coverage.sh: -------------------------------------------------------------------------------- 1 | ## Run Dart tests and output them at directory `./coverage`: 2 | dart run test --coverage=./coverage 3 | 4 | ## Activate package `coverage` (if needed): 5 | dart pub global activate coverage 6 | 7 | ## Format collected coverage to LCOV (only for directory "lib") 8 | dart pub global run coverage:format_coverage --packages=.dart_tool/package_config.json --report-on=lib --lcov -o ./coverage/lcov.info -i ./coverage 9 | 10 | ## Generate LCOV report: 11 | genhtml -o ./coverage/report ./coverage/lcov.info 12 | 13 | ## Open the HTML coverage report: 14 | open ./coverage/report/index.html 15 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | There are seven example projects 2 | 3 | * [**ChatGPT Plugin**](https://github.com/sisbell/stackwire-gpt/tree/main/example/plugins-quickstart) - shows how to run a mock server for quick prototyping of a ChatGPT Plugin. 4 | * [**Batch Command**](https://github.com/sisbell/stackwire-gpt/tree/main/example/batch) - demonstrates how to create a batch process using a data file 5 | * [**Image Generation**](https://github.com/sisbell/stackwire-gpt/tree/main/example/image) - shows how to create images with the DALL-E API 6 | * [**Simple Experiment**](https://github.com/sisbell/stackwire-gpt/tree/main/example/experiment-simple) - shows how to run a single prompt multiple times. 7 | * [**Chain Experiment with Single Prompt**](https://github.com/sisbell/stackwire-gpt/tree/main/example/experiment-chain-single) - shows how to run a single prompt in a chain. This technique is used for maintaining a strong context between requests 8 | * [**Chain Experiment with Multiple Prompts**](https://github.com/sisbell/stackwire-gpt/tree/main/example/experiment-chain-multiple) - shows how to run a multiple prompts in a chain. Specifically this example shows how to use prompts to have OpenAI simulate a user response based on the context on the chain. 9 | * [**Generating a Report**](https://github.com/sisbell/stackwire-gpt/tree/main/example/reporting) - shows how to add the reporting plugin to a block for the generating of an HTML report with request/response flow 10 | 11 | -------------------------------------------------------------------------------- /example/api_key: -------------------------------------------------------------------------------- 1 | {your_api_key} -------------------------------------------------------------------------------- /example/batch/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "prod_review" : [ 3 | "Got this panda plush toy for my daughter's birthday, who loves it and takes it everywhere. It's soft and super cute, and its face has a friendly look. It's a bit small for what I paid though. I think there might be other options that are bigger for the same price. It arrived a day earlier than expected, so I got to play with it myself before I gave it to her.", 4 | "Needed a nice lamp for my bedroom, and this one had additional storage and not too high of a price point. Got it fast - arrived in 2 days. The string to the lamp broke during the transit and the company happily sent over a new one. Came within a few days as well. It was easy to put together. Then I had a missing part, so I contacted their support and they very quickly got me the missing piece! Seems to me to be a great company that cares about their customers and products." 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /example/batch/product.prompt: -------------------------------------------------------------------------------- 1 | Your task is to generate a short summary of a product 2 | review from an ecommerce site to give feedback to the 3 | pricing department, responsible for determining the 4 | price of the product. 5 | 6 | Summarize the review below, delimited by triple 7 | backticks, in at most 30 words, and focusing on any aspects 8 | that are relevant to the price and perceived value. 9 | 10 | Review: ```${prod_review}``` -------------------------------------------------------------------------------- /example/batch/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: product-summary 3 | projectVersion: '2.8' 4 | apiKeyFile: "../../api_key" 5 | blocks: 6 | # Shows how to create a batch block 7 | - blockId: product-1 8 | pluginName: BatchGptPlugin 9 | blockRuns: 2 10 | configuration: 11 | requestParams: 12 | model: gpt-3.5-turbo 13 | temperature: 0.3 14 | top_p: 1 15 | max_tokens: 500 16 | executions: 17 | - id: batch-1 18 | dataFile: product.json 19 | prompt: product.prompt 20 | systemMessageFile: "../system-message.txt" 21 | 22 | - blockId: product-2 23 | pluginName: BatchGptPlugin 24 | configuration: 25 | requestParams: 26 | model: gpt-3.5-turbo 27 | temperature: .2 28 | top_p: .4 29 | max_tokens: 200 30 | executions: 31 | - id: batch-2 32 | dataFile: product.json 33 | prompt: product.prompt 34 | - id: batch-3 35 | dataFile: product.json 36 | prompt: product.prompt 37 | -------------------------------------------------------------------------------- /example/experiment-chain-multiple/character-action.prompt: -------------------------------------------------------------------------------- 1 | Give me an action for ${mainCharacterName} for the following story: 2 | ${story} 3 | 4 | The response must be in JSON using the following structure. 5 | Only use these fields. {"characterAction": ""} -------------------------------------------------------------------------------- /example/experiment-chain-multiple/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: experiment-chain-multiple 3 | projectVersion: '1.7' 4 | apiKeyFile: "../../api_key" 5 | blocks: 6 | - blockId: chain-1 7 | pluginName: ExperimentGptPlugin 8 | blockRuns: 2 9 | configuration: 10 | requestParams: 11 | model: gpt-3.5-turbo 12 | temperature: 1.2 13 | top_p: 1 14 | max_tokens: 500 15 | executions: 16 | - id: exp-1 17 | chainRuns: 2 18 | promptChain: 19 | - structured-story.prompt 20 | - character-action.prompt 21 | excludesMessageHistory: 22 | - character-action.prompt 23 | fixJson: true 24 | responseFormat: json 25 | properties: 26 | rank: Commander in Starfleet 27 | show: Star Trek 28 | mainCharacterName: '' 29 | story: '' 30 | characterAction: '' 31 | 32 | # Block demonstrates the use of importing properties 33 | - blockId: chain-2 34 | pluginName: ExperimentGptPlugin 35 | configuration: 36 | requestParams: 37 | model: gpt-3.5-turbo 38 | temperature: 1.2 39 | top_p: 1 40 | max_tokens: 500 41 | executions: 42 | - id: exp-2-import 43 | chainRuns: 2 44 | promptChain: 45 | - structured-story.prompt 46 | - character-action.prompt 47 | excludesMessageHistory: 48 | - character-action.prompt 49 | fixJson: true 50 | responseFormat: json 51 | properties: 52 | rank: Commander in Starfleet 53 | mainCharacterName: '' 54 | story: '' 55 | characterAction: '' 56 | # Import properties from a properties file 57 | import: 58 | propertiesFile: properties.json 59 | properties: 60 | rank: 3 61 | show: 2 62 | 63 | - blockId: report-1 64 | pluginName: ReportingGptPlugin 65 | executions: 66 | - id: report-1 67 | blockIds: 68 | - chain-1 69 | - chain-2 -------------------------------------------------------------------------------- /example/experiment-chain-multiple/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "show": [ 3 | "Original Star Trek", 4 | "Babylon 5" 5 | ], 6 | "rank": [ 7 | "Starfleet Academy Instructor", 8 | "Captain", 9 | "Ensign" 10 | ] 11 | } -------------------------------------------------------------------------------- /example/experiment-chain-multiple/structured-story.prompt: -------------------------------------------------------------------------------- 1 | The response will be in JSON Format. 2 | 3 | PREVIOUS SCENE 4 | ${story} 5 | 6 | CHARACTER 7 | Role: ${rank} 8 | Main Character Name: ${mainCharacterName} 9 | If no main character name is given, choose one based on role 10 | 11 | SHOW 12 | ${show} 13 | 14 | CHARACTER ACTION 15 | ${characterAction} 16 | 17 | Write me a story based on the character role. 18 | If character name, action and the previous scene are given also use those. 19 | Be very descriptive. Write two sentences only. 20 | 21 | RESPONSE 22 | The response must only be in JSON using the following structure. 23 | Only use these fields. {"mainCharacterName": "", "story": ""} -------------------------------------------------------------------------------- /example/experiment-chain-single/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: experiment-chain-single 3 | projectVersion: '1.1' 4 | apiKeyFile: "../../api_key" 5 | blocks: 6 | - blockId: single-1 7 | pluginName: ExperimentGptPlugin 8 | configuration: 9 | requestParams: 10 | model: gpt-3.5-turbo 11 | temperature: 1.2 12 | top_p: 1 13 | max_tokens: 500 14 | executions: 15 | - id: exp-1 16 | systemMessageFile: "../system-message.txt" 17 | responseFormat: json 18 | promptChain: 19 | - simple-story.prompt 20 | properties: 21 | character: Commander in Starfleet 22 | mainCharacterName: '' 23 | -------------------------------------------------------------------------------- /example/experiment-chain-single/simple-story.prompt: -------------------------------------------------------------------------------- 1 | Write me a story about ${character}. The main character is ${mainCharacterName}. 2 | If no main character is given, choose one. Write one sentence only. 3 | 4 | The response should be in JSON using the following structure. 5 | Only use these fields. {"mainCharacterName": "", "story": ""} -------------------------------------------------------------------------------- /example/experiment-simple/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: experiment-simple 3 | projectVersion: '1.1' 4 | apiKeyFile: "../../api_key" 5 | blocks: 6 | - blockId: simple-1 7 | pluginName: ExperimentGptPlugin 8 | blockRuns: 5 9 | configuration: 10 | requestParams: 11 | model: gpt-3.5-turbo 12 | temperature: 1.2 13 | top_p: 1 14 | max_tokens: 500 15 | executions: 16 | - id: exp-1 17 | systemMessageFile: "../system-message.txt" 18 | promptChain: 19 | - simple-story.prompt 20 | properties: 21 | character: Commander in Starfleet -------------------------------------------------------------------------------- /example/experiment-simple/simple-story.prompt: -------------------------------------------------------------------------------- 1 | Write me a story about ${character}. One Sentence Only. -------------------------------------------------------------------------------- /example/image/image.prompt: -------------------------------------------------------------------------------- 1 | Generate a picture of a ${creature} with ${feature} -------------------------------------------------------------------------------- /example/image/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: image-generation 3 | projectVersion: '2.3' 4 | apiKeyFile: "../../api_key" 5 | blocks: 6 | - blockId: image-1 7 | pluginName: ImageGptPlugin 8 | executions: 9 | #First Image 10 | - id: img-unicorn 11 | sizes: 12 | - 256 13 | prompt: image.prompt 14 | properties: 15 | creature: Unicorn 16 | feature: a gold horn and wings 17 | 18 | # Second Image 19 | - id: img-fish 20 | sizes: 21 | - 256 22 | - 512 23 | imageCount: 2 24 | responseFormat: b64_json 25 | prompt: image.prompt 26 | properties: 27 | creature: fish 28 | feature: giant eyes 29 | -------------------------------------------------------------------------------- /example/plugins-quickstart/defaultConfig/ai-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "v1", 3 | "name_for_human": "${nameForHuman}", 4 | "name_for_model": "${nameForModel}", 5 | "description_for_human": "${descriptionForHuman}", 6 | "description_for_model": "${manifestPrompt}", 7 | "auth": { 8 | "type": "none" 9 | }, 10 | "api": { 11 | "type": "openapi", 12 | "url": "http://localhost:${port}/openapi.yaml", 13 | "is_user_authenticated": false 14 | }, 15 | "logo_url": "http://localhost:${port}/logo.png", 16 | "contact_email": "legal@example.com", 17 | "legal_info_url": "http://example.com/legal" 18 | } 19 | -------------------------------------------------------------------------------- /example/plugins-quickstart/defaultConfig/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sisbell/stackwire-gpt/7fe2dc2d0947dc25deaceca219638ebbe20ec807/example/plugins-quickstart/defaultConfig/logo.png -------------------------------------------------------------------------------- /example/plugins-quickstart/defaultConfig/mocks/todos-global.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | "Finish reading the book", 4 | "Book a haircut appointment", 5 | "Buy birthday gift for friend", 6 | "Complete online course module", 7 | "Organize the garage" 8 | ] 9 | } -------------------------------------------------------------------------------- /example/plugins-quickstart/defaultConfig/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | info: 3 | title: TODO Plugin 4 | description: A plugin that allows the user to create and manage a TODO list using ChatGPT. 5 | version: 'v1' 6 | servers: 7 | - url: http://localhost:${port} 8 | paths: 9 | /todos/{username}: #Get this value 10 | get: #Get this value 11 | operationId: getTodos #Get this value 12 | summary: Get the list of todos 13 | parameters: 14 | - in: path 15 | name: username 16 | schema: 17 | type: string 18 | required: true 19 | description: The name of the user. 20 | responses: 21 | "200": #Get this value 22 | description: OK 23 | content: 24 | application/json: #Get this value 25 | schema: 26 | $ref: '#/components/schemas/getTodosResponse' 27 | post: #Get this value 28 | operationId: addTodo #Get this value 29 | summary: Add a todo to the list 30 | parameters: 31 | - in: path 32 | name: username 33 | schema: 34 | type: string 35 | required: true 36 | description: The name of the user. 37 | requestBody: 38 | required: true 39 | content: 40 | application/json: #Get this value 41 | schema: 42 | $ref: '#/components/schemas/addTodoRequest' 43 | responses: 44 | "200": #Get this value 45 | description: OK 46 | delete: 47 | operationId: deleteTodo 48 | summary: Delete a todo from the list 49 | parameters: 50 | - in: path 51 | name: username 52 | schema: 53 | type: string 54 | required: true 55 | description: The name of the user. 56 | requestBody: 57 | required: true 58 | content: 59 | application/json: 60 | schema: 61 | $ref: '#/components/schemas/deleteTodoRequest' 62 | responses: 63 | "200": 64 | description: OK 65 | 66 | components: 67 | schemas: 68 | getTodosResponse: 69 | type: object 70 | properties: 71 | todos: 72 | type: array 73 | items: 74 | type: string 75 | description: The list of todos. 76 | addTodoRequest: 77 | type: object 78 | required: 79 | - todo 80 | properties: 81 | todo: 82 | type: string 83 | description: The todo to add to the list. 84 | required: true 85 | deleteTodoRequest: 86 | type: object 87 | required: 88 | - todo_idx 89 | properties: 90 | todo_idx: 91 | type: integer 92 | description: The index of the todo to delete. 93 | required: true -------------------------------------------------------------------------------- /example/plugins-quickstart/defaultConfig/plugin-manifest.prompt: -------------------------------------------------------------------------------- 1 | A plugin that allows the user to create and manage a TODO list using ChatGPT. 2 | If you do not know the user's username, ask them first before making queries to the plugin. 3 | Otherwise, use the username global -------------------------------------------------------------------------------- /example/plugins-quickstart/pluginFlavors/kaleb/mocks/todos.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | "Repair a broken tractor", 4 | "Harvest the wheat field", 5 | "Shear the sheep", 6 | "Install new fencing for livestock", 7 | "Plant new crops for the season", 8 | "Get new haircut" 9 | ] 10 | } -------------------------------------------------------------------------------- /example/plugins-quickstart/pluginFlavors/mike/mocks/todos.json: -------------------------------------------------------------------------------- 1 | { 2 | "todos": [ 3 | "Clean out a septic tank", 4 | "Repair a broken sewer pipe", 5 | "Collect roadkill for disposal", 6 | "Assist in bee hive relocation", 7 | "Service a grease trap at a restaurant" 8 | ] 9 | } -------------------------------------------------------------------------------- /example/plugins-quickstart/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: plugin-quickstart 3 | projectVersion: '1.0' 4 | projectType: plugin 5 | defaultConfig: 6 | properties: 7 | port: 5003 8 | nameForHuman: TODO Plugin (no auth) 9 | nameForModel: todo 10 | descriptionForHuman: Plugin for managing a TODO list, you can add, remove and view your TODOs. 11 | mockedRequests: 12 | - path: "/todos/global" 13 | method: get 14 | mockedResponse: todos-global.json 15 | - path: "/todos/user" 16 | method: get 17 | mockedResponse: todos-global.json 18 | 19 | pluginServers: 20 | - serverId: todo-mike 21 | flavor: mike 22 | # mocked requests 23 | mockedRequests: 24 | - path: "/todos/mike" 25 | method: get 26 | mockedResponse: todos.json # returns the content of this file 27 | 28 | # Adds a different user for testing 29 | - serverId: todo-kaleb 30 | flavor: kaleb 31 | mockedRequests: 32 | - path: "/todos/kaleb" 33 | method: get 34 | mockedResponse: todos.json 35 | properties: 36 | showHttpHeaders: true # Show http headers in logs -------------------------------------------------------------------------------- /example/reporting/character-action.prompt: -------------------------------------------------------------------------------- 1 | Give me an action for ${mainCharacterName} for the following story: 2 | ${story} 3 | The response must be in JSON using the following structure. Only use these fields. {"characterAction": ""} -------------------------------------------------------------------------------- /example/reporting/project.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | projectName: experiment-reporting 3 | projectVersion: '1.7' 4 | apiKeyFile: "../../api_key" 5 | blocks: 6 | - blockId: chain-1 7 | pluginName: ExperimentGptPlugin 8 | blockRuns: 1 9 | configuration: 10 | requestParams: 11 | model: gpt-3.5-turbo 12 | temperature: 1.2 13 | top_p: 1 14 | max_tokens: 500 15 | executions: 16 | - id: exp-1 17 | chainRuns: 2 18 | promptChain: 19 | - structured-story.prompt 20 | - character-action.prompt 21 | excludesMessageHistory: 22 | - character-action.prompt 23 | fixJson: true 24 | responseFormat: json 25 | properties: 26 | rank: Commander in Starfleet 27 | show: Star Trek 28 | mainCharacterName: '' 29 | story: '' 30 | characterAction: '' 31 | # Generate HTML Report 32 | - blockId: report-1 33 | pluginName: ReportingGptPlugin 34 | executions: 35 | - id: report-execution 36 | blockIds: 37 | - chain-1 -------------------------------------------------------------------------------- /example/reporting/properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "show": [ 3 | "Original Star Trek", 4 | "Babylon 5" 5 | ], 6 | "rank": [ 7 | "Starfleet Academy Instructor", 8 | "Captain", 9 | "Ensign" 10 | ] 11 | } -------------------------------------------------------------------------------- /example/reporting/structured-story.prompt: -------------------------------------------------------------------------------- 1 | The response will be in JSON Format. 2 | 3 | PREVIOUS SCENE 4 | ${story} 5 | 6 | CHARACTER 7 | Role: ${rank} 8 | Main Character Name: ${mainCharacterName} 9 | If no main character name is given, choose one based on role 10 | 11 | SHOW 12 | ${show} 13 | 14 | CHARACTER ACTION 15 | ${characterAction} 16 | 17 | Write me a story based on the character role. If character name, action and the previous scene are given also use those. Be very descriptive. Write two sentences only. 18 | 19 | RESPONSE 20 | The response must only be in JSON using the following structure. Only use these fields. {"mainCharacterName": "", "story": ""} -------------------------------------------------------------------------------- /example/system-message.txt: -------------------------------------------------------------------------------- 1 | You are an assistant. -------------------------------------------------------------------------------- /lib/src/archetypes.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | import 'dart:io'; 4 | 5 | import 'package:archive/archive.dart'; 6 | import 'package:file/file.dart'; 7 | import 'package:http/http.dart' as http; 8 | import 'package:path/path.dart' as path; 9 | import 'package:yaml/yaml.dart'; 10 | 11 | import 'io_helper.dart'; 12 | 13 | class ArchetypeBuilder { 14 | final String archetypeVersion = "4"; 15 | 16 | final FileSystem fileSystem; 17 | 18 | ArchetypeBuilder(this.fileSystem); 19 | 20 | Future downloadArchetypeArchive({http.Client? client}) async { 21 | final ioHelper = IOHelper(fileSystem: fileSystem); 22 | client ??= http.Client(); 23 | final archetypeFileName = "archetypes-$archetypeVersion.zip"; 24 | Directory? archetypesDirectory; 25 | try { 26 | final Map paths = 27 | await getDirectoryPaths(archetypeVersion); 28 | final String stackwireDirectoryPath = paths['stackwireDirectoryPath']!; 29 | final String archetypesDirectoryPath = paths['archetypesDirectoryPath']!; 30 | final stackwireDirectory = fileSystem.directory(stackwireDirectoryPath); 31 | archetypesDirectory = fileSystem.directory(archetypesDirectoryPath); 32 | 33 | if (await archetypesDirectory.exists()) { 34 | print("Archetypes file found at: $archetypesDirectoryPath"); 35 | return archetypesDirectoryPath; 36 | } 37 | ioHelper.createDirectoryIfNotExist(archetypesDirectoryPath); 38 | print(stackwireDirectory); 39 | archetypesDirectory.createSync(recursive: true); 40 | final String zipUrl = 41 | 'https://storage.googleapis.com/zapvine-prod.appspot.com/archetypes/$archetypeFileName'; 42 | print("Downloading $zipUrl"); 43 | final http.Response response = await client.get(Uri.parse(zipUrl)); 44 | List bytes = response.bodyBytes; 45 | final Archive archive = ZipDecoder().decodeBytes(bytes); 46 | for (final file in archive) { 47 | final String filename = path.join(archetypesDirectoryPath, file.name); 48 | if (file.isFile) { 49 | final data = file.content as List; 50 | fileSystem.file(filename) 51 | ..createSync(recursive: true) 52 | ..writeAsBytesSync(data); 53 | } else { 54 | fileSystem.directory(filename).createSync(); 55 | } 56 | } 57 | print('Zip file extracted to $archetypesDirectoryPath'); 58 | return archetypesDirectoryPath; 59 | } catch (e) { 60 | if (archetypesDirectory != null) { 61 | archetypesDirectory.deleteSync(); 62 | } 63 | throw ArchiveDownloadException( 64 | 'Failed to download and extract archive: $e'); 65 | } 66 | } 67 | 68 | Future> getDirectoryPaths(archetypeVersion) async { 69 | final String homeDir = 70 | Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!; 71 | final String stackwireDirPath = path.join(homeDir, '.stackwire'); 72 | final String archetypesDirPath = 73 | path.join(stackwireDirPath, 'cache', 'archetypes-$archetypeVersion'); 74 | return { 75 | 'stackwireDirectoryPath': stackwireDirPath, 76 | 'archetypesDirectoryPath': archetypesDirPath, 77 | }; 78 | } 79 | 80 | Future readProjectYaml(String directoryPath) async { 81 | final projectYamlPath = '$directoryPath/project.yaml'; 82 | final projectYamlFile = fileSystem.file(projectYamlPath); 83 | 84 | if (await projectYamlFile.exists()) { 85 | final contents = await projectYamlFile.readAsString(encoding: utf8); 86 | return contents; 87 | } else { 88 | throw FileSystemException( 89 | 'project.yaml file not found', 90 | directoryPath, 91 | ); 92 | } 93 | } 94 | 95 | bool verifyYamlFormat(String yamlContent) { 96 | try { 97 | loadYaml(yamlContent); 98 | return true; 99 | } catch (_) { 100 | return false; 101 | } 102 | } 103 | } 104 | 105 | class ArchiveDownloadException implements Exception { 106 | final String message; 107 | 108 | ArchiveDownloadException(this.message); 109 | 110 | @override 111 | String toString() => 'ArchiveDownloadException: $message'; 112 | } 113 | -------------------------------------------------------------------------------- /lib/src/chatgpt/catalog_parser.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | Future parseCatalog(inputFile, outputFile) async { 4 | final jsonData = json.decode(await inputFile.readAsString()); 5 | final List items = jsonData['items']; 6 | final transformedItems = items.where((item) { 7 | return item['status'] == 'approved'; 8 | }).map((item) { 9 | return { 10 | 'id': item['domain'], 11 | 'name': item['manifest']['name_for_human'], 12 | 'description': item['manifest']['description_for_human'], 13 | 'logo': item['manifest']['logo_url'], 14 | }; 15 | }).toList(); 16 | await outputFile.writeAsString(json.encode(transformedItems)); 17 | } 18 | -------------------------------------------------------------------------------- /lib/src/chatgpt/plugin_server.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'package:file/file.dart'; 5 | import 'package:path/path.dart' as path; 6 | 7 | import '../io_helper.dart'; 8 | import '../prompts.dart'; 9 | 10 | class PluginServer { 11 | late File apiFile; 12 | 13 | late String defaultDir; 14 | 15 | final FileSystem fileSystem; 16 | 17 | String? flavorDir; 18 | 19 | late IOHelper ioHelper; 20 | 21 | late File logoFile; 22 | 23 | late String logoName; 24 | 25 | late File manifestFile; 26 | 27 | late int port; 28 | 29 | late List mockedRequestConfigs; 30 | 31 | late Map properties; 32 | 33 | late bool showHttpHeaders; 34 | 35 | PluginServer(this.fileSystem) { 36 | ioHelper = IOHelper(fileSystem: fileSystem); 37 | } 38 | 39 | Future setup(defaultConfig, serverConfig) async { 40 | final defaultProperties = 41 | defaultConfig['properties'] ?? {}; 42 | final serverProperties = serverConfig['properties'] ?? {}; 43 | 44 | final flavor = serverConfig["flavor"]; 45 | flavorDir = "pluginFlavors/$flavor"; 46 | defaultDir = "defaultConfig"; 47 | 48 | properties = {...serverProperties, ...defaultProperties}; 49 | final prompt = await findResource("plugin-manifest.prompt"); 50 | final content = await ioHelper.readFileAsString(prompt.path); 51 | final newContent = content.replaceAll('\n', '').replaceAll('\r', ''); 52 | properties.addAll({"manifestPrompt": newContent}); 53 | 54 | final serverId = serverConfig["serverId"]; 55 | final mockedRequests = serverConfig["mockedRequests"]; //merge 56 | 57 | manifestFile = await findResource("ai-plugin.json"); 58 | print("Found plugin manifest: ${manifestFile.path}"); 59 | String manifestTemplate = 60 | await ioHelper.readFileAsString(manifestFile.path); 61 | String manifestString = 62 | substituteTemplateProperties(manifestTemplate, properties); 63 | final manifest = jsonDecode(manifestString); 64 | final apiUrl = manifest["api"]["url"]; 65 | final logo = manifest["logo_url"]; 66 | List parts = logo.split('/'); 67 | logoName = parts.last; 68 | logoFile = await findResource(logoName); 69 | 70 | apiFile = await findResource("openapi.yaml"); 71 | print("Found OpenAPI Spec: ${apiFile.path}\n"); 72 | String apiTemplate = await ioHelper.readFileAsString(apiFile.path); 73 | String apiString = substituteTemplateProperties(apiTemplate, properties); 74 | final api = await ioHelper.readYaml(apiString); 75 | final serverUri = api["servers"][0]["url"]; 76 | if (properties.containsKey("port")) { 77 | port = properties["port"]; 78 | } else { 79 | final uri = Uri.parse(serverUri); 80 | port = uri.port; 81 | } 82 | showHttpHeaders = properties["showHttpHeaders"] ?? false; 83 | 84 | print("Setting up plugin server: $serverId"); 85 | print(serverUri); 86 | print("$serverUri/.well-known/ai-plugin.json"); 87 | print(apiUrl); 88 | print(logo); 89 | print("\nRegistering endpoints"); 90 | final defaultMocks = defaultConfig['mockedRequests'] ?? []; 91 | final defaultMockedRequests = createMockedRequestConfigs(defaultMocks); 92 | mockedRequestConfigs = createMockedRequestConfigs(mockedRequests); 93 | mockedRequestConfigs = 94 | mergeMockedRequestConfigs(mockedRequestConfigs, defaultMockedRequests); 95 | for (var config in mockedRequestConfigs) { 96 | print("${config.method.toUpperCase()} - $serverUri${config.path}"); 97 | } 98 | } 99 | 100 | void start() async { 101 | var server = await HttpServer.bind( 102 | InternetAddress.loopbackIPv4, 103 | port, 104 | ); 105 | print('\nListening on localhost:${server.port}\n'); 106 | 107 | await for (HttpRequest request in server) { 108 | handleRequest(request); 109 | } 110 | } 111 | 112 | void handleRequest(HttpRequest request) async { 113 | request.response.headers 114 | .add('Access-Control-Allow-Origin', 'https://chat.openai.com'); 115 | request.response.headers 116 | .add('Access-Control-Allow-Methods', 'GET,POST,DELETE,PUT,OPTIONS'); 117 | request.response.headers.add('Access-Control-Allow-Headers', '*'); 118 | final path = request.uri.path; 119 | final requestMethod = request.method; 120 | 121 | print("${DateTime.now()} Request: $requestMethod - $path"); 122 | if (showHttpHeaders) { 123 | request.headers.forEach((String name, List values) { 124 | print('\t$name: $values'); 125 | }); 126 | } 127 | var body = await utf8.decoder.bind(request).join(); 128 | if (body.isNotEmpty) { 129 | print(body); 130 | } 131 | if (path.endsWith(logoName)) { 132 | writeLogo(request); 133 | } else if (path.endsWith("openapi.yaml")) { 134 | await writeTemplate(request, apiFile, ContentType("text", "yaml")); 135 | } else if (path.endsWith("ai-plugin.json")) { 136 | await writeTemplate( 137 | request, manifestFile, ContentType("application", "json")); 138 | } else { 139 | final config = getRequestConfig(request); 140 | if (config != null) { 141 | var file = fileSystem.file(await findMock(config.mockedResponse)); 142 | if (file.existsSync()) { 143 | print("\tUsing response file ${file.path}"); 144 | final contentType = ContentType.parse(config.contentType); 145 | request.response 146 | ..headers.contentType = contentType 147 | ..add(file.readAsBytesSync()) 148 | ..close(); 149 | } else { 150 | print("\tFile for mock resource not found: ${file.path}"); 151 | } 152 | } else { 153 | print("\tNo handler for this request found"); 154 | request.response 155 | ..headers.contentType = ContentType.json 156 | ..write("{}") 157 | ..close(); 158 | } 159 | } 160 | } 161 | 162 | List createMockedRequestConfigs( 163 | List mockedRequests) { 164 | return mockedRequests 165 | .map((dynamic request) => MockedRequestConfig( 166 | path: (request as Map)['path'], 167 | mockedResponse: (request)['mockedResponse'], 168 | method: (request)['method'], 169 | contentType: (request)['contentType'] ?? "application/json")) 170 | .toList(); 171 | } 172 | 173 | List mergeMockedRequestConfigs( 174 | List firstList, 175 | List secondList, 176 | ) { 177 | final firstMap = 178 | Map.fromEntries(firstList.map((e) => MapEntry(e.method + e.path, e))); 179 | final secondMap = 180 | Map.fromEntries(secondList.map((e) => MapEntry(e.method + e.path, e))); 181 | firstMap.addAll(secondMap); 182 | return firstMap.values.toList(); 183 | } 184 | 185 | Future findResource(fileName) async { 186 | return await ioHelper.findFile(flavorDir, defaultDir, fileName); 187 | } 188 | 189 | Future findMock(fileName) async { 190 | return await findResource("mocks/$fileName"); 191 | } 192 | 193 | MockedRequestConfig? getRequestConfig(HttpRequest request) { 194 | for (var config in mockedRequestConfigs) { 195 | if (request.uri.path == config.path && 196 | request.method.toLowerCase() == config.method.toLowerCase()) { 197 | return config; 198 | } 199 | } 200 | return null; 201 | } 202 | 203 | void writeLogo(request) { 204 | var extensionWithDot = path.extension(logoFile.path); 205 | var extensionWithoutDot = 206 | extensionWithDot.isNotEmpty ? extensionWithDot.substring(1) : ''; 207 | request.response.headers.contentType = 208 | ContentType('image', extensionWithoutDot); 209 | logoFile.openRead().pipe(request.response).catchError((e) { 210 | print('Error occurred while reading image: $e'); 211 | }); 212 | } 213 | 214 | void writeString(request, contentFile, contentType) { 215 | request.response.headers.contentType = contentType; 216 | contentFile.openRead().pipe(request.response).catchError((e) { 217 | print('Error occurred: $e'); 218 | }); 219 | } 220 | 221 | Future writeTemplate(request, templateFile, contentType) async { 222 | final template = await ioHelper.readFileAsString(templateFile.path); 223 | final content = substituteTemplateProperties(template, properties); 224 | request.response 225 | ..headers.contentType = contentType 226 | ..write(content) 227 | ..close(); 228 | } 229 | } 230 | 231 | class MockedRequestConfig { 232 | final String path; 233 | final String mockedResponse; 234 | final String method; 235 | final String contentType; 236 | 237 | MockedRequestConfig( 238 | {required this.path, 239 | required this.mockedResponse, 240 | required this.method, 241 | required this.contentType}); 242 | 243 | @override 244 | bool operator ==(Object other) => 245 | identical(this, other) || 246 | other is MockedRequestConfig && 247 | runtimeType == other.runtimeType && 248 | path == other.path && 249 | mockedResponse == other.mockedResponse && 250 | method == other.method && 251 | contentType == other.contentType; 252 | 253 | @override 254 | int get hashCode => 255 | path.hashCode ^ 256 | mockedResponse.hashCode ^ 257 | method.hashCode ^ 258 | contentType.hashCode; 259 | } 260 | -------------------------------------------------------------------------------- /lib/src/gpt_plugin.dart: -------------------------------------------------------------------------------- 1 | library gpt_plugins; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | import 'dart:io'; 6 | 7 | import 'package:file/file.dart'; 8 | import 'package:file/local.dart'; 9 | import 'package:gpt/src/network_client.dart'; 10 | import 'package:gpt/src/reporter.dart'; 11 | import 'package:http/http.dart'; 12 | 13 | import 'io_helper.dart'; 14 | import 'prompts.dart'; 15 | 16 | part "plugins/batch_plugin.dart"; 17 | part "plugins/experiment_plugin.dart"; 18 | part "plugins/image_plugin.dart"; 19 | part "plugins/reporting_plugin.dart"; 20 | 21 | abstract class GptPlugin { 22 | final Map _projectConfig; 23 | Map block; 24 | late FileSystem fileSystem; 25 | late NetworkClient networkClient; 26 | 27 | late String dataDir; 28 | late String outputDir; 29 | late String projectName; 30 | late String projectVersion; 31 | late String reportDir; 32 | late String blockDataDir; 33 | late String blockId; 34 | late String pluginName; 35 | late int blockRuns; 36 | late String metricsFile; 37 | var currentBlockRun = 0; 38 | late Reporter reporter; 39 | late IOHelper ioHelper; 40 | 41 | GptPlugin(this._projectConfig, this.block, 42 | {FileSystem? fileSystem, NetworkClient? networkClient}) { 43 | this.fileSystem = fileSystem ?? LocalFileSystem(); 44 | 45 | outputDir = _projectConfig["outputDir"]; 46 | projectName = _projectConfig["projectName"]; 47 | projectVersion = _projectConfig["projectVersion"]; 48 | reportDir = _projectConfig["reportDir"]; 49 | dataDir = _projectConfig["dataDir"]; 50 | blockId = block["blockId"]; 51 | pluginName = block["pluginName"]; 52 | blockDataDir = "$dataDir/$blockId"; 53 | metricsFile = "$reportDir/metrics-$blockId.csv"; 54 | 55 | ioHelper = IOHelper(fileSystem: this.fileSystem); 56 | reporter = ConcreteReporter(ioHelper); 57 | final apiKey = _projectConfig["apiKey"]; 58 | this.networkClient = networkClient ?? 59 | NetworkClient(apiKey, reporter, this.fileSystem, Client()); 60 | } 61 | 62 | num apiCallCount() { 63 | return 0; 64 | } 65 | 66 | Future init(execution, pluginConfiguration) async {} 67 | 68 | Future report(results) async {} 69 | 70 | Future apiCallCountForBlock() async { 71 | num result = 0; 72 | final pluginConfiguration = block["configuration"]; 73 | final blockRuns = block["blockRuns"] ?? 1; 74 | for (var blockRun = 1; blockRun <= blockRuns; blockRun++) { 75 | final executions = block["executions"]; 76 | for (var i = 0; i < executions.length; i++) { 77 | final execution = executions[i]; 78 | await init(execution, pluginConfiguration); 79 | result += apiCallCount(); 80 | } 81 | } 82 | return result; 83 | } 84 | 85 | Future execute(dryRun) async { 86 | print("Running Project: $projectName-$projectVersion"); 87 | print("BlockId: $blockId, PluginName: $pluginName"); 88 | final startTime = DateTime.now(); 89 | final pluginConfiguration = block["configuration"]; 90 | blockRuns = block["blockRuns"] ?? 1; 91 | await ioHelper.createDirectoryIfNotExist(blockDataDir); 92 | final blockResults = []; 93 | for (var blockRun = 1; blockRun <= blockRuns; blockRun++) { 94 | print("----------\nStarting Block Run: $blockRun"); 95 | currentBlockRun = blockRun; 96 | final results = []; 97 | final executions = block["executions"]; 98 | for (var i = 0; i < executions.length; i++) { 99 | final execution = executions[i]; 100 | await init(execution, pluginConfiguration); 101 | print( 102 | "Starting execution: ${i + 1} - Requires ${apiCallCount()} calls to OpenAI"); 103 | await doExecution(results, dryRun); 104 | if (!dryRun) { 105 | await report(results); 106 | } 107 | print("Finished execution: ${i + 1}\n"); 108 | } 109 | final blockResult = {"blockRun": blockRun, "blockResults": results}; 110 | blockResults.add(blockResult); 111 | } 112 | if (!dryRun && 113 | blockResults.isNotEmpty && 114 | blockResults.first["blockResults"].isNotEmpty) { 115 | await reporter.writeProjectReport({ 116 | "projectName": projectName, 117 | "projectVersion": projectVersion, 118 | "blockId": blockId, 119 | "configuration": pluginConfiguration, 120 | "blockRuns": blockResults 121 | }, reportDir); 122 | } 123 | final endTime = DateTime.now(); 124 | Duration duration = endTime.difference(startTime); 125 | print( 126 | "\n--------\nFinished running project: ${duration.inSeconds} seconds"); 127 | } 128 | 129 | Future doExecution(results, dryRun) async {} 130 | 131 | Future writeMetrics(responseBody, executionId, tag) async { 132 | await reporter.writeMetrics(responseBody, executionId, tag, metricsFile); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/io_helper.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:file/file.dart'; 4 | import 'package:path/path.dart' as path; 5 | import 'package:yaml/yaml.dart'; 6 | 7 | class IOHelper { 8 | final FileSystem fileSystem; 9 | 10 | IOHelper({required this.fileSystem}); 11 | 12 | File file(path) { 13 | return fileSystem.file(path); 14 | } 15 | 16 | Future findFile(String? dir1, String dir2, String fileName) async { 17 | if (dir1 != null) { 18 | var file1 = fileSystem.directory(dir1).childFile(fileName); 19 | if (await file1.exists()) { 20 | return file1; 21 | } 22 | } 23 | 24 | var file2 = fileSystem.directory(dir2).childFile(fileName); 25 | 26 | if (await file2.exists()) { 27 | return file2; 28 | } else { 29 | throw FileSystemException('File not found: $fileName'); 30 | } 31 | } 32 | 33 | Future createDirectoryIfNotExist(String path) async { 34 | final dir = fileSystem.directory(path); 35 | if (!await dir.exists()) { 36 | await dir.create(recursive: true); 37 | } 38 | } 39 | 40 | Future writeString(String content, String path) async { 41 | final file = fileSystem.file(path); 42 | await file.writeAsString(content); 43 | } 44 | 45 | Future readFileAsString(String filePath) async { 46 | File file = fileSystem.file(filePath); 47 | try { 48 | String content = await file.readAsString(); 49 | return content; 50 | } catch (e) { 51 | throw FileSystemException( 52 | 'Error occurred while reading file: $e', filePath); 53 | } 54 | } 55 | 56 | Future> readJsonFile(String filePath) async { 57 | String jsonString = await readFileAsString(filePath); 58 | return jsonDecode(jsonString); 59 | } 60 | 61 | Future> readYamlFile(String filePath) async { 62 | String text = await readFileAsString(filePath); 63 | final yamlObject = loadYaml(text); 64 | return jsonDecode(json.encode(yamlObject)); 65 | } 66 | 67 | Future> readYaml(String content) async { 68 | final yamlObject = loadYaml(content); 69 | return jsonDecode(json.encode(yamlObject)); 70 | } 71 | 72 | Future writeMap(Map data, String filePath) async { 73 | File file = fileSystem.file(filePath); 74 | try { 75 | String jsonString = jsonEncode(data); 76 | await file.writeAsString(jsonString, flush: true); 77 | } catch (e) { 78 | throw FileSystemException( 79 | 'Error occurred while writing JSON to file: $e', filePath); 80 | } 81 | } 82 | 83 | Future copyDirectoryContents( 84 | Directory source, Directory destination) async { 85 | await createDirectoryIfNotExist(destination.path); 86 | await for (var entity 87 | in source.list(recursive: false, followLinks: false)) { 88 | final newPath = path.join(destination.path, path.basename(entity.path)); 89 | if (entity is Directory) { 90 | await fileSystem.directory(newPath).create(); 91 | await copyDirectoryContents(entity, fileSystem.directory(newPath)); 92 | } else if (entity is File) { 93 | await entity.copy(newPath); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/src/network_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:typed_data'; 4 | 5 | import 'package:file/file.dart'; 6 | import 'package:gpt/src/reporter.dart'; 7 | import 'package:http/http.dart'; 8 | 9 | class NetworkClient { 10 | final String apiKey; 11 | 12 | final Reporter reporter; 13 | 14 | late Client httpClient; 15 | 16 | late FileSystem fileSystem; 17 | 18 | NetworkClient( 19 | this.apiKey, this.reporter, this.fileSystem, Client? httpClient) { 20 | this.httpClient = httpClient ?? Client(); 21 | } 22 | 23 | Future> sendHttpPostRequest(requestBody, urlPath, logDir, 24 | {bool? dryRun}) async { 25 | dryRun = dryRun ?? false; 26 | final requestBodyStr = jsonEncode(requestBody); 27 | if (dryRun) { 28 | print("\tPOST to https://api.openai.com/$urlPath"); 29 | print("\t\t$requestBodyStr"); 30 | return {}; 31 | } 32 | print("\n\tMaking call to OpenAI: $urlPath"); 33 | try { 34 | final startTime = DateTime.now().millisecondsSinceEpoch; 35 | final response = await httpClient.post( 36 | Uri.parse("https://api.openai.com/$urlPath"), 37 | headers: { 38 | 'Content-Type': 'application/json; charset=UTF-8', 39 | 'Authorization': 'Bearer $apiKey', 40 | }, 41 | body: requestBodyStr, 42 | ); 43 | final endTime = DateTime.now().millisecondsSinceEpoch; 44 | print("\t\trequestTime: ${(endTime - startTime)}"); 45 | if (response.statusCode == 200) { 46 | print('\t\tOpenAI Request successful.'); 47 | } else { 48 | print( 49 | '\t\tOpenAI Request failed with status code: ${response.statusCode}'); 50 | print(requestBodyStr); 51 | await reporter.logFailedRequest(requestBodyStr, logDir); 52 | return {"errorCode": response.statusCode}; 53 | } 54 | Map responseBody = jsonDecode(response.body); 55 | responseBody.addAll({"requestTime": (endTime - startTime)}); 56 | await reporter.logRequestAndResponse( 57 | requestBodyStr, responseBody, logDir); 58 | //writeMetrics(responseBody, executionId, tag); 59 | return responseBody; 60 | } catch (e) { 61 | throw HttpException('Error occurred during the request: $e', 62 | uri: Uri.parse("https://api.openai.com/$urlPath")); 63 | } 64 | } 65 | 66 | Future downloadImage(String imageUrl, String savePath) async { 67 | final response = await httpClient.get(Uri.parse(imageUrl)); 68 | if (response.statusCode == 200) { 69 | final bytes = response.bodyBytes; 70 | final file = fileSystem.file(savePath); 71 | await file.writeAsBytes(bytes); 72 | } else { 73 | throw HttpException('Failed to download image', uri: Uri.parse(imageUrl)); 74 | } 75 | } 76 | 77 | Future saveBase64AsPng(String base64String, String filePath) async { 78 | Uint8List decodedBytes = base64Decode(base64String); 79 | File file = fileSystem.file(filePath); 80 | await file.writeAsBytes(decodedBytes); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/src/plugins/batch_plugin.dart: -------------------------------------------------------------------------------- 1 | part of gpt_plugins; 2 | 3 | class BatchGptPlugin extends GptPlugin { 4 | BatchGptPlugin(super.projectConfig, super.block, 5 | {super.fileSystem, super.networkClient}); 6 | 7 | late Map batchData; 8 | 9 | late String executionId; 10 | 11 | late String promptFile; 12 | 13 | late String promptTemplate; 14 | 15 | late Map requestParams; 16 | 17 | late String? systemMessage; 18 | 19 | @override 20 | num apiCallCount() { 21 | return batchData[batchData.keys.first].length; 22 | } 23 | 24 | @override 25 | Future init(execution, pluginConfiguration) async { 26 | executionId = execution["id"]; 27 | requestParams = Map.from(pluginConfiguration["requestParams"]); 28 | promptFile = execution["prompt"]; 29 | promptTemplate = await ioHelper.readFileAsString(promptFile); 30 | String? systemMessageFile = execution['systemMessageFile']; 31 | systemMessage = systemMessageFile != null 32 | ? await ioHelper.readFileAsString(systemMessageFile) 33 | : null; 34 | final dataFile = execution["dataFile"]; 35 | batchData = await ioHelper.readJsonFile(dataFile); 36 | } 37 | 38 | @override 39 | Future doExecution(results, dryRun) async { 40 | final dataSize = batchData[batchData.keys.first].length; 41 | for (int i = 0; i < dataSize; i++) { 42 | final messageHistory = MessageHistory(systemMessage); 43 | final prompt = createPromptByIndex(promptTemplate, batchData, i); 44 | messageHistory.addUserMessage(prompt); 45 | requestParams['messages'] = messageHistory.history; 46 | final responseBody = await makeChatCompletionRequest( 47 | requestParams, executionId, promptFile, dryRun); 48 | if (dryRun) { 49 | continue; 50 | } 51 | if (responseBody['errorCode'] != null) { 52 | throw HttpException( 53 | "Failed Chat Completion Request: ${responseBody['errorCode']}"); 54 | } 55 | final result = { 56 | "input": buildObject(batchData, i), 57 | "output": responseBody["choices"][0]["message"]["content"] 58 | }; 59 | results.add(result); 60 | } 61 | } 62 | 63 | Future> makeChatCompletionRequest( 64 | requestBody, executionId, tag, dryRun) async { 65 | final toDirectory = "$blockDataDir/$currentBlockRun"; 66 | return networkClient.sendHttpPostRequest( 67 | requestBody, "v1/chat/completions", toDirectory, 68 | dryRun: dryRun); 69 | } 70 | 71 | Map buildObject(inputMap, int index) { 72 | Map result = {}; 73 | 74 | inputMap.forEach((key, valueList) { 75 | if (index >= 0 && index < valueList.length) { 76 | result[key] = valueList[index]; 77 | } else { 78 | throw ArgumentError('Index out of range'); 79 | } 80 | }); 81 | 82 | return result; 83 | } 84 | } 85 | 86 | class MessageHistory { 87 | List> history = []; 88 | 89 | MessageHistory(systemMessage) { 90 | if (systemMessage != null) { 91 | history.add({"role": "system", "content": systemMessage}); 92 | } 93 | } 94 | 95 | void addAssistantMessage(content) { 96 | history.add(_createAssistantMessage(content)); 97 | } 98 | 99 | void addUserMessage(prompt) { 100 | history.add(_createUserMessage(prompt)); 101 | } 102 | 103 | Map _createAssistantMessage(content) { 104 | return {"role": "assistant", "content": content}; 105 | } 106 | 107 | Map _createUserMessage(prompt) { 108 | return {"role": "user", "content": prompt}; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lib/src/plugins/experiment_plugin.dart: -------------------------------------------------------------------------------- 1 | part of gpt_plugins; 2 | 3 | class ExperimentGptPlugin extends GptPlugin { 4 | ExperimentGptPlugin(super.projectConfig, super.block, 5 | {super.fileSystem, super.networkClient}); 6 | 7 | late int chainRuns; 8 | 9 | late List excludesMessageHistory; 10 | 11 | late bool fixJson; 12 | 13 | late Map importProperties; 14 | 15 | late String executionId; 16 | 17 | late List promptTemplates; 18 | 19 | late List promptChain; 20 | 21 | late Map promptValues; 22 | 23 | late Map requestParams; 24 | 25 | late String responseFormat; 26 | 27 | late String? systemMessage; 28 | 29 | @override 30 | num apiCallCount() { 31 | return chainRuns * promptChain.length; 32 | } 33 | 34 | @override 35 | Future init(execution, pluginConfiguration) async { 36 | executionId = execution["id"]; 37 | requestParams = Map.from(pluginConfiguration["requestParams"]); 38 | chainRuns = execution['chainRuns'] ?? 1; 39 | fixJson = execution["fixJson"] ?? false; 40 | String? systemMessageFile = execution['systemMessageFile']; 41 | systemMessage = systemMessageFile != null 42 | ? await ioHelper.readFileAsString(systemMessageFile) 43 | : null; 44 | promptChain = execution['promptChain']; 45 | List> futurePrompts = promptChain 46 | .map((e) async => await ioHelper.readFileAsString(e)) 47 | .toList(); 48 | promptTemplates = await Future.wait(futurePrompts); 49 | excludesMessageHistory = execution["excludesMessageHistory"] ?? []; 50 | final properties = execution['properties'] ?? {}; 51 | responseFormat = execution['responseFormat'] ?? "text"; 52 | final import = execution["import"]; 53 | if (import != null) { 54 | final propertiesFile = import["propertiesFile"] ?? "properties.json"; 55 | final data = await ioHelper.readJsonFile(propertiesFile); 56 | final props = import["properties"]; 57 | final calculatedData = getFieldsForAllProperties(data, props); 58 | promptValues = {...calculatedData, ...properties}; 59 | } else { 60 | promptValues = Map.from(properties); 61 | } 62 | } 63 | 64 | Map getFieldAtIndex(Map data, 65 | Map properties, String field) { 66 | if (data.containsKey(field) && properties.containsKey(field)) { 67 | int index = properties[field]! - 1; 68 | if (index >= 0 && index < data[field]!.length) { 69 | return {field: data[field]![index]}; 70 | } 71 | } 72 | return {}; 73 | } 74 | 75 | Map getFieldsForAllProperties( 76 | Map data, Map properties) { 77 | Map result = {}; 78 | for (String key in properties.keys) { 79 | result.addAll(getFieldAtIndex(data, properties, key)); 80 | } 81 | return result; 82 | } 83 | 84 | @override 85 | Future doExecution(results, dryRun) async { 86 | final messageHistory = MessageHistory(systemMessage); 87 | for (var chainRun = 1; chainRun <= chainRuns; chainRun++) { 88 | print("\nChain Run: $chainRun"); 89 | for (int i = 0; i < promptTemplates.length; i++) { 90 | var promptFileName = promptChain[i]; 91 | var promptTemplate = promptTemplates[i]; 92 | final prompt = 93 | substituteTemplateProperties(promptTemplate, promptValues); 94 | if (excludesMessageHistory.contains(promptFileName)) { 95 | requestParams['messages'] = [ 96 | {"role": "user", "content": prompt} 97 | ]; 98 | } else { 99 | messageHistory.addUserMessage(prompt); 100 | requestParams['messages'] = messageHistory.history; 101 | } 102 | 103 | final responseBody = await makeChatCompletionRequest( 104 | requestParams, executionId, promptFileName, dryRun); 105 | if (dryRun) { 106 | continue; 107 | } 108 | if (responseBody['errorCode'] != null) { 109 | results.add(createExperimentResult( 110 | "FAILURE", "Failed Request: ${responseBody['errorCode']}")); 111 | throw HttpException( 112 | "Failed Request for Chat Completion: ${responseBody['errorCode']}"); 113 | } 114 | results.add(createUserHistory( 115 | prompt, responseBody, promptFileName, promptValues, chainRun)); 116 | final content = responseBody["choices"][0]["message"]["content"]; 117 | if (!excludesMessageHistory.contains(promptFileName)) { 118 | messageHistory.addAssistantMessage(content); 119 | } 120 | 121 | try { 122 | if (responseFormat == "json") { 123 | addPromptValues(content, promptValues, fixJson); 124 | } 125 | } catch (e) { 126 | print(e); 127 | results.add(createAssistantHistory( 128 | content, responseBody, promptFileName, promptValues, chainRun)); 129 | results.add(createExperimentResult( 130 | "FAILURE", "Failure Parsing JSON Response")); 131 | rethrow; 132 | } 133 | results.add(createAssistantHistory( 134 | content, responseBody, promptFileName, promptValues, chainRun)); 135 | } 136 | } 137 | } 138 | 139 | Map createAssistantHistory( 140 | content, responseBody, promptFile, promptValues, chainRun) { 141 | final usage = responseBody["usage"]; 142 | return { 143 | "role": "assistant", 144 | "content": content, 145 | "promptFile": promptFile, 146 | "chainRun": chainRun, 147 | "completionTokens": usage['completion_tokens'], 148 | "totalTokens": usage['total_tokens'], 149 | "promptValues": Map.from(promptValues) 150 | }; 151 | } 152 | 153 | Map createUserHistory( 154 | prompt, responseBody, promptFile, promptValues, chainRun) { 155 | final usage = responseBody["usage"]; 156 | return { 157 | "role": "user", 158 | "content": prompt, 159 | "promptFile": promptFile, 160 | "chainRun": chainRun, 161 | "promptTokens": usage['prompt_tokens'], 162 | "promptValues": Map.from(promptValues) 163 | }; 164 | } 165 | 166 | Map createExperimentResult(result, String? message) { 167 | return {"result": result, "message": message}; 168 | } 169 | 170 | Future> makeChatCompletionRequest( 171 | requestBody, executionId, tag, dryRun) async { 172 | final toDirectory = "$blockDataDir/$currentBlockRun"; 173 | return networkClient.sendHttpPostRequest( 174 | requestBody, "v1/chat/completions", toDirectory, 175 | dryRun: dryRun); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /lib/src/plugins/image_plugin.dart: -------------------------------------------------------------------------------- 1 | part of gpt_plugins; 2 | 3 | class ImageGptPlugin extends GptPlugin { 4 | ImageGptPlugin(super.projectConfig, super.block, 5 | {super.fileSystem, super.networkClient}); 6 | 7 | late String executionId; 8 | 9 | late String imagePromptFile; 10 | 11 | late String imagesDir; 12 | 13 | late List imageRequests; 14 | 15 | @override 16 | num apiCallCount() { 17 | return imageRequests.length; 18 | } 19 | 20 | @override 21 | Future init(execution, pluginConfiguration) async { 22 | executionId = execution["id"]; 23 | imagePromptFile = execution["prompt"]; 24 | final promptTemplate = await ioHelper.readFileAsString(imagePromptFile); 25 | imagesDir = "$reportDir/images/$blockId"; 26 | await ioHelper.createDirectoryIfNotExist(imagesDir); 27 | imageRequests = await createImageRequest(execution, promptTemplate); 28 | } 29 | 30 | @override 31 | Future doExecution(results, dryRun) async { 32 | for (var i = 1; i <= imageRequests.length; i++) { 33 | final image = imageRequests[i - 1]; 34 | final response = await makeImageGenerationRequest( 35 | image, executionId, imagePromptFile, dryRun); 36 | final result = { 37 | "prompt": image["prompt"], 38 | "size": image["size"], 39 | "images": response["data"] 40 | }; 41 | results.add(result); 42 | } 43 | } 44 | 45 | @override 46 | Future report(results) async { 47 | for (var result in results) { 48 | final images = result["images"]; 49 | for (var image in images) { 50 | final url = image["url"]; 51 | if (url != null) { 52 | final imageName = getLastPathSegment(url); 53 | networkClient.downloadImage(url, "$imagesDir/$imageName"); 54 | } 55 | final b64 = image["b64_json"]; 56 | if (b64 != null) { 57 | final imageName = DateTime.now().millisecond; 58 | networkClient.saveBase64AsPng(b64, "$imagesDir/$imageName.png"); 59 | } 60 | } 61 | } 62 | print("Finished generating images"); 63 | } 64 | 65 | Future> createImageRequest(execution, promptTemplate) async { 66 | final templateProperties = execution["properties"]; 67 | final prompt = 68 | substituteTemplateProperties(promptTemplate, templateProperties); 69 | final responseFormat = execution["responseFormat"] ?? "url"; 70 | final imageCount = execution["imageCount"] ?? 1; 71 | final sizes = execution["sizes"]; 72 | final imageRequests = []; 73 | for (int size in sizes) { 74 | final imageRequest = { 75 | "prompt": prompt, 76 | "n": imageCount, 77 | "size": createImageSize(size), 78 | "response_format": responseFormat 79 | }; 80 | imageRequests.add(imageRequest); 81 | } 82 | return imageRequests; 83 | } 84 | 85 | String createImageSize(size) { 86 | if (size == 256) { 87 | return "256x256"; 88 | } else if (size == 512) { 89 | return "512x512"; 90 | } else if (size == 1024) { 91 | return "1024x1024"; 92 | } else { 93 | throw ArgumentError("Invalid image size: $size", "execution.sizes"); 94 | } 95 | } 96 | 97 | String getLastPathSegment(String url) { 98 | Uri uri = Uri.parse(url); 99 | return uri.pathSegments.isNotEmpty ? uri.pathSegments.last : ''; 100 | } 101 | 102 | Future> makeImageGenerationRequest( 103 | requestBody, executionId, tag, dryRun) async { 104 | final toDirectory = "$blockDataDir/$currentBlockRun"; 105 | return networkClient.sendHttpPostRequest( 106 | requestBody, "v1/images/generations", toDirectory, 107 | dryRun: dryRun); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/src/plugins/reporting_plugin.dart: -------------------------------------------------------------------------------- 1 | part of gpt_plugins; 2 | 3 | class ReportingGptPlugin extends GptPlugin { 4 | late List reports; 5 | 6 | ReportingGptPlugin(super.projectConfig, super.block, 7 | {super.fileSystem, super.networkClient}); 8 | 9 | @override 10 | Future init(execution, pluginConfiguration) async { 11 | List blockIds = execution["blockIds"]; 12 | List> futurePrompts = blockIds 13 | .map((e) async => await ioHelper.readFileAsString( 14 | "$reportDir/$projectName-$projectVersion-$e-report.json")) 15 | .toList(); 16 | reports = await Future.wait(futurePrompts); 17 | } 18 | 19 | @override 20 | Future doExecution(results, dryRun) async { 21 | final tables = StringBuffer(); 22 | for (String r in reports) { 23 | final report = jsonDecode(r); 24 | final blockId = report["blockId"]; 25 | final configuration = report["configuration"]; 26 | final header = generateBlockHeader(blockId, configuration); 27 | tables.write(header); 28 | final blockRuns = report["blockRuns"]; 29 | for (var blockRun in blockRuns) { 30 | final htmlContent = generateHtmlContent(blockRun); 31 | tables.write(htmlContent); 32 | } 33 | } 34 | final fileName = "$projectName-$projectVersion-report.html"; 35 | print("Writing project report: $reportDir/$fileName"); 36 | final report = generateHtmlWrapper(tables.toString()); 37 | final outputFile = fileSystem.file("$reportDir/$fileName"); 38 | await outputFile.writeAsString(report); 39 | } 40 | 41 | String generateBlockHeader(blockId, configuration) { 42 | final header = "

BLOCK: $blockId

"; 43 | final formattedJson = JsonEncoder.withIndent(' ') 44 | .convert(configuration) 45 | .replaceAll('\n', '
'); 46 | 47 | return "$header
$formattedJson
"; 48 | } 49 | 50 | String generateHtmlWrapper(String body) { 51 | final htmlBuffer = StringBuffer(); 52 | htmlBuffer.write(''); 53 | htmlBuffer.write( 54 | '
'); 55 | htmlBuffer 56 | .write("

$projectName:$projectVersion

${DateTime.now()}
"); 57 | htmlBuffer.write(body); 58 | htmlBuffer.write('
'); 59 | return htmlBuffer.toString(); 60 | } 61 | 62 | String generateHtmlContent(Map data) { 63 | final htmlBuffer = StringBuffer(); 64 | for (final node in data['blockResults']) { 65 | final tableColor = node['role'] == 'user' ? '#d0e1ff' : '#fff4d0'; 66 | 67 | htmlBuffer.write( 68 | ''); 69 | htmlBuffer.write( 70 | ''); 71 | htmlBuffer.write( 72 | '

${node['role'].toUpperCase()}

${node['promptFile']}

'); 73 | final contentWithNewlines = node['content'].replaceAll('\n', '
'); 74 | 75 | if (node['role'] == 'assistant') { 76 | try { 77 | final contentJson = jsonDecode(node['content']); 78 | final formattedJson = JsonEncoder.withIndent(' ') 79 | .convert(contentJson) 80 | .replaceAll('\n', '
'); 81 | final updatedJson = formattedJson.splitMapJoin( 82 | RegExp(r'(".+": ")([^"]+)(",?)'), 83 | onMatch: (match) { 84 | final key = match.group(1); 85 | final value = match.group(2); 86 | final comma = match.group(3); 87 | final formattedValue = value!.length > 80 88 | ? value.replaceAllMapped( 89 | RegExp(r'.{1,80}'), (match) => '${match.group(0)}
') 90 | : value; 91 | return '$key$formattedValue$comma'; 92 | }, 93 | onNonMatch: (nonMatch) => nonMatch, 94 | ); 95 | 96 | htmlBuffer.write('
$updatedJson
'); 97 | } catch (e) { 98 | htmlBuffer.write('$contentWithNewlines'); 99 | } 100 | } else { 101 | htmlBuffer.write('$contentWithNewlines'); 102 | } 103 | 104 | htmlBuffer.write('

'); 105 | } 106 | return htmlBuffer.toString(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/src/prompts.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | void addJsonContentToPromptValues(jsonContent, promptValues) { 4 | try { 5 | final newValues = jsonDecode(jsonContent); 6 | promptValues.addAll(newValues); 7 | } catch (e) { 8 | throw FormatException("Malformed JSON. Failing Experiment.", jsonContent); 9 | } 10 | } 11 | 12 | void addPromptValues(content, promptValues, fixJson) { 13 | try { 14 | addJsonContentToPromptValues(content, promptValues); 15 | } catch (e) { 16 | if (!fixJson) { 17 | rethrow; 18 | } 19 | final fixedJson = extractJson(content); 20 | if (fixedJson != null) { 21 | addJsonContentToPromptValues(fixedJson, promptValues); 22 | } else { 23 | rethrow; 24 | } 25 | } 26 | } 27 | 28 | RegExp placeholderPattern = RegExp(r'\$\{([^\}]+)\}'); 29 | 30 | String substituteTemplateProperties( 31 | String template, Map templateProperties) { 32 | String modifiedTemplate = template.replaceAllMapped(placeholderPattern, 33 | (Match match) => (templateProperties[match[1]] ?? "").toString()); 34 | return modifiedTemplate; 35 | } 36 | 37 | String createPromptByIndex(String template, templateProperties, index) { 38 | String modifiedTemplate = 39 | template.replaceAllMapped(placeholderPattern, (Match match) { 40 | if (templateProperties[match[1]] != null) { 41 | if (index < templateProperties[match[1]].length) { 42 | return templateProperties[match[1]][index] ?? ""; 43 | } else { 44 | throw RangeError( 45 | 'Invalid prompt index: $index is out of range for the property ${match[1]}'); 46 | } 47 | } 48 | return ""; 49 | }); 50 | return modifiedTemplate; 51 | } 52 | 53 | String? extractJson(content) { 54 | int bracketCount = 0; 55 | int startIndex = -1; 56 | int endIndex = -1; 57 | 58 | for (int i = 0; i < content.length; i++) { 59 | if (content[i] == '{') { 60 | if (startIndex == -1) { 61 | startIndex = i; 62 | } 63 | bracketCount++; 64 | } else if (content[i] == '}') { 65 | bracketCount--; 66 | if (bracketCount == 0) { 67 | endIndex = i; 68 | break; 69 | } 70 | } 71 | } 72 | 73 | if (startIndex != -1 && endIndex != -1) { 74 | return content.substring(startIndex, endIndex + 1); 75 | } else { 76 | return null; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/src/reporter.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | import 'io_helper.dart'; 5 | 6 | abstract class Reporter { 7 | Future logRequestAndResponse( 8 | requestBody, 9 | responseBody, 10 | toDirectory, 11 | ); 12 | 13 | Future logFailedRequest(requestBody, toDirectory); 14 | 15 | Future writeMetrics(responseBody, executionId, tag, metricsFile); 16 | 17 | Future writeProjectReport(results, reportDir); 18 | } 19 | 20 | class ConcreteReporter extends Reporter { 21 | IOHelper ioHelper; 22 | 23 | ConcreteReporter(this.ioHelper); 24 | 25 | @override 26 | Future logRequestAndResponse( 27 | requestBody, responseBody, toDirectory) async { 28 | await ioHelper.createDirectoryIfNotExist(toDirectory); 29 | final responseId = responseBody["id"] ?? ""; 30 | final timestamp = DateTime.now().millisecondsSinceEpoch; 31 | final outputRequestFile = 32 | "$toDirectory/$timestamp-$responseId-request.json"; 33 | final outputResponseFile = 34 | "$toDirectory/$timestamp-$responseId-response.json"; 35 | await ioHelper.writeString(requestBody, outputRequestFile); 36 | await ioHelper.writeString(jsonEncode(responseBody), outputResponseFile); 37 | } 38 | 39 | @override 40 | Future logFailedRequest(requestBody, toDirectory) async { 41 | await ioHelper.createDirectoryIfNotExist(toDirectory); 42 | final responseId = "failed"; 43 | final timestamp = DateTime.now().millisecondsSinceEpoch; 44 | await ioHelper.writeString( 45 | requestBody, "$toDirectory/$timestamp-$responseId-request.json"); 46 | } 47 | 48 | @override 49 | Future writeProjectReport(results, reportDir) async { 50 | await ioHelper.createDirectoryIfNotExist(reportDir); 51 | final projectName = results["projectName"]; 52 | final projectVersion = results["projectVersion"]; 53 | final blockId = results["blockId"]; 54 | final fileName = "$projectName-$projectVersion-$blockId-report.json"; 55 | print("Writing project report: $reportDir/$fileName"); 56 | await ioHelper.writeMap(results, "$reportDir/$fileName"); 57 | } 58 | 59 | @override 60 | Future writeMetrics(responseBody, executionId, tag, metricsFile) async { 61 | final responseId = responseBody["id"] ?? "N/A"; 62 | final usage = responseBody["usage"]; 63 | final promptTokens = usage?['prompt_tokens'] ?? 0; 64 | final completionTokens = usage?['completion_tokens'] ?? 0; 65 | final totalTokens = usage?['total_tokens'] ?? 0; 66 | 67 | final file = ioHelper.file(metricsFile); 68 | bool exists = await file.exists(); 69 | if (!exists) { 70 | await file.writeAsString( 71 | "request_id, executionId, tag, request_time, prompt_tokens, completion_tokens, total_tokens\n"); 72 | } 73 | 74 | try { 75 | final requestTime = responseBody['requestTime']; 76 | final dataToAppend = 77 | "$responseId, $executionId, $tag, $requestTime, $promptTokens, $completionTokens, $totalTokens\n"; 78 | await file.writeAsString(dataToAppend, 79 | mode: FileMode.append, flush: true); 80 | } catch (e) { 81 | throw FileSystemException( 82 | 'Error occurred while writing file: $e', file.path); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: e440ac42679dfc04bbbefb58ed225c994bc7e07fccc8a68ec7d3631a127e5da9 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "54.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "2c2e3721ee9fb36de92faa060f3480c81b23e904352b087e5c64224b1a044427" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "5.6.0" 20 | archive: 21 | dependency: "direct main" 22 | description: 23 | name: archive 24 | sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "3.3.7" 28 | args: 29 | dependency: "direct main" 30 | description: 31 | name: args 32 | sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.4.1" 36 | async: 37 | dependency: transitive 38 | description: 39 | name: async 40 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "2.11.0" 44 | boolean_selector: 45 | dependency: transitive 46 | description: 47 | name: boolean_selector 48 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "2.1.1" 52 | build: 53 | dependency: transitive 54 | description: 55 | name: build 56 | sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "2.3.1" 60 | build_config: 61 | dependency: transitive 62 | description: 63 | name: build_config 64 | sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "1.1.1" 68 | build_daemon: 69 | dependency: transitive 70 | description: 71 | name: build_daemon 72 | sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "3.1.1" 76 | build_resolvers: 77 | dependency: transitive 78 | description: 79 | name: build_resolvers 80 | sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "2.2.0" 84 | build_runner: 85 | dependency: "direct dev" 86 | description: 87 | name: build_runner 88 | sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "2.3.3" 92 | build_runner_core: 93 | dependency: transitive 94 | description: 95 | name: build_runner_core 96 | sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "7.2.7" 100 | built_collection: 101 | dependency: transitive 102 | description: 103 | name: built_collection 104 | sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "5.1.1" 108 | built_value: 109 | dependency: transitive 110 | description: 111 | name: built_value 112 | sha256: "2f17434bd5d52a26762043d6b43bb53b3acd029b4d9071a329f46d67ef297e6d" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "8.5.0" 116 | characters: 117 | dependency: transitive 118 | description: 119 | name: characters 120 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "1.3.0" 124 | checked_yaml: 125 | dependency: transitive 126 | description: 127 | name: checked_yaml 128 | sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "2.0.2" 132 | clock: 133 | dependency: transitive 134 | description: 135 | name: clock 136 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "1.1.1" 140 | code_builder: 141 | dependency: transitive 142 | description: 143 | name: code_builder 144 | sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "4.4.0" 148 | collection: 149 | dependency: transitive 150 | description: 151 | name: collection 152 | sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.17.1" 156 | convert: 157 | dependency: transitive 158 | description: 159 | name: convert 160 | sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "3.1.1" 164 | coverage: 165 | dependency: transitive 166 | description: 167 | name: coverage 168 | sha256: "2fb815080e44a09b85e0f2ca8a820b15053982b2e714b59267719e8a9ff17097" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "1.6.3" 172 | crypto: 173 | dependency: transitive 174 | description: 175 | name: crypto 176 | sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "3.0.2" 180 | dart_console: 181 | dependency: transitive 182 | description: 183 | name: dart_console 184 | sha256: cfbaeb901f23f3c838a6e70bf15c747b83bf53884bb850d1536c8512dc5005b3 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "1.1.2" 188 | dart_style: 189 | dependency: transitive 190 | description: 191 | name: dart_style 192 | sha256: "5be16bf1707658e4c03078d4a9b90208ded217fb02c163e207d334082412f2fb" 193 | url: "https://pub.dev" 194 | source: hosted 195 | version: "2.2.5" 196 | ffi: 197 | dependency: transitive 198 | description: 199 | name: ffi 200 | sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 201 | url: "https://pub.dev" 202 | source: hosted 203 | version: "2.0.2" 204 | file: 205 | dependency: "direct main" 206 | description: 207 | name: file 208 | sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" 209 | url: "https://pub.dev" 210 | source: hosted 211 | version: "6.1.4" 212 | fixnum: 213 | dependency: transitive 214 | description: 215 | name: fixnum 216 | sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" 217 | url: "https://pub.dev" 218 | source: hosted 219 | version: "1.0.1" 220 | frontend_server_client: 221 | dependency: transitive 222 | description: 223 | name: frontend_server_client 224 | sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" 225 | url: "https://pub.dev" 226 | source: hosted 227 | version: "3.2.0" 228 | glob: 229 | dependency: transitive 230 | description: 231 | name: glob 232 | sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" 233 | url: "https://pub.dev" 234 | source: hosted 235 | version: "2.1.1" 236 | graphs: 237 | dependency: transitive 238 | description: 239 | name: graphs 240 | sha256: "772db3d53d23361d4ffcf5a9bb091cf3ee9b22f2be52cd107cd7a2683a89ba0e" 241 | url: "https://pub.dev" 242 | source: hosted 243 | version: "2.3.0" 244 | http: 245 | dependency: "direct main" 246 | description: 247 | name: http 248 | sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" 249 | url: "https://pub.dev" 250 | source: hosted 251 | version: "0.13.5" 252 | http_multi_server: 253 | dependency: transitive 254 | description: 255 | name: http_multi_server 256 | sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" 257 | url: "https://pub.dev" 258 | source: hosted 259 | version: "3.2.1" 260 | http_parser: 261 | dependency: transitive 262 | description: 263 | name: http_parser 264 | sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" 265 | url: "https://pub.dev" 266 | source: hosted 267 | version: "4.0.2" 268 | interact: 269 | dependency: "direct main" 270 | description: 271 | name: interact 272 | sha256: b1abf79334bec42e58496a054cb7ee7ca74da6181f6a1fb6b134f1aa22bc4080 273 | url: "https://pub.dev" 274 | source: hosted 275 | version: "2.2.0" 276 | intl: 277 | dependency: transitive 278 | description: 279 | name: intl 280 | sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" 281 | url: "https://pub.dev" 282 | source: hosted 283 | version: "0.17.0" 284 | io: 285 | dependency: transitive 286 | description: 287 | name: io 288 | sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" 289 | url: "https://pub.dev" 290 | source: hosted 291 | version: "1.0.4" 292 | js: 293 | dependency: transitive 294 | description: 295 | name: js 296 | sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" 297 | url: "https://pub.dev" 298 | source: hosted 299 | version: "0.6.5" 300 | json_annotation: 301 | dependency: transitive 302 | description: 303 | name: json_annotation 304 | sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 305 | url: "https://pub.dev" 306 | source: hosted 307 | version: "4.8.0" 308 | lints: 309 | dependency: "direct dev" 310 | description: 311 | name: lints 312 | sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" 313 | url: "https://pub.dev" 314 | source: hosted 315 | version: "2.0.1" 316 | logging: 317 | dependency: transitive 318 | description: 319 | name: logging 320 | sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" 321 | url: "https://pub.dev" 322 | source: hosted 323 | version: "1.1.1" 324 | matcher: 325 | dependency: transitive 326 | description: 327 | name: matcher 328 | sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" 329 | url: "https://pub.dev" 330 | source: hosted 331 | version: "0.12.15" 332 | meta: 333 | dependency: transitive 334 | description: 335 | name: meta 336 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 337 | url: "https://pub.dev" 338 | source: hosted 339 | version: "1.9.1" 340 | mime: 341 | dependency: transitive 342 | description: 343 | name: mime 344 | sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e 345 | url: "https://pub.dev" 346 | source: hosted 347 | version: "1.0.4" 348 | mockito: 349 | dependency: "direct dev" 350 | description: 351 | name: mockito 352 | sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 353 | url: "https://pub.dev" 354 | source: hosted 355 | version: "5.4.0" 356 | node_preamble: 357 | dependency: transitive 358 | description: 359 | name: node_preamble 360 | sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" 361 | url: "https://pub.dev" 362 | source: hosted 363 | version: "2.0.2" 364 | package_config: 365 | dependency: transitive 366 | description: 367 | name: package_config 368 | sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" 369 | url: "https://pub.dev" 370 | source: hosted 371 | version: "2.1.0" 372 | path: 373 | dependency: "direct main" 374 | description: 375 | name: path 376 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 377 | url: "https://pub.dev" 378 | source: hosted 379 | version: "1.8.3" 380 | pointycastle: 381 | dependency: transitive 382 | description: 383 | name: pointycastle 384 | sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" 385 | url: "https://pub.dev" 386 | source: hosted 387 | version: "3.7.3" 388 | pool: 389 | dependency: transitive 390 | description: 391 | name: pool 392 | sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" 393 | url: "https://pub.dev" 394 | source: hosted 395 | version: "1.5.1" 396 | pub_semver: 397 | dependency: transitive 398 | description: 399 | name: pub_semver 400 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 401 | url: "https://pub.dev" 402 | source: hosted 403 | version: "2.1.4" 404 | pubspec_parse: 405 | dependency: transitive 406 | description: 407 | name: pubspec_parse 408 | sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 409 | url: "https://pub.dev" 410 | source: hosted 411 | version: "1.2.3" 412 | shelf: 413 | dependency: transitive 414 | description: 415 | name: shelf 416 | sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 417 | url: "https://pub.dev" 418 | source: hosted 419 | version: "1.4.1" 420 | shelf_packages_handler: 421 | dependency: transitive 422 | description: 423 | name: shelf_packages_handler 424 | sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" 425 | url: "https://pub.dev" 426 | source: hosted 427 | version: "3.0.2" 428 | shelf_static: 429 | dependency: transitive 430 | description: 431 | name: shelf_static 432 | sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e 433 | url: "https://pub.dev" 434 | source: hosted 435 | version: "1.1.2" 436 | shelf_web_socket: 437 | dependency: transitive 438 | description: 439 | name: shelf_web_socket 440 | sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" 441 | url: "https://pub.dev" 442 | source: hosted 443 | version: "1.0.4" 444 | source_gen: 445 | dependency: transitive 446 | description: 447 | name: source_gen 448 | sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" 449 | url: "https://pub.dev" 450 | source: hosted 451 | version: "1.3.2" 452 | source_map_stack_trace: 453 | dependency: transitive 454 | description: 455 | name: source_map_stack_trace 456 | sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" 457 | url: "https://pub.dev" 458 | source: hosted 459 | version: "2.1.1" 460 | source_maps: 461 | dependency: transitive 462 | description: 463 | name: source_maps 464 | sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" 465 | url: "https://pub.dev" 466 | source: hosted 467 | version: "0.10.12" 468 | source_span: 469 | dependency: transitive 470 | description: 471 | name: source_span 472 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 473 | url: "https://pub.dev" 474 | source: hosted 475 | version: "1.10.0" 476 | stack_trace: 477 | dependency: transitive 478 | description: 479 | name: stack_trace 480 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 481 | url: "https://pub.dev" 482 | source: hosted 483 | version: "1.11.0" 484 | stream_channel: 485 | dependency: transitive 486 | description: 487 | name: stream_channel 488 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 489 | url: "https://pub.dev" 490 | source: hosted 491 | version: "2.1.1" 492 | stream_transform: 493 | dependency: transitive 494 | description: 495 | name: stream_transform 496 | sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" 497 | url: "https://pub.dev" 498 | source: hosted 499 | version: "2.1.0" 500 | string_scanner: 501 | dependency: transitive 502 | description: 503 | name: string_scanner 504 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 505 | url: "https://pub.dev" 506 | source: hosted 507 | version: "1.2.0" 508 | term_glyph: 509 | dependency: transitive 510 | description: 511 | name: term_glyph 512 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 513 | url: "https://pub.dev" 514 | source: hosted 515 | version: "1.2.1" 516 | test: 517 | dependency: "direct dev" 518 | description: 519 | name: test 520 | sha256: "4f92f103ef63b1bbac6f4bd1930624fca81b2574464482512c4f0896319be575" 521 | url: "https://pub.dev" 522 | source: hosted 523 | version: "1.24.2" 524 | test_api: 525 | dependency: transitive 526 | description: 527 | name: test_api 528 | sha256: daadc9baabec998b062c9091525aa95786508b1c48e9c30f1f891b8bf6ff2e64 529 | url: "https://pub.dev" 530 | source: hosted 531 | version: "0.5.2" 532 | test_core: 533 | dependency: transitive 534 | description: 535 | name: test_core 536 | sha256: "3642b184882f79e76ca57a9230fb971e494c3c1fd09c21ae3083ce891bcc0aa1" 537 | url: "https://pub.dev" 538 | source: hosted 539 | version: "0.5.2" 540 | timing: 541 | dependency: transitive 542 | description: 543 | name: timing 544 | sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" 545 | url: "https://pub.dev" 546 | source: hosted 547 | version: "1.0.1" 548 | tint: 549 | dependency: transitive 550 | description: 551 | name: tint 552 | sha256: "9652d9a589f4536d5e392cf790263d120474f15da3cf1bee7f1fdb31b4de5f46" 553 | url: "https://pub.dev" 554 | source: hosted 555 | version: "2.0.1" 556 | typed_data: 557 | dependency: transitive 558 | description: 559 | name: typed_data 560 | sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c 561 | url: "https://pub.dev" 562 | source: hosted 563 | version: "1.3.2" 564 | vm_service: 565 | dependency: transitive 566 | description: 567 | name: vm_service 568 | sha256: eb3cf3f45fc1500ae30481ac9ab788302fa5e8edc3f3eaddf183945ee93a8bf3 569 | url: "https://pub.dev" 570 | source: hosted 571 | version: "11.2.0" 572 | watcher: 573 | dependency: transitive 574 | description: 575 | name: watcher 576 | sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" 577 | url: "https://pub.dev" 578 | source: hosted 579 | version: "1.0.2" 580 | web_socket_channel: 581 | dependency: transitive 582 | description: 583 | name: web_socket_channel 584 | sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b 585 | url: "https://pub.dev" 586 | source: hosted 587 | version: "2.4.0" 588 | webkit_inspection_protocol: 589 | dependency: transitive 590 | description: 591 | name: webkit_inspection_protocol 592 | sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" 593 | url: "https://pub.dev" 594 | source: hosted 595 | version: "1.2.0" 596 | win32: 597 | dependency: transitive 598 | description: 599 | name: win32 600 | sha256: "6b75ac2ddd42f5c226fdaf4498a2b04071c06f1f2b8f7ab1c3f77cc7f2285ff1" 601 | url: "https://pub.dev" 602 | source: hosted 603 | version: "2.7.0" 604 | yaml: 605 | dependency: "direct main" 606 | description: 607 | name: yaml 608 | sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" 609 | url: "https://pub.dev" 610 | source: hosted 611 | version: "3.1.1" 612 | sdks: 613 | dart: ">=3.0.0 <4.0.0" 614 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: gpt 2 | description: A command line tool for running GPT commands. This tool supports GPT Plugins, prompt-batching and prompt-chaining. 3 | version: 0.12.0 4 | repository: https://github.com/sisbell/stackwire-gpt 5 | issue_tracker: https://github.com/sisbell/stackwire-gpt/issues 6 | executables: 7 | air: 8 | platforms: 9 | linux: 10 | macos: 11 | windows: 12 | 13 | environment: 14 | sdk: '>=3.0.0 <4.0.0' 15 | 16 | dev_dependencies: 17 | lints: ^2.0.0 18 | test: ^1.16.0 19 | mockito: ^5.4.0 20 | build_runner: ^2.3.3 21 | 22 | dependencies: 23 | archive: ^3.3.7 24 | args: ^2.4.0 25 | http: ^0.13.5 26 | interact: ^2.2.0 27 | path: ^1.8.3 28 | yaml: ^3.1.1 29 | file: ^6.1.4 -------------------------------------------------------------------------------- /test/archeypes_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:archive/archive.dart'; 4 | import 'package:file/file.dart'; 5 | import 'package:file/memory.dart'; 6 | import 'package:gpt/src/archetypes.dart'; 7 | import 'package:http/http.dart' as http; 8 | import 'package:http/testing.dart'; 9 | import 'package:path/path.dart' as path; 10 | import 'package:test/test.dart'; 11 | 12 | void main() { 13 | group('getDirectoryPaths', () { 14 | test('should return correct stackwire and archetype directory paths', 15 | () async { 16 | FileSystem fileSystem = MemoryFileSystem.test(); 17 | final builder = ArchetypeBuilder(fileSystem); 18 | final directoryPaths = await builder.getDirectoryPaths("2"); 19 | final String homeDir = 20 | Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!; 21 | final String expectedStackwireDirPath = path.join(homeDir, '.stackwire'); 22 | final String expectedArchetypesDirPath = 23 | path.join(expectedStackwireDirPath, 'cache/archetypes-2'); 24 | 25 | expect( 26 | directoryPaths['stackwireDirectoryPath'], expectedStackwireDirPath); 27 | expect( 28 | directoryPaths['archetypesDirectoryPath'], expectedArchetypesDirPath); 29 | }); 30 | }); 31 | 32 | group('downloadArchetypeArchive', () { 33 | late ArchetypeBuilder builder; 34 | late MemoryFileSystem memoryFileSystem; 35 | 36 | setUp(() { 37 | memoryFileSystem = MemoryFileSystem(); 38 | builder = ArchetypeBuilder(memoryFileSystem); 39 | }); 40 | 41 | test( 42 | 'should download and extract the archive when the directory does not exist', 43 | () async { 44 | final archive = Archive(); 45 | final encoder = ZipEncoder(); 46 | final encodedArchive = encoder.encode(archive)!; 47 | 48 | final client = MockClient( 49 | (request) async { 50 | return http.Response.bytes(encodedArchive, 200); 51 | }, 52 | ); 53 | 54 | final archetypesDirPath = 55 | await builder.downloadArchetypeArchive(client: client); 56 | 57 | final homeDir = 58 | Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']!; 59 | final stackwireDirPath = path.join(homeDir, '.stackwire'); 60 | final stackwireDir = memoryFileSystem.directory(stackwireDirPath); 61 | expect(await stackwireDir.exists(), true); 62 | 63 | final extractedPath = 64 | (await builder.getDirectoryPaths("2"))["archetypesDirectoryPath"]; 65 | expect(archetypesDirPath, extractedPath); 66 | }); 67 | 68 | test('downloadArchetypeArchive throws ArchiveDownloadException on error', 69 | () async { 70 | final client = MockClient( 71 | (request) async { 72 | return http.Response.bytes([], 400); 73 | }, 74 | ); 75 | expect( 76 | () async => await builder.downloadArchetypeArchive(client: client), 77 | throwsA(isA()), 78 | ); 79 | }); 80 | }); 81 | 82 | group('readProjectYaml', () { 83 | late ArchetypeBuilder builder; 84 | late MemoryFileSystem memoryFileSystem; 85 | 86 | setUp(() { 87 | memoryFileSystem = MemoryFileSystem(); 88 | builder = ArchetypeBuilder(memoryFileSystem); 89 | }); 90 | 91 | test('should read and return the contents of the project.yaml file', 92 | () async { 93 | final directory = memoryFileSystem.directory('/test/'); 94 | directory.createSync(); 95 | final file = memoryFileSystem.file('/test/project.yaml'); 96 | file.writeAsStringSync('test: Test Project'); 97 | final result = await builder.readProjectYaml(directory.path); 98 | expect(result, equals('test: Test Project')); 99 | }); 100 | 101 | test('should throw an exception if the project.yaml file does not exist', 102 | () async { 103 | final directory = memoryFileSystem.directory('/test/'); 104 | directory.createSync(); 105 | expect(() async => await builder.readProjectYaml(directory.path), 106 | throwsA(isA())); 107 | }); 108 | }); 109 | 110 | group('verifyYamlFormat', () { 111 | late ArchetypeBuilder builder; 112 | late MemoryFileSystem memoryFileSystem; 113 | 114 | setUp(() { 115 | memoryFileSystem = MemoryFileSystem(); 116 | builder = ArchetypeBuilder(memoryFileSystem); 117 | }); 118 | test('returns true for correctly formatted YAML', () { 119 | final yamlString = ''' 120 | name: John Doe 121 | age: 30 122 | email: johndoe@example.com 123 | '''; 124 | expect(builder.verifyYamlFormat(yamlString), isTrue); 125 | }); 126 | 127 | test('returns false for incorrectly formatted YAML', () { 128 | final yamlString = ''' 129 | * name John Doe 130 | '''; 131 | expect(builder.verifyYamlFormat(yamlString), isFalse); 132 | }); 133 | 134 | test('returns true for an empty YAML string', () { 135 | expect(builder.verifyYamlFormat(''), isTrue); 136 | }); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /test/image_plugin_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:file/memory.dart'; 2 | import 'package:gpt/src/gpt_plugin.dart'; 3 | import 'package:test/test.dart'; 4 | 5 | import 'mocks.dart'; 6 | 7 | void main() { 8 | group('createImageRequest', () { 9 | test('should return list of image request for each size', () async { 10 | final promptTemplate = "Create image of a \${creature} with \${feature}"; 11 | 12 | final execution = { 13 | "id": "img-unicorn", 14 | "sizes": [256, 512], 15 | "prompt": "image.prompt", 16 | "properties": { 17 | "creature": "Unicorn", 18 | "feature": "a gold horn and wings" 19 | } 20 | }; 21 | final plugin = ImageGptPlugin(testProjectConfig, testBlock, 22 | fileSystem: MemoryFileSystem()); 23 | final imageRequests = 24 | await plugin.createImageRequest(execution, promptTemplate); 25 | 26 | expect(imageRequests, isNotNull); 27 | expect(imageRequests, isNotEmpty); 28 | expect(imageRequests.length, 2); 29 | expect(imageRequests[0]["prompt"], 30 | "Create image of a Unicorn with a gold horn and wings"); 31 | expect(imageRequests[0]["n"], 1); 32 | expect(imageRequests[0]["size"], "256x256"); 33 | expect(imageRequests[0]["response_format"], "url"); 34 | }); 35 | 36 | test('createImageRequest should return an empty list when sizes is empty', 37 | () async { 38 | final promptTemplate = "Create image of a \${creature} with \${feature}"; 39 | final plugin = ImageGptPlugin(testProjectConfig, testBlock, 40 | fileSystem: MemoryFileSystem()); 41 | final execution = { 42 | "properties": { 43 | "creature": "Unicorn", 44 | "feature": "a gold horn and wings" 45 | }, 46 | "responseFormat": "url", 47 | "imageCount": 1, 48 | "sizes": [] 49 | }; 50 | final result = await plugin.createImageRequest(execution, promptTemplate); 51 | expect(result, []); 52 | }); 53 | 54 | test('createImageRequest should use the provided responseFormat', () async { 55 | final promptTemplate = "Create image of a \${creature} with \${feature}"; 56 | final plugin = ImageGptPlugin(testProjectConfig, testBlock, 57 | fileSystem: MemoryFileSystem()); 58 | final execution = { 59 | "properties": { 60 | "creature": "Unicorn", 61 | "feature": "a gold horn and wings" 62 | }, 63 | "responseFormat": "b64_json", 64 | "imageCount": 1, 65 | "sizes": [256] 66 | }; 67 | final result = await plugin.createImageRequest(execution, promptTemplate); 68 | expect(result[0]['response_format'], 'b64_json'); 69 | }); 70 | 71 | test('createImageRequest should use url as the default responseFormat', 72 | () async { 73 | final promptTemplate = "Create image of a \${creature} with \${feature}"; 74 | final plugin = ImageGptPlugin(testProjectConfig, testBlock, 75 | fileSystem: MemoryFileSystem()); 76 | final execution = { 77 | "properties": { 78 | "creature": "Unicorn", 79 | "feature": "a gold horn and wings" 80 | }, 81 | "imageCount": 1, 82 | "sizes": [256] 83 | }; 84 | final result = await plugin.createImageRequest(execution, promptTemplate); 85 | expect(result[0]['response_format'], 'url'); 86 | }); 87 | }); 88 | 89 | group('createImageSize', () { 90 | late ImageGptPlugin plugin; 91 | 92 | setUp(() { 93 | plugin = ImageGptPlugin(testProjectConfig, testBlock, 94 | fileSystem: MemoryFileSystem()); 95 | }); 96 | 97 | test('returns "256x256" when size is 256', () { 98 | expect(plugin.createImageSize(256), equals('256x256')); 99 | }); 100 | 101 | test('returns "512x512" when size is 512', () { 102 | expect(plugin.createImageSize(512), equals('512x512')); 103 | }); 104 | 105 | test('returns "1024x1024" when size is 1024', () { 106 | expect(plugin.createImageSize(1024), equals('1024x1024')); 107 | }); 108 | 109 | test('throws ArgumentError when size is not 256, 512, or 1024', () { 110 | expect(() => plugin.createImageSize(123), throwsA(isA())); 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /test/io_helper_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:file/file.dart'; 4 | import 'package:file/memory.dart'; 5 | import 'package:gpt/src/io_helper.dart'; 6 | import 'package:path/path.dart' as path; 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | test('copyDirectoryContents should copy directory contents', () async { 11 | final fileSystem = MemoryFileSystem(); 12 | final sourceDir = fileSystem.directory('source'); 13 | final destDir = fileSystem.directory('destination'); 14 | 15 | await sourceDir.create(); 16 | await fileSystem.file('source/file1.txt').writeAsString('Hello'); 17 | await fileSystem.file('source/file2.txt').writeAsString('World'); 18 | await sourceDir.childDirectory('subdir').create(); 19 | await fileSystem.file('source/subdir/file3.txt').writeAsString('Subdir'); 20 | 21 | final ioHelper = IOHelper(fileSystem: fileSystem); 22 | await ioHelper.copyDirectoryContents(sourceDir, destDir); 23 | 24 | expect(await destDir.exists(), true); 25 | expect( 26 | await fileSystem.file('destination/file1.txt').readAsString(), 'Hello'); 27 | expect( 28 | await fileSystem.file('destination/file2.txt').readAsString(), 'World'); 29 | expect(await fileSystem.directory('destination/subdir').exists(), true); 30 | expect(await fileSystem.file('destination/subdir/file3.txt').readAsString(), 31 | 'Subdir'); 32 | }); 33 | 34 | test('copyDirectoryContents should copy an empty directory', () async { 35 | final fileSystem = MemoryFileSystem(); 36 | final ioHelper = IOHelper(fileSystem: fileSystem); 37 | final srcDir = fileSystem.directory('src'); 38 | final destDir = fileSystem.directory('dest'); 39 | await srcDir.create(); 40 | await destDir.create(); 41 | await ioHelper.copyDirectoryContents(srcDir, destDir); 42 | 43 | final destDirContents = destDir.listSync(); 44 | expect(destDirContents.isEmpty, isTrue); 45 | }); 46 | 47 | test( 48 | 'copyDirectoryContents should copy a directory with files only (no subdirectories)', 49 | () async { 50 | final fileSystem = MemoryFileSystem(); 51 | final ioHelper = IOHelper(fileSystem: fileSystem); 52 | 53 | final srcDir = fileSystem.directory('src'); 54 | final destDir = fileSystem.directory('dest'); 55 | await srcDir.create(); 56 | await destDir.create(); 57 | 58 | final file1 = fileSystem.file(path.join(srcDir.path, 'file1.txt')); 59 | final file2 = fileSystem.file(path.join(srcDir.path, 'file2.txt')); 60 | await file1.writeAsString('Content of file1.txt'); 61 | await file2.writeAsString('Content of file2.txt'); 62 | 63 | await ioHelper.copyDirectoryContents(srcDir, destDir); 64 | 65 | final destFile1 = fileSystem.file(path.join(destDir.path, 'file1.txt')); 66 | final destFile2 = fileSystem.file(path.join(destDir.path, 'file2.txt')); 67 | expect(await destFile1.exists(), isTrue); 68 | expect(await destFile2.exists(), isTrue); 69 | 70 | expect(await destFile1.readAsString(), 'Content of file1.txt'); 71 | expect(await destFile2.readAsString(), 'Content of file2.txt'); 72 | }); 73 | 74 | test('writeMap should write a JSON file from a map', () async { 75 | final fileSystem = MemoryFileSystem(); 76 | final ioHelper = IOHelper(fileSystem: fileSystem); 77 | 78 | final testMap = { 79 | 'key1': 'value1', 80 | 'key2': 'value2', 81 | }; 82 | 83 | final filePath = 'test.json'; 84 | 85 | await ioHelper.writeMap(testMap, filePath); 86 | 87 | final file = fileSystem.file(filePath); 88 | expect(await file.exists(), isTrue); 89 | expect(jsonDecode(await file.readAsString()), equals(testMap)); 90 | }); 91 | 92 | test('writeMap should write a JSON file from a map with special characters', 93 | () async { 94 | final fileSystem = MemoryFileSystem(); 95 | final ioHelper = IOHelper(fileSystem: fileSystem); 96 | 97 | final specialCharMap = { 98 | 'key@1': 'value#1', 99 | 'key\$2': 'value!2', 100 | }; 101 | 102 | final filePath = 'special_chars.json'; 103 | 104 | await ioHelper.writeMap(specialCharMap, filePath); 105 | 106 | final file = fileSystem.file(filePath); 107 | expect(await file.exists(), isTrue); 108 | expect(jsonDecode(await file.readAsString()), equals(specialCharMap)); 109 | }); 110 | 111 | test('writeMap should write a JSON file from a map with different data types', 112 | () async { 113 | final fileSystem = MemoryFileSystem(); 114 | final ioHelper = IOHelper(fileSystem: fileSystem); 115 | 116 | final multiTypeMap = { 117 | 'key1': 'value1', 118 | 'key2': 123, 119 | 'key3': true, 120 | 'key4': [1, 2, 3], 121 | 'key5': {'a': 1, 'b': 2}, 122 | }; 123 | 124 | final filePath = 'multi_types.json'; 125 | await ioHelper.writeMap(multiTypeMap, filePath); 126 | 127 | final file = fileSystem.file(filePath); 128 | expect(await file.exists(), isTrue); 129 | expect(jsonDecode(await file.readAsString()), equals(multiTypeMap)); 130 | }); 131 | 132 | late FileSystem fs; 133 | late IOHelper ioHelper; 134 | 135 | setUp(() { 136 | fs = MemoryFileSystem(); 137 | ioHelper = IOHelper(fileSystem: fs); 138 | }); 139 | 140 | group('IOHelper', () { 141 | test('finds file in the first directory when dir1 is not null', () async { 142 | var dir1 = '/dir1'; 143 | var dir2 = '/dir2'; 144 | var fileName = 'testFile1.txt'; 145 | var file1 = fs.directory(dir1).childFile(fileName); 146 | 147 | fs.directory(dir1).createSync(); 148 | file1.writeAsStringSync('Test content'); 149 | expect((await ioHelper.findFile(dir1, dir2, fileName)).path, file1.path); 150 | }); 151 | 152 | test('finds file in the second directory when dir1 is null', () async { 153 | var dir2 = '/dir2'; 154 | var fileName = 'testFile2.txt'; 155 | var file2 = fs.directory(dir2).childFile(fileName); 156 | 157 | fs.directory(dir2).createSync(); 158 | file2.writeAsStringSync('Test content'); 159 | expect((await ioHelper.findFile(null, dir2, fileName)).path, file2.path); 160 | }); 161 | 162 | test('finds file in the second directory when dir1 does not exist', () async { 163 | var dir1 = '/nonExistentDirectory'; 164 | var dir2 = '/dir2'; 165 | var fileName = 'testFile2.txt'; 166 | var file2 = fs.directory(dir2).childFile(fileName); 167 | 168 | fs.directory(dir2).createSync(); 169 | file2.writeAsStringSync('Test content'); 170 | 171 | expect((await ioHelper.findFile(dir1, dir2, fileName)).path, file2.path); 172 | }); 173 | 174 | test('throws when file is not found in both directories', () async { 175 | var dir1 = '/dir1'; 176 | var dir2 = '/dir2'; 177 | var fileName = 'nonExistentFile.txt'; 178 | 179 | fs.directory(dir1).createSync(); 180 | fs.directory(dir2).createSync(); 181 | 182 | expect(ioHelper.findFile(dir1, dir2, fileName), throwsA(isA())); 183 | }); 184 | }); 185 | 186 | } 187 | 188 | 189 | -------------------------------------------------------------------------------- /test/mocks.dart: -------------------------------------------------------------------------------- 1 | import 'package:mockito/mockito.dart'; 2 | 3 | class MockResponseBody extends Mock { 4 | final Map _data; 5 | 6 | MockResponseBody(this._data); 7 | 8 | dynamic operator [](String key) => _data[key]; 9 | } 10 | 11 | final testProjectConfig = { 12 | "apiKey": "sk-ssss", 13 | "outputDir": "output_dir", 14 | "projectName": "myProject", 15 | "projectVersion": "projectVersion", 16 | "reportDir": "reportDir", 17 | "dataDir": "dataDir" 18 | }; 19 | 20 | final testBlock = {"blockId": "blockId", "pluginName": "pluginName"}; 21 | -------------------------------------------------------------------------------- /test/plugin_server_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:file/memory.dart'; 4 | import 'package:gpt/src/chatgpt/plugin_server.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | void main() { 8 | test( 9 | 'createMockedRequestConfigs transforms list of maps into list of MockedRequestConfig objects', 10 | () { 11 | var mockedRequests = [ 12 | { 13 | 'path': '/time', 14 | 'method': 'get', 15 | 'mockedResponse': 'time.json', 16 | }, 17 | { 18 | 'path': '/events', 19 | 'method': 'get', 20 | 'mockedResponse': 'events.json', 21 | }, 22 | { 23 | 'path': '/image', 24 | 'method': 'get', 25 | 'mockedResponse': '195.png', 26 | 'contentType': 'image/png', 27 | }, 28 | ]; 29 | 30 | final server = PluginServer(MemoryFileSystem()); 31 | 32 | var result = server.createMockedRequestConfigs(mockedRequests); 33 | 34 | expect(result, isA>()); 35 | expect(result.length, 3); 36 | 37 | expect(result[0].path, '/time'); 38 | expect(result[0].method, 'get'); 39 | expect(result[0].mockedResponse, 'time.json'); 40 | expect(result[0].contentType, 'application/json'); 41 | 42 | expect(result[1].path, '/events'); 43 | expect(result[1].method, 'get'); 44 | expect(result[1].mockedResponse, 'events.json'); 45 | expect(result[1].contentType, 'application/json'); 46 | 47 | expect(result[2].path, '/image'); 48 | expect(result[2].method, 'get'); 49 | expect(result[2].mockedResponse, '195.png'); 50 | expect(result[2].contentType, 'image/png'); 51 | }); 52 | 53 | group('PluginServer', () { 54 | late PluginServer pluginServer; 55 | late MemoryFileSystem memoryFileSystem; 56 | 57 | setUp(() { 58 | memoryFileSystem = MemoryFileSystem(); 59 | pluginServer = PluginServer(memoryFileSystem); 60 | // Setup mock files 61 | final defaultConfigDir = memoryFileSystem.directory('defaultConfig') 62 | ..createSync(); 63 | defaultConfigDir 64 | .childFile('plugin-manifest.prompt') 65 | .writeAsStringSync('Prompt content'); 66 | defaultConfigDir 67 | .childFile('ai-plugin.json') 68 | .writeAsStringSync(jsonEncode({ 69 | "api": {"url": "http://localhost/api"}, 70 | "logo_url": "http://localhost/logo.png" 71 | })); 72 | defaultConfigDir.childFile('logo.png').writeAsStringSync('Logo content'); 73 | defaultConfigDir 74 | .childFile('openapi.yaml') 75 | .writeAsStringSync('servers:\n - url: http://localhost'); 76 | }); 77 | 78 | test('setup should initialize properties correctly', () async { 79 | final defaultConfig = {}; 80 | final serverConfig = { 81 | 'properties': {'port': 3000, 'showHttpHeaders': true}, 82 | 'flavor': 'flavor1', 83 | 'serverId': 'server1', 84 | 'mockedRequests': [ 85 | {'path': '/time', 'method': 'get', 'mockedResponse': 'time.json'}, 86 | ], 87 | }; 88 | 89 | await pluginServer.setup(defaultConfig, serverConfig); 90 | 91 | expect(pluginServer.flavorDir, 'pluginFlavors/flavor1'); 92 | expect(pluginServer.defaultDir, 'defaultConfig'); 93 | expect(pluginServer.properties['port'], 3000); 94 | expect(pluginServer.properties['manifestPrompt'], 'Prompt content'); 95 | expect(pluginServer.logoName, 'logo.png'); 96 | expect(pluginServer.port, 3000); 97 | expect(pluginServer.showHttpHeaders, true); 98 | expect(pluginServer.mockedRequestConfigs[0].path, '/time'); 99 | expect(pluginServer.mockedRequestConfigs[0].method, 'get'); 100 | expect(pluginServer.mockedRequestConfigs[0].mockedResponse, 'time.json'); 101 | }); 102 | 103 | test('setup should fallback to default port if no port is specified', 104 | () async { 105 | final defaultConfig = {}; 106 | final serverConfig = { 107 | 'flavor': 'flavor1', 108 | 'serverId': 'server1', 109 | 'configuration': {}, 110 | 'mockedRequests': [], 111 | }; 112 | 113 | await pluginServer.setup(defaultConfig, serverConfig); 114 | 115 | expect(pluginServer.port, 80); 116 | }); 117 | }); 118 | 119 | test('mergeMockedRequestConfigs merges and overrides correctly', () { 120 | final memoryFileSystem = MemoryFileSystem(); 121 | final pluginServer = PluginServer(memoryFileSystem); 122 | var firstList = [ 123 | MockedRequestConfig(path: "/path1", method: "get", mockedResponse: "response1", contentType: "json"), 124 | MockedRequestConfig(path: "/path2", method: "get", mockedResponse: "response2", contentType: "json"), 125 | ]; 126 | var secondList = [ 127 | MockedRequestConfig(path: "/path2", method: "get", mockedResponse: "response2_updated", contentType: "json"), 128 | MockedRequestConfig(path: "/path3", method: "get", mockedResponse: "response3", contentType: "json"), 129 | ]; 130 | 131 | var result = pluginServer.mergeMockedRequestConfigs(firstList, secondList); 132 | 133 | expect(result, [ 134 | MockedRequestConfig(path: "/path1", method: "get", mockedResponse: "response1", contentType: "json"), 135 | MockedRequestConfig(path: "/path2", method: "get", mockedResponse: "response2_updated", contentType: "json"), 136 | MockedRequestConfig(path: "/path3", method: "get", mockedResponse: "response3", contentType: "json"), 137 | ]); 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /test/prompts_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:gpt/src/prompts.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | void main() { 5 | group('addJsonContentToPromptValues', () { 6 | test('should add JSON content to promptValues', () { 7 | final jsonContent = '{"key1": "value1", "key2": "value2"}'; 8 | final Map promptValues = {"key3": "value3"}; 9 | 10 | addJsonContentToPromptValues(jsonContent, promptValues); 11 | 12 | expect(promptValues, { 13 | "key1": "value1", 14 | "key2": "value2", 15 | "key3": "value3", 16 | }); 17 | }); 18 | 19 | test( 20 | 'addJsonContentToPromptValues should throw an exception for malformed JSON', 21 | () { 22 | final malformedJson = '{"key1": "value1", "key2": "value2'; 23 | final promptValues = {}; 24 | expect( 25 | () => addJsonContentToPromptValues(malformedJson, promptValues), 26 | throwsA(isA()), 27 | ); 28 | }); 29 | }); 30 | 31 | group('addPromptValues', () { 32 | test('should add JSON content to promptValues', () { 33 | final content = '{"key1": "value1", "key2": "value2"}'; 34 | final Map promptValues = {"key3": "value3"}; 35 | 36 | addPromptValues(content, promptValues, false); 37 | 38 | expect(promptValues, { 39 | "key1": "value1", 40 | "key2": "value2", 41 | "key3": "value3", 42 | }); 43 | }); 44 | 45 | test('should throw an exception for malformed JSON when fixJson is false', 46 | () { 47 | final content = '{"key1": "value1", "key2": "value2'; 48 | final Map promptValues = {"key3": "value3"}; 49 | 50 | expect(() => addPromptValues(content, promptValues, false), 51 | throwsA(isA())); 52 | }); 53 | 54 | test( 55 | 'should add JSON content to promptValues when fixJson is true and JSON is extractable', 56 | () { 57 | final content = 58 | 'Some text before JSON {"key1": "value1", "key2": "value2"} some text after JSON'; 59 | final Map promptValues = {"key3": "value3"}; 60 | 61 | addPromptValues(content, promptValues, true); 62 | 63 | expect(promptValues, { 64 | "key1": "value1", 65 | "key2": "value2", 66 | "key3": "value3", 67 | }); 68 | }); 69 | 70 | test( 71 | 'should throw an exception for malformed JSON when fixJson is true and JSON is not extractable', 72 | () { 73 | final content = '{"key1": "value1", "key2": "value2'; 74 | final Map promptValues = {"key3": "value3"}; 75 | 76 | expect(() => addPromptValues(content, promptValues, true), 77 | throwsA(isA())); 78 | }); 79 | 80 | test('should handle empty JSON content', () { 81 | final content = '{}'; 82 | final Map promptValues = {"key1": "value1"}; 83 | 84 | addPromptValues(content, promptValues, false); 85 | 86 | expect(promptValues, {"key1": "value1"}); 87 | }); 88 | 89 | test('should handle JSON content with only whitespace', () { 90 | final content = '{ }'; 91 | final Map promptValues = {"key1": "value1"}; 92 | 93 | addPromptValues(content, promptValues, false); 94 | 95 | expect(promptValues, {"key1": "value1"}); 96 | }); 97 | 98 | test('should handle JSON content with non-string values', () { 99 | final content = 100 | '{"key1": 42, "key2": true, "key3": [1, 2, 3], "key4": {"nestedKey": "nestedValue"}}'; 101 | final Map promptValues = {"key5": "value5"}; 102 | 103 | addPromptValues(content, promptValues, false); 104 | 105 | expect(promptValues, { 106 | "key1": 42, 107 | "key2": true, 108 | "key3": [1, 2, 3], 109 | "key4": {"nestedKey": "nestedValue"}, 110 | "key5": "value5", 111 | }); 112 | }); 113 | 114 | test('should handle JSON content with duplicate keys', () { 115 | final content = '{"key1": "newValue1", "key2": "value2"}'; 116 | final Map promptValues = {"key1": "value1"}; 117 | 118 | addPromptValues(content, promptValues, false); 119 | 120 | expect(promptValues, { 121 | "key1": "newValue1", 122 | "key2": "value2", 123 | }); 124 | }); 125 | }); 126 | 127 | group('extractJson', () { 128 | test('should return JSON string when input contains a JSON object', () { 129 | final content = 'Some text before {"key": "value"} some text after'; 130 | final result = extractJson(content); 131 | expect(result, '{"key": "value"}'); 132 | }); 133 | 134 | test('should return null when input does not contain a JSON object', () { 135 | final content = 'Some text without any JSON object'; 136 | final result = extractJson(content); 137 | expect(result, isNull); 138 | }); 139 | 140 | test( 141 | 'should return the first JSON object when input contains multiple JSON objects', 142 | () { 143 | final content = 144 | 'Some text before {"key1": "value1"} some text in between {"key2": "value2"} some text after'; 145 | final result = extractJson(content); 146 | expect(result, '{"key1": "value1"}'); 147 | }); 148 | 149 | test('should return JSON object with nested JSON object', () { 150 | final content = 151 | 'Some text before {"key": {"nestedKey": "nestedValue"}} some text after'; 152 | final result = extractJson(content); 153 | expect(result, '{"key": {"nestedKey": "nestedValue"}}'); 154 | }); 155 | }); 156 | 157 | group('createPromptByIndex', () { 158 | test('should replace placeholders with values at specified index', () { 159 | final template = '\${key1} is friends with \${key2}.'; 160 | final templateProperties = { 161 | 'key1': ['Alice', 'Bob'], 162 | 'key2': ['Charlie', 'David'] 163 | }; 164 | 165 | final result = createPromptByIndex(template, templateProperties, 0); 166 | expect(result, 'Alice is friends with Charlie.'); 167 | }); 168 | 169 | test('should throw an error if the index is out of range', () { 170 | final template = 'Hello \${name}'; 171 | final templateProperties = { 172 | 'name': ['Alice', 'Bob'] 173 | }; 174 | final index = 2; 175 | 176 | expect(() => createPromptByIndex(template, templateProperties, index), 177 | throwsA(isA())); 178 | }); 179 | 180 | test('should return template unchanged if no placeholders found', () { 181 | final template = 'No placeholders in this template.'; 182 | final templateProperties = { 183 | 'key1': ['Alice', 'Bob'], 184 | 'key2': ['Charlie', 'David'] 185 | }; 186 | 187 | final result = createPromptByIndex(template, templateProperties, 0); 188 | expect(result, 'No placeholders in this template.'); 189 | }); 190 | }); 191 | 192 | group('substituteTemplateProperties', () { 193 | test('should replace placeholders with their corresponding values', () { 194 | final template = 'Hello \${name}, welcome to \${location}'; 195 | final templateProperties = {'name': 'Alice', 'location': 'Wonderland'}; 196 | 197 | final result = substituteTemplateProperties(template, templateProperties); 198 | 199 | expect(result, 'Hello Alice, welcome to Wonderland'); 200 | }); 201 | 202 | test('should replace unknown placeholders with an empty value', () { 203 | final template = 'Hello \${name}, welcome to \${location}'; 204 | final templateProperties = {'name': 'Alice'}; 205 | 206 | final result = substituteTemplateProperties(template, templateProperties); 207 | 208 | expect(result, 'Hello Alice, welcome to '); 209 | }); 210 | 211 | test('should return the original template if there are no placeholders', 212 | () { 213 | final template = 'Hello Alice, welcome to Wonderland'; 214 | final templateProperties = {}; 215 | 216 | final result = substituteTemplateProperties(template, templateProperties); 217 | 218 | expect(result, template); 219 | }); 220 | 221 | test( 222 | 'should replace placeholders with empty strings if properties are not in templateProperties', 223 | () { 224 | final template = 'Hello \${name}, welcome to \${location}'; 225 | final templateProperties = {'age': 30}; 226 | 227 | final result = substituteTemplateProperties(template, templateProperties); 228 | 229 | expect(result, 'Hello , welcome to '); 230 | }); 231 | }); 232 | } 233 | -------------------------------------------------------------------------------- /test/reporter_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:file/file.dart'; 4 | import 'package:file/memory.dart'; 5 | import 'package:gpt/src/io_helper.dart'; 6 | import 'package:gpt/src/reporter.dart'; 7 | import 'package:test/test.dart'; 8 | 9 | void main() { 10 | test( 11 | 'logRequestAndResponse should log request and response files in the specified directory', 12 | () async { 13 | final memoryFileSystem = MemoryFileSystem(); 14 | final ioHelper = IOHelper(fileSystem: memoryFileSystem); 15 | final reporter = ConcreteReporter(ioHelper); 16 | 17 | final requestBody = 'This is a sample request body.'; 18 | final responseBody = { 19 | 'id': '123', 20 | 'content': 'This is a sample response body.' 21 | }; 22 | final toDirectory = 'logs'; 23 | await reporter.logRequestAndResponse( 24 | requestBody, responseBody, toDirectory); 25 | 26 | final directory = memoryFileSystem.directory(toDirectory); 27 | expect(await directory.exists(), isTrue); 28 | 29 | final files = await directory.list().toList(); 30 | expect(files.length, equals(2)); 31 | 32 | final outputRequestFile = 33 | files.firstWhere((file) => file.path.contains('-request.json')) as File; 34 | final outputResponseFile = files 35 | .firstWhere((file) => file.path.contains('-response.json')) as File; 36 | 37 | final requestContent = await outputRequestFile.readAsString(); 38 | final responseContent = jsonDecode(await outputResponseFile.readAsString()); 39 | 40 | expect(requestContent, equals(requestBody)); 41 | expect(responseContent, equals(responseBody)); 42 | }); 43 | 44 | test('logFailedRequest should log requestBody with content', () async { 45 | final fileSystem = MemoryFileSystem(); 46 | final reporter = ConcreteReporter(IOHelper(fileSystem: fileSystem)); 47 | 48 | final requestBody = 'This is a sample failed request body.'; 49 | final toDirectory = '/failed_requests'; 50 | 51 | await reporter.logFailedRequest(requestBody, toDirectory); 52 | 53 | final dir = fileSystem.directory(toDirectory); 54 | expect(dir.existsSync(), isTrue); 55 | 56 | final files = dir.listSync(); 57 | expect(files.length, equals(1)); 58 | expect(files.first.path.contains('-failed-request.json'), isTrue); 59 | 60 | final content = await (files.first as File).readAsString(); 61 | expect(content, equals(requestBody)); 62 | }); 63 | 64 | test('logFailedRequest should log empty requestBody', () async { 65 | final fileSystem = MemoryFileSystem(); 66 | final reporter = ConcreteReporter(IOHelper(fileSystem: fileSystem)); 67 | 68 | final requestBody = ''; 69 | final toDirectory = '/empty_failed_requests'; 70 | 71 | await reporter.logFailedRequest(requestBody, toDirectory); 72 | 73 | final dir = fileSystem.directory(toDirectory); 74 | expect(dir.existsSync(), isTrue); 75 | 76 | final files = dir.listSync(); 77 | expect(files.length, equals(1)); 78 | expect(files.first.path.contains('-failed-request.json'), isTrue); 79 | 80 | final content = await (files.first as File).readAsString(); 81 | expect(content, equals(requestBody)); 82 | }); 83 | 84 | group('writeProjectReport', () { 85 | late MemoryFileSystem memoryFileSystem; 86 | late IOHelper fileSystem; 87 | late ConcreteReporter reporter; 88 | late Map results; 89 | 90 | setUp(() { 91 | memoryFileSystem = MemoryFileSystem(); 92 | fileSystem = IOHelper(fileSystem: memoryFileSystem); 93 | reporter = ConcreteReporter(fileSystem); 94 | 95 | results = { 96 | "projectName": "TestProject", 97 | "projectVersion": "1.0.0", 98 | "blockId": "001", 99 | "data": "Sample data", 100 | }; 101 | }); 102 | 103 | test('should create and write report to file', () async { 104 | await reporter.writeProjectReport(results, '/reports'); 105 | 106 | final reportFile = 107 | memoryFileSystem.file('/reports/TestProject-1.0.0-001-report.json'); 108 | expect(reportFile.existsSync(), true); 109 | 110 | final fileContent = await reportFile.readAsString(); 111 | expect(fileContent, 112 | '{"projectName":"TestProject","projectVersion":"1.0.0","blockId":"001","data":"Sample data"}'); 113 | }); 114 | 115 | test('should create nested report directories if not exist', () async { 116 | await reporter.writeProjectReport(results, '/nested/reports'); 117 | 118 | final reportFile = memoryFileSystem 119 | .file('/nested/reports/TestProject-1.0.0-001-report.json'); 120 | expect(reportFile.existsSync(), true); 121 | }); 122 | 123 | test('should throw an exception if there is an error while writing', 124 | () async { 125 | memoryFileSystem 126 | .file('/reports') 127 | .createSync(); // Creating a file instead of a directory 128 | expect( 129 | () async => await reporter.writeProjectReport(results, '/reports'), 130 | throwsA(isA()), 131 | ); 132 | }); 133 | }); 134 | 135 | group('writeMetrics', () { 136 | late MemoryFileSystem memoryFileSystem; 137 | late IOHelper ioHelper; 138 | late ConcreteReporter reporter; 139 | late Map responseBody; 140 | 141 | setUp(() { 142 | memoryFileSystem = MemoryFileSystem(); 143 | ioHelper = IOHelper(fileSystem: memoryFileSystem); 144 | reporter = ConcreteReporter(ioHelper); 145 | 146 | responseBody = { 147 | "id": "123", 148 | "requestTime": "2023-05-05T00:00:00", 149 | "usage": { 150 | "prompt_tokens": 10, 151 | "completion_tokens": 20, 152 | "total_tokens": 30, 153 | }, 154 | }; 155 | }); 156 | 157 | test('should create and write metrics to file', () async { 158 | await reporter.writeMetrics(responseBody, '001', 'test', '/metrics.csv'); 159 | 160 | final metricsFile = memoryFileSystem.file('/metrics.csv'); 161 | expect(metricsFile.existsSync(), true); 162 | 163 | final fileContent = await metricsFile.readAsString(); 164 | expect(fileContent, 165 | 'request_id, executionId, tag, request_time, prompt_tokens, completion_tokens, total_tokens\n123, 001, test, 2023-05-05T00:00:00, 10, 20, 30\n'); 166 | }); 167 | 168 | test('writeMetrics should append metrics to an existing file', () async { 169 | await reporter.writeMetrics(responseBody, '001', 'test', '/metrics.csv'); 170 | 171 | responseBody['id'] = '456'; 172 | responseBody['usage']['prompt_tokens'] = 15; 173 | responseBody['usage']['completion_tokens'] = 25; 174 | responseBody['usage']['total_tokens'] = 40; 175 | await reporter.writeMetrics(responseBody, '002', 'test2', '/metrics.csv'); 176 | 177 | final content = 178 | await memoryFileSystem.file('/metrics.csv').readAsString(); 179 | final lines = content.split('\n'); 180 | expect(lines.length, 4); // 2 data lines + header + empty line 181 | expect( 182 | lines[2], 183 | '456, 002, test2, 2023-05-05T00:00:00, 15, 25, 40', 184 | ); 185 | }); 186 | }); 187 | } 188 | --------------------------------------------------------------------------------