├── blueprints ├── __init__.py ├── utils.py ├── schema.py ├── blocks.py └── architect.py ├── .gitignore ├── .github └── workflows │ ├── check.yml │ └── main.yml ├── process_page.py ├── process_blueprint.py ├── generate_content.py ├── requirements.txt ├── gradio_app.py ├── README.md └── data └── process_data.py /blueprints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | blueprints/__pycache__ -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check file size 2 | on: # or directly `on: [push]` to run the action on every push on any branch 3 | pull_request: 4 | branches: [main] 5 | 6 | # to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | sync-to-hub: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check large files 14 | uses: ActionsDesk/lfs-warning@v2.0 15 | with: 16 | filesizelimit: 10485760 # this is 10MB so we can sync to HF Spaces 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Sync to Hugging Face hub 2 | on: 3 | push: 4 | branches: [main] 5 | 6 | # to run this workflow manually from the Actions tab 7 | workflow_dispatch: 8 | 9 | jobs: 10 | sync-to-hub: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | lfs: true 17 | - name: Push to hub 18 | env: 19 | HF_TOKEN: ${{ secrets.HF_TOKEN }} 20 | run: git push https://sbhatti2009:$HF_TOKEN@huggingface.co/spaces/sbhatti2009/NotionGPT main 21 | -------------------------------------------------------------------------------- /process_page.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | 4 | from blueprints.utils import process_page 5 | 6 | 7 | def pretty_print_blueprint(page_id): 8 | blueprint_json = process_page(page_id) 9 | print(json.dumps(blueprint_json, indent=4)) 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser(description="Process and print a Notion page as a JSON blueprint.") 14 | parser.add_argument("page_id", help="The Notion page ID to process") 15 | args = parser.parse_args() 16 | 17 | pretty_print_blueprint(args.page_id) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /process_blueprint.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | import os 4 | 5 | from blueprints.architect import process_blueprint 6 | 7 | 8 | def load_blueprint(filename): 9 | with open(filename, "r") as file: 10 | return json.load(file) 11 | 12 | 13 | def main(): 14 | parser = argparse.ArgumentParser(description="Process a JSON blueprint to create content in Notion.") 15 | parser.add_argument("json_file", help="Location of the JSON blueprint file") 16 | args = parser.parse_args() 17 | 18 | notion_page_id = os.environ["NOTION_PAGE_ID"] 19 | 20 | blueprint = load_blueprint(args.json_file) 21 | process_blueprint(notion_page_id, blueprint) 22 | 23 | 24 | if __name__ == "__main__": 25 | main() 26 | -------------------------------------------------------------------------------- /generate_content.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | from blueprints.architect import process_blueprint, generate_blueprint 5 | 6 | 7 | def main(): 8 | parser = argparse.ArgumentParser(description="Generates Notion content given a brief description") 9 | parser.add_argument("description", help="Text description of the desired content in Notion.") 10 | args = parser.parse_args() 11 | 12 | notion_page_id = os.environ["NOTION_PAGE_ID"] 13 | 14 | content = None 15 | for update in generate_blueprint(args.description): 16 | if isinstance(update, dict): 17 | content = update 18 | else: 19 | print(update, end="", flush=True) 20 | 21 | blueprint = content["blueprint"] 22 | process_blueprint(notion_page_id, blueprint) 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==23.2.1 2 | altair==5.3.0 3 | annotated-types==0.6.0 4 | anyio==4.3.0 5 | attrs==23.2.0 6 | certifi==2024.2.2 7 | charset-normalizer==3.3.2 8 | click==8.1.7 9 | contourpy==1.2.1 10 | cycler==0.12.1 11 | distro==1.9.0 12 | dnspython==2.6.1 13 | email_validator==2.1.1 14 | emoji==2.11.0 15 | exceptiongroup==1.2.0 16 | fastapi==0.111.0 17 | fastapi-cli==0.0.2 18 | ffmpy==0.3.2 19 | filelock==3.14.0 20 | fonttools==4.51.0 21 | fsspec==2024.3.1 22 | gradio==4.29.0 23 | gradio_client==0.16.1 24 | h11==0.14.0 25 | httpcore==1.0.5 26 | httptools==0.6.1 27 | httpx==0.27.0 28 | huggingface-hub==0.23.0 29 | idna==3.7 30 | importlib_resources==6.4.0 31 | Jinja2==3.1.4 32 | json_repair==0.* 33 | jsonschema==4.22.0 34 | jsonschema-specifications==2023.12.1 35 | kiwisolver==1.4.5 36 | markdown-it-py==3.0.0 37 | MarkupSafe==2.1.5 38 | matplotlib==3.8.4 39 | mdurl==0.1.2 40 | notion-client==2.2.1 41 | numpy==1.26.4 42 | openai==1.20.0 43 | orjson==3.10.3 44 | packaging==24.0 45 | pandas==2.2.2 46 | pillow==10.3.0 47 | pydantic==2.7.0 48 | pydantic_core==2.18.1 49 | pydub==0.25.1 50 | Pygments==2.18.0 51 | pyparsing==3.1.2 52 | python-dateutil==2.9.0.post0 53 | python-dotenv==1.0.1 54 | python-multipart==0.0.9 55 | pytz==2024.1 56 | PyYAML==6.0.1 57 | referencing==0.35.1 58 | requests==2.31.0 59 | rich==13.7.1 60 | rpds-py==0.18.1 61 | ruff==0.4.3 62 | semantic-version==2.10.0 63 | shellingham==1.5.4 64 | six==1.16.0 65 | sniffio==1.3.1 66 | starlette==0.37.2 67 | tomlkit==0.12.0 68 | toolz==0.12.1 69 | tqdm==4.66.3 70 | typer==0.12.3 71 | typing_extensions==4.11.0 72 | tzdata==2024.1 73 | ujson==5.9.0 74 | urllib3==2.2.1 75 | uvicorn==0.29.0 76 | uvloop==0.19.0 77 | watchfiles==0.21.0 78 | websockets==11.0.3 79 | zipp==3.18.1 80 | -------------------------------------------------------------------------------- /gradio_app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | 5 | import gradio as gr 6 | from pydantic import ValidationError 7 | 8 | from blueprints.architect import generate_blueprint, process_blueprint 9 | from blueprints.schema import OpenAIResponse 10 | 11 | NOTION_PAGE_ID = os.environ["NOTION_PAGE_ID"] 12 | MODEL_NAME = os.environ["NOTION_GPT_MODEL_NAME"] 13 | 14 | 15 | def gradio_blueprint_interface(description, model_name, force_json, auto_restart, temperature, top_p, error=None, failed_response=None): 16 | try: 17 | cumulative_content = "" 18 | for update in generate_blueprint(description, model_name, force_json, temperature, top_p, error, failed_response): 19 | if isinstance(update, dict): 20 | yield "Blueprint generation complete. Processing blueprint..." 21 | try: 22 | OpenAIResponse(**update) 23 | process_blueprint(NOTION_PAGE_ID, update.get("blueprint", {})) 24 | yield f"Blueprint successfully processed! 🎉" 25 | except ValidationError as e: 26 | error = json.dumps(e.json(), separators=(",", ":")) 27 | failed_response = json.dumps(update, separators=(",", ":")) 28 | if auto_restart: 29 | yield f"Validation failed: f{error}. Restarting..." 30 | time.sleep(5) 31 | yield from gradio_blueprint_interface(description, model_name, force_json, auto_restart, temperature, top_p, error, failed_response) 32 | else: 33 | yield f"Validation failed: f{error}." 34 | else: 35 | cumulative_content += update 36 | yield cumulative_content 37 | except Exception as e: 38 | if auto_restart: 39 | yield f"Error encountered: {str(e)}. Restarting..." 40 | time.sleep(5) 41 | yield from gradio_blueprint_interface(description, model_name, force_json, auto_restart, temperature, top_p) 42 | else: 43 | yield f"Error encountered: {str(e)}." 44 | 45 | 46 | def main(): 47 | iface = gr.Interface( 48 | fn=gradio_blueprint_interface, 49 | inputs=[ 50 | gr.Textbox(label="Describe your Notion page", value="Generate me a detailed and comprehensive Notion page to plan a 2-week vacation to Tokyo and Kyoto."), 51 | gr.Dropdown(label="OpenAI Model", choices=[MODEL_NAME, "gpt-4-turbo", "gpt-4o", "gpt-3.5-turbo"], value=MODEL_NAME), 52 | ], 53 | additional_inputs=[ 54 | gr.Checkbox(label="Force JSON", value=False), 55 | gr.Checkbox(label="Auto Restart", value=True), 56 | gr.Slider(label="Temperature", minimum=0.1, maximum=1.0, step=0.1, value=0.4), 57 | gr.Slider(label="Top P", minimum=0.1, maximum=1.0, step=0.1, value=0.9), 58 | ], 59 | outputs=[gr.Text(label="Process Output")], 60 | title="NotionGPT", 61 | description="Enter a description to generate and process a custom Notion page layout." 62 | ) 63 | iface.launch() 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NotionGPT 3 | emoji: 🚀 4 | colorFrom: purple 5 | colorTo: blue 6 | sdk: gradio 7 | sdk_version: 4.29.0 8 | app_file: gradio_app.py 9 | pinned: false 10 | --- 11 | 12 | # Quickstart Guide 13 | 14 | ## Notion API 15 | 16 | ### Creating your integration in Notion 17 | 18 | 1. Go to [Notion's My Integrations page](https://www.notion.so/my-integrations). 19 | 2. Click `+ New Integration`. 20 | 3. Enter the integration name. (NotionGPT is a good choice) 21 | 22 | ### Get your API key 23 | 24 | 1. Click on the integration you created. 25 | 2. Visit the `Secrets` tab. 26 | 3. Copy the `Internal Integration Secret` and save it for later. 27 | 28 | ### Give your integration page permissions 29 | 30 | 1. Create the page where NotionGPT will store all the generated content. (We named ours NotionGPT Generator) 31 | 2. Get the page ID by copying the last series of characters in the URL. (e.g. `https://www.notion.so/NotionGPT-Generator-`) 32 | 3. Click on the `...` more menu in the top-right corner of the page. 33 | 4. Scroll down to connections and click `Connect to`. 34 | 5. Search for the integration you created, click it, and select `Confirm`. 35 | 36 | ## OpenAI API 37 | 38 | ### Setting up your account 39 | 1. Create an account on [OpenAI](https://platform.openai.com/signup) or log in if you already have one. 40 | 2. Visit the [Billings page](https://platform.openai.com/settings/organization/billing/overview) and add a payment method to your account. 41 | 3. Click `Add to credit balance` and add some funds to your account, we recommend $10 to start. 42 | 43 | ### Get your API key 44 | 45 | 1. Go to the [API keys page](https://platform.openai.com/account/api-keys). (Note that you may have to verify your phone number to do so) 46 | 2. Click `+ Create new secret key`, and name it. (NotionGPT is a good choice) 47 | 3. Copy the key and save it for later, you won't be able to see it again. 48 | 49 | ### Finetuning the GPT model 50 | 51 | 1. Download `data/finetuning_data_cot_v12.jsonl` in the GitHub repository. 52 | 2. Visit the [OpenAI Fine-tuning page](https://platform.openai.com/finetune) and click `+ Create` in the top right corner. 53 | 3. Fill in the form with the following information, and leave the rest as default: 54 | - `Base Model`: `gpt-3.5-turbo-0125` (or any other model you prefer) 55 | - `Training data`: Upload the `finetuning_data_cot_v12.jsonl` file. 56 | - `Suffix`: NotionGPT (or any other name you prefer) 57 | 4. Click `Create` and wait for the model to finish training. 58 | 5. Once the model is trained, copy the model name and save it for later. (It should look something like `ft:gpt-3.5-turbo-0125:personal:notiongpt:`) 59 | 60 | ## Unsplash API 61 | 62 | ### Get your API key 63 | 64 | 1. Create an account on [Unsplash](https://unsplash.com/join) or log in if you already have one. 65 | 2. Go to [Unsplash's Applications page](https://unsplash.com/oauth/applications). 66 | 3. Click `New Application`. 67 | 4. Agree to all the terms and condtions and click `Accept terms`. 68 | 5. Fill in your application name and description. (NotionGPT is a good choice for both) 69 | 6. Click `Create application`. 70 | 7. Scroll down to keys and copy the `Access Key` and save it for later. 71 | 72 | ## Setting up the app 73 | 74 | ### Hugging Face Spaces 75 | 76 | 1. Create an account on [Hugging Face](https://huggingface.co/join) or log in if you already have one. 77 | 2. Visit the NotionGPT space [here](https://huggingface.co/spaces/sbhatti2009/NotionGPT). 78 | 3. Click on the `...` more menu in the top-right corner of the space. 79 | 4. Select `Duplicate this space`. 80 | 5. Keep everything the same, but set your `Space secrets` according to the information below: 81 | - `UNSPLASH_ACCESS_KEY`: Your Unsplash API key. 82 | - `OPENAI_API_KEY`: Your OpenAI API key. 83 | - `NOTION_KEY`: Your Notion API key. 84 | - `NOTION_PAGE_ID`: The page ID of the page you created in Notion. 85 | - `NOTION_GPT_MODEL_NAME`: The name of the fine-tuned model. 86 | 6. Click `Duplicate Space`, and wait for the application to build. (Note that this may take a few minutes) 87 | 7. Enjoy using NotionGPT! 🎉 88 | -------------------------------------------------------------------------------- /blueprints/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from notion_client import Client 4 | 5 | NOTION_KEY = os.environ["NOTION_KEY"] 6 | notion = Client(auth=NOTION_KEY) 7 | 8 | 9 | def process_page(page_id): 10 | page = notion.pages.retrieve(page_id) 11 | page_blueprint = { 12 | "type": "page", 13 | "title": page["properties"]["title"]["title"][0]["text"]["content"] 14 | } 15 | if page["icon"] is not None: 16 | page_blueprint["icon"] = page["icon"]["emoji"] 17 | page_blueprint["children"] = fetch_and_process_children(page_id) 18 | return page_blueprint 19 | 20 | 21 | def process_rich_text(rich_text): 22 | content = [] 23 | for text in rich_text: 24 | style = [] 25 | if text["annotations"]["bold"]: 26 | style.append("bold") 27 | if text["annotations"]["italic"]: 28 | style.append("italic") 29 | if text["annotations"]["strikethrough"]: 30 | style.append("strikethrough") 31 | if text["annotations"]["underline"]: 32 | style.append("underline") 33 | if text["annotations"]["code"]: 34 | style.append("code") 35 | content.append({ 36 | "text": text["plain_text"], 37 | "style": style 38 | }) 39 | return content 40 | 41 | 42 | def fetch_and_process_children(parent_id): 43 | children = [] 44 | block_children = notion.blocks.children.list(block_id=parent_id)["results"] 45 | 46 | list_buffer = None 47 | 48 | for child in block_children: 49 | if child["type"] in ["bulleted_list_item", "numbered_list_item", "to_do"]: 50 | list_type = "to_do_list" if child["type"] == "to_do" else "bulleted_list" if child["type"] == "bulleted_list_item" else "numbered_list" 51 | 52 | if list_buffer is None or list_buffer["type"] != list_type: 53 | if list_buffer: 54 | children.append(list_buffer) 55 | list_buffer = {"type": list_type, "items": []} 56 | 57 | if list_type == "to_do_list": 58 | todo_item_text = child["to_do"]["rich_text"][0]["text"]["content"] 59 | todo_item_checked = child["to_do"]["checked"] 60 | list_buffer["items"].append({"text": todo_item_text, "checked": todo_item_checked}) 61 | else: 62 | list_item_text = child[child["type"]]["rich_text"][0]["text"]["content"] 63 | list_buffer["items"].append(list_item_text) 64 | 65 | else: 66 | if list_buffer: 67 | children.append(list_buffer) 68 | list_buffer = None 69 | 70 | formatted_block = format_block(child) 71 | 72 | if child.get("has_children", False): 73 | formatted_block[ 74 | "columns" if child["type"] == "column_list" else "children"] = fetch_and_process_children( 75 | child["id"]) 76 | 77 | children.append(formatted_block) 78 | 79 | if list_buffer: 80 | children.append(list_buffer) 81 | 82 | return children 83 | 84 | 85 | def format_block(block): 86 | block_type = block["type"] 87 | block_json = {"type": block_type} 88 | 89 | if block_type == "child_page": 90 | block_json["type"] = "page" 91 | page_details = notion.pages.retrieve(block["id"]) 92 | block_json["title"] = page_details["properties"]["title"]["title"][0]["text"]["content"] 93 | if page_details["icon"] is not None: 94 | block_json["icon"] = page_details["icon"]["emoji"] 95 | 96 | elif block_type == "child_database": 97 | block_json["type"] = "database" 98 | database_details = notion.databases.retrieve(database_id=block["id"]) 99 | block_json["title"] = database_details["title"][0]["plain_text"] 100 | if database_details["icon"] is not None: 101 | block_json["icon"] = database_details["icon"]["emoji"] 102 | if database_details["is_inline"]: 103 | block_json["is_inline"] = database_details["is_inline"] 104 | properties = {} 105 | for prop_name, prop_details in database_details["properties"].items(): 106 | prop_type = prop_details["type"] 107 | if prop_type in ["select", "multi_select"]: 108 | options = [{"name": option["name"], "color": option.get("color", "default")} for option in 109 | prop_details[prop_type]["options"]] 110 | properties[prop_name] = {"type": prop_type, "options": options} 111 | elif prop_type == "number": 112 | properties[prop_name] = {"type": prop_type, "format": prop_details["number"].get("format", "none")} 113 | else: 114 | properties[prop_name] = {"type": prop_type} 115 | block_json["schema"] = properties 116 | 117 | if block_type == "paragraph": 118 | block_json["content"] = process_rich_text(block["paragraph"]["rich_text"]) 119 | 120 | elif block_type.startswith("heading_"): 121 | block_json["text"] = block[block_type]["rich_text"][0]["text"]["content"] 122 | 123 | elif block_type == "bulleted_list_item": 124 | block_json["items"] = [text["plain_text"] for text in block["bulleted_list_item"]["rich_text"]] 125 | 126 | elif block_type == "numbered_list_item": 127 | block_json["items"] = [text["plain_text"] for text in block["numbered_list_item"]["rich_text"]] 128 | 129 | elif block_type == "to_do": 130 | block_json["text"] = block["to_do"]["rich_text"][0]["text"]["content"] 131 | block_json["checked"] = block["to_do"]["checked"] 132 | 133 | elif block_type == "toggle": 134 | block_json["title"] = block["toggle"]["rich_text"][0]["text"]["content"] 135 | 136 | elif block_type == "callout": 137 | block_json["icon"] = block["callout"]["icon"]["emoji"] 138 | block_json["color"] = block["callout"]["color"] 139 | block_json["content"] = process_rich_text(block["callout"]["rich_text"]) 140 | 141 | elif block_type == "quote": 142 | block_json["content"] = process_rich_text(block["quote"]["rich_text"]) 143 | 144 | return block_json 145 | -------------------------------------------------------------------------------- /blueprints/schema.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import List, Union, Optional, Dict, Literal, Annotated 5 | 6 | from pydantic import BaseModel, Field, RootModel, model_validator 7 | 8 | 9 | class TextStyle(str, Enum): 10 | bold = "bold" 11 | italic = "italic" 12 | strikethrough = "strikethrough" 13 | underline = "underline" 14 | code = "code" 15 | 16 | 17 | class Color(str, Enum): 18 | blue = "blue" 19 | brown = "brown" 20 | default = "default" 21 | gray = "gray" 22 | green = "green" 23 | orange = "orange" 24 | pink = "pink" 25 | purple = "purple" 26 | red = "red" 27 | yellow = "yellow" 28 | 29 | 30 | class BackgroundColor(str, Enum): 31 | blue_background = "blue_background" 32 | brown_background = "brown_background" 33 | default = "default" 34 | gray_background = "gray_background" 35 | green_background = "gray_background" 36 | orange_background = "orange_background" 37 | pink_background = "pink_background" 38 | purple_background = "purple_background" 39 | red_background = "red_background" 40 | yellow_background = "yellow_background" 41 | 42 | 43 | class NumberFormat(str, Enum): 44 | argentinepeso = "argentinepeso" 45 | baht = "baht" 46 | australiandollar = "australiandollar" 47 | canadiandollar = "canadiandollar" 48 | chileanpeso = "chileanpeso" 49 | colombianpeso = "colombianpeso" 50 | danishkrone = "danishkrone" 51 | dirham = "dirham" 52 | dollar = "dollar" 53 | euro = "euro" 54 | forint = "forint" 55 | franc = "franc" 56 | hongkongdollar = "hongkongdollar" 57 | koruna = "koruna" 58 | krona = "krona" 59 | leu = "leu" 60 | lira = "lira" 61 | mexicanpeso = "mexicanpeso" 62 | newtaiwandollar = "newtaiwandollar" 63 | newzealanddollar = "newzealanddollar" 64 | norwegiankrone = "norwegiankrone" 65 | number = "number" 66 | numberwithcommas = "numberwithcommas" 67 | percent = "percent" 68 | philippinepeso = "philippinepeso" 69 | pound = "pound" 70 | peruviansol = "peruviansol" 71 | rand = "rand" 72 | real = "real" 73 | ringgit = "ringgit" 74 | riyal = "riyal" 75 | ruble = "ruble" 76 | rupee = "rupee" 77 | rupiah = "rupiah" 78 | shekel = "shekel" 79 | singaporedollar = "singaporedollar" 80 | uruguayanpeso = "uruguayanpeso" 81 | yen = "yen" 82 | yuan = "yuan" 83 | won = "won" 84 | zloty = "zloty" 85 | 86 | 87 | class PropertyType(str, Enum): 88 | checkbox = "checkbox" 89 | created_by = "created_by" 90 | created_time = "created_time" 91 | date = "date" 92 | email = "email" 93 | files = "files" 94 | last_edited_by = "last_edited_by" 95 | last_edited_time = "last_edited_time" 96 | multi_select = "multi_select" 97 | number = "number" 98 | people = "people" 99 | phone_number = "phone_number" 100 | rich_text = "rich_text" 101 | select = "select" 102 | title = "title" 103 | url = "url" 104 | 105 | 106 | class HeadingType(str, Enum): 107 | heading_1 = "heading_1" 108 | heading_2 = "heading_2" 109 | heading_3 = "heading_3" 110 | 111 | 112 | class ListType(str, Enum): 113 | bulleted_list = "bulleted_list" 114 | numbered_list = "numbered_list" 115 | 116 | 117 | class SchemaOption(BaseModel): 118 | name: str 119 | color: Color 120 | 121 | 122 | class PropertySchema(BaseModel): 123 | type: PropertyType 124 | format: Optional[NumberFormat] = None 125 | options: Optional[List[SchemaOption]] = None 126 | 127 | @model_validator(mode="before") 128 | def validate_format(cls, values): 129 | if "type" in values and values["type"] == PropertyType.number and not values.get("format"): 130 | raise ValueError(f"Property with type 'number' requires 'format' to be specified") 131 | return values 132 | 133 | @model_validator(mode="before") 134 | def validate_options(cls, values): 135 | if "type" in values and values["type"] in (PropertyType.select, PropertyType.multi_select) and not values.get("options"): 136 | raise ValueError(f"Property with type '{values['type']}' requires 'options' to be specified") 137 | return values 138 | 139 | 140 | class DatabaseSchema(RootModel[Dict[str, PropertySchema]]): 141 | @model_validator(mode="before") 142 | def validate_title(cls, values): 143 | title_count = sum(1 for prop in values.values() if isinstance(prop, dict) and prop.get("type") == PropertyType.title) 144 | if title_count != 1: 145 | raise ValueError("There must be exactly one property with type 'title'") 146 | return values 147 | 148 | 149 | class RichTextContent(BaseModel): 150 | text: str 151 | style: List[TextStyle] = [] 152 | 153 | 154 | class Divider(BaseModel): 155 | type: Literal["divider"] 156 | 157 | 158 | class TableOfContents(BaseModel): 159 | type: Literal["table_of_contents"] 160 | 161 | 162 | class Heading(BaseModel): 163 | type: Literal["heading_1", "heading_2", "heading_3"] 164 | text: str 165 | 166 | 167 | class Paragraph(BaseModel): 168 | type: Literal["paragraph"] 169 | content: List[RichTextContent] 170 | 171 | 172 | class ListBlock(BaseModel): 173 | type: Literal["bulleted_list", "numbered_list"] 174 | items: List[str] 175 | 176 | 177 | class ToDoListItem(BaseModel): 178 | text: str 179 | checked: bool 180 | 181 | 182 | class ToDoList(BaseModel): 183 | type: Literal["to_do_list"] 184 | items: List[ToDoListItem] 185 | 186 | 187 | class Toggle(BaseModel): 188 | type: Literal["toggle"] 189 | text: str 190 | children: List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, ColumnList, Callout, Quote]] 191 | 192 | 193 | class Column(BaseModel): 194 | type: Literal["column"] 195 | children: List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]] 196 | 197 | 198 | class ColumnList(BaseModel): 199 | type: Literal["column_list"] 200 | columns: List[Column] 201 | 202 | 203 | class Callout(BaseModel): 204 | type: Literal["callout"] 205 | icon: str 206 | color: BackgroundColor 207 | content: List[RichTextContent] 208 | children: Optional[List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]]] = None 209 | 210 | 211 | class Quote(BaseModel): 212 | type: Literal["quote"] 213 | content: List[RichTextContent] 214 | children: Optional[List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]]] = None 215 | 216 | 217 | class Database(BaseModel): 218 | type: Literal["database"] 219 | title: str 220 | icon: str 221 | is_inline: Optional[bool] = False 222 | schema: DatabaseSchema 223 | 224 | 225 | class Page(BaseModel): 226 | type: Literal["page"] 227 | title: str 228 | 229 | children: List[Annotated[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, ColumnList, Callout, Quote, Database, Page], Field(discriminator="type")]] 230 | 231 | 232 | class OpenAIResponse(BaseModel): 233 | response: str 234 | blueprint: Page 235 | 236 | @model_validator(mode='after') 237 | def validate_children(cls, values): 238 | if len(values.blueprint.children) <= 1: 239 | raise ValueError("The root Page (blueprint) must have more than one child. Note: DO NOT JUST GENERATE A SINGULAR CALLOUT BOX IN A PAGE AS YOUR BLUEPRINT. THIS IS VERY IMPORTANT.") 240 | return values 241 | 242 | 243 | Page.update_forward_refs() 244 | Toggle.update_forward_refs() 245 | Column.update_forward_refs() 246 | ColumnList.update_forward_refs() 247 | Callout.update_forward_refs() 248 | Quote.update_forward_refs() 249 | -------------------------------------------------------------------------------- /data/process_data.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import json 3 | 4 | 5 | def decode_unicode_escapes(blueprint): 6 | decoded = (blueprint.encode("latin_1").decode("unicode_escape") 7 | .encode("utf-16", "surrogatepass") 8 | .decode("utf-16") 9 | .replace("\u00A0", " ")) 10 | return decoded 11 | 12 | 13 | def escape_newlines(obj): 14 | if isinstance(obj, str): 15 | return obj.replace("\n", "\\n").replace('"', '\\"') 16 | elif isinstance(obj, dict): 17 | return {key: escape_newlines(value) for key, value in obj.items()} 18 | elif isinstance(obj, list): 19 | return [escape_newlines(item) for item in obj] 20 | return obj 21 | 22 | 23 | blueprints_df = pd.read_csv("./example_blueprints.csv", encoding="utf-8-sig") 24 | 25 | system_prompt = """You are NotionGPT, a state-of-the-art template designer for Notion, programmed to create custom JSON blueprints that represent detailed, organized, and highly functional Notion templates. Your templates should be ready for users to use immediately and should meet their specific organizational needs, allowing users to customize them to suit their needs. 26 | 27 | Please respond ONLY with valid json that conforms to the `OpenAIResponse(BaseModel)` class as defined by pydantic in the Python code below: 28 | 29 | ``` 30 | from __future__ import annotations 31 | 32 | from enum import Enum 33 | from typing import List, Union, Optional, Dict, Literal, Annotated 34 | 35 | from pydantic import BaseModel, Field, RootModel, model_validator 36 | 37 | 38 | class TextStyle(str, Enum): 39 | bold = "bold" 40 | italic = "italic" 41 | strikethrough = "strikethrough" 42 | underline = "underline" 43 | code = "code" 44 | 45 | 46 | class Color(str, Enum): 47 | blue = "blue" 48 | brown = "brown" 49 | default = "default" 50 | gray = "gray" 51 | green = "green" 52 | orange = "orange" 53 | pink = "pink" 54 | purple = "purple" 55 | red = "red" 56 | yellow = "yellow" 57 | 58 | 59 | class BackgroundColor(str, Enum): 60 | blue_background = "blue_background" 61 | brown_background = "brown_background" 62 | default = "default" 63 | gray_background = "gray_background" 64 | green_background = "gray_background" 65 | orange_background = "orange_background" 66 | pink_background = "pink_background" 67 | purple_background = "purple_background" 68 | red_background = "red_background" 69 | yellow_background = "yellow_background" 70 | 71 | 72 | class NumberFormat(str, Enum): 73 | argentinepeso = "argentinepeso" 74 | baht = "baht" 75 | australiandollar = "australiandollar" 76 | canadiandollar = "canadiandollar" 77 | chileanpeso = "chileanpeso" 78 | colombianpeso = "colombianpeso" 79 | danishkrone = "danishkrone" 80 | dirham = "dirham" 81 | dollar = "dollar" 82 | euro = "euro" 83 | forint = "forint" 84 | franc = "franc" 85 | hongkongdollar = "hongkongdollar" 86 | koruna = "koruna" 87 | krona = "krona" 88 | leu = "leu" 89 | lira = "lira" 90 | mexicanpeso = "mexicanpeso" 91 | newtaiwandollar = "newtaiwandollar" 92 | newzealanddollar = "newzealanddollar" 93 | norwegiankrone = "norwegiankrone" 94 | number = "number" 95 | numberwithcommas = "numberwithcommas" 96 | percent = "percent" 97 | philippinepeso = "philippinepeso" 98 | pound = "pound" 99 | peruviansol = "peruviansol" 100 | rand = "rand" 101 | real = "real" 102 | ringgit = "ringgit" 103 | riyal = "riyal" 104 | ruble = "ruble" 105 | rupee = "rupee" 106 | rupiah = "rupiah" 107 | shekel = "shekel" 108 | singaporedollar = "singaporedollar" 109 | uruguayanpeso = "uruguayanpeso" 110 | yen = "yen" 111 | yuan = "yuan" 112 | won = "won" 113 | zloty = "zloty" 114 | 115 | 116 | class PropertyType(str, Enum): 117 | checkbox = "checkbox" 118 | created_by = "created_by" 119 | created_time = "created_time" 120 | date = "date" 121 | email = "email" 122 | files = "files" 123 | last_edited_by = "last_edited_by" 124 | last_edited_time = "last_edited_time" 125 | multi_select = "multi_select" 126 | number = "number" 127 | people = "people" 128 | phone_number = "phone_number" 129 | rich_text = "rich_text" 130 | select = "select" 131 | title = "title" 132 | url = "url" 133 | 134 | 135 | class HeadingType(str, Enum): 136 | heading_1 = "heading_1" 137 | heading_2 = "heading_2" 138 | heading_3 = "heading_3" 139 | 140 | 141 | class ListType(str, Enum): 142 | bulleted_list = "bulleted_list" 143 | numbered_list = "numbered_list" 144 | 145 | 146 | class SchemaOption(BaseModel): 147 | name: str 148 | color: Color 149 | 150 | 151 | class PropertySchema(BaseModel): 152 | type: PropertyType 153 | format: Optional[NumberFormat] = None 154 | options: Optional[List[SchemaOption]] = None 155 | 156 | @model_validator(mode="before") 157 | def validate_format(cls, values): 158 | if "type" in values and values["type"] == PropertyType.number and not values.get("format"): 159 | raise ValueError(f"Property with type 'number' requires 'format' to be specified") 160 | return values 161 | 162 | @model_validator(mode="before") 163 | def validate_options(cls, values): 164 | if "type" in values and values["type"] in (PropertyType.select, PropertyType.multi_select) and not values.get("options"): 165 | raise ValueError(f"Property with type '{values['type']}' requires 'options' to be specified") 166 | return values 167 | 168 | 169 | class DatabaseSchema(RootModel[Dict[str, PropertySchema]]): 170 | @model_validator(mode="before") 171 | def validate_title(cls, values): 172 | title_count = sum(1 for prop in values.values() if isinstance(prop, dict) and prop.get("type") == PropertyType.title) 173 | if title_count != 1: 174 | raise ValueError("There must be exactly one property with type 'title'") 175 | return values 176 | 177 | 178 | class RichTextContent(BaseModel): 179 | text: str 180 | style: List[TextStyle] = [] 181 | 182 | 183 | class Divider(BaseModel): 184 | type: Literal["divider"] 185 | 186 | 187 | class TableOfContents(BaseModel): 188 | type: Literal["table_of_contents"] 189 | 190 | 191 | class Heading(BaseModel): 192 | type: Literal["heading_1", "heading_2", "heading_3"] 193 | text: str 194 | 195 | 196 | class Paragraph(BaseModel): 197 | type: Literal["paragraph"] 198 | content: List[RichTextContent] 199 | 200 | 201 | class ListBlock(BaseModel): 202 | type: Literal["bulleted_list", "numbered_list"] 203 | items: List[str] 204 | 205 | 206 | class ToDoListItem(BaseModel): 207 | text: str 208 | checked: bool 209 | 210 | 211 | class ToDoList(BaseModel): 212 | type: Literal["to_do_list"] 213 | items: List[ToDoListItem] 214 | 215 | 216 | class Toggle(BaseModel): 217 | type: Literal["toggle"] 218 | text: str 219 | children: List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, ColumnList, Callout, Quote]] 220 | 221 | 222 | class Column(BaseModel): 223 | type: Literal["column"] 224 | children: List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]] 225 | 226 | 227 | class ColumnList(BaseModel): 228 | type: Literal["column_list"] 229 | columns: List[Column] 230 | 231 | 232 | class Callout(BaseModel): 233 | type: Literal["callout"] 234 | icon: str 235 | color: BackgroundColor 236 | content: List[RichTextContent] 237 | children: Optional[List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]]] = None 238 | 239 | 240 | class Quote(BaseModel): 241 | type: Literal["quote"] 242 | content: List[RichTextContent] 243 | children: Optional[List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]]] = None 244 | 245 | 246 | class Database(BaseModel): 247 | type: Literal["database"] 248 | title: str 249 | icon: str 250 | is_inline: Optional[bool] = False 251 | schema: DatabaseSchema 252 | 253 | 254 | class Page(BaseModel): 255 | type: Literal["page"] 256 | title: str 257 | 258 | children: List[Annotated[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, ColumnList, Callout, Quote, Database, Page], Field(discriminator="type")]] 259 | 260 | 261 | class OpenAIResponse(BaseModel): 262 | response: str 263 | blueprint: Page 264 | 265 | @model_validator(mode='after') 266 | def validate_children(cls, values): 267 | if len(values.blueprint.children) <= 1: 268 | raise ValueError("The root Page (blueprint) must have more than one child. Note: DO NOT JUST GENERATE A SINGULAR CALLOUT BOX IN A PAGE AS YOUR BLUEPRINT. THIS IS VERY IMPORTANT.") 269 | return values 270 | 271 | 272 | Page.update_forward_refs() 273 | Toggle.update_forward_refs() 274 | Column.update_forward_refs() 275 | ColumnList.update_forward_refs() 276 | Callout.update_forward_refs() 277 | Quote.update_forward_refs() 278 | ``` 279 | 280 | Do not include any additional text other than the object json as we will load this object with json.loads() and pydantic. Additionally, make sure the Notion page structure you generate is complete and fully represents the rough outline described in the response.""" 281 | 282 | messages = [] 283 | for _, row in blueprints_df.iterrows(): 284 | response = { 285 | "response": row["Response"], 286 | "blueprint": json.loads(row["Blueprint"]) 287 | } 288 | response = escape_newlines(response) 289 | response = json.dumps(response, separators=(",", ":")) 290 | response = decode_unicode_escapes(response) 291 | 292 | entry = { 293 | "messages": [ 294 | {"role": "system", "content": system_prompt}, 295 | {"role": "user", "content": row["Prompt"]}, 296 | {"role": "assistant", "content": response} 297 | ] 298 | } 299 | messages.append(entry) 300 | 301 | with open("finetuning_data_cot_v12.jsonl", "w") as f: 302 | for message in messages: 303 | print(json.dumps(message, ensure_ascii=False), file=f) 304 | -------------------------------------------------------------------------------- /blueprints/blocks.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import requests 4 | from notion_client import Client 5 | 6 | NOTION_KEY = os.environ["NOTION_KEY"] 7 | UNSPLASH_ACCESS_KEY = os.environ["UNSPLASH_ACCESS_KEY"] 8 | 9 | notion = Client(auth=NOTION_KEY) 10 | 11 | 12 | def get_unsplash_image_url(query): 13 | params = { 14 | "query": query, 15 | "client_id": UNSPLASH_ACCESS_KEY, 16 | "orientation": "landscape" 17 | } 18 | response = requests.get("https://api.unsplash.com/search/photos", params=params) 19 | 20 | if response.status_code == 200: 21 | results = response.json()["results"] 22 | if results: 23 | return results[0]["urls"]["regular"] 24 | return None 25 | 26 | 27 | def process_rich_text_content(text_blocks): 28 | rich_text_content = [] 29 | for block in text_blocks: 30 | text_content = { 31 | "type": "text", 32 | "text": { 33 | "content": block["text"], 34 | "link": None 35 | }, 36 | "annotations": { 37 | "bold": "bold" in block.get("style", []), 38 | "italic": "italic" in block.get("style", []), 39 | "strikethrough": "strikethrough" in block.get("style", []), 40 | "underline": "underline" in block.get("style", []), 41 | "code": "code" in block.get("style", []), 42 | "color": "default" 43 | } 44 | } 45 | rich_text_content.append(text_content) 46 | return rich_text_content 47 | 48 | 49 | def create_page(parent_id, title, icon, cover_image=True): 50 | image_url = get_unsplash_image_url(title) if cover_image else None 51 | 52 | page_payload = { 53 | "parent": { 54 | "page_id": parent_id 55 | }, 56 | "icon": { 57 | "emoji": icon 58 | }, 59 | "properties": { 60 | "title": { 61 | "title": [ 62 | { 63 | "type": "text", 64 | "text": { 65 | "content": title, 66 | }, 67 | } 68 | ] 69 | }, 70 | } 71 | } 72 | if image_url: 73 | page_payload["cover"] = { 74 | "type": "external", 75 | "external": { 76 | "url": image_url 77 | } 78 | } 79 | 80 | new_page = notion.pages.create(**page_payload) 81 | return new_page 82 | 83 | 84 | def create_database(parent_id, title, icon, schema, is_inline=False, cover_image=True): 85 | image_url = get_unsplash_image_url(title) if cover_image else None 86 | approved_types = ("checkbox", "created_by", "created_time", "date", "email", "files", "last_edited_by", 87 | "last_edited_time", "multi_select", "number", "people", "phone_number", "rich_text", "select", 88 | "title", "url") 89 | 90 | properties = {} 91 | for name, info in schema.items(): 92 | prop_type = info["type"] 93 | if prop_type not in approved_types: 94 | raise ValueError(f"Property type '{prop_type}' is currently unsupported.") 95 | 96 | if prop_type in ["select", "multi_select"]: 97 | properties[name] = {prop_type: {"options": info["options"]}} 98 | elif prop_type == "number": 99 | properties[name] = {prop_type: {"format": info.get("format", "number")}} 100 | else: 101 | properties[name] = {prop_type: {}} 102 | 103 | database_payload = { 104 | "parent": { 105 | "page_id": parent_id 106 | }, 107 | "icon": { 108 | "emoji": icon 109 | }, 110 | "title": [ 111 | { 112 | "type": "text", 113 | "text": { 114 | "content": title 115 | } 116 | } 117 | ], 118 | "properties": properties, 119 | "is_inline": is_inline 120 | } 121 | 122 | if image_url: 123 | database_payload["cover"] = { 124 | "type": "external", 125 | "external": { 126 | "url": image_url 127 | } 128 | } 129 | 130 | new_database = notion.databases.create(**database_payload) 131 | return new_database 132 | 133 | 134 | def create_divider(parent_id): 135 | divider_block = notion.blocks.children.append(**{ 136 | "block_id": parent_id, 137 | "children": [ 138 | { 139 | "object": "block", 140 | "type": "divider", 141 | "divider": {}, 142 | } 143 | ] 144 | }) 145 | return divider_block 146 | 147 | 148 | def create_table_of_contents(parent_id): 149 | toc_block = notion.blocks.children.append(**{ 150 | "block_id": parent_id, 151 | "children": [ 152 | { 153 | "object": "block", 154 | "type": "table_of_contents", 155 | "table_of_contents": {} 156 | } 157 | ] 158 | }) 159 | return toc_block 160 | 161 | 162 | def create_heading(parent_id, text, level): 163 | heading_block = notion.blocks.children.append(**{ 164 | "block_id": parent_id, 165 | "children": [ 166 | { 167 | "object": "block", 168 | "type": f"heading_{level}", 169 | f"heading_{level}": { 170 | "rich_text": [ 171 | { 172 | "type": "text", 173 | "text": { 174 | "content": text 175 | } 176 | } 177 | ], 178 | }, 179 | } 180 | ] 181 | }) 182 | return heading_block 183 | 184 | 185 | def create_paragraph(parent_id, text_blocks): 186 | paragraph_block = notion.blocks.children.append(**{ 187 | "block_id": parent_id, 188 | "children": [ 189 | { 190 | "object": "block", 191 | "type": "paragraph", 192 | "paragraph": { 193 | "rich_text": process_rich_text_content(text_blocks) 194 | } 195 | } 196 | ] 197 | }) 198 | return paragraph_block 199 | 200 | 201 | def create_list(parent_id, items, bulleted=True): 202 | list_type = "bulleted_list_item" if bulleted else "numbered_list_item" 203 | children = [{ 204 | "object": "block", 205 | "type": list_type, 206 | list_type: { 207 | "rich_text": [ 208 | { 209 | "type": "text", 210 | "text": { 211 | "content": item 212 | } 213 | } 214 | ] 215 | } 216 | } for item in items] 217 | 218 | list_block = notion.blocks.children.append(**{ 219 | "block_id": parent_id, 220 | "children": children 221 | }) 222 | return list_block 223 | 224 | 225 | def create_todo_list(parent_id, items): 226 | children = [{ 227 | "object": "block", 228 | "type": "to_do", 229 | "to_do": { 230 | "rich_text": [ 231 | { 232 | "type": "text", 233 | "text": { 234 | "content": item["text"] 235 | } 236 | } 237 | ], 238 | "checked": item.get("checked", False), 239 | }, 240 | } for item in items] 241 | 242 | todo_block = notion.blocks.children.append(**{ 243 | "block_id": parent_id, 244 | "children": children 245 | }) 246 | return todo_block 247 | 248 | 249 | def create_toggle(parent_id, title): 250 | toggle_block = notion.blocks.children.append(**{ 251 | "block_id": parent_id, 252 | "children": [ 253 | { 254 | "object": "block", 255 | "type": "toggle", 256 | "toggle": { 257 | "rich_text": [ 258 | { 259 | "type": "text", 260 | "text": { 261 | "content": title 262 | } 263 | } 264 | ], 265 | } 266 | } 267 | ] 268 | }) 269 | return toggle_block 270 | 271 | 272 | def create_column_list(parent_id, num_columns=2): 273 | column_list_block = notion.blocks.children.append(**{ 274 | "block_id": parent_id, 275 | "children": [ 276 | { 277 | "object": "block", 278 | "type": "column_list", 279 | "column_list": { 280 | "children": [ 281 | { 282 | "object": "block", 283 | "type": "column", 284 | "column": { 285 | "children": [ 286 | { 287 | "object": "block", 288 | "type": "paragraph", 289 | "paragraph": { 290 | "rich_text": [ 291 | { 292 | "type": "text", 293 | "text": { 294 | "content": "placeholder", 295 | } 296 | } 297 | ] 298 | } 299 | } 300 | ] 301 | } 302 | } for _ in range(num_columns) 303 | ] 304 | } 305 | } 306 | ] 307 | }) 308 | 309 | column_details = notion.blocks.children.list(block_id=column_list_block["results"][0]["id"]) 310 | column_ids = [] 311 | 312 | for column in column_details["results"]: 313 | column_ids.append(column["id"]) 314 | children_details = notion.blocks.children.list(block_id=column["id"]) 315 | for child in children_details["results"]: 316 | if "paragraph" in child and child["paragraph"]["rich_text"][0]["text"]["content"] == "placeholder": 317 | notion.blocks.delete(child["id"]) 318 | 319 | return column_ids 320 | 321 | 322 | def create_callout(parent_id, text_blocks, icon, color="default"): 323 | callout_block = notion.blocks.children.append(**{ 324 | "block_id": parent_id, 325 | "children": [ 326 | { 327 | "object": "block", 328 | "type": "callout", 329 | "callout": { 330 | "rich_text": process_rich_text_content(text_blocks), 331 | "icon": { 332 | "type": "emoji", 333 | "emoji": icon 334 | }, 335 | "color": color 336 | } 337 | } 338 | ] 339 | }) 340 | return callout_block 341 | 342 | 343 | def create_quote(parent_id, text_blocks): 344 | quote_block = notion.blocks.children.append(**{ 345 | "block_id": parent_id, 346 | "children": [ 347 | { 348 | "object": "block", 349 | "type": "quote", 350 | "quote": { 351 | "rich_text": process_rich_text_content(text_blocks) 352 | } 353 | } 354 | ] 355 | }) 356 | return quote_block 357 | -------------------------------------------------------------------------------- /blueprints/architect.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | 4 | import emoji 5 | import json_repair 6 | from notion_client import Client 7 | from openai import OpenAI 8 | 9 | from .blocks import (create_page, create_database, create_divider, create_table_of_contents, create_heading, 10 | create_paragraph, create_list, create_todo_list, create_toggle, create_column_list, create_callout, 11 | create_quote) 12 | 13 | NOTION_KEY = os.environ["NOTION_KEY"] 14 | MODEL_NAME = os.environ["NOTION_GPT_MODEL_NAME"] 15 | 16 | notion = Client(auth=NOTION_KEY) 17 | client = OpenAI() 18 | 19 | 20 | def process_blueprint(parent_id, block_json): 21 | block_type = block_json["type"] 22 | 23 | if block_type == "page": 24 | title = block_json.get("title", "Untitled Page") 25 | emojis = [em for em in emoji.EMOJI_DATA if not any(char in em for char in ["🏻", "🏼", "🏽", "🏾", "🏿", "\u200d"]) and not em.startswith(("🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "🇭", "🇮", "🇯", "🇰", "🇱", "🇲", "🇳", "🇴", "🇵", "🇶", "🇷", "🇸", "🇹", "🇺", "🇻", "🇼", "🇽", "🇾", "🇿"))] 26 | icon = block_json.get("icon", random.choice(emojis)) 27 | page_id = create_page(parent_id, title, icon, cover_image=True)["id"] 28 | for child in block_json.get("children", []): 29 | process_blueprint(page_id, child) 30 | 31 | elif block_type == "database": 32 | title = block_json.get("title", "Untitled Database") 33 | emojis = [em for em in emoji.EMOJI_DATA if not any(char in em for char in ["🏻", "🏼", "🏽", "🏾", "🏿", "\u200d"]) and not em.startswith(("🇦", "🇧", "🇨", "🇩", "🇪", "🇫", "🇬", "🇭", "🇮", "🇯", "🇰", "🇱", "🇲", "🇳", "🇴", "🇵", "🇶", "🇷", "🇸", "🇹", "🇺", "🇻", "🇼", "🇽", "🇾", "🇿"))] 34 | icon = block_json.get("icon", random.choice(emojis)) 35 | schema = block_json.get("schema", {}) 36 | is_inline = block_json.get("is_inline", False) 37 | create_database(parent_id, title, icon, schema, is_inline=is_inline, cover_image=True) 38 | 39 | elif block_type == "divider": 40 | create_divider(parent_id) 41 | 42 | elif block_type == "table_of_contents": 43 | create_table_of_contents(parent_id) 44 | 45 | elif block_type.startswith("heading_"): 46 | text = block_json.get("text", "") 47 | level = int(block_type.split("_")[1]) 48 | create_heading(parent_id, text, level) 49 | 50 | elif block_type == "paragraph": 51 | text_blocks = block_json.get("content", []) 52 | create_paragraph(parent_id, text_blocks) 53 | 54 | elif block_type in ["bulleted_list", "numbered_list"]: 55 | items = block_json.get("items", []) 56 | create_list(parent_id, items, bulleted=True if block_type == "bulleted_list" else False) 57 | 58 | elif block_type == "to_do_list": 59 | items = block_json.get("items", []) 60 | create_todo_list(parent_id, items) 61 | 62 | elif block_type == "toggle": 63 | title = block_json.get("title", "Untitled Toggle") 64 | toggle_id = create_toggle(parent_id, title)["results"][0]["id"] 65 | for child in block_json.get("children", []): 66 | process_blueprint(toggle_id, child) 67 | 68 | elif block_type == "column_list": 69 | num_columns = len(block_json.get("columns", [])) 70 | column_ids = create_column_list(parent_id, num_columns) 71 | 72 | for i, column in enumerate(block_json.get("columns", [])): 73 | for child in column.get("children", []): 74 | process_blueprint(column_ids[i], child) 75 | 76 | elif block_type == "callout": 77 | text_blocks = block_json.get("content", []) 78 | icon = block_json.get("icon", "💡") 79 | color = block_json.get("color", "default") 80 | callout_id = create_callout(parent_id, text_blocks, icon, color)["results"][0]["id"] 81 | for child in block_json.get("children", []): 82 | process_blueprint(callout_id, child) 83 | 84 | elif block_type == "quote": 85 | text_blocks = block_json.get("content", []) 86 | quote_id = create_quote(parent_id, text_blocks)["results"][0]["id"] 87 | for child in block_json.get("children", []): 88 | process_blueprint(quote_id, child) 89 | 90 | 91 | def generate_blueprint(description, model_name, force_json=False, temperature=0.8, top_p=0.3, error=None, failed_response=None): 92 | system_prompt = """You are NotionGPT, a state-of-the-art template designer for Notion, programmed to create custom JSON blueprints that represent detailed, organized, and highly functional Notion templates. Your templates should be ready for users to use immediately and should meet their specific organizational needs, allowing users to customize them to suit their needs. 93 | 94 | Please respond ONLY with valid json that conforms to the `OpenAIResponse(BaseModel)` class as defined by pydantic in the Python code below: 95 | 96 | ``` 97 | from __future__ import annotations 98 | 99 | from enum import Enum 100 | from typing import List, Union, Optional, Dict, Literal, Annotated 101 | 102 | from pydantic import BaseModel, Field, RootModel, model_validator 103 | 104 | 105 | class TextStyle(str, Enum): 106 | bold = "bold" 107 | italic = "italic" 108 | strikethrough = "strikethrough" 109 | underline = "underline" 110 | code = "code" 111 | 112 | 113 | class Color(str, Enum): 114 | blue = "blue" 115 | brown = "brown" 116 | default = "default" 117 | gray = "gray" 118 | green = "green" 119 | orange = "orange" 120 | pink = "pink" 121 | purple = "purple" 122 | red = "red" 123 | yellow = "yellow" 124 | 125 | 126 | class BackgroundColor(str, Enum): 127 | blue_background = "blue_background" 128 | brown_background = "brown_background" 129 | default = "default" 130 | gray_background = "gray_background" 131 | green_background = "gray_background" 132 | orange_background = "orange_background" 133 | pink_background = "pink_background" 134 | purple_background = "purple_background" 135 | red_background = "red_background" 136 | yellow_background = "yellow_background" 137 | 138 | 139 | class NumberFormat(str, Enum): 140 | argentinepeso = "argentinepeso" 141 | baht = "baht" 142 | australiandollar = "australiandollar" 143 | canadiandollar = "canadiandollar" 144 | chileanpeso = "chileanpeso" 145 | colombianpeso = "colombianpeso" 146 | danishkrone = "danishkrone" 147 | dirham = "dirham" 148 | dollar = "dollar" 149 | euro = "euro" 150 | forint = "forint" 151 | franc = "franc" 152 | hongkongdollar = "hongkongdollar" 153 | koruna = "koruna" 154 | krona = "krona" 155 | leu = "leu" 156 | lira = "lira" 157 | mexicanpeso = "mexicanpeso" 158 | newtaiwandollar = "newtaiwandollar" 159 | newzealanddollar = "newzealanddollar" 160 | norwegiankrone = "norwegiankrone" 161 | number = "number" 162 | numberwithcommas = "numberwithcommas" 163 | percent = "percent" 164 | philippinepeso = "philippinepeso" 165 | pound = "pound" 166 | peruviansol = "peruviansol" 167 | rand = "rand" 168 | real = "real" 169 | ringgit = "ringgit" 170 | riyal = "riyal" 171 | ruble = "ruble" 172 | rupee = "rupee" 173 | rupiah = "rupiah" 174 | shekel = "shekel" 175 | singaporedollar = "singaporedollar" 176 | uruguayanpeso = "uruguayanpeso" 177 | yen = "yen" 178 | yuan = "yuan" 179 | won = "won" 180 | zloty = "zloty" 181 | 182 | 183 | class PropertyType(str, Enum): 184 | checkbox = "checkbox" 185 | created_by = "created_by" 186 | created_time = "created_time" 187 | date = "date" 188 | email = "email" 189 | files = "files" 190 | last_edited_by = "last_edited_by" 191 | last_edited_time = "last_edited_time" 192 | multi_select = "multi_select" 193 | number = "number" 194 | people = "people" 195 | phone_number = "phone_number" 196 | rich_text = "rich_text" 197 | select = "select" 198 | title = "title" 199 | url = "url" 200 | 201 | 202 | class HeadingType(str, Enum): 203 | heading_1 = "heading_1" 204 | heading_2 = "heading_2" 205 | heading_3 = "heading_3" 206 | 207 | 208 | class ListType(str, Enum): 209 | bulleted_list = "bulleted_list" 210 | numbered_list = "numbered_list" 211 | 212 | 213 | class SchemaOption(BaseModel): 214 | name: str 215 | color: Color 216 | 217 | 218 | class PropertySchema(BaseModel): 219 | type: PropertyType 220 | format: Optional[NumberFormat] = None 221 | options: Optional[List[SchemaOption]] = None 222 | 223 | @model_validator(mode="before") 224 | def validate_format(cls, values): 225 | if "type" in values and values["type"] == PropertyType.number and not values.get("format"): 226 | raise ValueError(f"Property with type 'number' requires 'format' to be specified") 227 | return values 228 | 229 | @model_validator(mode="before") 230 | def validate_options(cls, values): 231 | if "type" in values and values["type"] in (PropertyType.select, PropertyType.multi_select) and not values.get("options"): 232 | raise ValueError(f"Property with type '{values['type']}' requires 'options' to be specified") 233 | return values 234 | 235 | 236 | class DatabaseSchema(RootModel[Dict[str, PropertySchema]]): 237 | @model_validator(mode="before") 238 | def validate_title(cls, values): 239 | title_count = sum(1 for prop in values.values() if isinstance(prop, dict) and prop.get("type") == PropertyType.title) 240 | if title_count != 1: 241 | raise ValueError("There must be exactly one property with type 'title'") 242 | return values 243 | 244 | 245 | class RichTextContent(BaseModel): 246 | text: str 247 | style: List[TextStyle] = [] 248 | 249 | 250 | class Divider(BaseModel): 251 | type: Literal["divider"] 252 | 253 | 254 | class TableOfContents(BaseModel): 255 | type: Literal["table_of_contents"] 256 | 257 | 258 | class Heading(BaseModel): 259 | type: Literal["heading_1", "heading_2", "heading_3"] 260 | text: str 261 | 262 | 263 | class Paragraph(BaseModel): 264 | type: Literal["paragraph"] 265 | content: List[RichTextContent] 266 | 267 | 268 | class ListBlock(BaseModel): 269 | type: Literal["bulleted_list", "numbered_list"] 270 | items: List[str] 271 | 272 | 273 | class ToDoListItem(BaseModel): 274 | text: str 275 | checked: bool 276 | 277 | 278 | class ToDoList(BaseModel): 279 | type: Literal["to_do_list"] 280 | items: List[ToDoListItem] 281 | 282 | 283 | class Toggle(BaseModel): 284 | type: Literal["toggle"] 285 | text: str 286 | children: List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, ColumnList, Callout, Quote]] 287 | 288 | 289 | class Column(BaseModel): 290 | type: Literal["column"] 291 | children: List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]] 292 | 293 | 294 | class ColumnList(BaseModel): 295 | type: Literal["column_list"] 296 | columns: List[Column] 297 | 298 | 299 | class Callout(BaseModel): 300 | type: Literal["callout"] 301 | icon: str 302 | color: BackgroundColor 303 | content: List[RichTextContent] 304 | children: Optional[List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]]] = None 305 | 306 | 307 | class Quote(BaseModel): 308 | type: Literal["quote"] 309 | content: List[RichTextContent] 310 | children: Optional[List[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, Callout, Quote]]] = None 311 | 312 | 313 | class Database(BaseModel): 314 | type: Literal["database"] 315 | title: str 316 | icon: str 317 | is_inline: Optional[bool] = False 318 | schema: DatabaseSchema 319 | 320 | 321 | class Page(BaseModel): 322 | type: Literal["page"] 323 | title: str 324 | 325 | children: List[Annotated[Union[Divider, TableOfContents, Heading, Paragraph, ListBlock, ToDoList, Toggle, ColumnList, Callout, Quote, Database, Page], Field(discriminator="type")]] 326 | 327 | 328 | class OpenAIResponse(BaseModel): 329 | response: str 330 | blueprint: Page 331 | 332 | @model_validator(mode='after') 333 | def validate_children(cls, values): 334 | if len(values.blueprint.children) <= 1: 335 | raise ValueError("The root Page (blueprint) must have more than one child. Note: DO NOT JUST GENERATE A SINGULAR CALLOUT BOX IN A PAGE AS YOUR BLUEPRINT. THIS IS VERY IMPORTANT.") 336 | return values 337 | 338 | 339 | Page.update_forward_refs() 340 | Toggle.update_forward_refs() 341 | Column.update_forward_refs() 342 | ColumnList.update_forward_refs() 343 | Callout.update_forward_refs() 344 | Quote.update_forward_refs() 345 | ``` 346 | 347 | Do not include any additional text other than the object json as we will load this object with json.loads() and pydantic. Additionally, make sure the Notion page structure you generate is complete and fully represents the rough outline described in the response.""" 348 | 349 | if error and failed_response: 350 | response = client.chat.completions.create( 351 | model=model_name, 352 | response_format={"type": "json_object" if force_json else "text"}, 353 | messages=[ 354 | { 355 | "role": "system", 356 | "content": system_prompt 357 | }, 358 | { 359 | "role": "user", 360 | "content": description 361 | }, 362 | { 363 | "role": "assistant", 364 | "content": failed_response 365 | }, 366 | { 367 | "role": "user", 368 | "content": f"While processing your response with Pydantic, I encountered the following error: {error}. Please edit your previous response to correct the error following the exact same format." 369 | } 370 | ], 371 | temperature=temperature, 372 | max_tokens=4096, 373 | top_p=top_p, 374 | frequency_penalty=0, 375 | presence_penalty=0, 376 | stream=True 377 | ) 378 | else: 379 | response = client.chat.completions.create( 380 | model=model_name, 381 | response_format={"type": "json_object" if force_json else "text"}, 382 | messages=[ 383 | { 384 | "role": "system", 385 | "content": system_prompt 386 | }, 387 | { 388 | "role": "user", 389 | "content": "I'm the CEO of Acme Inc., a company dedicated to helping people plan and organize, and I want a system to manage my marketing campaign. I want to store key documents, plan our strategy, handle deliverables and track performance." 390 | }, 391 | { 392 | "role": "assistant", 393 | "content": "{\"response\":\"Alright, I will set up a comprehensive Notion page titled \"Campaign Brief\" for Acme Inc. to manage your upcoming marketing campaign. Here's a sneak peek of what you can expect:\\n\\n1. Overview: A brief summary of the campaign's objectives to get others up to speed, with an example to get you started.\\n2. Documents: A hub with linked essential documents like the product roadmap, weekly updates, performance reports, budget plans, and more.\\n3. Strategy: Details on campaign goals, target audience, and channel strategy to aid in strategic planning.\\n4. Execution: A section to track deliverables and monitor key campaign performance metrics effectively.\\n\\nThis organized structure will help you manage key documents, strategize effectively, track progress, and analyze campaign performance for a successful marketing push. Now, I’ll create this exact structure in Notion.\",\"blueprint\":{\"type\":\"page\",\"title\":\"Campaign Brief\",\"icon\":\"📋\",\"children\":[{\"type\":\"heading_1\",\"text\":\"Overview\"},{\"type\":\"callout\",\"icon\":\"➡️\",\"color\":\"purple_background\",\"content\":[{\"text\":\"Provide a summary of the campaign. At a glance, others should be able to quickly get up to speed on the project’s “what” and “why”, as well as the “why now”.\",\"style\":[]}],\"children\":[{\"type\":\"paragraph\",\"content\":[{\"text\":\"Example:\",\"style\":[\"bold\"]},{\"text\":\" In Q4, Acme Inc. will launch an awareness campaign for personal users across key global markets to drive new site visits and account creation. The campaign will focus on highlighting the customizability of Acme and how it can be used for planning, organizing, and managing one's life. The primary objectives of the campaign are to increase brand awareness and drive traffic to the site. The campaign will showcase Acme's organization and goal-setting features, including databases, to-do lists, and calendars, as well as its sharing and collaboration capabilities.\",\"style\":[]}]}]},{\"type\":\"paragraph\",\"content\":[]},{\"type\":\"toggle\",\"title\":\"Table of Contents\",\"children\":[{\"type\":\"table_of_contents\"}]},{\"type\":\"heading_1\",\"text\":\"📘 Documents\"},{\"type\":\"divider\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Link to all key documents that are relevant for this campaign.\",\"style\":[]}]},{\"type\":\"page\",\"title\":\"Documents Hub\",\"icon\":\"📘\",\"children\":[{\"type\":\"database\",\"title\":\"Product roadmap\",\"icon\":\"🎯\",\"schema\":{\"Project\":{\"type\":\"title\"},\"Assign\":{\"type\":\"people\"},\"Date\":{\"type\":\"date\"},\"Status\":{\"type\":\"select\",\"options\":[{\"name\":\"Not started\",\"color\":\"red\"},{\"name\":\"In progress\",\"color\":\"yellow\"},{\"name\":\"Completed\",\"color\":\"green\"}]}}},{\"type\":\"database\",\"title\":\"Weekly product update\",\"icon\":\"📄\",\"schema\":{\"Name\":{\"type\":\"title\"},\"Author\":{\"type\":\"people\"}}},{\"type\":\"page\",\"title\":\"Performance report\",\"icon\":\"📈\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Budget plan\",\"icon\":\"💸\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Editorial calendar\",\"icon\":\"📅\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Overview:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Content strategy\",\"icon\":\"📝\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using different header options so that it’s easier to read.\",\"style\":[]}]}]}]},{\"type\":\"heading_1\",\"text\":\"🧭 Strategy\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Goals & objectives\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Use this section to list all campaign goals and objectives. \",\"style\":[]}]},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Campaign objectives:\",\"style\":[\"bold\"]}]},{\"type\":\"numbered_list\",\"items\":[\"Increasing awareness of brand\",\"Driving traffic to site\"]},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Product goals:\",\"style\":[\"bold\"]}]},{\"type\":\"numbered_list\",\"items\":[\"Showcase organization and goal-setting in Acme with databases, to-do lists, calendars, and other productivity features\",\"Highlight sharing and collaborating with friends and family\"]},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Company goals:\",\"style\":[\"bold\"]}]},{\"type\":\"numbered_list\",\"items\":[\"Show Acme’s product flexibility\",\"Increase overall signups \"]},{\"type\":\"heading_2\",\"text\":\"Target audience(s)\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Provide a summary of your target audience(s) in as much detail as possible. The more defined your audience is, the more targeted your campaign will be. The toggle below includes a few additional questions to ask when narrowing in on your target audience.\",\"style\":[]}]},{\"type\":\"toggle\",\"title\":\"Additional audience insights:\",\"children\":[{\"type\":\"bulleted_list\",\"items\":[\"What do they think and how do they behave?\",\"What problem or challenge are we trying to solve for them?\",\"What makes their experience with Acme unique?\",\"Why are they not doing what we want them to?\",\"What challenges of theirs can Acme help solve?\",\"What channels does this audience or community interact with most?\",\"What unique behaviors do we want to focus on as part of our marketing effort?\"]}]},{\"type\":\"paragraph\",\"content\":[{\"text\":\"The target audience will include personal users across various demographics and regions, with a focus on those who are seeking a more efficient and customizable productivity tool. \",\"style\":[]}]},{\"type\":\"bulleted_list\",\"items\":[\"Prospects that don’t use Acme for Personal or Teams → Try Acme!\",\"Personal users that don’t use Acme at work → Plan personal and work goals with Acme!\"]},{\"type\":\"heading_1\",\"text\":\"📨 Channel strategy\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Overview\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Use this section to summarize at a glance the major campaign moments and deliverables.\",\"style\":[]},{\"text\":\" \",\"style\":[]}]},{\"type\":\"bulleted_list\",\"items\":[\"Identify potential partners for cross-promotion, such as productivity and lifestyle influencers\",\"Create social media content that highlights Acme's customization and collaboration features, targeting personal users who are seeking a more efficient productivity tool\",\"Utilize email marketing to drive traffic to the site and showcase Acme's organization and goal-setting features, including databases, to-do lists, and calendars\",\"Host webinars and workshops to educate new users on how to use Acme for personal productivity and organization\",\"Use targeted ads on social media platforms and search engines to reach potential users who are searching for productivity tools\",\"Develop partnerships with relevant organizations or companies to offer Acme as a productivity tool for their employees or members\"]},{\"type\":\"heading_1\",\"text\":\"🚀 Execution\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Deliverables tracker\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Use this section to track any actions or deliverables needed to launch the campaign. Use the “Channel” column to link deliverables to the relevant channel.\",\"style\":[]}]},{\"type\":\"database\",\"title\":\"Deliverables tracker\",\"icon\":\"🕒\",\"is_inline\":true,\"schema\":{\"Deliverable\":{\"type\":\"title\"},\"Status\":{\"type\":\"select\",\"options\":[{\"name\":\"Not started\",\"color\":\"red\"},{\"name\":\"In progress\",\"color\":\"yellow\"},{\"name\":\"Design in progress\",\"color\":\"yellow\"},{\"name\":\"Design finished\",\"color\":\"orange\"},{\"name\":\"Sent to vendor\",\"color\":\"blue\"},{\"name\":\"Ordered\",\"color\":\"blue\"},{\"name\":\"Shipped\",\"color\":\"blue\"},{\"name\":\"Received\",\"color\":\"green\"},{\"name\":\"Complete\",\"color\":\"green\"}]},\"Assignee\":{\"type\":\"people\"},\"Contributors\":{\"type\":\"people\"},\"Due\":{\"type\":\"date\"},\"Channel\":{\"type\":\"select\",\"options\":[{\"name\":\"Community\",\"color\":\"purple\"},{\"name\":\"Social Media\",\"color\":\"green\"},{\"name\":\"Content\",\"color\":\"pink\"},{\"name\":\"Events\",\"color\":\"brown\"},{\"name\":\"Communications\",\"color\":\"gray\"},{\"name\":\"Performance\",\"color\":\"yellow\"},{\"name\":\"Product Marketing\",\"color\":\"default\"},{\"name\":\"Demand Gen\",\"color\":\"orange\"}]}}},{\"type\":\"heading_1\",\"text\":\"📈 Campaign performance\"},{\"type\":\"divider\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Add post-campaign performance reports and metrics here.\",\"style\":[]}]},{\"type\":\"callout\",\"icon\":\"✨\",\"color\":\"default\",\"content\":[{\"text\":\"Overall goal:\",\"style\":[\"bold\"]}]},{\"type\":\"callout\",\"icon\":\"✍️\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Executive Summary\",\"style\":[\"bold\"]}]},{\"type\":\"heading_2\",\"text\":\"Conversions\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Impressions\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Web traffic\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Email\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Social & Influencers\"},{\"type\":\"divider\"},{\"type\":\"heading_2\",\"text\":\"Internal Team Retro\"},{\"type\":\"divider\"}]}}" 394 | }, 395 | { 396 | "role": "user", 397 | "content": "I run a CS club and want a centralized dashboard where I can list events, store a repository of resources, and have a news section. Oh, and a place for club photos would also be nice, just so it looks more aesthetic." 398 | }, 399 | { 400 | "role": "assistant", 401 | "content": "{\"response\":\"Certainly! I'll make a centralized Notion dashboard for your club titled \"Computer Science Club Home.\" This page will serve as a central hub where you can manage and showcase various aspects of your club. It will also include sections for upcoming events, a repository of resources, the latest news, and a gallery for club photos to enhance the aesthetic appeal.\\n\\nWe’ll start with the welcome message in a callout box at the top, that will invite new and prospective members with an application link and a brief introduction to the club’s purpose. Then, we’ll follow up with your news section, which will be set up to feature updates and announcements that can be chronologically organized for ease of use. Additionally, events will be detailed on their individual pages linked from a main events section, ensuring that members know what, when, and where activities are happening.\\n\\nFor resources, there will be individual pages under one umbrella, each containing valuable materials ranging from internship opportunities to tech talks and software deals. This will be an invaluable asset for both new and existing members. Lastly, the photo gallery will serve as a vibrant collage of recent activities and memorable moments from the club, encouraging a sense of community and belonging! Now, I’ll go ahead and create your computer science club dashboard in Notion.\",\"blueprint\":{\"type\":\"page\",\"title\":\"Computer science club home\",\"icon\":\"🤖\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Overview: \",\"style\":[\"bold\"]},{\"text\":\"Use this template if you’re running a club at school and need a homepage to showcase your events, news, resources, and more! This template is specifically designed for Computer Science or Engineering-themed clubs, but you can adapt it for any group that runs programming and events.\",\"style\":[]}]},{\"type\":\"column_list\",\"columns\":[{\"type\":\"column\",\"children\":[{\"type\":\"callout\",\"icon\":\"💾\",\"color\":\"blue_background\",\"content\":[{\"text\":\"Hello world!\",\"style\":[\"code\"]},{\"text\":\" Welcome to our computer science club homepage. Want to join? Fill this form out:\\n\",\"style\":[]},{\"text\":\"…attach link here\",\"style\":[\"italic\"]},{\"text\":\"\\n\\nScroll down to learn more about our club and check out some useful resources. Check out recent news, upcoming events, club photos, and more! \",\"style\":[]}]}]},{\"type\":\"column\",\"children\":[{\"type\":\"quote\",\"content\":[{\"text\":\"Table of contents\",\"style\":[\"bold\"]}],\"children\":[{\"type\":\"table_of_contents\"}]}]}]},{\"type\":\"heading_1\",\"text\":\"News\"},{\"type\":\"database\",\"title\":\"News\",\"icon\":\"📰\",\"is_inline\":true,\"schema\":{\"Name\":{\"type\":\"title\"},\"Date\":{\"type\":\"date\"}}},{\"type\":\"heading_1\",\"text\":\"Events\"},{\"type\":\"page\",\"title\":\"Code party — September 2, 2022 7:00 PM (EDT)\",\"icon\":\"🎉\",\"children\":[{\"type\":\"heading_2\",\"text\":\"What?\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"We’re hosting a code party for anyone and everyone! Use the time to work on your CS homework, a side project, or just hang out with the crew 😎\",\"style\":[]}]},{\"type\":\"paragraph\",\"content\":[{\"text\":\"We’ll also have some snacks available free of charge! Come one, come all!\",\"style\":[]}]},{\"type\":\"heading_2\",\"text\":\"When?\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"September 2, 2022 7:00 PM (EDT)\",\"style\":[]}]},{\"type\":\"heading_2\",\"text\":\"Where?\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"MN Building — Room 112\",\"style\":[]}]},{\"type\":\"heading_2\",\"text\":\"Who?\"},{\"type\":\"paragraph\",\"content\":[{\"text\":\"Anyone and everyone!\",\"style\":[]}]},{\"type\":\"paragraph\",\"content\":[]}]},{\"type\":\"page\",\"title\":\"Capture the flag — September 7, 2022 7:00 PM (EDT)\",\"icon\":\"🏴‍☠️\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip: \",\"style\":[\"bold\"]},{\"text\":\"Use this space below to provide more details about this event. You can also add the event schedule, embed a form, or even videos of past events.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Python 101 — September 9, 2022 7:00 PM (EDT)\",\"icon\":\"🐍\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip: \",\"style\":[\"bold\"]},{\"text\":\"Use this space below to provide more details about this event. You can also add the event schedule, embed a form, or even videos of past events.\",\"style\":[]}]}]},{\"type\":\"heading_1\",\"text\":\"Resources\"},{\"type\":\"page\",\"title\":\"Internships 2022\",\"icon\":\"🏢\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using our different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Our services\",\"icon\":\"⚙️\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using our different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Tech talks\",\"icon\":\"🎤\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using our different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Meet the team\",\"icon\":\"👥\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using our different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"Software deals\",\"icon\":\"💿\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using our different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"page\",\"title\":\"First year guide\",\"icon\":\"🎒\",\"children\":[{\"type\":\"callout\",\"icon\":\"💡\",\"color\":\"gray_background\",\"content\":[{\"text\":\"Template Tip:\",\"style\":[\"bold\"]},{\"text\":\" Add more details right in this page, including toggles, images, and more. Break up the page by using our different header options so that it’s easier to read.\",\"style\":[]}]}]},{\"type\":\"heading_1\",\"text\":\"Photos\"},{\"type\":\"quote\",\"content\":[{\"text\":\"This is where you add photos of recent club meetups or events, be bold!\",\"style\":[]}]}]}}" 402 | }, 403 | { 404 | "role": "user", 405 | "content": description 406 | } 407 | ], 408 | temperature=temperature, 409 | max_tokens=4096, 410 | top_p=top_p, 411 | frequency_penalty=0, 412 | presence_penalty=0, 413 | stream=True 414 | ) 415 | 416 | in_response = False 417 | accumulated_json = "" 418 | response_buffer = "" 419 | partial_newline = False 420 | 421 | for chunk in response: 422 | text = chunk.choices[0].delta.content 423 | 424 | if text is None: 425 | text = "" 426 | 427 | accumulated_json += text 428 | 429 | if partial_newline: 430 | if text.startswith("n"): 431 | response_buffer += "\n" 432 | text = text[1:] 433 | partial_newline = False 434 | 435 | if text.endswith("\\"): 436 | partial_newline = True 437 | text = text[:-1] 438 | 439 | text = text.replace("\\n", "\n") 440 | 441 | if not in_response: 442 | response_start_index = accumulated_json[-20 - len(text):].find('"response":"') 443 | if response_start_index != -1: 444 | in_response = True 445 | start_index = len(accumulated_json) - (20 + len(text)) + response_start_index + len('"response":"') 446 | else: 447 | start_index = 0 448 | else: 449 | start_index = 0 450 | 451 | if in_response: 452 | response_buffer += text[start_index:] 453 | 454 | end_index = response_buffer.find('",') 455 | if end_index != -1: 456 | yield response_buffer[:end_index] 457 | in_response = False 458 | response_buffer = "" 459 | else: 460 | yield response_buffer[start_index:] 461 | response_buffer = "" 462 | 463 | content = json_repair.loads(accumulated_json) 464 | yield content 465 | --------------------------------------------------------------------------------