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: add create and write google docs tools #360

Merged
merged 1 commit into from
Jan 23, 2025
Merged
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
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
thedadams marked this conversation as resolved.
Show resolved Hide resolved

#!/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)
njhale marked this conversation as resolved.
Show resolved Hide resolved
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()