+ Added/refactored new custom GPTs scripts
- added gen_gpt_templ script - improved Custom GPTs template generator
This commit is contained in:
@@ -1,77 +0,0 @@
|
||||
# idxtool
|
||||
|
||||
The `idxtool` is a GPT indexing and searching tool for the CSP repo (ChatGPT System Prompt).
|
||||
|
||||
Contributions to `idxtool` are welcome. Please submit pull requests or issues to the CSP repo for review.
|
||||
|
||||
## Command line
|
||||
|
||||
```
|
||||
usage: idxtool.py [-h] [--toc [TOC]] [--find-gpt FIND_GPT]
|
||||
[--template TEMPLATE] [--parse-gptfile PARSE_GPTFILE]
|
||||
[--rename]
|
||||
|
||||
idxtool: A GPT indexing and searching tool for the CSP repo
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--toc [TOC] Rebuild the table of contents of GPT custom instructions
|
||||
--find-gpt FIND_GPT Find a GPT file by its ID or full ChatGPT URL
|
||||
--template TEMPLATE Creates an empty GPT template file from a ChatGPT URL
|
||||
--parse-gptfile PARSE_GPTFILE
|
||||
Parses a GPT file name
|
||||
--rename Rename the GPT file names to include their GPT ID
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Rebuild TOC: Use `--toc` to rebuild the table of contents for GPT custom instructions.
|
||||
- Find GPT File: Use `--find-gpt [GPTID or Full ChatGPT URL or a response file with IDs/URLs]` to find a GPT by its ID or URL.
|
||||
- Rename GPT: Use `--rename` to rename all the GPTs to include their GPTID as prefix.
|
||||
- Create a starter template GPT file: Use `--template [Full ChatGPT URL]` to create a starter template GPT file.
|
||||
- Help: Use `--help` to display the help message and usage instructions.
|
||||
|
||||
## Example
|
||||
|
||||
To rebuild the custom GPTs files, run:
|
||||
|
||||
```bash
|
||||
python idxtool.py --toc
|
||||
```
|
||||
|
||||
To find a GPT by its ID, run:
|
||||
|
||||
```bash
|
||||
python idxtool.py --find-gpt 3rtbLUIUO
|
||||
```
|
||||
|
||||
or by URL:
|
||||
|
||||
```bash
|
||||
python idxtool.py --find-gpt https://chat.openai.com/g/g-svehnI9xP-retro-adventures
|
||||
```
|
||||
|
||||
Additionally, you can have a file with a list of IDs or URLs and pass it to the `--find-gpt` option:
|
||||
|
||||
```bash
|
||||
python idxtool.py --find-gpt @gptids.txt
|
||||
```
|
||||
|
||||
(note the '@' symbol).
|
||||
|
||||
The `gptids.txt` file contains a list of IDs or URLs, one per line:
|
||||
|
||||
```text
|
||||
3rtbLUIUO
|
||||
https://chat.openai.com/g/g-svehnI9xP-retro-adventures
|
||||
#vYzt7bvAm
|
||||
w2yOasK1r
|
||||
waDWNw2J3
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This tool is open-sourced under the GNU General Public License (GPL). Under this license, you are free to use, modify, and redistribute this software, provided that all copies and derivative works are also licensed under the GPL.
|
||||
|
||||
For more details, see the [GPLv3 License](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
@@ -5,6 +5,7 @@ The Big Prompt Library repository is a collection of various system prompts, cus
|
||||
<u>Topics</u>:
|
||||
|
||||
- [Articles](./Articles/README.md)
|
||||
- [Tools and scripts](./Tools/README.md)
|
||||
- [Custom Instructions](./CustomInstructions/README.md)
|
||||
- [System Prompts](./SystemPrompts/README.md)
|
||||
- [Jailbreak Prompts](./Jailbreak/README.md)
|
||||
|
||||
13
Tools/README.md
Normal file
13
Tools/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# TheBigPromptLibrary Tools
|
||||
|
||||
This directory contains various tools and utilities for working with ChatGPT's custom GPTs.
|
||||
|
||||
## Available Tools
|
||||
|
||||
- A [collection of scripts](./openai_gpts/README.md) for managing and working with ChatGPT Custom GPTs
|
||||
|
||||
## License
|
||||
|
||||
These tools are open-sourced under the GNU General Public License (GPL). Under this license, you are free to use, modify, and redistribute this software, provided that all copies and derivative works are also licensed under the GPL.
|
||||
|
||||
For more details, see the [GPLv3 License](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
177
Tools/openai_gpts/README.md
Normal file
177
Tools/openai_gpts/README.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Custom GPTs scripts and tools
|
||||
|
||||
This directory contains utilities for working with ChatGPT Custom GPTs in TheBigPromptLibrary:
|
||||
|
||||
- **idxtool.py** - GPT indexing and searching tool
|
||||
- **gen_gpt_templ.py** - Generate markdown templates for ChatGPT GPTs by downloading and parsing their metadata
|
||||
- **oneoff.py** - One-off operations on GPT files (e.g., batch reformatting)
|
||||
|
||||
## idxtool
|
||||
|
||||
The `idxtool` script is a Custom GPT indexing and searching tool used in TheBigPromptLibrary.
|
||||
|
||||
### Command line
|
||||
|
||||
```
|
||||
usage: idxtool.py [-h] [--toc [TOC]] [--find-gpt FIND_GPT]
|
||||
[--template TEMPLATE] [--parse-gptfile PARSE_GPTFILE]
|
||||
[--rename]
|
||||
|
||||
idxtool: A GPT indexing and searching tool for the CSP repo
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--toc [TOC] Rebuild the table of contents of GPT custom instructions
|
||||
--find-gpt FIND_GPT Find a GPT file by its ID or full ChatGPT URL
|
||||
--template TEMPLATE Creates an empty GPT template file from a ChatGPT URL
|
||||
--parse-gptfile PARSE_GPTFILE
|
||||
Parses a GPT file name
|
||||
--rename Rename the GPT file names to include their GPT ID
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- Rebuild TOC: Use `--toc` to rebuild the table of contents for GPT custom instructions.
|
||||
- Find GPT File: Use `--find-gpt [GPTID or Full ChatGPT URL or a response file with IDs/URLs]` to find a GPT by its ID or URL.
|
||||
- Rename GPT: Use `--rename` to rename all the GPTs to include their GPTID as prefix.
|
||||
- Create a starter template GPT file: Use `--template [Full ChatGPT URL]` to create a starter template GPT file.
|
||||
- Help: Use `--help` to display the help message and usage instructions.
|
||||
|
||||
### Example
|
||||
|
||||
To rebuild the custom GPTs files, run:
|
||||
|
||||
```bash
|
||||
python idxtool.py --toc
|
||||
```
|
||||
|
||||
To find a GPT by its ID, run:
|
||||
|
||||
```bash
|
||||
python idxtool.py --find-gpt 3rtbLUIUO
|
||||
```
|
||||
|
||||
or by URL:
|
||||
|
||||
```bash
|
||||
python idxtool.py --find-gpt https://chat.openai.com/g/g-svehnI9xP-retro-adventures
|
||||
```
|
||||
|
||||
Additionally, you can have a file with a list of IDs or URLs and pass it to the `--find-gpt` option:
|
||||
|
||||
```bash
|
||||
python idxtool.py --find-gpt @gptids.txt
|
||||
```
|
||||
|
||||
(note the '@' symbol).
|
||||
|
||||
The `gptids.txt` file contains a list of IDs or URLs, one per line:
|
||||
|
||||
```text
|
||||
3rtbLUIUO
|
||||
https://chat.openai.com/g/g-svehnI9xP-retro-adventures
|
||||
#vYzt7bvAm
|
||||
w2yOasK1r
|
||||
waDWNw2J3
|
||||
```
|
||||
|
||||
## gen_gpt_templ
|
||||
|
||||
The `gen_gpt_templ` script generates markdown templates for ChatGPT GPTs by downloading and parsing their metadata from the ChatGPT website.
|
||||
|
||||
### Command line
|
||||
|
||||
```bash
|
||||
usage: gen_gpt_templ.py [-h] [--debug] [--dump] [input]
|
||||
|
||||
Generate markdown template for ChatGPT GPTs
|
||||
|
||||
positional arguments:
|
||||
input GPT URL, GPT ID, g-prefixed GPT ID, or @response_file
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--debug Save debug files (HTML and dump)
|
||||
--dump Save parsed names and values to .txt file
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- Downloads GPT metadata from ChatGPT URLs
|
||||
- Parses GPT information including title, description, author, and profile picture
|
||||
- Generates markdown templates with GPT metadata
|
||||
- Supports multiple input formats:
|
||||
- Full ChatGPT URL: `https://chatgpt.com/g/g-VgbIr9TQQ-ida-pro-c-sdk-and-decompiler`
|
||||
- Conversation URL: `https://chatgpt.com/g/g-m5lMeGifF-sql-expert-querygpt/c/682cd38c-ca8c-800d-b6e2-33b8ba763824`
|
||||
- GPT ID: `VgbIr9TQQ`
|
||||
- Prefixed GPT ID: `g-VgbIr9TQQ`
|
||||
- Response file: `@gptids.txt` (processes multiple GPTs from a file)
|
||||
|
||||
### Examples
|
||||
|
||||
Generate template for a single GPT:
|
||||
|
||||
```bash
|
||||
python gen_gpt_templ.py https://chatgpt.com/g/g-VgbIr9TQQ-ida-pro-c-sdk-and-decompiler
|
||||
```
|
||||
|
||||
Process multiple GPTs from a file:
|
||||
|
||||
```bash
|
||||
python gen_gpt_templ.py @gptids.txt
|
||||
```
|
||||
|
||||
Generate template with debug output:
|
||||
|
||||
```bash
|
||||
python gen_gpt_templ.py g-VgbIr9TQQ --debug --dump
|
||||
```
|
||||
|
||||
## Differences between idxtool and gen_gpt_templ
|
||||
|
||||
### idxtool --template
|
||||
- Uses gen_gpt_templ internally to download actual GPT metadata from ChatGPT
|
||||
- Creates templates with real GPT information (title, description, author, logo)
|
||||
- Generates properly named files (`{gpt_id}.md`) without RENAMEME suffix
|
||||
- Simpler interface for basic template generation within the idxtool workflow
|
||||
|
||||
### gen_gpt_templ
|
||||
- Full-featured standalone tool with additional capabilities:
|
||||
- `--dump` flag to save all parsed metadata to .txt file
|
||||
- `--debug` flag to save HTML and debug information
|
||||
- Batch processing with @response_file for multiple GPTs
|
||||
- More detailed console output showing download and parsing progress
|
||||
- Can be used as a module by other tools (like idxtool)
|
||||
|
||||
Use `idxtool --template` when you need a quick template as part of your GPT file management workflow. Use `gen_gpt_templ` directly when you need the advanced features like metadata dumping or batch processing.
|
||||
|
||||
## oneoff
|
||||
|
||||
The `oneoff` script performs one-off operations on GPT files, primarily batch processing tasks.
|
||||
|
||||
### Features
|
||||
|
||||
- **Reformat GPT files**: Reformats all GPT markdown files in a source directory and saves them to a destination directory
|
||||
- Validates GPT file structure during processing
|
||||
- Preserves GPT metadata (ID, name) during reformatting
|
||||
|
||||
### Usage
|
||||
|
||||
The script is designed for batch operations. Currently supports:
|
||||
|
||||
1. **Batch reformatting**: Process all `.md` files in a source directory, reformat them according to the standard GPT markdown structure, and save to a destination directory.
|
||||
|
||||
Example usage in code:
|
||||
|
||||
```python
|
||||
from oneoff import reformat_gpt_files
|
||||
|
||||
success, message = reformat_gpt_files("source_gpts/", "formatted_gpts/")
|
||||
print(message)
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This tool is open-sourced under the GNU General Public License (GPL). Under this license, you are free to use, modify, and redistribute this software, provided that all copies and derivative works are also licensed under the GPL.
|
||||
|
||||
For more details, see the [GPLv3 License](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
654
Tools/openai_gpts/gen_gpt_templ.py
Normal file
654
Tools/openai_gpts/gen_gpt_templ.py
Normal file
@@ -0,0 +1,654 @@
|
||||
"""
|
||||
Generate markdown templates for ChatGPT GPTs by downloading and parsing their metadata.
|
||||
|
||||
By Elias Bachaalany
|
||||
|
||||
Usage:
|
||||
gen_template.py <gpt_url|gpt_id|g-prefixed_id> [--debug]
|
||||
gen_template.py @response_file.txt [--debug]
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import requests
|
||||
from collections import namedtuple
|
||||
|
||||
# Named tuple for generate_template return value
|
||||
GenerateTemplateResult = namedtuple('GenerateTemplateResult',
|
||||
['template', 'short_url', 'gpt_id', 'parser'])
|
||||
|
||||
# Global template string
|
||||
TEMPLATE = """GPT URL: https://chatgpt.com/g/{short_url}
|
||||
|
||||
GPT logo: <img src="{profile_pic}" width="100px" />
|
||||
|
||||
GPT Title: {title}
|
||||
|
||||
GPT Description: {description} - By {author_display_name}
|
||||
|
||||
GPT instructions:
|
||||
|
||||
```markdown
|
||||
|
||||
```"""
|
||||
|
||||
# ----------------------------------------------------------
|
||||
def parse_gpt_id(url):
|
||||
"""
|
||||
Parse the GPT ID from a ChatGPT URL
|
||||
|
||||
Args:
|
||||
url (str): Full ChatGPT URL like https://chatgpt.com/g/g-VgbIr9TQQ-ida-pro-c-sdk-and-decompiler
|
||||
|
||||
Returns:
|
||||
str or None: The GPT ID (e.g., 'VgbIr9TQQ') or None if not found
|
||||
"""
|
||||
# Pattern to match g- followed by 9 characters
|
||||
pattern = r'/g/g-([a-zA-Z0-9]{9})'
|
||||
match = re.search(pattern, url)
|
||||
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
# ----------------------------------------------------------
|
||||
# Compile regex to extract streamController.enqueue arguments
|
||||
_ENQUEUE_RE = re.compile(
|
||||
r'window\.__reactRouterContext\.streamController\.enqueue\(\s*' # find the call
|
||||
r'(?P<q>["\'])' # capture whether it's " or '
|
||||
r'(?P<raw>(?:\\.|(?!\1).)*?)' # any escaped-char or char not the opening quote
|
||||
r'(?P=q)\s*' # matching closing quote
|
||||
r'\)',
|
||||
flags=re.DOTALL
|
||||
)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
def extract_enqueue_args(html_text, decode_escapes=True):
|
||||
"""
|
||||
Scans html_text for all streamController.enqueue(...) calls,
|
||||
returns a list of the raw string-literals inside the quotes.
|
||||
"""
|
||||
args = []
|
||||
for m in _ENQUEUE_RE.finditer(html_text):
|
||||
raw = m.group('raw')
|
||||
if decode_escapes:
|
||||
# Only decode actual escape sequences, not Unicode characters
|
||||
# This prevents double-encoding of emojis and other Unicode chars
|
||||
try:
|
||||
# First try to parse as JSON string to handle escapes properly
|
||||
raw = json.loads('"' + raw + '"')
|
||||
except:
|
||||
# Fallback to simple replacement of common escapes
|
||||
raw = raw.replace('\\n', '\n').replace('\\t', '\t').replace('\\"', '"').replace("\\'", "'").replace('\\\\', '\\')
|
||||
args.append(raw)
|
||||
return args
|
||||
|
||||
|
||||
# ----------------------------------------------------------
|
||||
class CustomGPTParser:
|
||||
def __init__(self):
|
||||
self._parse_cache = {} # Cache for parsed data
|
||||
self._parsed_items = None # Store parsed items internally
|
||||
|
||||
def parse(self, source, debug: bool = False):
|
||||
# Determine if source is a filename or content
|
||||
# First check if it could be a file (avoid treating content as filename)
|
||||
is_likely_filename = (
|
||||
len(source) < 1000 and # Reasonable filename length
|
||||
'|' not in source and # Filenames don't contain pipes
|
||||
os.path.isfile(source)
|
||||
)
|
||||
|
||||
if is_likely_filename:
|
||||
try:
|
||||
with open(source, encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
return (False, f"Error reading file: {e}")
|
||||
else:
|
||||
# Treat as content
|
||||
content = source
|
||||
|
||||
# Parse the content
|
||||
if not (enqueue_args := extract_enqueue_args(content)):
|
||||
msg = "No enqueue arguments found in the provided string."
|
||||
if debug:
|
||||
print(msg)
|
||||
return (False, msg)
|
||||
|
||||
if not enqueue_args: # Additional safety check for empty list
|
||||
msg = "No enqueue arguments found in the provided string."
|
||||
if debug:
|
||||
print(msg)
|
||||
return (False, msg)
|
||||
|
||||
try:
|
||||
# Use the argument with the longest length (most likely the Gizmo data)
|
||||
s = max(enqueue_args, key=len)
|
||||
data = json.loads(s)
|
||||
parsed_items = []
|
||||
for item in data:
|
||||
if isinstance(item, dict):
|
||||
for k, v in item.items():
|
||||
parsed_items.append((k, v))
|
||||
else:
|
||||
if debug:
|
||||
print(f" {item} (type: {type(item).__name__})")
|
||||
parsed_items.append(item)
|
||||
|
||||
self._parsed_items = parsed_items
|
||||
return (True, None)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
return (False, f"JSON decoding error: {e}")
|
||||
|
||||
def get_title(self):
|
||||
"""
|
||||
Extract the title of the GPT by finding the item preceding 'description'.
|
||||
|
||||
The algorithm walks through items to find 'description', then returns
|
||||
the immediately preceding item as the title.
|
||||
|
||||
Returns:
|
||||
str: The title value or empty string on failure
|
||||
"""
|
||||
# Check cache first
|
||||
if 'title' in self._parse_cache:
|
||||
return self._parse_cache['title']
|
||||
|
||||
# Need parsed items to work with
|
||||
if not self._parsed_items:
|
||||
return ''
|
||||
|
||||
# Convert to list if not already to allow indexing
|
||||
items_list = list(self._parsed_items)
|
||||
|
||||
# Find 'description' and get the preceding item
|
||||
for i, item in enumerate(items_list):
|
||||
# Skip tuples
|
||||
if isinstance(item, tuple):
|
||||
continue
|
||||
|
||||
# Found 'description'?
|
||||
if item == 'description' and i > 0:
|
||||
# Get the previous item as title
|
||||
prev_item = items_list[i - 1]
|
||||
|
||||
# Make sure it's a string value, not a tuple
|
||||
if isinstance(prev_item, str):
|
||||
self._parse_cache['title'] = prev_item
|
||||
return prev_item
|
||||
|
||||
# Not found
|
||||
return ''
|
||||
|
||||
def get_author_display_name(self):
|
||||
"""
|
||||
Extract the author display name by finding the item after 'user-{id}'.
|
||||
|
||||
The pattern is:
|
||||
- 'user_id' (literal string)
|
||||
- 'user-{actual_user_id}' (e.g., 'user-IUwuaeXwGuwv0UoRPaeEqlzs')
|
||||
- '{author_display_name}' (e.g., 'Elias Bachaalany')
|
||||
|
||||
Returns:
|
||||
str: The author display name or empty string on failure
|
||||
"""
|
||||
# Check cache first
|
||||
if 'author_display_name' in self._parse_cache:
|
||||
return self._parse_cache['author_display_name']
|
||||
|
||||
# Need parsed items to work with
|
||||
if not self._parsed_items:
|
||||
return ''
|
||||
|
||||
# Convert to list if not already to allow indexing
|
||||
items_list = list(self._parsed_items)
|
||||
|
||||
# Find pattern: 'user_id' -> 'user-{id}' -> '{display_name}'
|
||||
for i, item in enumerate(items_list):
|
||||
# Skip tuples
|
||||
if isinstance(item, tuple):
|
||||
continue
|
||||
|
||||
# Found 'user_id'?
|
||||
if item == 'user_id' and i + 2 < len(items_list):
|
||||
# Check if next item is a user ID (starts with 'user-')
|
||||
next_item = items_list[i + 1]
|
||||
if isinstance(next_item, str) and next_item.startswith('user-'):
|
||||
# The item after that should be the display name
|
||||
display_name_item = items_list[i + 2]
|
||||
if isinstance(display_name_item, str):
|
||||
self._parse_cache['author_display_name'] = display_name_item
|
||||
return display_name_item
|
||||
|
||||
# Not found
|
||||
return ''
|
||||
def get_str_value(self, name: str, default: str = None):
|
||||
"""
|
||||
Get a string value by name from the parsed items.
|
||||
|
||||
Args:
|
||||
name: The key/name to search for
|
||||
default: Default value if not found
|
||||
|
||||
Returns:
|
||||
str: The value associated with the name or default
|
||||
"""
|
||||
# Check cache first
|
||||
if name in self._parse_cache:
|
||||
return self._parse_cache[name]
|
||||
|
||||
# Need parsed items to work with
|
||||
if not self._parsed_items:
|
||||
return default
|
||||
|
||||
# Search through items
|
||||
it = iter(self._parsed_items)
|
||||
for item in it:
|
||||
# Handle tuple items (key-value pairs from dictionaries)
|
||||
if isinstance(item, tuple):
|
||||
# We don't handle tuple items now
|
||||
continue
|
||||
|
||||
# Handle flat list items (name followed by value)
|
||||
if item == name:
|
||||
try:
|
||||
val = next(it)
|
||||
# Cache and return the value
|
||||
self._parse_cache[name] = str(val)
|
||||
return str(val)
|
||||
except StopIteration:
|
||||
return default
|
||||
|
||||
return default
|
||||
|
||||
def clear_cache(self):
|
||||
"""Clear the internal cache"""
|
||||
self._parse_cache.clear()
|
||||
|
||||
def get_parsed_items(self):
|
||||
"""Get the parsed items (for backward compatibility)"""
|
||||
return self._parsed_items if self._parsed_items else []
|
||||
|
||||
def dump(self, safe_ascii=True):
|
||||
"""
|
||||
Dump all parsed items in a formatted way.
|
||||
|
||||
Args:
|
||||
safe_ascii (bool): If True, encode non-ASCII characters safely
|
||||
|
||||
Returns:
|
||||
None (prints to stdout)
|
||||
"""
|
||||
if not self._parsed_items:
|
||||
print("No parsed items to dump")
|
||||
return
|
||||
|
||||
print(f"Dumping {len(self._parsed_items)} parsed items:")
|
||||
print("-" * 60)
|
||||
|
||||
for item in self._parsed_items:
|
||||
if isinstance(item, tuple) and len(item) == 2:
|
||||
# Handle key-value pairs from dictionaries
|
||||
k, v = item
|
||||
if safe_ascii:
|
||||
# Handle Unicode characters safely by encoding to ASCII with replacement
|
||||
k_safe = str(k).encode('ascii', errors='replace').decode('ascii')
|
||||
v_safe = str(v).encode('ascii', errors='replace').decode('ascii')
|
||||
print(f" {k_safe}: {v_safe} (type: {type(v).__name__})")
|
||||
else:
|
||||
print(f" {k}: {v} (type: {type(v).__name__})")
|
||||
else:
|
||||
# Handle non-tuple items
|
||||
if safe_ascii:
|
||||
# Handle Unicode characters safely for non-dict items
|
||||
item_safe = str(item).encode('ascii', errors='replace').decode('ascii')
|
||||
print(f" {item_safe} (type: {type(item).__name__})")
|
||||
else:
|
||||
print(f" {item} (type: {type(item).__name__})")
|
||||
|
||||
print("-" * 60)
|
||||
|
||||
# ----------------------------------------------------------
|
||||
def download_page(url: str, out_filename: str = '') -> tuple[bool, object]:
|
||||
"""
|
||||
Download a page using browser-like headers
|
||||
|
||||
Args:
|
||||
url (str): The full URL to download
|
||||
out_filename (str): Optional filename to save to. If empty, no file is written.
|
||||
|
||||
Returns:
|
||||
tuple[bool, object]: (success, content/error_message)
|
||||
- (True, content) if successful
|
||||
- (False, error_message) if failed
|
||||
"""
|
||||
# Ensure we have a full URL
|
||||
if not url.startswith('http'):
|
||||
return (False, "Please provide a full URL starting with http:// or https://")
|
||||
|
||||
# Base headers from the sample request
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'Accept-Language': 'en-US,en;q=0.5',
|
||||
# Remove Accept-Encoding to get uncompressed response
|
||||
# 'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||
'DNT': '1',
|
||||
'Sec-GPC': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Priority': 'u=0, i',
|
||||
'TE': 'trailers'
|
||||
}
|
||||
|
||||
try:
|
||||
# Create a session to handle cookies and connections properly
|
||||
session = requests.Session()
|
||||
session.headers.update(headers)
|
||||
|
||||
# Make the GET request
|
||||
response = session.get(url, timeout=30)
|
||||
|
||||
# Check if request was successful
|
||||
response.raise_for_status()
|
||||
|
||||
# Save to file if filename provided
|
||||
if out_filename:
|
||||
with open(out_filename, 'w', encoding='utf-8') as f:
|
||||
f.write(response.text)
|
||||
|
||||
return (True, response.text)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
return (False, f"Error downloading page: {e}")
|
||||
except Exception as e:
|
||||
return (False, f"Unexpected error: {e}")
|
||||
|
||||
# ----------------------------------------------------------
|
||||
def process_gpt_input(input_str):
|
||||
"""
|
||||
Process GPT input which can be:
|
||||
- Full URL: https://chatgpt.com/g/g-VgbIr9TQQ-ida-pro-c-sdk-and-decompiler
|
||||
- Conversation URL: https://chatgpt.com/g/g-m5lMeGifF-sql-expert-querygpt/c/682cd38c-ca8c-800d-b6e2-33b8ba763824
|
||||
- GPT ID: VgbIr9TQQ
|
||||
- Prefixed GPT ID: g-VgbIr9TQQ
|
||||
|
||||
Returns:
|
||||
tuple: (full_url, gpt_id)
|
||||
"""
|
||||
# Check if it's a full URL
|
||||
if input_str.startswith('https://') or input_str.startswith('http://'):
|
||||
gpt_id = parse_gpt_id(input_str)
|
||||
if not gpt_id:
|
||||
raise ValueError(f"Could not parse GPT ID from URL: {input_str}")
|
||||
|
||||
# If it's a conversation URL (contains /c/), extract the base GPT URL
|
||||
if '/c/' in input_str:
|
||||
# Extract the GPT part before /c/
|
||||
base_url = input_str.split('/c/')[0]
|
||||
return (base_url, gpt_id)
|
||||
|
||||
return (input_str, gpt_id)
|
||||
|
||||
# Check if it's a prefixed GPT ID (g-XXXXXXXXX)
|
||||
if input_str.startswith('g-') and len(input_str) >= 11:
|
||||
# Extract just the 9-character ID after 'g-'
|
||||
gpt_id = input_str[2:11] # Get exactly 9 characters after 'g-'
|
||||
url = f"https://chatgpt.com/g/{input_str}"
|
||||
return (url, gpt_id)
|
||||
|
||||
# Assume it's a bare GPT ID (9 characters)
|
||||
if len(input_str) == 9:
|
||||
url = f"https://chatgpt.com/g/g-{input_str}"
|
||||
return (url, input_str)
|
||||
|
||||
raise ValueError(f"Invalid GPT input format: {input_str}")
|
||||
|
||||
def generate_template(url, debug=False, dump=False):
|
||||
"""
|
||||
Download and parse GPT data, then generate markdown template
|
||||
|
||||
Args:
|
||||
url: Full GPT URL
|
||||
debug: Whether to save debug files (HTML and dump)
|
||||
dump: Whether to print parsed items to console
|
||||
|
||||
Returns:
|
||||
tuple: (success, result_or_error)
|
||||
- (True, GenerateTemplateResult) if successful
|
||||
- (False, error_message) if failed
|
||||
"""
|
||||
print(f"[DOWNLOAD] Fetching page from: {url}")
|
||||
# Download the page
|
||||
save_file = None
|
||||
if debug:
|
||||
save_file = "debug_download.html"
|
||||
print(f"[DEBUG] Will save HTML to: {save_file}")
|
||||
|
||||
success, content = download_page(url, save_file)
|
||||
if not success:
|
||||
return (False, f"Download failed: {content}")
|
||||
|
||||
print(f"[DOWNLOAD] Successfully downloaded {len(content)} bytes")
|
||||
|
||||
# Parse the content
|
||||
print(f"[PARSE] Parsing GPT data...")
|
||||
parser = CustomGPTParser()
|
||||
success, error = parser.parse(content)
|
||||
if not success:
|
||||
return (False, f"Parsing failed: {error}")
|
||||
|
||||
print(f"[PARSE] Successfully parsed {len(parser.get_parsed_items())} items")
|
||||
|
||||
# Save dump if debug mode
|
||||
if debug:
|
||||
from io import StringIO
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = buffer = StringIO()
|
||||
parser.dump(safe_ascii=True)
|
||||
dump_content = buffer.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
|
||||
dump_file = "debug_dump.txt"
|
||||
with open(dump_file, 'w', encoding='utf-8') as f:
|
||||
f.write(dump_content)
|
||||
print(f"[DEBUG] Saved parsed data dump to: {dump_file}")
|
||||
|
||||
# Extract required fields
|
||||
print(f"[EXTRACT] Extracting GPT metadata...")
|
||||
short_url = parser.get_str_value('short_url', 'UNKNOWN')
|
||||
profile_pic = parser.get_str_value('profile_picture_url', '')
|
||||
title = parser.get_title()
|
||||
description = parser.get_str_value('description', '')
|
||||
author_display_name = parser.get_author_display_name()
|
||||
|
||||
print(f"[EXTRACT] Found:")
|
||||
print(f" - Short URL: {short_url}")
|
||||
print(f" - Title: {title}")
|
||||
print(f" - Author: {author_display_name}")
|
||||
try:
|
||||
print(f" - Description: {description[:50]}..." if len(description) > 50 else f" - Description: {description}")
|
||||
except UnicodeEncodeError:
|
||||
# Handle special characters that can't be printed to console
|
||||
safe_desc = description.encode('ascii', errors='replace').decode('ascii')
|
||||
print(f" - Description: {safe_desc[:50]}..." if len(safe_desc) > 50 else f" - Description: {safe_desc}")
|
||||
print(f" - Profile Pic: {'Yes' if profile_pic else 'No'}")
|
||||
|
||||
# Dump parsed items if requested
|
||||
if dump:
|
||||
print("\n[DUMP] Parsed items:")
|
||||
parser.dump(safe_ascii=False)
|
||||
|
||||
# Generate template
|
||||
template = TEMPLATE.format(
|
||||
short_url=short_url,
|
||||
profile_pic=profile_pic,
|
||||
title=title,
|
||||
description=description,
|
||||
author_display_name=author_display_name
|
||||
)
|
||||
|
||||
# Extract GPT ID from short_url (remove 'g-' prefix if it exists)
|
||||
gpt_id = short_url[2:] if short_url.startswith('g-') else short_url
|
||||
|
||||
return (True, GenerateTemplateResult(template, short_url, gpt_id, parser))
|
||||
|
||||
def process_response_file(filename, debug=False, dump=False):
|
||||
"""
|
||||
Process a response file containing multiple GPT URLs/IDs
|
||||
|
||||
Args:
|
||||
filename: Path to the response file
|
||||
debug: Whether to save debug files
|
||||
dump: Whether to dump parsed items
|
||||
|
||||
Returns:
|
||||
tuple: (success_count, error_count)
|
||||
"""
|
||||
try:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
except Exception as e:
|
||||
print(f"Error reading response file: {e}")
|
||||
return (0, 1)
|
||||
|
||||
# Process each non-empty line
|
||||
inputs = [line.strip() for line in lines if line.strip() and not line.strip().startswith('#')]
|
||||
|
||||
if not inputs:
|
||||
print(f"No valid inputs found in {filename}")
|
||||
return (0, 0)
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f"PROCESSING RESPONSE FILE: {filename}")
|
||||
print(f"Found {len(inputs)} items to process")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for i, input_str in enumerate(inputs, 1):
|
||||
print(f"\n[ITEM {i}/{len(inputs)}] Processing: {input_str}")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
# Process input
|
||||
url, gpt_id = process_gpt_input(input_str)
|
||||
print(f"[PARSED] Full URL: {url}")
|
||||
print(f"[PARSED] GPT ID: {gpt_id}")
|
||||
|
||||
# Generate template
|
||||
success, result = generate_template(url, debug, dump)
|
||||
if success:
|
||||
filename = f"{result.gpt_id}.md"
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(result.template)
|
||||
print(f"[SUCCESS] Template saved to: {filename}")
|
||||
|
||||
# Save dump file if requested
|
||||
if dump:
|
||||
dump_filename = f"{result.gpt_id}.txt"
|
||||
from io import StringIO
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = buffer = StringIO()
|
||||
result.parser.dump(safe_ascii=True)
|
||||
dump_content = buffer.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
|
||||
with open(dump_filename, 'w', encoding='utf-8') as f:
|
||||
f.write(dump_content)
|
||||
print(f"[SUCCESS] Dump saved to: {dump_filename}")
|
||||
|
||||
success_count += 1
|
||||
else:
|
||||
print(f"[FAILED] Error: {result}")
|
||||
error_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Exception: {e}")
|
||||
error_count += 1
|
||||
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f"RESPONSE FILE COMPLETE")
|
||||
print(f"Success: {success_count}, Errors: {error_count}")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
return (success_count, error_count)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate markdown template for ChatGPT GPTs')
|
||||
parser.add_argument('input', nargs='?', help='GPT URL, GPT ID, g-prefixed GPT ID, or @response_file')
|
||||
parser.add_argument('--debug', action='store_true', help='Save debug files (HTML and dump)')
|
||||
parser.add_argument('--dump', action='store_true', help='Save parsed names and values to .txt file')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if input was provided
|
||||
if not args.input:
|
||||
parser.print_help()
|
||||
return 1
|
||||
|
||||
try:
|
||||
# Check if input is a response file
|
||||
if args.input.startswith('@'):
|
||||
# Process response file
|
||||
filename = args.input[1:] # Remove the @ prefix
|
||||
success_count, error_count = process_response_file(filename, args.debug, args.dump)
|
||||
sys.exit(0 if error_count == 0 else 1)
|
||||
else:
|
||||
# Process single input
|
||||
print(f"\n[INPUT] Processing: {args.input}")
|
||||
url, gpt_id = process_gpt_input(args.input)
|
||||
print(f"[PARSED] Full URL: {url}")
|
||||
print(f"[PARSED] GPT ID: {gpt_id}")
|
||||
|
||||
# Generate template
|
||||
success, result = generate_template(url, args.debug, args.dump)
|
||||
if success:
|
||||
# Save to file
|
||||
filename = f"{result.gpt_id}.md"
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(result.template)
|
||||
print(f"Template saved to: {filename}")
|
||||
|
||||
# Save dump file if requested
|
||||
if args.dump:
|
||||
dump_filename = f"{result.gpt_id}.txt"
|
||||
from io import StringIO
|
||||
old_stdout = sys.stdout
|
||||
sys.stdout = buffer = StringIO()
|
||||
result.parser.dump(safe_ascii=True)
|
||||
dump_content = buffer.getvalue()
|
||||
sys.stdout = old_stdout
|
||||
|
||||
with open(dump_filename, 'w', encoding='utf-8') as f:
|
||||
f.write(dump_content)
|
||||
print(f"Dump saved to: {dump_filename}")
|
||||
|
||||
# Also print the template
|
||||
print("\nGenerated template:")
|
||||
print("=" * 50)
|
||||
try:
|
||||
print(result.template)
|
||||
except UnicodeEncodeError:
|
||||
# Handle special characters that can't be printed to console
|
||||
safe_template = result.template.encode('ascii', errors='replace').decode('ascii')
|
||||
print(safe_template)
|
||||
else:
|
||||
print(f"Error: {result}")
|
||||
return 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -136,8 +136,8 @@ def parse_gpturl(url: str) -> Union[GptIdentifier, None]:
|
||||
|
||||
|
||||
def get_prompts_path() -> str:
|
||||
"""Return the path to the prompts directory."""
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'CustomInstructions', 'ChatGPT'))
|
||||
"""Return the path to the Custom GPTs prompts directory."""
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'CustomInstructions', 'ChatGPT'))
|
||||
|
||||
def enum_gpts() -> Generator[Tuple[bool, Union[GptMarkdownFile, str]], None, None]:
|
||||
"""Enumerate all the GPT files in the prompts directory, parse them and return the parsed GPT object."""
|
||||
58
.scripts/idxtool.py → Tools/openai_gpts/idxtool.py
Executable file → Normal file
58
.scripts/idxtool.py → Tools/openai_gpts/idxtool.py
Executable file → Normal file
@@ -13,8 +13,9 @@ from urllib.parse import quote
|
||||
|
||||
import gptparser
|
||||
from gptparser import enum_gpts, parse_gpturl, enum_gpt_files, get_prompts_path
|
||||
import gen_gpt_templ
|
||||
|
||||
TOC_FILENAME = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'CustomInstructions/README.md'))
|
||||
TOC_FILENAME = os.path.abspath(os.path.join(get_prompts_path(), '..', 'README.md'))
|
||||
TOC_GPT_MARKER_LINE = '## ChatGPT GPT instructions'
|
||||
|
||||
def rename_gpts():
|
||||
@@ -146,39 +147,54 @@ def rebuild_toc(toc_out: str = '') -> Tuple[bool, str]:
|
||||
print(msg)
|
||||
return (ok, msg)
|
||||
|
||||
def make_template(url, verbose=True):
|
||||
"""Creates an empty GPT template file from a ChatGPT URL"""
|
||||
if not (gpt_info := parse_gpturl(url)):
|
||||
msg = f"Invalid ChatGPT URL: '{url}'"
|
||||
def make_template(input_str, verbose=True):
|
||||
"""Creates a GPT template file from a ChatGPT URL/ID by downloading metadata"""
|
||||
try:
|
||||
# Process the input to handle URLs, IDs, conversation URLs, etc.
|
||||
url, gpt_id = gen_gpt_templ.process_gpt_input(input_str)
|
||||
|
||||
if verbose:
|
||||
print(f"[PARSED] Full URL: {url}")
|
||||
print(f"[PARSED] GPT ID: {gpt_id}")
|
||||
|
||||
# Use gen_gpt_templ to generate the template with actual metadata
|
||||
success, result = gen_gpt_templ.generate_template(url, debug=False, dump=False)
|
||||
|
||||
if not success:
|
||||
msg = f"Failed to generate template: {result}"
|
||||
if verbose:
|
||||
print(msg)
|
||||
return (False, msg)
|
||||
|
||||
filename = os.path.join(get_prompts_path(), f"{gpt_info.id}_RENAMEME.md")
|
||||
# Extract the template content and gpt_id from the result
|
||||
template_content = result.template
|
||||
gpt_id = result.gpt_id
|
||||
|
||||
# Save to the current working directory with the proper filename
|
||||
filename = f"{gpt_id}.md"
|
||||
|
||||
# Check if file already exists
|
||||
if os.path.exists(filename):
|
||||
msg = f"File '{filename}' already exists."
|
||||
if verbose:
|
||||
print(msg)
|
||||
return (False, msg)
|
||||
|
||||
# Write the template content
|
||||
with open(filename, 'w', encoding='utf-8') as file:
|
||||
for field, info in gptparser.SUPPORTED_FIELDS.items():
|
||||
if field == 'verif_status':
|
||||
continue
|
||||
if field == 'url':
|
||||
file.write(f"{gptparser.FIELD_PREFIX} {info.display}: {url}\n\n")
|
||||
elif field == 'instructions':
|
||||
file.write(f"{gptparser.FIELD_PREFIX} {info.display}:\n```markdown\n{info.display} here...\n```\n\n")
|
||||
elif field == 'logo':
|
||||
file.write(f"{gptparser.FIELD_PREFIX} {info.display}: <img ...>\n\n")
|
||||
else:
|
||||
file.write(f"{gptparser.FIELD_PREFIX} {info.display}: {info.display} goes here...\n\n")
|
||||
file.write(template_content)
|
||||
|
||||
msg = f"Created template '{filename}' for URL '{url}'"
|
||||
if verbose:
|
||||
print(msg)
|
||||
return (True, msg)
|
||||
|
||||
except Exception as e:
|
||||
msg = f"Error creating template: {str(e)}"
|
||||
if verbose:
|
||||
print(msg)
|
||||
return (False, msg)
|
||||
|
||||
def find_gptfile(keyword, verbose=True):
|
||||
"""Find a GPT file by its ID or full ChatGPT URL
|
||||
The ID can be prefixed with '@' to indicate a file containing a list of GPT IDs.
|
||||
@@ -223,7 +239,7 @@ def main():
|
||||
|
||||
parser.add_argument('--toc', nargs='?', const='', type=str, help='Rebuild the table of contents of custom GPTs')
|
||||
parser.add_argument('--find-gpt', type=str, help='Find a GPT file by its ID or full ChatGPT URL')
|
||||
parser.add_argument('--template', type=str, help='Creates an empty GPT template file from a ChatGPT URL')
|
||||
parser.add_argument('--template', type=str, help='Creates a GPT template file from a ChatGPT URL, GPT ID, or g-prefixed ID')
|
||||
parser.add_argument('--parse-gptfile', type=str, help='Parses a GPT file name')
|
||||
parser.add_argument('--rename', action='store_true', help='Rename the GPT file names to include their GPT ID')
|
||||
|
||||
@@ -231,6 +247,12 @@ def main():
|
||||
ok = True
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Check if no arguments were provided
|
||||
if not any(vars(args).values()):
|
||||
parser.print_help()
|
||||
sys.exit(0)
|
||||
|
||||
if args.parse_gptfile:
|
||||
ok, err = parse_gpt_file(args.parse_gptfile)
|
||||
if not ok:
|
||||
Reference in New Issue
Block a user