├── .gitignore ├── LICENSE ├── README.md ├── app ├── __init__.py ├── add.py ├── db.py ├── db_wrappers.py ├── exporting.py ├── home.py ├── projects.py ├── schema.sql ├── static │ ├── add.css │ ├── manifest.css │ ├── projects.css │ ├── style.css │ ├── style_viewer.css │ ├── table.css │ └── view.css ├── style.py ├── tasks.py ├── templates │ ├── add.html │ ├── base.html │ ├── export.html │ ├── exports.html │ ├── home.html │ ├── manifest.html │ ├── project.html │ ├── projects.html │ ├── style.html │ ├── tasks.html │ └── view.html ├── utils.py └── view.py ├── requirements.txt ├── sample-datasets └── alpaca.json └── screenshots ├── praetor-completion-screen.png ├── praetor-prompts-screen.png └── praetor-style-screen.png /.gitignore: -------------------------------------------------------------------------------- 1 | venv 2 | instance 3 | app/__pycache__ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Praetor 2 | 3 | Praetor is a dataset exploration tool. It is made for LLM finetuning data and prompts. 4 | 5 | ## Screenshots 6 | 7 | ![The Praetor prompt search screen](screenshots/praetor-prompts-screen.png) 8 | 9 | --- 10 | 11 | ![The Praetor style viewer](screenshots/praetor-style-screen.png) 12 | 13 | ## Why 14 | 15 | I got sick of scanning through large json files to try and get a feeling for a dataset. Then when I wanted to remove some data, I was using control F - this is obviously dumb. Praetor is a flask app that lets you mix, edit, view, and export datasets for LLMs. To keep it simple, the frontend is very straightforward. It uses an sqlite database and jinja templating under the hood. You can import and export datasets in a json format, so it is very easy to use Praetor along with other tools and scripts you already have in place. 16 | 17 | ## About 18 | 19 | Praetor is a super lightweight finetuning data and prompt management tool. The setup requirements are minimal, and so is the complexity. 20 | 21 | In general, the system works like this: you start with a Project. A project has associated with it some prompt styles, which define format strings for the prompts. You can then add prompts in a particular style and then one or more completions to those prompts. When you're done editing prompts or completions, you can export that data in a json format. You can also import data in the same format. 22 | 23 | Thus the hierarchy goes: 24 | 25 | ``` 26 | Projects --> Styles --> Prompts --> Completions 27 | ``` 28 | 29 | ## Usage 30 | 31 | First, it is recommended that you use a virtual environment if you do not already have one set up for your project. You'll need to install the requirements in any case (it's pretty much just Flask!): 32 | 33 | ``` 34 | python3 -m venv venv 35 | source venv/bin/activate 36 | pip install -r requirements.txt 37 | ``` 38 | 39 | Then you will need to initialize the database using 40 | 41 | ``` 42 | flask init-db 43 | ``` 44 | 45 | And then run the server: 46 | 47 | ``` 48 | flask run 49 | ``` 50 | 51 | ## License 52 | 53 | This software is distributed under the Apache 2.0 open source license. 54 | 55 | ## Contributing 56 | 57 | Everyone should feel free to open an issue if you have any comments, questions, or suggestions. 58 | 59 | Pull requests should be submitted to the development branch. Please do not make additions to the requirements unless they are necessary. Frontend frameworks are not necessary. The frontend should be as simple and functional as possible. 60 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | 5 | 6 | def create_app(test_config=None): 7 | # create and configure the app 8 | app = Flask(__name__, instance_relative_config=True) 9 | app.config.from_mapping( 10 | SECRET_KEY='dev', 11 | DATABASE=os.path.join(app.instance_path, 'app.sqlite'), 12 | EXPORTS_PATH=os.path.join(app.instance_path, 'exports') 13 | ) 14 | 15 | if test_config is None: 16 | # load the instance config, if it exists, when not testing 17 | app.config.from_pyfile('config.py', silent=True) 18 | else: 19 | # load the test config if passed in 20 | app.config.from_mapping(test_config) 21 | 22 | # ensure the instance folder exists 23 | try: 24 | os.makedirs(app.instance_path) 25 | except OSError: 26 | pass 27 | try: 28 | os.makedirs(os.path.join(app.instance_path, 'exports')) 29 | except OSError: 30 | pass 31 | 32 | from . import view 33 | app.register_blueprint(view.bp) 34 | 35 | from . import home 36 | app.register_blueprint(home.bp) 37 | 38 | from . import add 39 | app.register_blueprint(add.bp) 40 | 41 | from . import tasks 42 | app.register_blueprint(tasks.bp) 43 | 44 | from . import exporting 45 | app.register_blueprint(exporting.bp) 46 | 47 | from . import projects 48 | app.register_blueprint(projects.bp) 49 | 50 | from . import style 51 | app.register_blueprint(style.bp) 52 | 53 | # connect database 54 | from . import db 55 | db.init_app(app) 56 | 57 | return app 58 | -------------------------------------------------------------------------------- /app/add.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | request, 5 | redirect 6 | ) 7 | from app.db import get_db 8 | from app.db_wrappers import add_prompt, add_bulk, get_project_by_id, get_style_by_id, get_styles_by_project_id, get_keys_by_style_id, check_running 9 | from app.utils import tag_string_to_list 10 | import json 11 | 12 | bp = Blueprint('add', __name__) 13 | 14 | @bp.route('/add', methods=('GET', 'POST')) 15 | def add(): 16 | 17 | db = get_db() 18 | 19 | if request.method == "POST" and not check_running(db): 20 | 21 | project_id = request.form.get('project_id') 22 | style_id = request.form.get('style_id') 23 | 24 | # Was it a bulk upload? 25 | file = request.files.get('file') 26 | if file: 27 | file_contents = file.read() 28 | # Read as json 29 | # btw this is probably a security vulnerability 30 | # Probably should check for filesize as well 31 | data = json.loads(file_contents) 32 | tags = request.form.get("tags") 33 | tags = tag_string_to_list(tags) 34 | res = add_bulk(db, data, tags, project_id, style_id) 35 | return redirect("/tasks") 36 | else: 37 | style_keys = get_keys_by_style_id(db, style_id) 38 | style = get_style_by_id(db, style_id) 39 | 40 | # don't need completion key for style_keys 41 | if 'completion_key' in style: 42 | completion_key = style['completion_key'] 43 | style_keys = [x for x in style_keys if x['name'] != completion_key] 44 | 45 | keys = {x['name']: request.form.get("key." + x['name']) for x in style_keys} 46 | tags = request.form.get("tags") 47 | if tags: 48 | tags = tag_string_to_list(tags) 49 | else: 50 | tags = [] 51 | id = add_prompt(db, keys=keys, tags=tags, project_id=project_id, style_id=style_id) 52 | return redirect(f"/view?prompt_id={id}") 53 | else: 54 | project_id = request.args.get('project_id') 55 | style_id = request.args.get('style_id') 56 | 57 | error = None 58 | if request.method == 'POST' and check_running(db): 59 | project_id = request.form.get('project_id') 60 | style_id = request.form.get('style_id') 61 | error = "Cannot add new data while current task is incomplete." 62 | 63 | style = get_style_by_id(db, style_id) 64 | project = get_project_by_id(db, project_id) 65 | style_keys = get_keys_by_style_id(db, style_id) 66 | # don't need completion key for style_keys 67 | if 'completion_key' in style: 68 | completion_key = style['completion_key'] 69 | style_keys = [x for x in style_keys if x['name'] != completion_key] 70 | 71 | return render_template('add.html', style=style, project=project, style_keys=style_keys, error=error) -------------------------------------------------------------------------------- /app/db.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | import json 3 | import click 4 | from flask import current_app, g 5 | import os 6 | import shutil 7 | 8 | 9 | def dict_factory(cursor, row): 10 | """ 11 | Converts each row to a dictionary 12 | """ 13 | d = {} 14 | for idx, col in enumerate(cursor.description): 15 | d[col[0]] = row[idx] 16 | return d 17 | 18 | def get_db(): 19 | if 'db' not in g: 20 | 21 | need_to_init = not os.path.exists(current_app.config['DATABASE']) 22 | 23 | g.db = sqlite3.connect( 24 | current_app.config['DATABASE'], 25 | detect_types=sqlite3.PARSE_DECLTYPES 26 | ) 27 | g.db.row_factory = dict_factory 28 | 29 | if need_to_init: 30 | with current_app.open_resource('schema.sql') as f: 31 | g.db.executescript(f.read().decode('utf8')) 32 | 33 | return g.db 34 | 35 | 36 | def get_tmp_db(instance_path, old_db_path): 37 | 38 | # TODO: make unique for each process? 39 | new_db_path = os.path.join(instance_path, "tmp.sqlite") 40 | shutil.copyfile(old_db_path, new_db_path) 41 | 42 | db = sqlite3.connect( 43 | new_db_path, 44 | detect_types=sqlite3.PARSE_DECLTYPES 45 | ) 46 | db.row_factory = dict_factory 47 | 48 | return db, new_db_path 49 | 50 | 51 | def close_db(e=None): 52 | db = g.pop('db', None) 53 | 54 | if db is not None: 55 | db.close() 56 | 57 | def init_db(): 58 | db = get_db() 59 | 60 | with current_app.open_resource('schema.sql') as f: 61 | db.executescript(f.read().decode('utf8')) 62 | 63 | 64 | @click.command('init-db') 65 | def init_db_command(): 66 | """Clear the existing data and create new tables.""" 67 | init_db() 68 | click.echo('Initialized the database.') 69 | 70 | def init_app(app): 71 | app.teardown_appcontext(close_db) 72 | app.cli.add_command(init_db_command) 73 | 74 | 75 | class SQLiteJSONEncoder(json.JSONEncoder): 76 | def default(self, obj): 77 | if isinstance(obj, sqlite3.Row): 78 | return dict(obj) 79 | return json.JSONEncoder.default(self, obj) -------------------------------------------------------------------------------- /app/db_wrappers.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import json 3 | import os 4 | from unicodedata import name 5 | from app.db import SQLiteJSONEncoder 6 | from flask import current_app, session 7 | from app.utils import get_named_arguments 8 | from app.db import get_tmp_db 9 | import shutil 10 | import psutil 11 | 12 | 13 | """ 14 | 15 | All functions that depend on the peculiarities of the database 16 | 17 | """ 18 | 19 | # EXAMPLES 20 | 21 | def add_example(db, prompt_id, completion, tags): 22 | c = db.cursor() 23 | c.execute("INSERT INTO examples (completion, prompt_id) VALUES (?, ?)", (completion, prompt_id)) 24 | item_id = c.lastrowid 25 | 26 | # Add tags 27 | for tag in tags: 28 | c.execute("INSERT INTO tags (example_id, value) VALUES (?, ?)", (item_id, tag)) 29 | db.commit() 30 | return item_id 31 | 32 | 33 | def delete_example(db, example_id): 34 | db.execute("DELETE FROM examples WHERE id = ?", (example_id,)) 35 | db.commit() 36 | 37 | 38 | def update_example(db, example_id, completion, tags): 39 | c = db.cursor() 40 | 41 | # Remove all tags 42 | c.execute("DELETE FROM tags WHERE example_id = ?", (example_id,)) 43 | # Update text 44 | c.execute("UPDATE examples SET completion = ? WHERE id = ?", (completion, example_id)) 45 | for t in tags: 46 | c.execute("INSERT INTO tags (example_id, value) VALUES (?, ?)", (example_id, t)) 47 | db.commit() 48 | return example_id 49 | 50 | 51 | # PROMPTS 52 | 53 | def delete_prompt(db, prompt_id): 54 | 55 | # Remove all tags 56 | # Remove all prompt_values 57 | # remove associated examples 58 | 59 | db.execute("DELETE FROM prompts WHERE id = ?", (prompt_id,)) 60 | db.execute("DELETE FROM tags WHERE prompt_id = ?", (prompt_id,)) 61 | db.execute("DELETE FROM prompt_values WHERE prompt_id = ?", (prompt_id,)) 62 | db.execute("DELETE FROM examples WHERE prompt_id = ?", (prompt_id,)) 63 | db.commit() 64 | 65 | def update_prompt(db, prompt_id, prompt_values, tags): 66 | c = db.cursor() 67 | 68 | # Update prompt values 69 | for key in prompt_values: 70 | c.execute("SELECT * FROM prompt_values WHERE prompt_id = ? AND key = ?", (prompt_id, key)) 71 | res = c.fetchone() 72 | if res: 73 | sql = """ 74 | UPDATE prompt_values SET value = ? WHERE prompt_id = ? AND key = ? 75 | """ 76 | c.execute(sql, (prompt_values[key], prompt_id, key)) 77 | else: 78 | sql = """ 79 | INSERT INTO prompt_values (value, prompt_id, key) VALUES (?, ?, ?) 80 | """ 81 | c.execute(sql, (prompt_values[key], prompt_id, key)) 82 | # Update tags 83 | # Remove all prior tags 84 | sql = """ 85 | DELETE FROM tags 86 | WHERE prompt_id = ? 87 | """ 88 | c.execute(sql, (prompt_id,)) 89 | for tag in tags: 90 | c.execute("INSERT INTO tags (value, prompt_id) VALUES (?, ?)", (tag, prompt_id)) 91 | 92 | db.commit() 93 | return prompt_id 94 | 95 | # Adds prompt to database and returns that prompt's id 96 | def add_prompt(db, **kwargs): 97 | 98 | tags = kwargs['tags'] 99 | keys = kwargs['keys'] 100 | project_id = kwargs['project_id'] 101 | style_id = kwargs['style_id'] 102 | 103 | c = db.cursor() 104 | 105 | c.execute("INSERT INTO prompts (project_id, style) VALUES (?, ?)", (project_id, style_id)) 106 | prompt_id = c.lastrowid 107 | 108 | for t in tags: 109 | c.execute("INSERT INTO tags (value, prompt_id) VALUES (?, ?)", (t, prompt_id)) 110 | 111 | for k in keys: 112 | c.execute("INSERT INTO prompt_values (prompt_id, key, value) VALUES (?, ?, ?)", (prompt_id, k, keys[k])) 113 | 114 | db.commit() 115 | return prompt_id 116 | 117 | # Takes data and inserts prompts and examples 118 | # Data is a list of dictionaries (i.e. json data) 119 | # tags is a list 120 | def add_bulk(db, data, tags, project_id, style_id): 121 | c = db.cursor() 122 | sql = """ 123 | INSERT INTO tasks (`type`, `status`) 124 | VALUES ("bulk_upload", 125 | "in_progress" 126 | ); 127 | """ 128 | c.execute(sql) 129 | task_id = c.lastrowid 130 | db.commit() 131 | 132 | p = multiprocessing.Process(target=add_bulk_background, args=(current_app.instance_path, current_app.config['DATABASE'], task_id, data, tags, project_id, style_id)) 133 | p.start() 134 | 135 | sql = """ 136 | UPDATE tasks 137 | SET pid = ? 138 | WHERE id = ? 139 | """ 140 | db.execute(sql, (p.pid, task_id)) 141 | db.commit() 142 | 143 | # Make session token to warn user about parallelism 144 | session['warn_parallelism'] = True 145 | 146 | status = { 147 | 'pid': p.pid, 148 | 'status': 'in_progress' 149 | } 150 | return status 151 | 152 | # NOTE: doesn't support example tags yet or multiple examples per prompt 153 | def add_bulk_background(instance_path, old_db_path, task_id, data, tags, project_id, style_id): 154 | db, new_db_path = get_tmp_db(instance_path, old_db_path) 155 | 156 | try: 157 | c = db.cursor() 158 | 159 | # Get the correct keys and note which is the completion key 160 | sql = """ 161 | SELECT * FROM style_keys 162 | WHERE style_id = ? 163 | """ 164 | res = c.execute(sql, (style_id,)) 165 | style_keys = res.fetchall() 166 | 167 | # Get the style 168 | sql = """ 169 | SELECT * FROM styles 170 | WHERE id = ? 171 | """ 172 | res = c.execute(sql, (style_id)) 173 | style_info = res.fetchone() 174 | 175 | completion_key = style_info['completion_key'] 176 | 177 | prompt_values_keys = [x['name'] for x in style_keys if x['name'] != completion_key] 178 | db.commit() 179 | 180 | for item in data: 181 | 182 | # Add new prompt 183 | sql = """ 184 | INSERT INTO prompts (style, project_id) VALUES (?, ?) 185 | """ 186 | c.execute(sql, (style_id, project_id)) 187 | prompt_id = c.lastrowid 188 | 189 | # Add examples and prompt values 190 | for key in item: 191 | if key == completion_key: 192 | sql = """ 193 | INSERT INTO examples (prompt_id, completion) VALUES (?, ?) 194 | """ 195 | c.execute(sql, (prompt_id, item[key])) 196 | elif key in prompt_values_keys: # need to avoid tags, other irrelevant values 197 | sql = """ 198 | INSERT INTO prompt_values (prompt_id, key, value) VALUES (?, ?, ?) 199 | """ 200 | c.execute(sql, (prompt_id, key, item[key])) 201 | 202 | # Add tags to prompt 203 | for tag in tags: 204 | sql = """ 205 | INSERT INTO tags (prompt_id, value) VALUES (?, ?) 206 | """ 207 | c.execute(sql, (prompt_id, tag)) 208 | 209 | db.commit() 210 | 211 | sql = """ 212 | UPDATE tasks 213 | SET status = 'completed' 214 | WHERE id = ? 215 | """ 216 | db.execute(sql, (task_id,)) 217 | db.commit() 218 | except: 219 | sql = """ 220 | UPDATE tasks 221 | SET status = 'failed' 222 | WHERE id = ?; 223 | """ 224 | db.execute(sql, (task_id,)) 225 | db.commit() 226 | 227 | shutil.copyfile(new_db_path, old_db_path) 228 | 229 | """ 230 | 231 | Will export a json file, which is a list of dictionaries with each key matching 232 | a named argument in the template format string. 233 | All named arguments are present in every dictionary. 234 | Does not include tags at the moment. 235 | 236 | If filename is None or "", the filename becomes "export.json" 237 | 238 | """ 239 | def export(db, filename, tags=[], content="", example="", project_id=None, style_id=None): 240 | 241 | if not filename: 242 | filename = "export.json" 243 | 244 | c = db.cursor() 245 | sql = """ 246 | INSERT INTO tasks (`type`, `status`) 247 | VALUES ("export", 248 | "in_progress" 249 | ); 250 | """ 251 | c.execute(sql) 252 | db.commit() 253 | 254 | task_id = c.lastrowid 255 | 256 | p = multiprocessing.Process(target=export_background, args=(current_app.instance_path, current_app.config['DATABASE'], current_app.config['EXPORTS_PATH'], task_id, filename, content, tags, example, style_id, project_id)) 257 | p.start() 258 | 259 | sql = """ 260 | UPDATE tasks SET `pid` = ? 261 | WHERE id = ? 262 | """ 263 | c.execute(sql, (p.pid, task_id)) 264 | 265 | # Make session token to warn user about parallelism 266 | session['warn_parallelism'] = True 267 | 268 | db.commit() 269 | status = { 270 | 'pid': p.pid, 271 | 'status': 'in_progress' 272 | } 273 | return status 274 | 275 | def export_background(instance_path, old_db_path, exports_path, task_id, filename, content, tags, example, style_id, project_id): 276 | 277 | db, new_db_path = get_tmp_db(instance_path, old_db_path) 278 | 279 | # The try catch is not compehensive 280 | # There should be an option for the user to check on the program itself (via its pid) 281 | try: 282 | if not example: 283 | example = "%" 284 | if not content: 285 | content = "%" 286 | 287 | args = [example] 288 | 289 | tag_query_str = "" 290 | if tags and len(tags) > 0: 291 | tag_query_str = f"JOIN tags ON prompts.id = tags.prompt_id AND (" 292 | for i, tag in enumerate(tags): 293 | tag_query_str += f"tags.value LIKE ?" 294 | args.append(tag) 295 | if i < len(tags) - 1: 296 | tag_query_str += " OR " 297 | tag_query_str += ")" 298 | 299 | args.append(content) 300 | 301 | proj_id_query = "" 302 | if project_id: 303 | proj_id_query = "AND prompts.project_id = ?" 304 | args.append(project_id) 305 | 306 | style_id_query = "" 307 | if style_id: 308 | style_id_query = "AND prompts.style = ?" 309 | args.append(style_id) 310 | 311 | # notice that in string, there is no option to put "LEFT" join on examples 312 | # this is beacuse we really do only want prompts with examples in this case 313 | sql = f""" 314 | SELECT DISTINCT prompts.*, styles.template as template, styles.completion_key as completion_key 315 | FROM prompts 316 | JOIN prompt_values ON prompts.id = prompt_values.prompt_id 317 | JOIN styles ON prompts.style = styles.id AND prompt_values.key = styles.preview_key 318 | JOIN examples ON prompts.id = examples.prompt_id AND examples.completion LIKE ? 319 | {tag_query_str} 320 | WHERE prompt_values.value LIKE ? 321 | {proj_id_query} 322 | {style_id_query} 323 | """ 324 | c = db.cursor() 325 | res = c.execute(sql, tuple(args)) 326 | prompts = res.fetchall() 327 | 328 | path = os.path.join(exports_path, filename) 329 | fhand = open(path, 'w') 330 | fhand.write('[\n') 331 | 332 | encoder = SQLiteJSONEncoder(indent=4) 333 | 334 | for k, prompt in enumerate(prompts): 335 | sql = """ 336 | SELECT * FROM examples 337 | WHERE prompt_id = ? 338 | """ 339 | examples = c.execute(sql, (prompt['id'],)).fetchall() 340 | 341 | sql = """ 342 | SELECT * FROM prompt_values 343 | WHERE prompt_id = ? 344 | """ 345 | prompt_values = c.execute(sql, (prompt['id'],)).fetchall() 346 | prompt_value_kwargs = {x['key']: x['value'] for x in prompt_values} 347 | 348 | template = prompt['template'] 349 | named_args = get_named_arguments(template) 350 | 351 | kwargs = {} 352 | for arg in named_args: 353 | if arg in prompt_value_kwargs: 354 | kwargs[arg] = prompt_value_kwargs[arg] 355 | elif arg != prompt['completion_key']: 356 | kwargs[arg] = "" 357 | 358 | for i, ex in enumerate(examples): 359 | kwargs[prompt['completion_key']] = ex['completion'] 360 | 361 | json_data = encoder.encode(kwargs) 362 | 363 | fhand.write(json_data) 364 | 365 | if i < len(examples) - 1: 366 | fhand.write(",\n") 367 | 368 | if k < len(prompts) - 1: 369 | fhand.write(",\n") 370 | 371 | fhand.write(']') 372 | fhand.close() 373 | 374 | sql = """ 375 | INSERT INTO exports (`filename`) 376 | VALUES (?); 377 | """ 378 | db.execute(sql, (filename,)) 379 | 380 | sql = """ 381 | UPDATE tasks 382 | SET status = 'completed' 383 | WHERE id = ?; 384 | """ 385 | db.execute(sql, (task_id,)) 386 | db.commit() 387 | except Exception as e: 388 | sql = """ 389 | UPDATE tasks 390 | SET status = 'failed' 391 | WHERE pid = ?; 392 | """ 393 | db.execute(sql, (os.getpid(),)) 394 | db.commit() 395 | print(f"Error occurred: {e}") 396 | 397 | shutil.copyfile(new_db_path, old_db_path) 398 | 399 | def search_prompts(db, limit=None, offset=None, content_arg=None, example_arg=None, tags_arg=None, project_id=None, style_id=None): 400 | 401 | offset = 0 if not offset else offset 402 | limit = 100 if not limit else limit 403 | content_arg = "%" if not content_arg else "%" + content_arg + "%" 404 | 405 | x = "" if example_arg else "LEFT" 406 | example_arg = "%" if not example_arg else "%" + example_arg + "%" 407 | 408 | args = [example_arg] 409 | 410 | tag_query_str = "" 411 | if tags_arg and len(tags_arg) > 0: 412 | tag_query_str = f"JOIN tags ON prompts.id = tags.prompt_id AND (" 413 | for i, tag in enumerate(tags_arg): 414 | tag_query_str += f"tags.value LIKE ?" 415 | args.append(tag) 416 | if i < len(tags_arg) - 1: 417 | tag_query_str += " OR " 418 | tag_query_str += ")" 419 | 420 | args.extend([content_arg]) 421 | 422 | proj_id_query = "" 423 | if project_id: 424 | proj_id_query = "AND prompts.project_id = ?" 425 | args.append(project_id) 426 | 427 | style_id_query = "" 428 | if style_id: 429 | style_id_query = "AND prompts.style = ?" 430 | args.append(style_id) 431 | 432 | args.extend([limit, offset]) 433 | 434 | sql = f""" 435 | WITH main_search AS 436 | ( 437 | SELECT DISTINCT prompts.*, prompt_values.* 438 | FROM prompts 439 | JOIN prompt_values ON prompts.id = prompt_values.prompt_id 440 | JOIN styles ON prompts.style = styles.id AND prompt_values.key = styles.preview_key 441 | {x} JOIN examples ON prompts.id = examples.prompt_id AND examples.completion LIKE ? 442 | {tag_query_str} 443 | WHERE prompt_values.value LIKE ? 444 | {proj_id_query} 445 | {style_id_query} 446 | ) 447 | SELECT main_search.*, GROUP_CONCAT(t.value) AS tags, COUNT(*) OVER() AS total_results 448 | FROM main_search 449 | LEFT JOIN tags t ON main_search.prompt_id = t.prompt_id 450 | GROUP BY main_search.id 451 | LIMIT ? 452 | OFFSET ? 453 | """ 454 | 455 | results = db.execute(sql, tuple(args)) 456 | fetched = results.fetchall() 457 | total_results = 0 if len(fetched) == 0 else fetched[0]['total_results'] 458 | return fetched, total_results 459 | 460 | 461 | def check_running(db): 462 | tasks = get_tasks(db) 463 | has_oustanding = False 464 | for task in tasks: 465 | if task['status'] == 'in_progress': 466 | task_id = task['id'] 467 | pid = task['pid'] 468 | 469 | def update_records(task_id): 470 | sql = """ 471 | UPDATE tasks 472 | SET status = 'failed' 473 | WHERE id = ?; 474 | """ 475 | db.execute(sql, (task_id,)) 476 | db.commit() 477 | try: 478 | process = psutil.Process(pid) 479 | # Check if process with pid exists 480 | if process.is_running() and process.ppid() == os.getpid(): 481 | # is still in progress, nothing to do 482 | has_oustanding = True 483 | else: 484 | update_records(task_id) 485 | except psutil.NoSuchProcess: 486 | update_records(task_id) 487 | if not has_oustanding: 488 | session['warn_parallelism'] = False 489 | 490 | return has_oustanding 491 | 492 | 493 | def get_tasks(db): 494 | sql = """ 495 | SELECT * FROM tasks ORDER BY created_at DESC 496 | """ 497 | tasks = db.execute(sql) 498 | return tasks.fetchall() 499 | 500 | def get_exports(db): 501 | sql = """ 502 | SELECT * FROM exports ORDER BY created_at DESC 503 | """ 504 | exports = db.execute(sql) 505 | return exports.fetchall() 506 | 507 | def get_export_by_id(db, id): 508 | sql = """ 509 | SELECT * FROM exports WHERE id = ? 510 | """ 511 | export = db.execute(sql, id) 512 | return export.fetchone() 513 | 514 | def get_prompt_by_id(db, id): 515 | sql = """ 516 | SELECT * FROM prompts WHERE id = ? 517 | """ 518 | prompt = db.execute(sql, (id,)) 519 | return prompt.fetchone() 520 | 521 | def get_examples_by_prompt_id(db, prompt_id, with_tags=True): 522 | if with_tags: 523 | sql = """ 524 | SELECT e.*, GROUP_CONCAT(t.value) AS tags 525 | FROM examples e 526 | LEFT JOIN tags t ON e.id = t.example_id 527 | WHERE e.prompt_id = ? 528 | GROUP BY e.id 529 | """ 530 | else: 531 | sql = """ 532 | SELECT * FROM examples WHERE prompt_id = ? 533 | """ 534 | examples = db.execute(sql, (prompt_id,)) 535 | return examples.fetchall() 536 | 537 | def get_projects(db): 538 | sql = """ 539 | SELECT * FROM projects ORDER BY created_at DESC 540 | """ 541 | examples = db.execute(sql) 542 | return examples.fetchall() 543 | 544 | def get_project_by_id(db, id): 545 | sql = """ 546 | SELECT * FROM projects WHERE id = ? 547 | """ 548 | examples = db.execute(sql, (id,)) 549 | return examples.fetchone() 550 | 551 | def get_styles_by_project_id(db, id): 552 | sql = """ 553 | SELECT * FROM styles WHERE project_id = ? 554 | """ 555 | examples = db.execute(sql, (id,)) 556 | return examples.fetchall() 557 | 558 | def get_styles(db): 559 | sql = """ 560 | SELECT * FROM styles ORDER BY created_at DESC 561 | """ 562 | styles = db.execute(sql) 563 | return styles.fetchall() 564 | 565 | def get_style_by_id(db, id): 566 | sql = """ 567 | SELECT * FROM styles WHERE id = ? 568 | """ 569 | style = db.execute(sql, (id,)) 570 | return style.fetchone() 571 | 572 | def get_keys_by_style_id(db, id): 573 | sql = """ 574 | SELECT * FROM style_keys WHERE style_id = ? 575 | """ 576 | keys = db.execute(sql, (id,)) 577 | return keys.fetchall() 578 | 579 | def get_tags_by_prompt_id(db, prompt_id): 580 | sql = """ 581 | SELECT * FROM tags WHERE prompt_id = ? 582 | """ 583 | tags = db.execute(sql, (prompt_id,)) 584 | return tags.fetchall() 585 | 586 | def get_prompt_values_by_prompt_id(db, prompt_id): 587 | c = db.cursor() 588 | sql = """ 589 | SELECT * FROM prompt_values WHERE prompt_id = ? 590 | """ 591 | vals = c.execute(sql, (prompt_id,)) 592 | return vals.fetchall() 593 | 594 | def add_project(db, name, description): 595 | sql = """ 596 | INSERT INTO projects (name, desc) 597 | VALUES (?, ?) 598 | """ 599 | c = db.cursor() 600 | c.execute(sql, (name, description)) 601 | proj_id = c.lastrowid 602 | db.commit() 603 | return proj_id 604 | 605 | def add_style(db, idtext, format_string, completion_key, preview_key, project_id, style_keys): 606 | sql = """ 607 | INSERT INTO styles (id_text, template, completion_key, preview_key, project_id) 608 | VALUES (?, ?, ?, ?, ?) 609 | """ 610 | c = db.cursor() 611 | c.execute(sql, (idtext, format_string, completion_key, preview_key, project_id)) 612 | style_id = c.lastrowid 613 | db.commit() 614 | 615 | # Now add style keys 616 | for key in style_keys: 617 | sql = """ 618 | INSERT INTO style_keys (name, style_id) 619 | VALUES (?, ?) 620 | """ 621 | c.execute(sql, (key, style_id)) 622 | db.commit() 623 | 624 | return style_id 625 | 626 | def delete_project(db, project_id): 627 | c = db.cursor() 628 | # Remove all associated completions 629 | sql = """ 630 | DELETE FROM examples 631 | WHERE prompt_id IN ( 632 | SELECT id FROM prompts 633 | WHERE project_id = ? 634 | ); 635 | """ 636 | c.execute(sql, (project_id,)) 637 | 638 | # Remove all associated prompts 639 | c.execute("DELETE FROM prompts WHERE project_id = ?", (project_id,)) 640 | 641 | # Remove all associated styles 642 | c.execute("DELETE FROM styles WHERE project_id = ?", (project_id,)) 643 | 644 | # Remove project 645 | c.execute("DELETE FROM projects WHERE id = ?", (project_id,)) 646 | 647 | db.commit() 648 | 649 | def update_project(db, project_id, description, name): 650 | c = db.cursor() 651 | 652 | if description: 653 | c.execute("UPDATE projects SET desc = ? WHERE id = ?", (description, project_id)) 654 | if name: 655 | c.execute("UPDATE projects SET name = ? WHERE id = ?", (name, project_id)) 656 | 657 | db.commit() 658 | 659 | def update_style(db, style_id, id_text, template, completion_key, preview_key): 660 | c = db.cursor() 661 | 662 | new_keys = get_named_arguments(template) 663 | c.execute("SELECT * FROM style_keys WHERE style_id = ?", (style_id,)) 664 | old_keys = c.fetchall() 665 | 666 | missing_keys = [x['name'] for x in old_keys if x['name'] not in new_keys] 667 | added_keys = [x for x in new_keys if x not in [y['name'] for y in old_keys]] 668 | 669 | for key in missing_keys: 670 | c.execute("DELETE FROM style_keys WHERE name LIKE ? AND style_id = ?", (key, style_id)) 671 | # Also we delete all the prompt values associated with the removed key 672 | c.execute(""" 673 | DELETE FROM prompt_values 674 | WHERE prompt_values.key LIKE ? 675 | AND prompt_values.prompt_id IN ( 676 | SELECT prompts.id FROM prompts 677 | JOIN styles ON prompts.style = styles.id 678 | WHERE styles.id = ? 679 | ) 680 | """, (key, style_id)) 681 | 682 | for key in added_keys: 683 | sql = """ 684 | INSERT INTO style_keys (name, style_id) 685 | VALUES (?, ?) 686 | """ 687 | c.execute(sql, (key, style_id)) 688 | 689 | if id_text: 690 | c.execute("UPDATE styles SET id_text = ? WHERE id = ?", (id_text, style_id)) 691 | if template: 692 | c.execute("UPDATE styles SET template = ? WHERE id = ?", (template, style_id)) 693 | if completion_key: 694 | c.execute("UPDATE styles SET completion_key = ? WHERE id = ?", (completion_key, style_id)) 695 | if preview_key: 696 | c.execute("UPDATE styles SET preview_key = ? WHERE id = ?", (preview_key, style_id)) 697 | 698 | db.commit() 699 | 700 | 701 | def delete_style(db, style_id): 702 | c = db.cursor() 703 | # Remove all associated completions 704 | sql = """ 705 | DELETE FROM examples 706 | WHERE prompt_id IN ( 707 | SELECT id FROM prompts 708 | WHERE style = ? 709 | ); 710 | """ 711 | c.execute(sql, (style_id,)) 712 | 713 | # Remove all associated prompts 714 | c.execute("DELETE FROM prompts WHERE style = ?", (style_id,)) 715 | 716 | # Remove the style 717 | c.execute("DELETE FROM styles WHERE id = ?", (style_id,)) 718 | 719 | db.commit() 720 | 721 | -------------------------------------------------------------------------------- /app/exporting.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import ( 3 | Blueprint, 4 | redirect, 5 | render_template, 6 | request, 7 | send_file, 8 | current_app, 9 | session 10 | ) 11 | from app.db import get_db 12 | from app.db_wrappers import export, get_exports, get_export_by_id, get_styles, get_projects, check_running 13 | from app.utils import tag_string_to_list 14 | 15 | bp = Blueprint('exporting', __name__) 16 | 17 | @bp.route('/export', methods=('GET', 'POST')) 18 | def exp(): 19 | db = get_db() 20 | if request.method == "POST": 21 | 22 | if check_running(db): 23 | return render_template('export.html', styles=get_styles(db), projects=get_projects(db), error="Cannot create new export while current task is incomplete.") 24 | 25 | filename = request.form.get('filename') if request.form.get('filename') else "export.json" 26 | 27 | tags = request.form.get('tags') 28 | tags = tag_string_to_list(tags) 29 | 30 | content = request.form.get('content') 31 | style_id = request.form.get('style_id') 32 | project_id = request.form.get('project_id') 33 | example = request.form.get('example') 34 | 35 | export(db, filename=filename, tags=tags, content=content, example=example, project_id=project_id, style_id=style_id) 36 | return redirect("/tasks") 37 | 38 | return render_template('export.html', styles=get_styles(db), projects=get_projects(db)) 39 | 40 | @bp.route('/exports', methods=('GET',)) 41 | def exps(): 42 | download_id = request.args.get('download_id') 43 | db = get_db() 44 | if download_id: 45 | row = get_export_by_id(db, download_id) 46 | path = os.path.join(current_app.config.get('EXPORTS_PATH'), row['filename']) 47 | return send_file(path, as_attachment=True) 48 | 49 | exports = get_exports(db) 50 | 51 | return render_template('exports.html', exports=exports) 52 | -------------------------------------------------------------------------------- /app/home.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | request 5 | ) 6 | from app.db import get_db 7 | from app.db_wrappers import delete_project, get_projects, search_prompts, get_styles, delete_prompt 8 | from app.utils import tag_string_to_list 9 | import json 10 | 11 | bp = Blueprint('home', __name__) 12 | 13 | @bp.route('/', methods=('GET',)) 14 | def home(): 15 | return render_template('home.html') 16 | 17 | 18 | @bp.route('/manifest', methods=('GET', 'POST')) 19 | def manifest(): 20 | 21 | db = get_db() 22 | 23 | # When a user selects prompts via check box and clicks a button 24 | # This will trigger a special json response, not the web page 25 | if request.method == 'POST': 26 | form_data = request.json 27 | selected_prompts = form_data['prompt_ids'] 28 | action = form_data['action'] 29 | if action == 'delete': 30 | # One day, this should probably be batched 31 | for prompt_id in selected_prompts: 32 | delete_prompt(db, prompt_id) 33 | response_text = json.dumps({'response': 'success', 'prompts_affected': selected_prompts, 'action': action}) 34 | return response_text 35 | 36 | limit = 100 37 | offset = request.args.get('offset') 38 | if not offset: 39 | offset = 0 40 | 41 | content_arg = request.args.get("content") 42 | example_arg = request.args.get("example") 43 | tags_arg = request.args.get("tags") 44 | tags_arg = tag_string_to_list(tags_arg) 45 | project_id_arg = request.args.get("project_id") 46 | if project_id_arg == "": 47 | project_id_arg = None 48 | style_id_arg = request.args.get("style_id") 49 | if style_id_arg == "": 50 | style_id_arg = None 51 | 52 | projects = get_projects(db) 53 | styles = get_styles(db) 54 | 55 | prompts, total_results = search_prompts(db, limit, offset, content_arg, example_arg, tags_arg, project_id_arg, style_id_arg) 56 | 57 | return render_template('manifest.html', prompts=prompts, page_size=limit, total_results=total_results, projects=projects, styles=styles) 58 | -------------------------------------------------------------------------------- /app/projects.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | request, 5 | redirect 6 | ) 7 | from app.db import get_db 8 | from app.db_wrappers import delete_project, get_projects, get_project_by_id, get_styles_by_project_id, add_project, add_style, delete_project, update_project 9 | from app.utils import get_named_arguments 10 | 11 | bp = Blueprint('projects', __name__) 12 | 13 | @bp.route('/projects', methods=('GET', 'POST')) 14 | def projects(): 15 | 16 | if request.method == "POST": 17 | name = request.form.get('name') 18 | description = request.form.get('description') 19 | add_project(get_db(), name, description) 20 | 21 | projects = get_projects(get_db()) 22 | return render_template('projects.html', projects=projects) 23 | 24 | @bp.route('/project', methods=('GET', 'POST')) 25 | def project(): 26 | 27 | db = get_db() 28 | 29 | errors = [] 30 | 31 | if request.method == 'POST': 32 | project_id = request.form.get('project_id') 33 | form_type = request.form.get('form_type') 34 | 35 | if form_type == 'delete': 36 | if project_id: 37 | delete_project(db, project_id) 38 | # if there's no project id, we'll just redirect the user back to projects 39 | elif form_type == 'update': 40 | name = request.form.get('name') 41 | description = request.form.get('description') 42 | update_project(db, project_id, description, name) 43 | else: # form_type might be null 44 | # Add style 45 | 46 | idtext = request.form.get('id_text') 47 | format_string = request.form.get('format_string') 48 | completion_key = request.form.get('completion_key') 49 | preview_key = request.form.get('preview_key') 50 | 51 | style_keys = get_named_arguments(format_string) 52 | 53 | # In the future, should throw errors when completion/preview key not in style_keys 54 | if completion_key not in style_keys: 55 | errors.append("Completion key not in the format string") 56 | elif preview_key not in style_keys: 57 | errors.append("Preview key not in the format string") 58 | else: 59 | add_style(db, idtext, format_string, completion_key, preview_key, project_id, style_keys) 60 | 61 | project_id = request.args.get("id") 62 | 63 | if not project_id: 64 | return redirect('/projects') 65 | 66 | project = get_project_by_id(get_db(), project_id) 67 | styles = get_styles_by_project_id(get_db(), project_id) 68 | return render_template('project.html', project=project, styles=styles, errors=errors) 69 | -------------------------------------------------------------------------------- /app/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS examples; 2 | DROP TABLE IF EXISTS prompts; 3 | DROP TABLE IF EXISTS metrics; 4 | DROP TABLE IF EXISTS tasks; 5 | DROP TABLE IF EXISTS exports; 6 | DROP TABLE IF EXISTS styles; 7 | DROP TABLE IF EXISTS projects; 8 | DROP TABLE IF EXISTS tags; 9 | DROP TABLE IF EXISTS prompt_values; 10 | DROP TABLE IF EXISTS style_keys; 11 | 12 | CREATE TABLE examples ( 13 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 14 | `prompt_id` INTEGER, 15 | `completion` TEXT, 16 | `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 17 | ); 18 | 19 | CREATE TABLE tags ( 20 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 21 | `value` TEXT, 22 | `prompt_id` INTEGER, /* Optional prompt association */ 23 | `example_id` INTEGER /* Optional example association */ 24 | ); 25 | 26 | CREATE TABLE prompts ( 27 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 28 | `style` INTEGER, 29 | `project_id` INTEGER, 30 | `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 31 | ); 32 | 33 | CREATE TABLE prompt_values ( 34 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 35 | `prompt_id` INTEGER, 36 | `key` TEXT, 37 | `value` TEXT 38 | ); 39 | 40 | CREATE TABLE styles ( 41 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 42 | `id_text` TEXT, /* like 'instruct' or 'chat' */ 43 | `project_id` INTEGER, 44 | `template` TEXT, 45 | `completion_key` TEXT, /* Where the LLM is meant to add */ 46 | `preview_key` TEXT, /* For table previews of the prompts */ 47 | `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 48 | ); 49 | 50 | CREATE TABLE style_keys ( 51 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 52 | `name` TEXT, 53 | `style_id` INTEGER 54 | ); 55 | 56 | CREATE TABLE projects ( 57 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 58 | `name` TEXT, 59 | `desc` TEXT, 60 | `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 61 | ); 62 | 63 | CREATE TABLE metrics ( 64 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 65 | `name` TEXT, 66 | `score` TEXT, 67 | `example_id` INTEGER, 68 | `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 69 | ); 70 | 71 | CREATE TABLE tasks ( 72 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 73 | `user_id` INTEGER, 74 | `type` TEXT, 75 | `pid` INTEGER, 76 | `status` TEXT, 77 | `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 78 | ); 79 | 80 | CREATE TABLE exports ( 81 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 82 | `user_id` INTEGER, 83 | `filename` TEXT, 84 | `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP 85 | ); 86 | 87 | INSERT INTO projects (`name`, `desc`) 88 | VALUES ("Sample Project", 89 | "My first project" 90 | ); 91 | 92 | INSERT INTO styles (`id_text`, `project_id`, `template`, `completion_key`, `preview_key`) 93 | VALUES ("instruct", 94 | 1, 95 | "Instruction: {instruction} 96 | Input: {input} 97 | Output: {output}", 98 | "output", 99 | "instruction" 100 | ); 101 | 102 | INSERT INTO style_keys (`name`, `style_id`) 103 | VALUES ("instruction", 104 | 1 105 | ); 106 | 107 | INSERT INTO style_keys (`name`, `style_id`) 108 | VALUES ("input", 109 | 1 110 | ); 111 | 112 | INSERT INTO style_keys (`name`, `style_id`) 113 | VALUES ("output", 114 | 1 115 | ); 116 | 117 | INSERT INTO prompts (style, project_id) 118 | VALUES (1, 119 | 1 120 | ); 121 | 122 | INSERT INTO prompt_values (`prompt_id`, `key`, `value`) 123 | VALUES (1, 124 | "instruction", 125 | "Write a line of Python that prints Hello World" 126 | ); 127 | 128 | INSERT INTO examples (prompt_id, completion) 129 | VALUES (1, 130 | "print('Hello World')" 131 | ); 132 | 133 | INSERT INTO tags (`example_id`, `value`) 134 | VALUES (1, 135 | "coding" 136 | ); 137 | -------------------------------------------------------------------------------- /app/static/add.css: -------------------------------------------------------------------------------- 1 | 2 | .prompt-textarea { 3 | width: 50%; 4 | height: 10em; 5 | } 6 | -------------------------------------------------------------------------------- /app/static/manifest.css: -------------------------------------------------------------------------------- 1 | #search-form { 2 | max-width: calc(100% - 2em); 3 | margin-bottom: 1em; 4 | background-color: #f2f2f2; 5 | padding: 1em; 6 | } 7 | 8 | label { 9 | margin: 10px 0; 10 | } 11 | 12 | button{ 13 | margin-top: 10px; 14 | } 15 | 16 | .pagination { 17 | display: flex; 18 | padding: .5rem; 19 | flex-wrap: wrap; 20 | } 21 | 22 | .pagination a { 23 | margin-right: 1rem; 24 | text-decoration: none; 25 | color: black; 26 | } 27 | 28 | .pagination .selected-page-number { 29 | font-weight: bold; 30 | text-decoration: underline; 31 | } 32 | -------------------------------------------------------------------------------- /app/static/projects.css: -------------------------------------------------------------------------------- 1 | 2 | #add-project textarea{ 3 | width: 50%; 4 | height: 10em; 5 | } 6 | 7 | #add-project input[type=text]{ 8 | width: 50%; 9 | } -------------------------------------------------------------------------------- /app/static/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 3 | background: white; 4 | } 5 | 6 | html, body { 7 | height: 100%; 8 | } 9 | 10 | nav { 11 | padding-bottom: .5em; 12 | border-bottom: 1px black solid; 13 | } 14 | 15 | nav a{ 16 | all: unset; 17 | } 18 | 19 | .nav-bar-text, .nav-bar-main-header { 20 | padding-right: .75em; 21 | cursor: pointer; 22 | } 23 | 24 | .nav-bar-main-header { 25 | font-size: 1.5em; 26 | } 27 | 28 | #footer { 29 | height: auto; 30 | padding: 20px; 31 | background-color: #f0f0f0; 32 | color: #808080; 33 | font-size: .75em; 34 | } 35 | 36 | #footer a{ 37 | all: unset; 38 | } 39 | 40 | #wrapper { 41 | min-height: calc(100% + 40px); 42 | display: flex; 43 | flex-direction: column; 44 | } 45 | 46 | #page-content { 47 | flex-grow: 1; 48 | margin-bottom: 200px; /* minimum 200px distance from the footer */ 49 | } 50 | 51 | .warning { 52 | background-color: #f0f0f0; 53 | text-align: center; 54 | padding: 5px; 55 | margin-bottom: 10px; 56 | } -------------------------------------------------------------------------------- /app/static/style_viewer.css: -------------------------------------------------------------------------------- 1 | .textarea-template { 2 | width: 50%; 3 | height: 20em; 4 | } -------------------------------------------------------------------------------- /app/static/table.css: -------------------------------------------------------------------------------- 1 | table { 2 | border-collapse: collapse; 3 | width: 100%; 4 | } 5 | 6 | td, th { 7 | border: 1px solid #ddd; 8 | padding: .5rem; 9 | text-align: left; 10 | } 11 | 12 | th { 13 | background-color: #f2f2f2; 14 | } -------------------------------------------------------------------------------- /app/static/view.css: -------------------------------------------------------------------------------- 1 | 2 | .prompt, .completion-editor { 3 | padding: 12px; 4 | background-color: #f0f0f0; 5 | border: 1px solid #808080; 6 | } 7 | 8 | #add-completion { 9 | display: none; 10 | } 11 | 12 | #add-completion textarea{ 13 | width: 50%; 14 | height: 10em; 15 | } 16 | 17 | .completion-editor { 18 | width: calc(100% - 24px); 19 | margin-top: .5em; 20 | } 21 | 22 | .edit-completion-button, .edit-prompt-button, .edit-style{ 23 | margin-top: .5em; 24 | } 25 | 26 | .completions-header { 27 | font-size: 1.5em; 28 | } 29 | 30 | .delete-button { 31 | margin-top: .5em; 32 | } -------------------------------------------------------------------------------- /app/style.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | request, 5 | redirect 6 | ) 7 | from app.db import get_db 8 | from app.db_wrappers import delete_style, get_project_by_id, get_style_by_id, get_keys_by_style_id, update_style 9 | from app.utils import get_named_arguments 10 | import json 11 | 12 | bp = Blueprint('style', __name__) 13 | 14 | @bp.route('/style', methods=('GET', 'POST')) 15 | def style(): 16 | 17 | errors = [] 18 | 19 | db = get_db() 20 | if request.method == "POST": 21 | style_id = request.form.get('style_id') 22 | form_type = request.form.get('form_type') 23 | if form_type == "update": 24 | template = request.form.get('template') 25 | id_text = request.form.get('id_text') 26 | completion_key = request.form.get('completion_key') 27 | preview_key = request.form.get('preview_key') 28 | 29 | keys = get_named_arguments(template) 30 | if preview_key not in keys: 31 | errors.append("Preview key must be in template") 32 | if completion_key not in keys: 33 | errors.append("Completion key must be in template") 34 | 35 | if len(errors) == 0: 36 | update_style(db, style_id, id_text, template, completion_key, preview_key) 37 | elif form_type == 'delete': 38 | if style_id: 39 | delete_style(db, style_id) 40 | 41 | 42 | id = request.args.get('id') 43 | 44 | if not id: 45 | return redirect("/projects") 46 | 47 | style = get_style_by_id(db, id) 48 | proj_id = style['project_id'] 49 | project = get_project_by_id(db, proj_id) 50 | 51 | keys = get_keys_by_style_id(db, id) 52 | 53 | return render_template('style.html', style=style, project=project, keys=keys, errors=errors) 54 | -------------------------------------------------------------------------------- /app/tasks.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | render_template, 4 | request 5 | ) 6 | from app.db import get_db 7 | from app.db_wrappers import get_tasks, check_running 8 | 9 | bp = Blueprint('tasks', __name__) 10 | 11 | @bp.route('/tasks', methods=('GET',)) 12 | def tasks(): 13 | db = get_db() 14 | check_running(db) 15 | tasks = get_tasks(db) 16 | return render_template('tasks.html', tasks=tasks) 17 | -------------------------------------------------------------------------------- /app/templates/add.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Add{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |

