arcade-google-slides 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arcade_google_slides/__init__.py +17 -0
- arcade_google_slides/converters.py +329 -0
- arcade_google_slides/decorators.py +24 -0
- arcade_google_slides/enum.py +165 -0
- arcade_google_slides/file_picker.py +49 -0
- arcade_google_slides/templates.py +5 -0
- arcade_google_slides/tools/__init__.py +18 -0
- arcade_google_slides/tools/comment.py +99 -0
- arcade_google_slides/tools/create.py +122 -0
- arcade_google_slides/tools/get.py +33 -0
- arcade_google_slides/tools/search.py +125 -0
- arcade_google_slides/types.py +223 -0
- arcade_google_slides/utils.py +166 -0
- arcade_google_slides-0.1.0.dist-info/METADATA +27 -0
- arcade_google_slides-0.1.0.dist-info/RECORD +17 -0
- arcade_google_slides-0.1.0.dist-info/WHEEL +4 -0
- arcade_google_slides-0.1.0.dist-info/licenses/LICENSE +35 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from arcade_google_slides.tools import (
|
|
2
|
+
comment_on_presentation,
|
|
3
|
+
create_presentation,
|
|
4
|
+
create_slide,
|
|
5
|
+
get_presentation_as_markdown,
|
|
6
|
+
list_presentation_comments,
|
|
7
|
+
search_presentations,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"create_presentation",
|
|
12
|
+
"create_slide",
|
|
13
|
+
"get_presentation_as_markdown",
|
|
14
|
+
"search_presentations",
|
|
15
|
+
"comment_on_presentation",
|
|
16
|
+
"list_presentation_comments",
|
|
17
|
+
]
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modular converter class for converting Google Slides presentations to Markdown.
|
|
3
|
+
This converter strictly follows the TypedDict structure definitions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from arcade_google_slides.types import (
|
|
9
|
+
Bullet,
|
|
10
|
+
Page,
|
|
11
|
+
PageElement,
|
|
12
|
+
ParagraphMarker,
|
|
13
|
+
Presentation,
|
|
14
|
+
Shape,
|
|
15
|
+
TextContent,
|
|
16
|
+
TextElement,
|
|
17
|
+
TextRun,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BulletConverter:
|
|
24
|
+
"""Converts bullet elements to markdown."""
|
|
25
|
+
|
|
26
|
+
def convert(self, bullet: Bullet) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Convert a bullet to markdown representation.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
bullet: Bullet TypedDict with glyph and nestingLevel
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Markdown string with appropriate indentation and markdown bullet syntax
|
|
35
|
+
"""
|
|
36
|
+
nesting_level = bullet.get("nestingLevel", 0)
|
|
37
|
+
indent = " " * nesting_level
|
|
38
|
+
return f"{indent}* "
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ParagraphMarkerConverter:
|
|
42
|
+
"""Converts paragraph markers to markdown."""
|
|
43
|
+
|
|
44
|
+
def __init__(self) -> None:
|
|
45
|
+
self.bullet_converter = BulletConverter()
|
|
46
|
+
|
|
47
|
+
def convert(self, paragraph_marker: ParagraphMarker) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Convert a paragraph marker to markdown.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
paragraph_marker: ParagraphMarker TypedDict
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Markdown string representation
|
|
56
|
+
"""
|
|
57
|
+
if "bullet" in paragraph_marker:
|
|
58
|
+
return self.bullet_converter.convert(paragraph_marker["bullet"])
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class TextRunConverter:
|
|
63
|
+
"""Converts text runs to markdown with styling support."""
|
|
64
|
+
|
|
65
|
+
def convert(self, text_run: TextRun) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Convert a text run to markdown with styling.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
text_run: TextRun TypedDict with content and optional style
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The text content with markdown formatting applied
|
|
74
|
+
"""
|
|
75
|
+
content = text_run.get("content", "")
|
|
76
|
+
|
|
77
|
+
style = text_run.get("style")
|
|
78
|
+
if not style:
|
|
79
|
+
return content
|
|
80
|
+
|
|
81
|
+
# Extract leading and trailing spaces to apply formatting correctly
|
|
82
|
+
# Markdown formatting must be adjacent to text, not spaces
|
|
83
|
+
leading_spaces = len(content) - len(content.lstrip())
|
|
84
|
+
trailing_spaces = len(content) - len(content.rstrip())
|
|
85
|
+
|
|
86
|
+
# Get the spaces
|
|
87
|
+
prefix = content[:leading_spaces] if leading_spaces > 0 else ""
|
|
88
|
+
suffix = content[-trailing_spaces:] if trailing_spaces > 0 else ""
|
|
89
|
+
|
|
90
|
+
trimmed_content = content.strip()
|
|
91
|
+
|
|
92
|
+
if not trimmed_content:
|
|
93
|
+
return content
|
|
94
|
+
|
|
95
|
+
# Apply text formatting to the trimmed content
|
|
96
|
+
# Note: Order matters for proper markdown rendering
|
|
97
|
+
|
|
98
|
+
if style.get("strikethrough", False):
|
|
99
|
+
trimmed_content = f"~~{trimmed_content}~~"
|
|
100
|
+
|
|
101
|
+
is_bold = style.get("bold", False)
|
|
102
|
+
is_italic = style.get("italic", False)
|
|
103
|
+
|
|
104
|
+
if is_bold and is_italic:
|
|
105
|
+
# Both bold and italic: ***text***
|
|
106
|
+
trimmed_content = f"***{trimmed_content}***"
|
|
107
|
+
elif is_bold:
|
|
108
|
+
# Bold only: **text**
|
|
109
|
+
trimmed_content = f"**{trimmed_content}**"
|
|
110
|
+
elif is_italic:
|
|
111
|
+
# Italic only: *text*
|
|
112
|
+
trimmed_content = f"*{trimmed_content}*"
|
|
113
|
+
|
|
114
|
+
# Note: This may not render in all markdown viewers
|
|
115
|
+
if style.get("underline", False):
|
|
116
|
+
trimmed_content = f"<u>{trimmed_content}</u>"
|
|
117
|
+
|
|
118
|
+
link = style.get("link")
|
|
119
|
+
if link:
|
|
120
|
+
url = link.get("url", "")
|
|
121
|
+
if url:
|
|
122
|
+
trimmed_content = f"[{trimmed_content}]({url})"
|
|
123
|
+
|
|
124
|
+
return prefix + trimmed_content + suffix
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TextElementConverter:
|
|
128
|
+
"""Converts text elements to markdown."""
|
|
129
|
+
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
self.paragraph_marker_converter = ParagraphMarkerConverter()
|
|
132
|
+
self.text_run_converter = TextRunConverter()
|
|
133
|
+
|
|
134
|
+
def convert(self, text_element: TextElement) -> str:
|
|
135
|
+
"""
|
|
136
|
+
Convert a text element to markdown.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
text_element: Either TextElementWithParagraphMarker or TextElementWithTextRun
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Markdown string representation
|
|
143
|
+
"""
|
|
144
|
+
if "paragraphMarker" in text_element:
|
|
145
|
+
return self.paragraph_marker_converter.convert(text_element["paragraphMarker"]) # type: ignore[typeddict-item]
|
|
146
|
+
|
|
147
|
+
elif "textRun" in text_element:
|
|
148
|
+
return self.text_run_converter.convert(text_element["textRun"])
|
|
149
|
+
|
|
150
|
+
return ""
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TextContentConverter:
|
|
154
|
+
"""Converts text content to markdown."""
|
|
155
|
+
|
|
156
|
+
def __init__(self) -> None:
|
|
157
|
+
self.text_element_converter = TextElementConverter()
|
|
158
|
+
|
|
159
|
+
def convert(self, text_content: TextContent) -> str:
|
|
160
|
+
"""
|
|
161
|
+
Convert text content to markdown.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
text_content: TextContent TypedDict with textElements list
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Markdown string representation
|
|
168
|
+
"""
|
|
169
|
+
markdown_parts = []
|
|
170
|
+
|
|
171
|
+
text_elements = text_content.get("textElements", [])
|
|
172
|
+
|
|
173
|
+
current_line = ""
|
|
174
|
+
for element in text_elements:
|
|
175
|
+
converted = self.text_element_converter.convert(element)
|
|
176
|
+
|
|
177
|
+
# If it's a bullet marker, start a new line if needed
|
|
178
|
+
if "paragraphMarker" in element and "bullet" in element["paragraphMarker"]: # type: ignore[typeddict-item]
|
|
179
|
+
if current_line:
|
|
180
|
+
markdown_parts.append(current_line)
|
|
181
|
+
current_line = converted
|
|
182
|
+
else:
|
|
183
|
+
current_line += converted
|
|
184
|
+
|
|
185
|
+
# Check if the text run ends with a newline
|
|
186
|
+
if "textRun" in element:
|
|
187
|
+
content = element["textRun"].get("content", "") # type: ignore[typeddict-item]
|
|
188
|
+
if content.endswith("\n"):
|
|
189
|
+
markdown_parts.append(current_line.rstrip("\n"))
|
|
190
|
+
current_line = ""
|
|
191
|
+
|
|
192
|
+
# Add any remaining text
|
|
193
|
+
if current_line:
|
|
194
|
+
markdown_parts.append(current_line)
|
|
195
|
+
|
|
196
|
+
return "\n".join(markdown_parts)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class ShapeConverter:
|
|
200
|
+
"""Converts shapes to markdown."""
|
|
201
|
+
|
|
202
|
+
def __init__(self) -> None:
|
|
203
|
+
self.text_content_converter = TextContentConverter()
|
|
204
|
+
|
|
205
|
+
def convert(self, shape: Shape) -> str:
|
|
206
|
+
"""
|
|
207
|
+
Convert a shape to markdown.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
shape: Shape TypedDict with shapeType and text
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Markdown string representation
|
|
214
|
+
"""
|
|
215
|
+
markdown = ""
|
|
216
|
+
|
|
217
|
+
if "text" in shape:
|
|
218
|
+
text_markdown = self.text_content_converter.convert(shape["text"])
|
|
219
|
+
if text_markdown:
|
|
220
|
+
markdown += text_markdown
|
|
221
|
+
if not text_markdown.endswith("\n"):
|
|
222
|
+
markdown += "\n"
|
|
223
|
+
|
|
224
|
+
return markdown
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class PageElementConverter:
|
|
228
|
+
"""Converts page elements to markdown."""
|
|
229
|
+
|
|
230
|
+
def __init__(self) -> None:
|
|
231
|
+
self.shape_converter = ShapeConverter()
|
|
232
|
+
|
|
233
|
+
def convert(self, page_element: PageElement) -> str:
|
|
234
|
+
"""
|
|
235
|
+
Convert a page element to markdown.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
page_element: PageElement TypedDict with objectId and shape
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Markdown string representation
|
|
242
|
+
"""
|
|
243
|
+
if "shape" in page_element:
|
|
244
|
+
return self.shape_converter.convert(page_element["shape"])
|
|
245
|
+
return ""
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class PageConverter:
|
|
249
|
+
"""Converts pages (slides) to markdown."""
|
|
250
|
+
|
|
251
|
+
def __init__(self) -> None:
|
|
252
|
+
self.page_element_converter = PageElementConverter()
|
|
253
|
+
|
|
254
|
+
def convert(self, page: Page, page_number: int) -> str:
|
|
255
|
+
"""
|
|
256
|
+
Convert a page (slide) to markdown.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
page: Page TypedDict with objectId and pageElements
|
|
260
|
+
page_number: The page/slide number (1-indexed)
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Markdown string representation
|
|
264
|
+
"""
|
|
265
|
+
is_hidden = page.get("slideProperties", {}).get("isSkipped", False)
|
|
266
|
+
is_hidden_str = " (hidden)" if is_hidden else ""
|
|
267
|
+
markdown = f"## Slide {page_number}{is_hidden_str}\n\n"
|
|
268
|
+
|
|
269
|
+
slide_id = page.get("objectId", "")
|
|
270
|
+
if slide_id:
|
|
271
|
+
markdown += f"**Slide ID:** {slide_id}\n\n"
|
|
272
|
+
|
|
273
|
+
# Process all page elements
|
|
274
|
+
page_elements = page.get("pageElements", [])
|
|
275
|
+
for element in page_elements:
|
|
276
|
+
element_markdown = self.page_element_converter.convert(element)
|
|
277
|
+
if element_markdown:
|
|
278
|
+
markdown += element_markdown
|
|
279
|
+
if not element_markdown.endswith("\n\n"):
|
|
280
|
+
markdown += "\n"
|
|
281
|
+
|
|
282
|
+
return markdown
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class PresentationMarkdownConverter:
|
|
286
|
+
"""
|
|
287
|
+
Main converter class for converting Google Slides presentations to markdown.
|
|
288
|
+
This converter strictly follows the TypedDict structure definitions.
|
|
289
|
+
"""
|
|
290
|
+
|
|
291
|
+
def __init__(self) -> None:
|
|
292
|
+
self.page_converter = PageConverter()
|
|
293
|
+
|
|
294
|
+
def convert(self, presentation: Presentation) -> str:
|
|
295
|
+
"""
|
|
296
|
+
Convert a Google Slides presentation to markdown format.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
presentation: Presentation TypedDict with presentationId, title, and slides
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
A markdown string representation of the presentation
|
|
303
|
+
"""
|
|
304
|
+
if not presentation:
|
|
305
|
+
return ""
|
|
306
|
+
|
|
307
|
+
# Extract metadata
|
|
308
|
+
title = presentation.get("title", "Untitled Presentation")
|
|
309
|
+
presentation_id = presentation.get("presentationId", "")
|
|
310
|
+
slides = presentation.get("slides", [])
|
|
311
|
+
|
|
312
|
+
# Build markdown
|
|
313
|
+
markdown = f"# {title}\n\n"
|
|
314
|
+
|
|
315
|
+
if presentation_id:
|
|
316
|
+
markdown += f"**Presentation ID:** {presentation_id}\n\n"
|
|
317
|
+
|
|
318
|
+
if slides:
|
|
319
|
+
markdown += "---\n\n"
|
|
320
|
+
|
|
321
|
+
for i, slide in enumerate(slides, 1):
|
|
322
|
+
slide_markdown = self.page_converter.convert(slide, i)
|
|
323
|
+
markdown += slide_markdown
|
|
324
|
+
|
|
325
|
+
# Adds separator between slides
|
|
326
|
+
if i < len(slides):
|
|
327
|
+
markdown += "\n---\n\n"
|
|
328
|
+
|
|
329
|
+
return markdown.strip()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from arcade_tdk import ToolContext
|
|
6
|
+
from googleapiclient.errors import HttpError
|
|
7
|
+
|
|
8
|
+
from arcade_google_slides.file_picker import generate_google_file_picker_url
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def with_filepicker_fallback(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
12
|
+
""" """
|
|
13
|
+
|
|
14
|
+
@functools.wraps(func)
|
|
15
|
+
async def async_wrapper(context: ToolContext, *args: Any, **kwargs: Any) -> Any:
|
|
16
|
+
try:
|
|
17
|
+
return await func(context, *args, **kwargs)
|
|
18
|
+
except HttpError as e:
|
|
19
|
+
if e.status_code in [403, 404]:
|
|
20
|
+
file_picker_response = generate_google_file_picker_url(context)
|
|
21
|
+
return file_picker_response
|
|
22
|
+
raise
|
|
23
|
+
|
|
24
|
+
return async_wrapper
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class PredefinedLayout(str, Enum):
|
|
5
|
+
"""Partial implementation of the REST Resource PredefinedLayout.
|
|
6
|
+
|
|
7
|
+
Reference: https://developers.google.com/workspace/slides/api/reference/rest/v1/presentations/request#PredefinedLayout
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
BLANK = "BLANK" # Blank layout, with no placeholders.
|
|
11
|
+
CAPTION_ONLY = "CAPTION_ONLY" # Layout with a caption at the bottom.
|
|
12
|
+
TITLE = "TITLE" # Layout with a title and a subtitle.
|
|
13
|
+
TITLE_AND_BODY = "TITLE_AND_BODY" # Layout with a title and body.
|
|
14
|
+
TITLE_AND_TWO_COLUMNS = "TITLE_AND_TWO_COLUMNS" # Layout with a title and two columns.
|
|
15
|
+
TITLE_ONLY = "TITLE_ONLY" # Layout with only a title.
|
|
16
|
+
SECTION_HEADER = "SECTION_HEADER" # Layout with a section title.
|
|
17
|
+
SECTION_TITLE_AND_DESCRIPTION = "SECTION_TITLE_AND_DESCRIPTION" # Layout with a title and subtitle on one side and description on the other. # noqa: E501
|
|
18
|
+
ONE_COLUMN_TEXT = (
|
|
19
|
+
"ONE_COLUMN_TEXT" # Layout with one title and one body, arranged in a single column.
|
|
20
|
+
)
|
|
21
|
+
MAIN_POINT = "MAIN_POINT" # Layout with a main point.
|
|
22
|
+
BIG_NUMBER = "BIG_NUMBER" # Layout with a big number heading.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ShapeType(Enum):
|
|
26
|
+
"""
|
|
27
|
+
The type of shape. For now, only TEXT_BOX is supported.
|
|
28
|
+
|
|
29
|
+
Reference: https://developers.google.com/workspace/slides/api/reference/rest/v1/presentations.pages/shapes#type
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
TEXT_BOX = "TEXT_BOX"
|
|
33
|
+
# TODO: Support rectangle or table cell?
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class PlaceholderType(str, Enum):
|
|
37
|
+
"""Partial implementation of the REST Resource 'Type'.
|
|
38
|
+
|
|
39
|
+
The type of placeholder shape.
|
|
40
|
+
|
|
41
|
+
Reference: https://developers.google.com/workspace/slides/api/reference/rest/v1/presentations.pages/other#Page.Type_3
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
TITLE = "TITLE"
|
|
45
|
+
BODY = "BODY"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --------------------------------------------------------- #
|
|
49
|
+
# Drive API Enums
|
|
50
|
+
# --------------------------------------------------------- #
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Corpora(str, Enum):
|
|
54
|
+
"""
|
|
55
|
+
Bodies of items (files/documents) to which the query applies.
|
|
56
|
+
Prefer 'user' or 'drive' to 'allDrives' for efficiency.
|
|
57
|
+
By default, corpora is set to 'user'.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
USER = "user"
|
|
61
|
+
DOMAIN = "domain"
|
|
62
|
+
DRIVE = "drive"
|
|
63
|
+
ALL_DRIVES = "allDrives"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DocumentFormat(str, Enum):
|
|
67
|
+
MARKDOWN = "markdown"
|
|
68
|
+
HTML = "html"
|
|
69
|
+
GOOGLE_API_JSON = "google_api_json"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class OrderBy(str, Enum):
|
|
73
|
+
"""
|
|
74
|
+
Sort keys for ordering files in Google Drive.
|
|
75
|
+
Each key has both ascending and descending options.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
CREATED_TIME = (
|
|
79
|
+
# When the file was created (ascending)
|
|
80
|
+
"createdTime"
|
|
81
|
+
)
|
|
82
|
+
CREATED_TIME_DESC = (
|
|
83
|
+
# When the file was created (descending)
|
|
84
|
+
"createdTime desc"
|
|
85
|
+
)
|
|
86
|
+
FOLDER = (
|
|
87
|
+
# The folder ID, sorted using alphabetical ordering (ascending)
|
|
88
|
+
"folder"
|
|
89
|
+
)
|
|
90
|
+
FOLDER_DESC = (
|
|
91
|
+
# The folder ID, sorted using alphabetical ordering (descending)
|
|
92
|
+
"folder desc"
|
|
93
|
+
)
|
|
94
|
+
MODIFIED_BY_ME_TIME = (
|
|
95
|
+
# The last time the file was modified by the user (ascending)
|
|
96
|
+
"modifiedByMeTime"
|
|
97
|
+
)
|
|
98
|
+
MODIFIED_BY_ME_TIME_DESC = (
|
|
99
|
+
# The last time the file was modified by the user (descending)
|
|
100
|
+
"modifiedByMeTime desc"
|
|
101
|
+
)
|
|
102
|
+
MODIFIED_TIME = (
|
|
103
|
+
# The last time the file was modified by anyone (ascending)
|
|
104
|
+
"modifiedTime"
|
|
105
|
+
)
|
|
106
|
+
MODIFIED_TIME_DESC = (
|
|
107
|
+
# The last time the file was modified by anyone (descending)
|
|
108
|
+
"modifiedTime desc"
|
|
109
|
+
)
|
|
110
|
+
NAME = (
|
|
111
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (ascending)
|
|
112
|
+
"name"
|
|
113
|
+
)
|
|
114
|
+
NAME_DESC = (
|
|
115
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (descending)
|
|
116
|
+
"name desc"
|
|
117
|
+
)
|
|
118
|
+
NAME_NATURAL = (
|
|
119
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (ascending)
|
|
120
|
+
"name_natural"
|
|
121
|
+
)
|
|
122
|
+
NAME_NATURAL_DESC = (
|
|
123
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (descending)
|
|
124
|
+
"name_natural desc"
|
|
125
|
+
)
|
|
126
|
+
QUOTA_BYTES_USED = (
|
|
127
|
+
# The number of storage quota bytes used by the file (ascending)
|
|
128
|
+
"quotaBytesUsed"
|
|
129
|
+
)
|
|
130
|
+
QUOTA_BYTES_USED_DESC = (
|
|
131
|
+
# The number of storage quota bytes used by the file (descending)
|
|
132
|
+
"quotaBytesUsed desc"
|
|
133
|
+
)
|
|
134
|
+
RECENCY = (
|
|
135
|
+
# The most recent timestamp from the file's date-time fields (ascending)
|
|
136
|
+
"recency"
|
|
137
|
+
)
|
|
138
|
+
RECENCY_DESC = (
|
|
139
|
+
# The most recent timestamp from the file's date-time fields (descending)
|
|
140
|
+
"recency desc"
|
|
141
|
+
)
|
|
142
|
+
SHARED_WITH_ME_TIME = (
|
|
143
|
+
# When the file was shared with the user, if applicable (ascending)
|
|
144
|
+
"sharedWithMeTime"
|
|
145
|
+
)
|
|
146
|
+
SHARED_WITH_ME_TIME_DESC = (
|
|
147
|
+
# When the file was shared with the user, if applicable (descending)
|
|
148
|
+
"sharedWithMeTime desc"
|
|
149
|
+
)
|
|
150
|
+
STARRED = (
|
|
151
|
+
# Whether the user has starred the file (ascending)
|
|
152
|
+
"starred"
|
|
153
|
+
)
|
|
154
|
+
STARRED_DESC = (
|
|
155
|
+
# Whether the user has starred the file (descending)
|
|
156
|
+
"starred desc"
|
|
157
|
+
)
|
|
158
|
+
VIEWED_BY_ME_TIME = (
|
|
159
|
+
# The last time the file was viewed by the user (ascending)
|
|
160
|
+
"viewedByMeTime"
|
|
161
|
+
)
|
|
162
|
+
VIEWED_BY_ME_TIME_DESC = (
|
|
163
|
+
# The last time the file was viewed by the user (descending)
|
|
164
|
+
"viewedByMeTime desc"
|
|
165
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from arcade_tdk import ToolContext, ToolMetadataKey
|
|
5
|
+
from arcade_tdk.errors import ToolExecutionError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def generate_google_file_picker_url(context: ToolContext) -> dict:
|
|
9
|
+
"""Generate a Google File Picker URL for user-driven file selection and authorization.
|
|
10
|
+
|
|
11
|
+
Generates a URL that directs the end-user to a Google File Picker interface where
|
|
12
|
+
where they can select or upload Google Drive files. Users can grant permission to access their
|
|
13
|
+
Drive files, providing a secure and authorized way to interact with their files.
|
|
14
|
+
|
|
15
|
+
This is particularly useful when prior tools (e.g., those accessing or modifying
|
|
16
|
+
Google Docs, Google Sheets, etc.) encountered failures due to file non-existence
|
|
17
|
+
(Requested entity was not found) or permission errors. Once the user completes the file
|
|
18
|
+
picker flow, the prior tool can be retried.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
A dictionary containing the URL and instructions for the llm to instruct the user.
|
|
22
|
+
"""
|
|
23
|
+
client_id = context.get_metadata(ToolMetadataKey.CLIENT_ID)
|
|
24
|
+
client_id_parts = client_id.split("-")
|
|
25
|
+
if not client_id_parts:
|
|
26
|
+
raise ToolExecutionError(
|
|
27
|
+
message="Invalid Google Client ID",
|
|
28
|
+
developer_message=f"Google Client ID '{client_id}' is not valid",
|
|
29
|
+
)
|
|
30
|
+
app_id = client_id_parts[0]
|
|
31
|
+
cloud_coordinator_url = context.get_metadata(ToolMetadataKey.COORDINATOR_URL).strip("/")
|
|
32
|
+
|
|
33
|
+
config = {
|
|
34
|
+
"auth": {
|
|
35
|
+
"client_id": client_id,
|
|
36
|
+
"app_id": app_id,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
config_json = json.dumps(config)
|
|
40
|
+
config_base64 = base64.urlsafe_b64encode(config_json.encode("utf-8")).decode("utf-8")
|
|
41
|
+
url = f"{cloud_coordinator_url}/google/drive_picker?config={config_base64}"
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
"url": url,
|
|
45
|
+
"llm_instructions": (
|
|
46
|
+
"Instruct the user to click the following link to open the Google Drive File Picker. "
|
|
47
|
+
f"This will allow them to select files and grant access permissions: {url}"
|
|
48
|
+
),
|
|
49
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
optional_file_picker_instructions_template = (
|
|
2
|
+
"Ensure the user knows that they have the option to select and grant access permissions to "
|
|
3
|
+
"additional documents via the Google Drive File Picker. "
|
|
4
|
+
"The user can pick additional documents via the following link: {url}"
|
|
5
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from arcade_google_slides.tools.comment import (
|
|
2
|
+
comment_on_presentation,
|
|
3
|
+
list_presentation_comments,
|
|
4
|
+
)
|
|
5
|
+
from arcade_google_slides.tools.create import create_presentation, create_slide
|
|
6
|
+
from arcade_google_slides.tools.get import get_presentation_as_markdown
|
|
7
|
+
from arcade_google_slides.tools.search import (
|
|
8
|
+
search_presentations,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"create_presentation",
|
|
13
|
+
"create_slide",
|
|
14
|
+
"get_presentation_as_markdown",
|
|
15
|
+
"search_presentations",
|
|
16
|
+
"comment_on_presentation",
|
|
17
|
+
"list_presentation_comments",
|
|
18
|
+
]
|