arcade-google-docs 2.0.0rc1__tar.gz
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_docs-2.0.0rc1/.gitignore +175 -0
- arcade_google_docs-2.0.0rc1/.pre-commit-config.yaml +18 -0
- arcade_google_docs-2.0.0rc1/.ruff.toml +46 -0
- arcade_google_docs-2.0.0rc1/Makefile +55 -0
- arcade_google_docs-2.0.0rc1/PKG-INFO +23 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/__init__.py +17 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/decorators.py +24 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/doc_to_html.py +99 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/doc_to_markdown.py +64 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/enum.py +116 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/file_picker.py +49 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/templates.py +5 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/tools/__init__.py +19 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/tools/create.py +82 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/tools/get.py +35 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/tools/search.py +219 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/tools/update.py +60 -0
- arcade_google_docs-2.0.0rc1/arcade_google_docs/utils.py +119 -0
- arcade_google_docs-2.0.0rc1/conftest.py +967 -0
- arcade_google_docs-2.0.0rc1/evals/eval_google_docs.py +384 -0
- arcade_google_docs-2.0.0rc1/pyproject.toml +62 -0
- arcade_google_docs-2.0.0rc1/tests/__init__.py +0 -0
- arcade_google_docs-2.0.0rc1/tests/test_doc_to_markdown.py +10 -0
- arcade_google_docs-2.0.0rc1/tests/test_google_docs.py +179 -0
- arcade_google_docs-2.0.0rc1/tests/test_search.py +276 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
credentials.yaml
|
|
3
|
+
docker/credentials.yaml
|
|
4
|
+
|
|
5
|
+
*.lock
|
|
6
|
+
|
|
7
|
+
# example data
|
|
8
|
+
examples/data
|
|
9
|
+
scratch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
docs/source
|
|
13
|
+
|
|
14
|
+
# From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore
|
|
15
|
+
|
|
16
|
+
# Byte-compiled / optimized / DLL files
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*$py.class
|
|
20
|
+
|
|
21
|
+
# C extensions
|
|
22
|
+
*.so
|
|
23
|
+
|
|
24
|
+
# Distribution / packaging
|
|
25
|
+
.Python
|
|
26
|
+
build/
|
|
27
|
+
develop-eggs/
|
|
28
|
+
dist/
|
|
29
|
+
downloads/
|
|
30
|
+
eggs/
|
|
31
|
+
.eggs/
|
|
32
|
+
lib/
|
|
33
|
+
lib64/
|
|
34
|
+
parts/
|
|
35
|
+
sdist/
|
|
36
|
+
var/
|
|
37
|
+
wheels/
|
|
38
|
+
share/python-wheels/
|
|
39
|
+
*.egg-info/
|
|
40
|
+
.installed.cfg
|
|
41
|
+
*.egg
|
|
42
|
+
MANIFEST
|
|
43
|
+
|
|
44
|
+
# PyInstaller
|
|
45
|
+
# Usually these files are written by a python script from a template
|
|
46
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
47
|
+
*.manifest
|
|
48
|
+
*.spec
|
|
49
|
+
|
|
50
|
+
# Installer logs
|
|
51
|
+
pip-log.txt
|
|
52
|
+
pip-delete-this-directory.txt
|
|
53
|
+
|
|
54
|
+
# Unit test / coverage reports
|
|
55
|
+
htmlcov/
|
|
56
|
+
.tox/
|
|
57
|
+
.nox/
|
|
58
|
+
.coverage
|
|
59
|
+
.coverage.*
|
|
60
|
+
.cache
|
|
61
|
+
nosetests.xml
|
|
62
|
+
coverage.xml
|
|
63
|
+
*.cover
|
|
64
|
+
*.py,cover
|
|
65
|
+
.hypothesis/
|
|
66
|
+
.pytest_cache/
|
|
67
|
+
cover/
|
|
68
|
+
|
|
69
|
+
# Translations
|
|
70
|
+
*.mo
|
|
71
|
+
*.pot
|
|
72
|
+
|
|
73
|
+
# Django stuff:
|
|
74
|
+
*.log
|
|
75
|
+
local_settings.py
|
|
76
|
+
db.sqlite3
|
|
77
|
+
db.sqlite3-journal
|
|
78
|
+
|
|
79
|
+
# Flask stuff:
|
|
80
|
+
instance/
|
|
81
|
+
.webassets-cache
|
|
82
|
+
|
|
83
|
+
# Scrapy stuff:
|
|
84
|
+
.scrapy
|
|
85
|
+
|
|
86
|
+
# Sphinx documentation
|
|
87
|
+
docs/_build/
|
|
88
|
+
|
|
89
|
+
# PyBuilder
|
|
90
|
+
.pybuilder/
|
|
91
|
+
target/
|
|
92
|
+
|
|
93
|
+
# Jupyter Notebook
|
|
94
|
+
.ipynb_checkpoints
|
|
95
|
+
|
|
96
|
+
# IPython
|
|
97
|
+
profile_default/
|
|
98
|
+
ipython_config.py
|
|
99
|
+
|
|
100
|
+
# pyenv
|
|
101
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
102
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
103
|
+
# .python-version
|
|
104
|
+
|
|
105
|
+
# pipenv
|
|
106
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
107
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
108
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
109
|
+
# install all needed dependencies.
|
|
110
|
+
#Pipfile.lock
|
|
111
|
+
|
|
112
|
+
# poetry
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
114
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
115
|
+
# commonly ignored for libraries.
|
|
116
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
117
|
+
poetry.lock
|
|
118
|
+
|
|
119
|
+
# pdm
|
|
120
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
121
|
+
#pdm.lock
|
|
122
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
123
|
+
# in version control.
|
|
124
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
125
|
+
.pdm.toml
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# SageMath parsed files
|
|
135
|
+
*.sage.py
|
|
136
|
+
|
|
137
|
+
# Environments
|
|
138
|
+
.env
|
|
139
|
+
.venv
|
|
140
|
+
env/
|
|
141
|
+
venv/
|
|
142
|
+
ENV/
|
|
143
|
+
env.bak/
|
|
144
|
+
venv.bak/
|
|
145
|
+
|
|
146
|
+
# Spyder project settings
|
|
147
|
+
.spyderproject
|
|
148
|
+
.spyproject
|
|
149
|
+
|
|
150
|
+
# Rope project settings
|
|
151
|
+
.ropeproject
|
|
152
|
+
|
|
153
|
+
# mkdocs documentation
|
|
154
|
+
/site
|
|
155
|
+
|
|
156
|
+
# mypy
|
|
157
|
+
.mypy_cache/
|
|
158
|
+
.dmypy.json
|
|
159
|
+
dmypy.json
|
|
160
|
+
|
|
161
|
+
# Pyre type checker
|
|
162
|
+
.pyre/
|
|
163
|
+
|
|
164
|
+
# pytype static type analyzer
|
|
165
|
+
.pytype/
|
|
166
|
+
|
|
167
|
+
# Cython debug symbols
|
|
168
|
+
cython_debug/
|
|
169
|
+
|
|
170
|
+
# PyCharm
|
|
171
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
172
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
173
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
174
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
175
|
+
#.idea/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
files: ^.*/google_docs/.*
|
|
2
|
+
repos:
|
|
3
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
4
|
+
rev: "v4.4.0"
|
|
5
|
+
hooks:
|
|
6
|
+
- id: check-case-conflict
|
|
7
|
+
- id: check-merge-conflict
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-yaml
|
|
10
|
+
- id: end-of-file-fixer
|
|
11
|
+
- id: trailing-whitespace
|
|
12
|
+
|
|
13
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
|
+
rev: v0.6.7
|
|
15
|
+
hooks:
|
|
16
|
+
- id: ruff
|
|
17
|
+
args: [--fix]
|
|
18
|
+
- id: ruff-format
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
target-version = "py310"
|
|
2
|
+
line-length = 100
|
|
3
|
+
fix = true
|
|
4
|
+
|
|
5
|
+
[lint]
|
|
6
|
+
select = [
|
|
7
|
+
# flake8-2020
|
|
8
|
+
"YTT",
|
|
9
|
+
# flake8-bandit
|
|
10
|
+
"S",
|
|
11
|
+
# flake8-bugbear
|
|
12
|
+
"B",
|
|
13
|
+
# flake8-builtins
|
|
14
|
+
"A",
|
|
15
|
+
# flake8-comprehensions
|
|
16
|
+
"C4",
|
|
17
|
+
# flake8-debugger
|
|
18
|
+
"T10",
|
|
19
|
+
# flake8-simplify
|
|
20
|
+
"SIM",
|
|
21
|
+
# isort
|
|
22
|
+
"I",
|
|
23
|
+
# mccabe
|
|
24
|
+
"C90",
|
|
25
|
+
# pycodestyle
|
|
26
|
+
"E", "W",
|
|
27
|
+
# pyflakes
|
|
28
|
+
"F",
|
|
29
|
+
# pygrep-hooks
|
|
30
|
+
"PGH",
|
|
31
|
+
# pyupgrade
|
|
32
|
+
"UP",
|
|
33
|
+
# ruff
|
|
34
|
+
"RUF",
|
|
35
|
+
# tryceratops
|
|
36
|
+
"TRY",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[lint.per-file-ignores]
|
|
40
|
+
"*" = ["TRY003", "B904"]
|
|
41
|
+
"**/tests/*" = ["S101", "E501"]
|
|
42
|
+
"**/evals/*" = ["S101", "E501"]
|
|
43
|
+
|
|
44
|
+
[format]
|
|
45
|
+
preview = true
|
|
46
|
+
skip-magic-trailing-comma = false
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
.PHONY: help
|
|
2
|
+
|
|
3
|
+
help:
|
|
4
|
+
@echo "🛠️ github Commands:\n"
|
|
5
|
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
|
|
6
|
+
|
|
7
|
+
.PHONY: install
|
|
8
|
+
install: ## Install the uv environment and install all packages with dependencies
|
|
9
|
+
@echo "🚀 Creating virtual environment and installing all packages using uv"
|
|
10
|
+
@uv sync --active --all-extras --no-sources
|
|
11
|
+
@if [ -f .pre-commit-config.yaml ]; then uv run --no-sources pre-commit install; fi
|
|
12
|
+
@echo "✅ All packages and dependencies installed via uv"
|
|
13
|
+
|
|
14
|
+
.PHONY: install-local
|
|
15
|
+
install-local: ## Install the uv environment and install all packages with dependencies with local Arcade sources
|
|
16
|
+
@echo "🚀 Creating virtual environment and installing all packages using uv"
|
|
17
|
+
@uv sync --active --all-extras
|
|
18
|
+
@if [ -f .pre-commit-config.yaml ]; then uv run pre-commit install; fi
|
|
19
|
+
@echo "✅ All packages and dependencies installed via uv"
|
|
20
|
+
|
|
21
|
+
.PHONY: build
|
|
22
|
+
build: clean-build ## Build wheel file using poetry
|
|
23
|
+
@echo "🚀 Creating wheel file"
|
|
24
|
+
uv build
|
|
25
|
+
|
|
26
|
+
.PHONY: clean-build
|
|
27
|
+
clean-build: ## clean build artifacts
|
|
28
|
+
@echo "🗑️ Cleaning dist directory"
|
|
29
|
+
rm -rf dist
|
|
30
|
+
|
|
31
|
+
.PHONY: test
|
|
32
|
+
test: ## Test the code with pytest
|
|
33
|
+
@echo "🚀 Testing code: Running pytest"
|
|
34
|
+
@uv run --no-sources pytest -W ignore -v --cov --cov-config=pyproject.toml --cov-report=xml
|
|
35
|
+
|
|
36
|
+
.PHONY: coverage
|
|
37
|
+
coverage: ## Generate coverage report
|
|
38
|
+
@echo "coverage report"
|
|
39
|
+
@uv run --no-sources coverage report
|
|
40
|
+
@echo "Generating coverage report"
|
|
41
|
+
@uv run --no-sources coverage html
|
|
42
|
+
|
|
43
|
+
.PHONY: bump-version
|
|
44
|
+
bump-version: ## Bump the version in the pyproject.toml file by a patch version
|
|
45
|
+
@echo "🚀 Bumping version in pyproject.toml"
|
|
46
|
+
uv version --no-sources --bump patch
|
|
47
|
+
|
|
48
|
+
.PHONY: check
|
|
49
|
+
check: ## Run code quality tools.
|
|
50
|
+
@if [ -f .pre-commit-config.yaml ]; then\
|
|
51
|
+
echo "🚀 Linting code: Running pre-commit";\
|
|
52
|
+
uv run --no-sources pre-commit run -a;\
|
|
53
|
+
fi
|
|
54
|
+
@echo "🚀 Static type checking: Running mypy"
|
|
55
|
+
@uv run --no-sources mypy --config-file=pyproject.toml
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arcade_google_docs
|
|
3
|
+
Version: 2.0.0rc1
|
|
4
|
+
Summary: Arcade.dev LLM tools for Google Docs
|
|
5
|
+
Author-email: Arcade <dev@arcade.dev>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: arcade-tdk<3.0.0,>=2.0.0
|
|
8
|
+
Requires-Dist: google-api-core<3.0.0,>=2.19.1
|
|
9
|
+
Requires-Dist: google-api-python-client<3.0.0,>=2.137.0
|
|
10
|
+
Requires-Dist: google-auth-httplib2<1.0.0,>=0.2.0
|
|
11
|
+
Requires-Dist: google-auth<3.0.0,>=2.32.0
|
|
12
|
+
Requires-Dist: googleapis-common-protos<2.0.0,>=1.63.2
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.4; extra == 'dev'
|
|
15
|
+
Requires-Dist: arcade-serve<3.0.0,>=2.0.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: mypy<1.6.0,>=1.5.1; extra == 'dev'
|
|
17
|
+
Requires-Dist: pre-commit<3.5.0,>=3.4.0; extra == 'dev'
|
|
18
|
+
Requires-Dist: pytest-asyncio<0.25.0,>=0.24.0; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-cov<4.1.0,>=4.0.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-mock<3.12.0,>=3.11.1; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest<8.4.0,>=8.3.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff<0.8.0,>=0.7.4; extra == 'dev'
|
|
23
|
+
Requires-Dist: tox<4.12.0,>=4.11.1; extra == 'dev'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from arcade_google_docs.tools import (
|
|
2
|
+
create_blank_document,
|
|
3
|
+
create_document_from_text,
|
|
4
|
+
get_document_by_id,
|
|
5
|
+
insert_text_at_end_of_document,
|
|
6
|
+
search_and_retrieve_documents,
|
|
7
|
+
search_documents,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"create_blank_document",
|
|
12
|
+
"create_document_from_text",
|
|
13
|
+
"get_document_by_id",
|
|
14
|
+
"insert_text_at_end_of_document",
|
|
15
|
+
"search_and_retrieve_documents",
|
|
16
|
+
"search_documents",
|
|
17
|
+
]
|
|
@@ -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_docs.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,99 @@
|
|
|
1
|
+
def convert_document_to_html(document: dict) -> str:
|
|
2
|
+
html = (
|
|
3
|
+
"<html><head>"
|
|
4
|
+
f"<title>{document['title']}</title>"
|
|
5
|
+
f'<meta name="documentId" content="{document["documentId"]}">'
|
|
6
|
+
"</head><body>"
|
|
7
|
+
)
|
|
8
|
+
for element in document["body"]["content"]:
|
|
9
|
+
html += convert_structural_element(element)
|
|
10
|
+
html += "</body></html>"
|
|
11
|
+
return html
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def convert_structural_element(element: dict, wrap_paragraphs: bool = True) -> str:
|
|
15
|
+
if "sectionBreak" in element or "tableOfContents" in element:
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
elif "paragraph" in element:
|
|
19
|
+
paragraph_content = ""
|
|
20
|
+
|
|
21
|
+
prepend, append = get_paragraph_style_tags(
|
|
22
|
+
style=element["paragraph"]["paragraphStyle"],
|
|
23
|
+
wrap_paragraphs=wrap_paragraphs,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
for item in element["paragraph"]["elements"]:
|
|
27
|
+
if "textRun" not in item:
|
|
28
|
+
continue
|
|
29
|
+
paragraph_content += extract_paragraph_content(item["textRun"])
|
|
30
|
+
|
|
31
|
+
if not paragraph_content:
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
return f"{prepend}{paragraph_content.strip()}{append}"
|
|
35
|
+
|
|
36
|
+
elif "table" in element:
|
|
37
|
+
table = [
|
|
38
|
+
[
|
|
39
|
+
"".join([
|
|
40
|
+
convert_structural_element(element=cell_element, wrap_paragraphs=False)
|
|
41
|
+
for cell_element in cell["content"]
|
|
42
|
+
])
|
|
43
|
+
for cell in row["tableCells"]
|
|
44
|
+
]
|
|
45
|
+
for row in element["table"]["tableRows"]
|
|
46
|
+
]
|
|
47
|
+
return table_list_to_html(table)
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"Unknown document body element type: {element}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_paragraph_content(text_run: dict) -> str:
|
|
54
|
+
content = text_run["content"]
|
|
55
|
+
style = text_run["textStyle"]
|
|
56
|
+
return apply_text_style(content, style)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def apply_text_style(content: str, style: dict) -> str:
|
|
60
|
+
content = content.rstrip("\n")
|
|
61
|
+
content = content.replace("\n", "<br>")
|
|
62
|
+
italic = style.get("italic", False)
|
|
63
|
+
bold = style.get("bold", False)
|
|
64
|
+
if italic:
|
|
65
|
+
content = f"<i>{content}</i>"
|
|
66
|
+
if bold:
|
|
67
|
+
content = f"<b>{content}</b>"
|
|
68
|
+
return content
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_paragraph_style_tags(style: dict, wrap_paragraphs: bool = True) -> tuple[str, str]:
|
|
72
|
+
named_style = style["namedStyleType"]
|
|
73
|
+
if named_style == "NORMAL_TEXT":
|
|
74
|
+
return ("<p>", "</p>") if wrap_paragraphs else ("", "")
|
|
75
|
+
elif named_style == "TITLE":
|
|
76
|
+
return "<h1>", "</h1>"
|
|
77
|
+
elif named_style == "SUBTITLE":
|
|
78
|
+
return "<h2>", "</h2>"
|
|
79
|
+
elif named_style.startswith("HEADING_"):
|
|
80
|
+
try:
|
|
81
|
+
heading_level = int(named_style.split("_")[1])
|
|
82
|
+
except ValueError:
|
|
83
|
+
return ("<p>", "</p>") if wrap_paragraphs else ("", "")
|
|
84
|
+
else:
|
|
85
|
+
return f"<h{heading_level}>", f"</h{heading_level}>"
|
|
86
|
+
return ("<p>", "</p>") if wrap_paragraphs else ("", "")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def table_list_to_html(table: list[list[str]]) -> str:
|
|
90
|
+
html = "<table>"
|
|
91
|
+
for row in table:
|
|
92
|
+
html += "<tr>"
|
|
93
|
+
for cell in row:
|
|
94
|
+
if cell.endswith("<br>"):
|
|
95
|
+
cell = cell[:-4]
|
|
96
|
+
html += f"<td>{cell}</td>"
|
|
97
|
+
html += "</tr>"
|
|
98
|
+
html += "</table>"
|
|
99
|
+
return html
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import arcade_google_docs.doc_to_html as doc_to_html
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def convert_document_to_markdown(document: dict) -> str:
|
|
5
|
+
md = f"---\ntitle: {document['title']}\ndocumentId: {document['documentId']}\n---\n"
|
|
6
|
+
for element in document["body"]["content"]:
|
|
7
|
+
md += convert_structural_element(element)
|
|
8
|
+
return md
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def convert_structural_element(element: dict) -> str:
|
|
12
|
+
if "sectionBreak" in element or "tableOfContents" in element:
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
elif "paragraph" in element:
|
|
16
|
+
md = ""
|
|
17
|
+
prepend = get_paragraph_style_prepend_str(element["paragraph"]["paragraphStyle"])
|
|
18
|
+
for item in element["paragraph"]["elements"]:
|
|
19
|
+
if "textRun" not in item:
|
|
20
|
+
continue
|
|
21
|
+
content = extract_paragraph_content(item["textRun"])
|
|
22
|
+
md += f"{prepend}{content}"
|
|
23
|
+
return md
|
|
24
|
+
|
|
25
|
+
elif "table" in element:
|
|
26
|
+
return doc_to_html.convert_structural_element(element)
|
|
27
|
+
|
|
28
|
+
else:
|
|
29
|
+
raise ValueError(f"Unknown document body element type: {element}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def extract_paragraph_content(text_run: dict) -> str:
|
|
33
|
+
content = text_run["content"]
|
|
34
|
+
style = text_run["textStyle"]
|
|
35
|
+
return apply_text_style(content, style)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def apply_text_style(content: str, style: dict) -> str:
|
|
39
|
+
append = "\n" if content.endswith("\n") else ""
|
|
40
|
+
content = content.rstrip("\n")
|
|
41
|
+
italic = style.get("italic", False)
|
|
42
|
+
bold = style.get("bold", False)
|
|
43
|
+
if italic:
|
|
44
|
+
content = f"_{content}_"
|
|
45
|
+
if bold:
|
|
46
|
+
content = f"**{content}**"
|
|
47
|
+
return f"{content}{append}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_paragraph_style_prepend_str(style: dict) -> str:
|
|
51
|
+
named_style = style["namedStyleType"]
|
|
52
|
+
if named_style == "NORMAL_TEXT":
|
|
53
|
+
return ""
|
|
54
|
+
elif named_style == "TITLE":
|
|
55
|
+
return "# "
|
|
56
|
+
elif named_style == "SUBTITLE":
|
|
57
|
+
return "## "
|
|
58
|
+
elif named_style.startswith("HEADING_"):
|
|
59
|
+
try:
|
|
60
|
+
heading_level = int(named_style.split("_")[1])
|
|
61
|
+
return f"{'#' * heading_level} "
|
|
62
|
+
except ValueError:
|
|
63
|
+
return ""
|
|
64
|
+
return ""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Corpora(str, Enum):
|
|
5
|
+
"""
|
|
6
|
+
Bodies of items (files/documents) to which the query applies.
|
|
7
|
+
Prefer 'user' or 'drive' to 'allDrives' for efficiency.
|
|
8
|
+
By default, corpora is set to 'user'.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
USER = "user"
|
|
12
|
+
DOMAIN = "domain"
|
|
13
|
+
DRIVE = "drive"
|
|
14
|
+
ALL_DRIVES = "allDrives"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DocumentFormat(str, Enum):
|
|
18
|
+
MARKDOWN = "markdown"
|
|
19
|
+
HTML = "html"
|
|
20
|
+
GOOGLE_API_JSON = "google_api_json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OrderBy(str, Enum):
|
|
24
|
+
"""
|
|
25
|
+
Sort keys for ordering files in Google Drive.
|
|
26
|
+
Each key has both ascending and descending options.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
CREATED_TIME = (
|
|
30
|
+
# When the file was created (ascending)
|
|
31
|
+
"createdTime"
|
|
32
|
+
)
|
|
33
|
+
CREATED_TIME_DESC = (
|
|
34
|
+
# When the file was created (descending)
|
|
35
|
+
"createdTime desc"
|
|
36
|
+
)
|
|
37
|
+
FOLDER = (
|
|
38
|
+
# The folder ID, sorted using alphabetical ordering (ascending)
|
|
39
|
+
"folder"
|
|
40
|
+
)
|
|
41
|
+
FOLDER_DESC = (
|
|
42
|
+
# The folder ID, sorted using alphabetical ordering (descending)
|
|
43
|
+
"folder desc"
|
|
44
|
+
)
|
|
45
|
+
MODIFIED_BY_ME_TIME = (
|
|
46
|
+
# The last time the file was modified by the user (ascending)
|
|
47
|
+
"modifiedByMeTime"
|
|
48
|
+
)
|
|
49
|
+
MODIFIED_BY_ME_TIME_DESC = (
|
|
50
|
+
# The last time the file was modified by the user (descending)
|
|
51
|
+
"modifiedByMeTime desc"
|
|
52
|
+
)
|
|
53
|
+
MODIFIED_TIME = (
|
|
54
|
+
# The last time the file was modified by anyone (ascending)
|
|
55
|
+
"modifiedTime"
|
|
56
|
+
)
|
|
57
|
+
MODIFIED_TIME_DESC = (
|
|
58
|
+
# The last time the file was modified by anyone (descending)
|
|
59
|
+
"modifiedTime desc"
|
|
60
|
+
)
|
|
61
|
+
NAME = (
|
|
62
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (ascending)
|
|
63
|
+
"name"
|
|
64
|
+
)
|
|
65
|
+
NAME_DESC = (
|
|
66
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (descending)
|
|
67
|
+
"name desc"
|
|
68
|
+
)
|
|
69
|
+
NAME_NATURAL = (
|
|
70
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (ascending)
|
|
71
|
+
"name_natural"
|
|
72
|
+
)
|
|
73
|
+
NAME_NATURAL_DESC = (
|
|
74
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (descending)
|
|
75
|
+
"name_natural desc"
|
|
76
|
+
)
|
|
77
|
+
QUOTA_BYTES_USED = (
|
|
78
|
+
# The number of storage quota bytes used by the file (ascending)
|
|
79
|
+
"quotaBytesUsed"
|
|
80
|
+
)
|
|
81
|
+
QUOTA_BYTES_USED_DESC = (
|
|
82
|
+
# The number of storage quota bytes used by the file (descending)
|
|
83
|
+
"quotaBytesUsed desc"
|
|
84
|
+
)
|
|
85
|
+
RECENCY = (
|
|
86
|
+
# The most recent timestamp from the file's date-time fields (ascending)
|
|
87
|
+
"recency"
|
|
88
|
+
)
|
|
89
|
+
RECENCY_DESC = (
|
|
90
|
+
# The most recent timestamp from the file's date-time fields (descending)
|
|
91
|
+
"recency desc"
|
|
92
|
+
)
|
|
93
|
+
SHARED_WITH_ME_TIME = (
|
|
94
|
+
# When the file was shared with the user, if applicable (ascending)
|
|
95
|
+
"sharedWithMeTime"
|
|
96
|
+
)
|
|
97
|
+
SHARED_WITH_ME_TIME_DESC = (
|
|
98
|
+
# When the file was shared with the user, if applicable (descending)
|
|
99
|
+
"sharedWithMeTime desc"
|
|
100
|
+
)
|
|
101
|
+
STARRED = (
|
|
102
|
+
# Whether the user has starred the file (ascending)
|
|
103
|
+
"starred"
|
|
104
|
+
)
|
|
105
|
+
STARRED_DESC = (
|
|
106
|
+
# Whether the user has starred the file (descending)
|
|
107
|
+
"starred desc"
|
|
108
|
+
)
|
|
109
|
+
VIEWED_BY_ME_TIME = (
|
|
110
|
+
# The last time the file was viewed by the user (ascending)
|
|
111
|
+
"viewedByMeTime"
|
|
112
|
+
)
|
|
113
|
+
VIEWED_BY_ME_TIME_DESC = (
|
|
114
|
+
# The last time the file was viewed by the user (descending)
|
|
115
|
+
"viewedByMeTime desc"
|
|
116
|
+
)
|
|
@@ -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
|
+
}
|