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

enhance: Wordpress - function to guarantee correct post format and bug fix #433

Merged
merged 7 commits into from
Feb 14, 2025
7 changes: 4 additions & 3 deletions wordpress/credential/tool.gpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Tools: ../../generic-credential
"fields" : [
{
"name": "Wordpress Site URL",
"description": "Enter your WordPress site URL in the format: https://example.com. Do not include a trailing slash or suffix like /wp-admin or /wp-json.",
"description": "To find the correct URL, go to your WordPress site’s admin dashboard, select the `Settings` tab (visible only to admin users), and check the `Site Address (URL)` field under `General Settings`.",
"env": "WORDPRESS_SITE"
},
{
Expand All @@ -29,5 +29,6 @@ Tools: ../../generic-credential
}
],
"message": "WARNING: This tool does not support WordPress.com sites.\n\nPREREQUISITES:\n1. Create an application password to enable post creation:\n - Go to your WordPress site admin dashboard.\n - In the left sidebar, click `Users`, then edit your user profile.\n - Scroll to the `Application Passwords` section and click `Add New Application Password`.\n2. Configure permalinks for the WordPress API to work:\n - In the dashboard, hover over `Settings` and select `Permalinks`.\n - Choose any structure other than `Plain` and save the changes."
}
}
},
"validationTool": "github.com/obot-platform/tools/wordpress/validate-credential.gpt"
}
7 changes: 6 additions & 1 deletion wordpress/main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from tools import posts, users, site, media # import tool registry
from tools.helper import tool_registry, create_session
from tools.helper import tool_registry, create_session, setup_logger
import json
import sys

logger = setup_logger(__name__)

logger.info(f"Registered WordPress tools: {tool_registry.list_tools()}")


def main():
if len(sys.argv) < 2:
Expand All @@ -13,6 +17,7 @@ def main():
try:
client = create_session()

logger.info(f"Calling tool: {command}")
json_response = tool_registry.get(command)(client)
print(json.dumps(json_response, indent=4))
except Exception as e:
Expand Down
3 changes: 2 additions & 1 deletion wordpress/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
requests
git+https://github.com/gptscript-ai/py-gptscript.git@0cebee3afa51b8c56006e479a990c262bd693ba6#egg=gptscript
git+https://github.com/gptscript-ai/py-gptscript.git@0cebee3afa51b8c56006e479a990c262bd693ba6#egg=gptscript
mistune==3.1.1 # BSD-3-Clause License
16 changes: 9 additions & 7 deletions wordpress/tool.gpt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@
Name: Wordpress
Description: Tools for interacting with self-hosted and hosted Wordpress sites that support basic auth. Wordpress.com sites are not supported.
Metadata: bundle: true
Share Tools: List Users, Get User, List Posts, Retrieve Post, Create Post, Update Post, Delete Post, List Media, Upload Media, Update Media, Delete Media, Get Site Settings
Share Tools: List Users, Get Me, List Posts, Retrieve Post, Create Post, Update Post, Delete Post, List Media, Upload Media, Update Media, Delete Media, Get Site Settings

---
Name: List Users
Description: List users in wordpress site
Description: List users in wordpress site. Only admin users have permission to do this.
Credential: ./credential
Share Context: Wordpress Context
Param: context: (optional) the context of the users to list, must be one of: view, embed, edit, default is view. Set to edit if you want to reveal more metadata.
Param: has_published_posts: (optional) whether to show users who haven't published posts, default is false. must be one of: true, false.

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

---
Name: Get User
Description: Get the metadata of a user in wordpress site
Name: Get Me
Description: Get the all metadata of the current user in wordpress site, including the user's role and capabilities. Failed to get user info indicates that the user is not authenticated correctly.
Credential: ./credential
Share Context: Wordpress Context
Param: user_id: the id of the user to get
Param: context: (optional) the context of the user, must be one of: view, embed, edit, default is edit. Set to view if you want to validate authentication.

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

---
Name: List Posts
Expand Down Expand Up @@ -53,7 +55,7 @@ Param: password: (optional) the password of the post, default is None