Add Data

12 | {% if error %} 13 |
14 | Error: {{ error }} 15 |
16 | {% endif %} 17 |
18 |
19 |

20 | Add Prompt 21 |

22 | 23 | Prompt Style: {{ style.id_text }} 24 |

25 | {% for key in style_keys %} 26 | {{ key.name }}
27 |

28 | {% endfor %} 29 | 30 | Tags: 31 | 32 |

33 | 34 | 35 | 36 | 37 |
38 |
39 |
40 |
41 |

42 | Bulk Upload 43 |

44 |

45 | Bulk uploading lets you add a batch of data in a particular style. 46 | The file should be in json format, with its root element being a list of objects that have 47 | as keys the keys of the style (named template arguments). 48 |

49 |

50 | 51 | Tags: 52 | 53 |

54 | 55 | 56 | 57 |
58 |
59 |
60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}{% endblock %} - Praetor 4 | 5 | {% block meta %}{% endblock %} 6 | 7 | 8 | {% if session['warn_parallelism'] %} 9 |
10 | Warning: no changes may be made to the database while a task is still outstanding. 11 |
12 | {% endif %} 13 | 20 |
21 |
22 | {% block content %}{% endblock %} 23 |
24 | 30 |
-------------------------------------------------------------------------------- /app/templates/export.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Export{% endblock %} 4 | 5 | {% block meta %} 6 | {% endblock %} 7 | 8 | {% block content %} 9 |
10 |

