Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Wordpress Tool #349

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions basic-auth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type input struct {
UsernameEnv string `json:"username_env,omitempty"`
PasswordField string `json:"password_field,omitempty"`
PasswordEnv string `json:"password_env,omitempty"`
URLField string `json:"url_field,omitempty"`
URLEnv string `json:"url_env,omitempty"`
Metadata map[string]string
}

Expand All @@ -43,49 +45,62 @@ func main() {
os.Exit(1)
}

username, password, err := getCredentials(ctx, in)
username, password, url, err := getCredentials(ctx, in)
if err != nil {
fmt.Println("Error getting credentials:", err)
fmt.Println("Error getting credentials and URL:", err)
os.Exit(1)
}
fmt.Printf(`{"env": {"%s": "%s", "%s": "%s"}}`, in.UsernameEnv, username, in.PasswordEnv, password)

if in.URLEnv != "" {
fmt.Printf(`{"env": {"%s": "%s", "%s": "%s", "%s": "%s"}}`, in.UsernameEnv, username, in.PasswordEnv, password, in.URLEnv, url)
} else {
fmt.Printf(`{"env": {"%s": "%s", "%s": "%s"}}`, in.UsernameEnv, username, in.PasswordEnv, password)
}
}

func getCredentials(ctx context.Context, in input) (string, string, error) {
func getCredentials(ctx context.Context, in input) (string, string, string, error) {
client, err := gptscript.NewGPTScript()
if err != nil {
fmt.Println("Error creating GPTScript client:", err)
return "", "", fmt.Errorf("Error creating GPTScript client: %w", err)
return "", "", "", fmt.Errorf("Error creating GPTScript client: %w", err)
}
defer client.Close()

fields := []string{in.UsernameField, in.PasswordField}
if in.URLField != "" {
fields = append(fields, in.URLField)
}

sysPromptIn, err := json.Marshal(sysPromptInput{
Message: in.Message,
Fields: strings.Join([]string{in.UsernameField, in.PasswordField}, ","),
Fields: strings.Join(fields, ","),
Sensitive: strconv.FormatBool(true),
Metadata: in.Metadata,
})
if err != nil {
return "", "", fmt.Errorf("Error marshalling sys prompt input: %w", err)
return "", "", "", fmt.Errorf("Error marshalling sys prompt input: %w", err)
}

run, err := client.Run(ctx, "sys.prompt", gptscript.Options{
Input: string(sysPromptIn),
})
if err != nil {
return "", "", fmt.Errorf("Error running GPTScript prompt: %w", err)
return "", "", "", fmt.Errorf("Error running GPTScript prompt: %w", err)
}

res, err := run.Text()
if err != nil {
return "", "", fmt.Errorf("Error getting GPTScript response: %w", err)
return "", "", "", fmt.Errorf("Error getting GPTScript response: %w", err)
}

username := gjson.Get(res, in.UsernameField).String()
password := gjson.Get(res, in.PasswordField).String()
url := ""
if in.URLField != "" {
url = gjson.Get(res, in.URLField).String()
}

return username, password, nil

return username, password, url, nil
}

func getInput() (input, error) {
Expand All @@ -112,6 +127,7 @@ func getInput() (input, error) {
in.ToolDisplayName = cleanField(in.ToolDisplayName, "Basic Auth Credential")
in.UsernameField = cleanField(in.UsernameField, "username")
in.PasswordField = cleanField(in.PasswordField, "password")
in.URLField = cleanField(in.URLField, "")

// Set environment variables and validate
var validEnvPattern = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
Expand All @@ -136,6 +152,13 @@ func getInput() (input, error) {
return input{}, err
}

if in.URLField != "" {
in.URLEnv, err = cleanEnv(in.URLEnv, in.URLField)
if err != nil {
return input{}, err
}
}

in.Message = fmt.Sprintf("Enter your %s and %s", in.UsernameField, in.PasswordField)

in.Metadata = map[string]string{
Expand Down
2 changes: 2 additions & 0 deletions basic-auth/tool.gpt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@ Param: username_field: The name of the username field
Param: username_env: The name of the environment variable to set for the captured username
Param: password_field: The name of the password field
Param: password_env: The name of the environment variable to set for the captured password
Param: url_field: The name of the URL field
Param: url_env: The name of the environment variable to set for the captured URL

#!${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool "${GPTSCRIPT_INPUT}"
3 changes: 3 additions & 0 deletions index.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ tools:
atlassian-jira:
reference: ./atlassian/jira
all: true
wordpress:
reference: ./wordpress
all: true
excel:
reference: ./excel
all: true
Expand Down
56 changes: 56 additions & 0 deletions wordpress/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Wordpress Tools

## Development with Wordpress API
### (optional) Run Wordpress locally with docker:
create a yaml file `wordpress.yaml`:

```yaml
services:

wordpress:
image: wordpress
restart: always
ports:
- 8070:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: dbuser
WORDPRESS_DB_PASSWORD: dbpassword
WORDPRESS_DB_NAME: exampledb
WORDPRESS_CONFIG_EXTRA: |
define('WP_ENVIRONMENT_TYPE', 'local');
volumes:
- wordpress:/var/www/html

db:
image: mysql:8.0
restart: always
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: dbuser
MYSQL_PASSWORD: dbpassword
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql

volumes:
wordpress:
db:

```
and run `docker compose -f wordpress.yaml up`.

Navigate to localhost:8070. The first time you run the container, it will prompt you to register the site name and create a new user.

### Obot Basic Auth Configuration

- username: the username you created first time you nagivate to the site
- password: the password of the user you created first time you nagivate to the site
- url: the url of your wordpress site. For local, it's `http://localhost:8070`. For hosted, it's the url of your wordpress site, such as `https://example.wpenginepowered.com`.

### CRITICAL: Configure Permalinks in Settings
1. Without configuring permalinks, the Wordpress API will not work, because the `/wp-json` endpoint will not be available.
To do this, go to your wordpress site dashboard, on the left sidebar, select `Settings` -> `Permalinks`, then select any non-plain Permalink structure.
2. You must create an application password to be able to create posts.
To do this, go to your wordpress site dashboard, on the left sidebar, select `Users`, edit your user profile and scroll down to the `Application Passwords` section, then select `Add New`.

10 changes: 10 additions & 0 deletions wordpress/credential/tool.gpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Name: Wordpress Basic Auth Credential
Share Credential: ../../basic-auth as wordpress
with wordpress_site as url_field and
WORDPRESS_SITE as url_env and
wordpress_username as username_field and
WORDPRESS_USERNAME as username_env and
wordpress_password as password_field and
WORDPRESS_PASSWORD as password_env and
"Enter your Wordpress site url, username and password or application password, it's recommended to use application password." as message
Type: credential
24 changes: 24 additions & 0 deletions wordpress/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from tools import posts, users, site # import to register tools
from tools.helper import tool_registry, create_session
import json
import sys


def main():
if len(sys.argv) < 2:
print("Usage: python main.py <command>")
sys.exit(1)

command = sys.argv[1]
try:
client = create_session()

json_response = tool_registry.get(command)(client)
print(json.dumps(json_response, indent=4))
except Exception as e:
print(f"Running command: {' '.join(sys.argv)} yielded error: {e}")
sys.exit(1)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions wordpress/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
requests
146 changes: 146 additions & 0 deletions wordpress/tool.gpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
Name: Wordpress
Description: Tools for interacting with self-hosted and hosted Wordpress sites that support basic auth.
Metadata: bundle: true
Share Tools: List Users, Get User, List Posts, Retrieve Post, Create Post, Update Post, Delete Post, Get Site Settings

---
Name: List Users
Description: List users in wordpress site
Credential: ./credential
Share Context: Wordpress Context

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py ListUsers

---
Name: Get User
Description: Get the metadata of a user in wordpress site
Credential: ./credential
Share Context: Wordpress Context
Param: user_id: the id of the user to get

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py GetUser

---
Name: List Posts
Description: List posts in wordpress site
Credential: ./credential
Share Context: Wordpress Context
Param: context: (optional) the context of the posts to list, must be one of: view, embed, edit, default is view
Param: page: (optional) the page number to list, default is 1
Param: per_page: (optional) the number of posts per page to list, default is 10
Param: author_ids: (optional) a list of comma separated author ids, default is None
Param: search_query: (optional) the search query to filter posts, default is None
Param: statuses: (optional) a comma separated list of statuses to filter posts, default is publish. Valid statuses are: publish, future, draft, pending, private, trash, auto-draft, inherit, request-pending, request-confirmed, request-failed, request-completed
Param: publish_after: (optional) the date and time to filter posts published after, default is None
Param: publish_before: (optional) the date and time to filter posts published before, default is None
Param: modified_after: (optional) the date and time to filter posts modified after, default is None
Param: modified_before: (optional) the date and time to filter posts modified before, default is None
Param: order: (optional) the order to sort posts, must be one of: asc, desc, default is desc.

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py ListPosts

---
Name: Retrieve Post
Description: Retrieve a post in wordpress site
Credential: ./credential
Share Context: Wordpress Context
Param: post_id: the id of the post
Param: context: (optional) the context of the post, must be one of: view, embed, edit, default is view
Param: password: (optional) the password of the post, default is None

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py RetrievePost

---
Name: Get Site Settings
Description: Get the settings of the wordpress site
Credential: ./credential
Share Context: Wordpress Context

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py GetSiteSettings

---
Name: Create Post
Description: Create a post in user'swordpress site
Credential: ./credential
Share Context: Wordpress Context
Param: title: the title of the post
Param: content: the content of the post
Param: status: (optional) the status of the post, must be one of: publish, future, draft, pending, private, default is publish
Param: comment_status: (optional) the comment status of the post, must be one of: open, closed. default is open
Param: sticky: (optional) whether the post is sticky to the top of the page, default is false
Param: password: (optional) the password of the post, default is None
Param: slug: (optional) the slug of the post, default is None
Param: date: (optional) the date of the post, default is None. Must be a valid ISO 8601 date string, in the format of YYYY-MM-DDTHH:MM:SS, or YYYY-MM-DDTHH:MM:SS+HH:MM for timezone aware date. If the date is a future date, the post will be scheduled to be published at that time.
Param: format: (optional) the format of the post to create, must be one of: standard, aside, chat, gallery, link, image, quote, status, video, audio. default is standard
Param: author_id: (optional) the id of the author of the post. If not provided, the current user will be used.
Param: excerpt: (optional) the excerpt of the post, default is None
Param: ping_status: (optional) the ping status of the post, must be one of: open, closed. default is open

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py CreatePost

---
Name: Update Post
Description: Update a post in user's wordpress site. Only the fields that are provided will be updated.
Credential: ./credential
Share Context: Wordpress Context
Param: post_id: the id of the post to update
Param: title: (optional) the title of the post
Param: content: (optional) the content of the post
Param: status: (optional) the status of the post, must be one of: publish, future, draft, pending, private, default is publish
Param: comment_status: (optional) the comment status of the post, must be one of: open, closed, default is open
Param: sticky: (optional) whether the post is sticky to the top of the page, default is false
Param: password: (optional) the password of the post, default is None
Param: slug: (optional) the slug of the post, default is None
Param: date: (optional) the date of the post, default is None. Must be a valid ISO 8601 date string, in the format of YYYY-MM-DDTHH:MM:SS, or YYYY-MM-DDTHH:MM:SS+HH:MM for timezone aware date. If the date is a future date, the post will be scheduled to be published at that time.
Param: format: (optional) the format of the post to create, must be one of: standard, aside, chat, gallery, link, image, quote, status, video, audio. default is standard
Param: author_id: (optional) the id of the author of the post, default is None
Param: excerpt: (optional) the excerpt of the post, default is None
Param: ping_status: (optional) the ping status of the post, must be one of: open, closed, default is open

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py UpdatePost

---
Name: Delete Post
Description: Delete a post in user's wordpress site
Credential: ./credential
Share Context: Wordpress Context
Param: post_id: the id of the post to delete
Param: force: (optional) whether to force delete the post, default is false. If true, the post will be deleted permanently, instead of being moved to trash.

#!/usr/bin/env python3 ${GPTSCRIPT_TOOL_DIR}/main.py DeletePost

---
Name: Wordpress Context
Type: context
Share Context: ../time

#!sys.echo

## Instructions for using Wordpress tools

You have access to a set of tools to interact with a Wordpress workspace.

Display all dates and times in the user's preferred timezone. When the user gives values for dates and times, assume they are in the user's preferred timezone unless otherwise specified by the user.
Wordpress post doesn't render markdown syntax, so when you create or update a post, you should not fill the content with markdown syntax.
Wordpress post supports a list of different formats, here are the definitions:
- standard: The default post format.
- aside: Typically styled without a title. Similar to a Facebook note update.
- gallery: A gallery of images. Post will likely contain a gallery shortcode and will have image attachments.
- link: A link to another site. Themes may wish to use the first <a href=""> tag in the post content as the external link for that post. An alternative approach could be if the post consists only of a URL, then that will be the URL and the title (post_title) will be the name attached to the anchor for it.
- image: A single image. The first <img> tag in the post could be considered the image. Alternatively, if the post consists only of a URL, that will be the image URL and the title of the post (post_title) will be the title attribute for the image.
- quote: A quotation. Probably will contain a blockquote holding the quote content. Alternatively, the quote may be just the content, with the source/author being the title.
- status: A short status update, similar to a Twitter status update.
- video: A single video or video playlist. The first <video> tag or object/embed in the post content could be considered the video. Alternatively, if the post consists only of a URL, that will be the video URL. May also contain the video as an attachment to the post, if video support is enabled on the blog (like via a plugin).
- audio: An audio file or playlist. Could be used for Podcasting.
- chat: A chat transcript.

## End of instructions for using Wordpress tools

---
!metadata:*:category
WordPress

---
!metadata:*:icon
/admin/assets/wordpress-logo.png
Empty file added wordpress/tools/__init__.py
Empty file.
Loading