---
Name: Get Site Settings
Description: Get the settings of the wordpress site
Description: Get the settings of the wordpress site, only admin users have permission to do this.
Credential: ./credential
Share Context: Wordpress Context

Expand Down
62 changes: 52 additions & 10 deletions wordpress/tools/helper.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
import requests
import os
import sys
from requests.auth import HTTPBasicAuth
from datetime import datetime
import gptscript
import asyncio
import logging


def setup_logger(name):
"""Setup a logger that writes to sys.stderr. This will eventually show up in GPTScript's debugging logs.

Args:
name (str): The name of the logger.

Returns:
logging.Logger: The logger.
"""
# Create a logger
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG) # Set the logging level

# Create a stream handler that writes to sys.stderr
stderr_handler = logging.StreamHandler(sys.stderr)

# Create a log formatter
formatter = logging.Formatter(
"[WordPress Tool Debugging Log]: %(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
stderr_handler.setFormatter(formatter)

# Add the handler to the logger
logger.addHandler(stderr_handler)

return logger


logger = setup_logger(__name__)

if "WORDPRESS_USERNAME" not in os.environ:
raise ValueError("WORDPRESS_USERNAME is not set")

Expand All @@ -28,9 +60,10 @@
def clean_wordpress_site_url(site_url):

if not site_url.startswith("https://") and not site_url.startswith("http://"):
raise ValueError(
f"Error: Invalid site URL: {site_url}. No scheme supplied, must start with protocol, e.g. https:// or http://"
print(
f"Error: Invalid site URL: [{site_url}]. No scheme supplied, must start with protocol, e.g. https:// or http://"
)
sys.exit(1)

site_url = site_url.rstrip("/")
if site_url.endswith("/wp-json"):
Expand All @@ -48,6 +81,7 @@ def is_valid_iso8601(date_string):
datetime.fromisoformat(date_string)
return True
except ValueError:
logger.error(f"Invalid ISO 8601 date string: {date_string}")
return False


Expand Down Expand Up @@ -93,15 +127,23 @@ def _prepend_base_path(base_path: str, file_path: str):


def load_from_gptscript_workspace(filepath: str) -> bytes:
gptscript_client = gptscript.GPTScript()
wksp_file_path = _prepend_base_path("files", filepath)

try:
return asyncio.run(gptscript_client.read_file_in_workspace(wksp_file_path))
except RuntimeError: # If there's already an event loop running
loop = asyncio.get_running_loop()
return loop.run_until_complete(
gptscript_client.read_file_in_workspace(wksp_file_path)
gptscript_client = gptscript.GPTScript()
wksp_file_path = _prepend_base_path("files", filepath)

try:
return asyncio.run(gptscript_client.read_file_in_workspace(wksp_file_path))
except RuntimeError: # If there's already an event loop running
loop = asyncio.get_running_loop()
return loop.run_until_complete(
gptscript_client.read_file_in_workspace(wksp_file_path)
)
except Exception as e:
logger.error(
f"Failed to load file {filepath} from GPTScript workspace. Exception: {e}"
)
raise Exception(
f"Failed to load file {filepath} from GPTScript workspace. Exception: {e}"
)


Expand Down
102 changes: 65 additions & 37 deletions wordpress/tools/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,44 @@
tool_registry,
is_valid_iso8601,
load_from_gptscript_workspace,
setup_logger,
)
import os
import urllib.parse
import io
import mimetypes
from typing import Union
import sys
import json

logger = setup_logger(__name__)

def _format_media_response(response_json: dict):
new_response_json = {}
keys = [
"id",
"date",
"date_gmt",
"modified",
"modified_gmt",
"slug",
"status",
"type",
"link",
"title",
"author",
"media_type",
"mime_type",
]
for key in keys:
new_response_json[key] = response_json[key]
return new_response_json

def _format_media_response(response_json: Union[dict, list]) -> Union[dict, list]:
# response is either a list of dict or a single dict
try:
if isinstance(response_json, list):
return [_format_media_response(media) for media in response_json]
else:
keys = [
"id",
"date",
"date_gmt",
"modified",
"modified_gmt",
"slug",
"status",
"type",
"link",
"title",
"author",
"media_type",
"mime_type",
]
return {key: response_json[key] for key in keys if key in response_json}
except Exception as e:
logger.error(f"Error formatting media response: {e}")
return response_json


@tool_registry.register("RetrieveMedia")
Expand All @@ -39,7 +50,7 @@ def retrieve_media(client):

context = os.getenv("CONTEXT", "view").lower()
context_enum = {"view", "embed", "edit"}

query_params = {}
if context not in context_enum:
raise ValueError(
Expand All @@ -55,7 +66,7 @@ def retrieve_media(client):
return response.json()
else:
print(
f"Failed to retrieve post. Error: {response.status_code}, {response.text}"
f"Failed to retrieve media. Error: {response.status_code}, {response.text}"
)


Expand Down Expand Up @@ -134,10 +145,18 @@ def list_media(client):
query_params["order"] = order

response = client.get(url, params=query_params)
if response.status_code >= 200 and response.status_code < 300:
return [_format_media_response(media) for media in response.json()]
if response.status_code == 200:
return _format_media_response(response.json())
elif response.status_code == 401:
print(
f"Authentication failed: {response.status_code}. Error Message: {response.text}"
)
elif response.status_code == 403:
print(
f"Permission denied: {response.status_code}. Error Message: {response.text}"
)
else:
print(f"Failed to list posts. Error: {response.status_code}, {response.text}")
print(f"Failed to list media. Error code: {response.status_code}")


@tool_registry.register("UploadMedia")
Expand All @@ -148,7 +167,7 @@ def upload_media(client):
client (Client): The client object to use for the request.

Raises:
ValueError: If the file path is not provided or the file is not found.
ValueError: If the media file path is not provided or the file is not found.

Returns:
dict: The response from the WordPress site. Includes the metadata of the uploaded media.
Expand All @@ -159,12 +178,7 @@ def upload_media(client):
if media_file_path == "":
raise ValueError("Error: Media file path is required to upload media file.")

try:
data = load_from_gptscript_workspace(media_file_path)
except Exception as e:
raise Exception(
f"Failed to load file {media_file_path} from GPTScript workspace. Exception: {e}"
)
data = load_from_gptscript_workspace(media_file_path)

# with open(file_path, "rb") as file:
# data = file.read()
Expand All @@ -176,10 +190,14 @@ def upload_media(client):

response = client.post(upload_url, files=files)

if response.status_code >= 200 and response.status_code < 300:
if response.status_code == 201:
return _format_media_response(response.json())
elif response.status_code == 401:
print(f"Authentication failed: {response.status_code}, {response.text}")
elif response.status_code == 403:
print(f"Permission denied: {response.status_code}, {response.text}")
else:
print(f"Failed to create media. Error: {response.status_code}, {response.text}")
print(f"Failed to create/upload media. Error code: {response.status_code}")


@tool_registry.register("UpdateMedia")
Expand Down Expand Up @@ -218,8 +236,12 @@ def update_media(client):
media_data["author"] = int(author_id)

response = client.post(url, json=media_data)
if response.status_code >= 200 and response.status_code < 300:
if response.status_code == 200:
return _format_media_response(response.json())
elif response.status_code == 401:
print(f"Authentication failed: {response.status_code}, {response.text}")
elif response.status_code == 403:
print(f"Permission denied: {response.status_code}, {response.text}")
else:
print(f"Failed to update media. Error: {response.status_code}, {response.text}")

Expand All @@ -232,7 +254,13 @@ def delete_media(client):
url = f"{WORDPRESS_API_URL}/media/{media_id}" # not allowed to put media to trash thru rest api

response = client.delete(url, params=query_params)
if response.status_code >= 200 and response.status_code < 300:
return {"message": "Media deleted successfully"}
if response.status_code == 200:
return {
"message": f"{response.status_code}. Media {media_id} deleted successfully"
}
elif response.status_code == 401:
print(f"Authentication failed: {response.status_code}, {response.text}")
elif response.status_code == 403:
print(f"Permission denied: {response.status_code}, {response.text}")
else:
print(f"Failed to delete media. Error: {response.status_code}, {response.text}")
Loading
Loading