11 | Export 12 |

13 | {% if error %} 14 |
15 | Error: {{ error }} 16 |
17 | {% endif %} 18 |
19 |
20 |

21 | Filters 22 |

23 | 24 | 25 | 26 |

27 | 28 | 29 |

30 | 31 | 32 |

33 | 34 | 40 |

41 | 42 | 48 |

49 | Options 50 |

51 | 52 | 53 |

54 | 55 |
56 |
57 | 61 |
62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /app/templates/exports.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Exports{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |

12 | Exports 13 |

14 |

15 | Exports live in instance/exports, but downloading one will add a copy to your downloads folder. 16 |

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% set ns = namespace(count=0) %} 27 | {% for row in exports %} 28 | {% set ns.count = ns.count + 1 %} 29 | 30 | 31 | 32 | 33 | 34 | {% endfor %} 35 | {% if ns.count == 0 %} 36 | No exports made yet - you can export completions from the prompt manifest screen (the prompt search area).

37 | {% endif %} 38 | 39 |
DownloadTimeFilename
Download{{ row.created_at }}{{ row.filename }}
40 |
41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /app/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Home{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

8 | Welcome! 9 |

10 |

11 | Welcome to Praetor, a dataset exploration tool. 12 |

13 |

14 | See your prompt manifest here. 15 |

16 |
17 |

18 | About 19 |

20 |

21 | Praetor is a super lightweight finetuning data and prompt management tool. 22 | The setup requirements are minimal, and so is the complexity. 23 |

24 |

25 | In general, the system works like this: you start with a Project. 26 | A project has associated with it some prompt styles, which define format strings for the prompts. 27 | You can then add prompts in a particular style and then one or more completions to those prompts. 28 | When you're done editing prompts or completions, you can export that data in a json format. 29 | You can also import data in the same format. 30 |

31 | Thus the hierarchy goes: 32 |

Projects --> Styles --> Prompts --> Completions
33 |

34 |

35 | Sample Data 36 |

37 |

38 | A sample project is added for you when the database is initialized. The existing style placed in the project will work with alpaca.json, 39 | which is in the sample-datasets folder. Go to Projects >> Sample Project >> add data (in style "instruct") >> Bulk Upload from sample-datasets/alpaca.json. 40 |

41 |
42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /app/templates/manifest.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}View{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 | 12 | {% set query_params = request.args.copy() %} 13 | 14 |
15 |

16 | Prompts 17 |

18 |
19 |
20 |
21 | 22 | 23 | 24 |

25 | 26 | 27 | 28 | 29 | {% if request.args %} 30 | 31 | 32 | 33 | {% endif %} 34 | 59 |
60 |
61 |
62 | Total Results: 63 | {{ "{:,d}".format(total_results) }} 64 |   65 | Total Selected: 66 | 67 | 71 | 🗑 76 | 77 |
78 |
79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {% for row in prompts %} 90 | 91 | 94 | 95 | 96 | 97 | 98 | {% endfor %} 99 | 100 |
LinkPromptTags
92 | 93 | View {{ row.value }}{{ row.tags }}
101 |
102 | {% set total_pages = ((total_results - 1) // page_size) + 1 %} 103 | {% set current_offset = query_params['offset']|default(0)|int %} 104 | {% set current_page = current_offset // page_size + 1 %} 105 | {% set page_window = 5 %} 106 | {% set last_shown_page = [current_page+page_window, total_pages]|min %} 107 | 108 | 138 |
139 | 220 |
221 | {% endblock %} 222 | -------------------------------------------------------------------------------- /app/templates/project.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Projects{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | 26 | {% endblock %} 27 | 28 | {% block content %} 29 |
30 |
31 |

32 | 33 |

34 | 35 | 70 | 71 |
72 |

73 | Description 74 |

75 | 76 | 81 |
82 | 83 | 84 |
85 |

Styles

86 | 98 | 99 | 126 |
127 |
128 |
129 | {% for error in errors %} 130 |
131 | Error submitting style: {{error}} 132 |
133 | {% endfor %} 134 |
135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | {% for row in styles %} 146 | 147 | 148 | 149 | 150 | 151 | 152 | {% endfor %} 153 | 154 |
LinkNameAdd Data in StyleTime Created
View{{ row.id_text }}{{ row.created_at }}
155 |
156 | {% endblock %} 157 | -------------------------------------------------------------------------------- /app/templates/projects.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Projects{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | 8 | {% endblock %} 9 | 10 | {% block content %} 11 |
12 |

13 | Projects 14 |

15 |

16 | Projects contain styles and prompts made in the image of those styles. 17 |

18 | 30 | 31 |
32 |
33 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% for row in projects %} 57 | 58 | 59 | 60 | 61 | 62 | 63 | {% endfor %} 64 | 65 |
LinkTimeNameDescription
View{{ row.created_at }}{{ row.name }}{{ row.desc }}
66 |
67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /app/templates/style.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Style{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | 19 | {% endblock %} 20 | 21 | {% block content %} 22 |
23 |

24 | Style 25 |

26 |
27 |
28 | Associated Project: {{ project.name }} 29 |
30 |
31 | Name: 32 |
33 |
34 | 35 |

36 | 37 | 71 | 72 | {% if errors|length > 0 %} 73 |
74 | {% endif %} 75 | {% for error in errors %} 76 |
77 |
78 | Error: {{ error }} 79 |
80 | {% endfor %} 81 |
82 |

Prompt Template

83 | 84 |
85 |

Keys:

86 | {% for row in keys %} 87 |
88 | {{ row.name }} 89 |
90 | {% endfor %} 91 |
92 | {% for row in keys %} 93 | {% if row.name == style.completion_key %} 94 |
95 | Completion key: 96 |
97 | {% endif %} 98 | {% if row.name == style.preview_key %} 99 |
100 | Preview Key: 101 |
102 | {% endif %} 103 | {% endfor %} 104 | 105 | 106 |
107 |
108 | {% endblock %} 109 | -------------------------------------------------------------------------------- /app/templates/tasks.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}Tasks{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |

12 | Tasks 13 |

14 |

15 | Tasks are long-running processes, such as large exports or large imports. 16 |

17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for row in tasks %} 27 | 28 | 29 | 30 | 31 | 32 | {% endfor %} 33 | 34 |
TimeTypeStatus
{{ row.created_at }}{{ row.type }}{{ row.status }}
35 |
36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /app/templates/view.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %}View{% endblock %} 4 | 5 | {% block meta %} 6 | 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 22 |
23 | {% if prompt %} 24 |
25 |

26 | Prompt 27 |

28 |
29 | 30 | Prompt Style: {{ style.id_text }} 31 |

32 | {% for val in prompt_values %} 33 | {{ val.key }}
34 |

35 | 40 | {% endfor %} 41 | Tags:

42 | 43 | 46 |
47 |
48 | 49 | 52 |
53 |
54 |
55 |
56 |
57 | 58 | Completions 59 | 60 | 61 |
62 |
63 |
64 | 65 |
66 | 67 |
68 |
69 | 70 | 71 |
72 |
73 |

74 |
75 | 78 | {% for row in completions %} 79 |
80 | 81 | 86 |
87 | Tags: 88 |
89 | 90 | 91 | 94 |
95 |
96 | 97 | 98 | 101 |
102 |
103 | {% endfor %} 104 |
105 |
106 | {% else %} 107 |

No Prompt Found

108 | {% endif %} 109 |
110 | {% endblock %} 111 | -------------------------------------------------------------------------------- /app/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | """ 4 | 5 | Utility functions 6 | 7 | """ 8 | 9 | def tag_string_to_list(tags): 10 | if not tags: 11 | return [] 12 | return [tag for tag in tags.replace(" ", "").split(",") if tag] 13 | 14 | 15 | # NOTE: the named arguments can be escaped 16 | def get_named_arguments(fmt_string): 17 | pattern = re.compile(r'{(?P\w+)}') 18 | return pattern.findall(fmt_string) 19 | 20 | -------------------------------------------------------------------------------- /app/view.py: -------------------------------------------------------------------------------- 1 | from flask import ( 2 | Blueprint, 3 | redirect, 4 | render_template, 5 | request 6 | ) 7 | from app.db import get_db 8 | from app.db_wrappers import add_example, delete_example, get_keys_by_style_id, update_example, get_examples_by_prompt_id, get_prompt_by_id, get_prompt_values_by_prompt_id, get_style_by_id, get_tags_by_prompt_id, update_prompt, delete_prompt 9 | from app.utils import tag_string_to_list 10 | 11 | bp = Blueprint('view', __name__) 12 | 13 | @bp.route('/view', methods=('GET', 'POST')) 14 | def view(): 15 | 16 | prompt_id = request.args.get("prompt_id") 17 | 18 | db = get_db() 19 | 20 | if request.method == "POST": 21 | 22 | update_type = request.form.get("update_type") 23 | 24 | if update_type == "update_prompt": 25 | new_prompt_values = {} 26 | for key, value in request.form.items(): 27 | if key.find("key.") == 0: 28 | new_prompt_values[key[4:]] = value 29 | tags = request.form.get("tags") 30 | tags = tag_string_to_list(tags) 31 | update_prompt(db, prompt_id, new_prompt_values, tags) 32 | elif update_type == "delete_prompt": 33 | delete_prompt(db, prompt_id) 34 | elif update_type == "add_completion": 35 | completion = request.form.get("completion") 36 | tags = request.form.get("tags") 37 | tags = tag_string_to_list(tags) 38 | add_example(db, prompt_id, completion, tags) 39 | elif update_type == "update_completion": 40 | example_id = request.form.get("id") 41 | completion = request.form.get("completion") 42 | tags = request.form.get("tags") 43 | tags = tag_string_to_list(tags) 44 | update_example(db, example_id, completion, tags) 45 | elif update_type == "delete_completion": 46 | example_id = request.form.get("id") 47 | delete_example(db, example_id) 48 | 49 | prompt_dict = get_prompt_by_id(db, prompt_id) 50 | tags = None 51 | prompt_values = None 52 | completions = None 53 | style = None 54 | tags_str = None 55 | if prompt_dict: 56 | tags = get_tags_by_prompt_id(db, prompt_id) 57 | tags_str = ",".join([tag['value'] for tag in tags]) 58 | 59 | prompt_values = get_prompt_values_by_prompt_id(db, prompt_id) 60 | style = get_style_by_id(db, prompt_dict['style']) 61 | style_keys = get_keys_by_style_id(db, prompt_dict['style']) 62 | prompt_value_keys = [x['key'] for x in prompt_values] 63 | for key in style_keys: 64 | if key['name'] == style['completion_key']: 65 | continue 66 | if key['name'] not in prompt_value_keys: 67 | prompt_values.append({ 68 | 'key': key['name'], 69 | 'value': "" 70 | }) 71 | 72 | 73 | completions = get_examples_by_prompt_id(db, prompt_id, with_tags=True) 74 | 75 | return render_template('view.html', prompt=prompt_dict, style=style, prompt_values=prompt_values, prompt_tags=tags_str, completions=completions) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask 2 | psutil 3 | -------------------------------------------------------------------------------- /screenshots/praetor-completion-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodreasonai/praetor-data/b7af7eaea1c2e5657d9b3f3c9321650c5886aac9/screenshots/praetor-completion-screen.png -------------------------------------------------------------------------------- /screenshots/praetor-prompts-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodreasonai/praetor-data/b7af7eaea1c2e5657d9b3f3c9321650c5886aac9/screenshots/praetor-prompts-screen.png -------------------------------------------------------------------------------- /screenshots/praetor-style-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodreasonai/praetor-data/b7af7eaea1c2e5657d9b3f3c9321650c5886aac9/screenshots/praetor-style-screen.png --------------------------------------------------------------------------------