Skip to content

Commit

Permalink
enhance: add create and write google docs tools
Browse files Browse the repository at this point in the history
Signed-off-by: Nick Hale <[email protected]>
  • Loading branch information
njhale committed Jan 23, 2025
1 parent b282c7e commit af5a039
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 9 deletions.
2 changes: 1 addition & 1 deletion google/credential/tool.gpt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Share Credential: ../../oauth2 as google
openid
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/spreadsheets
https://www.googleapis.com/auth/documents.readonly
https://www.googleapis.com/auth/documents
https://mail.google.com/
https://www.googleapis.com/auth/gmail.readonly
https://www.googleapis.com/auth/gmail.compose" as scope
Expand Down
27 changes: 27 additions & 0 deletions google/docs/create_doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import sys
import os
import json
from googleapiclient.discovery import build
from auth import client

def main():
try:
# Get the title from the DOC_TITLE environment variable or default to 'Untitled Document'
title = os.getenv('DOC_TITLE', 'Untitled Document')

# Authenticate and build the Docs API service
service = client('docs', 'v1')

# Create a new Google Doc with the specified title
document = service.documents().create(body={"title": title}).execute()

# Get the document ID
document_id = document.get('documentId')

print(f"New document created with ID: {document_id}")
except Exception as err:
sys.stderr.write(f"Error: {err}\n")
sys.exit(1)

if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions google/docs/id.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ def extract_file_id(ref):
# If the input is already a document ID, return it as is
if re.match(r"^[a-zA-Z0-9_-]{33,}$", ref):
return ref

# Regular expression to match Google Drive document links
pattern = r"(?:https?://(?:drive|docs)\.google\.com/(?:file/d/|document/d/|open\?id=|uc\?id=))([a-zA-Z0-9_-]{33,})"

# Try to extract the document ID from the link
match = re.search(pattern, ref)
if match:
return match.group(1)

# If the input doesn't match a known pattern, raise an error
raise ValueError("Invalid Google document ID or link format")
3 changes: 1 addition & 2 deletions google/docs/read_doc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import io
import sys
import os

Expand Down Expand Up @@ -54,4 +53,4 @@ def parse_table(table):
return md_table

if __name__ == "__main__":
main()
main()
2 changes: 2 additions & 0 deletions google/docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
google-api-python-client
google-auth-httplib2
google-auth-oauthlib
beautifulsoup4
markdown
43 changes: 39 additions & 4 deletions google/docs/tool.gpt
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
---
Name: Google Docs
Name: Google Docs
Metadata: bundle: true
Description: Tools for managing Google Docs
Share Tools: Read Doc
Share Tools: Read Google Doc, Create Google Doc, Update Google Doc

---
Name: Read Doc
Description: Returns the content of a Google Doc in markdown format
Name: Read Google Doc
Description: Returns the content of a Google Doc in markdown format.
Share Context: Google Docs Context
Credential: ../credential
Param: doc_ref: Google Docs ID or share link of the document to read

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

---
Name: Update Google Doc
Description: Updates a Google Doc
Share Tools: Create Google Doc
Share Context: Google Docs Context, Update Google Doc Context
Credential: ../credential
Param: doc_ref: Google Docs ID or share link of the document to read
Param: new_doc_content: Markdown formatted content to replace the existing content of the document with

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

---
Name: Create Google Doc
Description: Creates a blank Google Doc
Share Context: Google Docs Context
Credential: ../credential
Param: doc_title: The title of the document to create

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

---
Name: Google Docs Context
Type: context
Expand All @@ -28,6 +48,21 @@ If the user does not provide a URL for the Google Doc they want to work with, as

## End of instructions for using Google Docs tools


---
Name: Update Google Doc Context
Type: context

#!sys.echo

## Instructions for using the Update Google Doc tool

When calling the Update Google Doc tool, provide the entire modified document as a markdown formatted
string using the new_doc_content parameter. The entire document is replaced by the content of the new_doc_content
argument.

## End of instructions for using the Update Google Doc tool

---
!metadata:*:category
Google Docs
Expand Down
139 changes: 139 additions & 0 deletions google/docs/update_doc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import sys
import os
import json

import markdown
from bs4 import BeautifulSoup

from auth import client
from id import extract_file_id

def markdown_to_google_doc_requests(markdown_content):
# Convert markdown content to HTML
html_content = markdown.markdown(markdown_content)

# Parse the HTML content using BeautifulSoup
soup = BeautifulSoup(html_content, 'html.parser')

requests = []

# Track the current index for insertion
current_index = 1

# Helper to add text with styles
def add_text_request(text, bold=False, italic=False, underline=False, link=None):
nonlocal current_index
text_style = {}
# Add styles only if explicitly specified
if bold:
text_style['bold'] = True
if italic:
text_style['italic'] = True
if underline:
text_style['underline'] = True
if link:
text_style['link'] = {"url": link}

# Add text insertion request
text_length = len(text)
requests.append({
"insertText": {
"location": {"index": current_index},
"text": text
}
})

# Add styling request only if any styles are present
if text_style:
requests.append({
"updateTextStyle": {
"range": {
"startIndex": current_index,
"endIndex": current_index + text_length
},
"textStyle": text_style,
"fields": ",".join(text_style.keys())
}
})

# Update the current index to account for the added text
current_index += text_length

# Process elements in the markdown
for element in soup.contents:
if element.name in ['p']:
add_text_request(element.get_text() + "\n")
elif element.name in ['h1', 'h2', 'h3']:
# Apply bold style for headers
add_text_request(element.get_text() + "\n", bold=True)
elif element.name in ['ul']:
for li in element.find_all('li'):
add_text_request("\u2022 " + li.get_text() + "\n")
elif element.name in ['ol']:
for i, li in enumerate(element.find_all('li'), start=1):
add_text_request(f"{i}. " + li.get_text() + "\n")
elif element.name == 'a':
# Add link
add_text_request(element.get_text(), link=element['href'])
elif element.name == 'table':
for row in element.find_all('tr'):
row_text = "\t".join([cell.get_text() for cell in row.find_all(['td', 'th'])]) + "\n"
add_text_request(row_text)
else:
# Default handling for unknown elements
add_text_request(element.get_text() + "\n")

return requests


def main():
try:
doc_ref = os.getenv('DOC_REF')
new_doc_content = os.getenv('NEW_DOC_CONTENT')

if not doc_ref:
raise ValueError('DOC_REF environment variable is missing or empty')

if not new_doc_content:
raise ValueError('NEW_DOC_CONTENT environment variable is missing or empty')

try:
requests = markdown_to_google_doc_requests(new_doc_content)
except Exception as e:
raise ValueError(f"Failed to parse NEW_DOC_CONTENT: {e}")

file_id = extract_file_id(doc_ref)
service = client('docs', 'v1')

# Retrieve the document to determine its length
document = service.documents().get(documentId=file_id).execute()
content = document.get('body').get('content')
document_length = content[-1].get('endIndex') if content and 'endIndex' in content[-1] else 1

if document_length > 2:
# Prepare requests to clear existing document content
requests = [
{
"deleteContentRange": {
"range": {
"startIndex": 1,
"endIndex": document_length - 1
}
}
}
] + requests

# Issue a batch update request to clear and apply new content
response = service.documents().batchUpdate(
documentId=file_id,
body={"requests": requests}
).execute()

print(f"Document updated successfully: {file_id}")

except Exception as err:
sys.stderr.write(f"Error: {err}\n")
sys.exit(1)

if __name__ == "__main__":
main()

0 comments on commit af5a039

Please sign in to comment.