arcade-google-docs 4.0.0__py3-none-any.whl → 4.2.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.
@@ -0,0 +1,150 @@
1
+ import asyncio
2
+ import json
3
+ from typing import Literal
4
+
5
+ from arcade_tdk.errors import ToolExecutionError
6
+ from openai import OpenAI
7
+ from pydantic import BaseModel
8
+
9
+ from arcade_google_docs.docmd import DocMD
10
+ from arcade_google_docs.tools.edit_agent.models.planning import EditItem, Step
11
+ from arcade_google_docs.tools.edit_agent.prompts import (
12
+ DETERMINE_BLOCK_ID_SYSTEM_PROMPT,
13
+ ERROR_FEEDBACK_PROMPT,
14
+ GENERATE_EDIT_REQUEST_SYSTEM_PROMPT,
15
+ GENERATE_EDIT_REQUEST_SYSTEM_PROMPT_WITH_LOCATION_TAGS,
16
+ )
17
+
18
+
19
+ def _determine_block_id(openai_client: OpenAI, docmd: DocMD, edit_instruction: str) -> str:
20
+ """Determine the block id that the edit instruction is targeting."""
21
+
22
+ class ValidBlockIds(BaseModel):
23
+ block_id: Literal[tuple(docmd.block_ids)] # type: ignore[valid-type]
24
+
25
+ completion = openai_client.beta.chat.completions.parse(
26
+ model="gpt-4o",
27
+ messages=[
28
+ {"role": "developer", "content": DETERMINE_BLOCK_ID_SYSTEM_PROMPT},
29
+ {"role": "user", "content": f"{docmd}\n\nEDIT REQUEST:\n{edit_instruction}"},
30
+ ],
31
+ response_format=ValidBlockIds,
32
+ )
33
+ parsed_response = completion.choices[0].message.parsed
34
+ if not parsed_response:
35
+ raise ToolExecutionError("Failed to determine a block id from the edit instruction")
36
+ return str(parsed_response.block_id)
37
+
38
+
39
+ async def _generate_api_request_for_edit_item(
40
+ openai_client: OpenAI,
41
+ docmd: DocMD,
42
+ edit_item: EditItem,
43
+ previous_error: str | None = None,
44
+ failed_requests: list[dict] | None = None,
45
+ ) -> dict | None:
46
+ """Generate a single edit request asynchronously.
47
+
48
+ Returns a request dictionary for the batchUpdate endpoint or None
49
+ """
50
+ edit_instruction = edit_item.edit_instruction
51
+ edit_request_type = edit_item.edit_request_type
52
+ edit_item_thoughts = edit_item.thoughts
53
+
54
+ messages = []
55
+ if edit_request_type.is_location_based():
56
+ # Run blocking operation in thread pool to avoid blocking event loop
57
+ loop = asyncio.get_event_loop()
58
+ block_id = await loop.run_in_executor(
59
+ None, _determine_block_id, openai_client, docmd, edit_instruction
60
+ )
61
+ annotated_docmd = docmd.get_docmd_with_annotated_block(block_id).to_string()
62
+ messages.extend([
63
+ {
64
+ "role": "developer",
65
+ "content": GENERATE_EDIT_REQUEST_SYSTEM_PROMPT_WITH_LOCATION_TAGS,
66
+ },
67
+ {
68
+ "role": "user",
69
+ "content": (
70
+ "DOCUMENT:\n"
71
+ f"{annotated_docmd}\n\n"
72
+ "THOUGHTS THAT OCCURRED WHEN CONSTRUCTING YOUR INSTRUCTIONS:\n"
73
+ f"{edit_item_thoughts}\n\n"
74
+ "YOUR JOB IS TO CONSTRUCT A SINGLE EDIT REQUEST OBJECT THAT SATISFIES "
75
+ "THE FOLLOWING INSTRUCTIONS:\n"
76
+ f"{edit_instruction}"
77
+ ),
78
+ },
79
+ ])
80
+ else:
81
+ messages.extend([
82
+ {"role": "developer", "content": GENERATE_EDIT_REQUEST_SYSTEM_PROMPT},
83
+ {
84
+ "role": "user",
85
+ "content": (
86
+ "DOCUMENT:\n"
87
+ f"{docmd.to_string()}\n\n"
88
+ "THOUGHTS THAT OCCURRED WHEN CONSTRUCTING YOUR INSTRUCTIONS:\n"
89
+ f"{edit_item_thoughts}\n\n"
90
+ "YOUR JOB AND SOLE PURPOSE IS TO CONSTRUCT A SINGLE EDIT REQUEST "
91
+ "OBJECT THAT SATISFIES THE FOLLOWING INSTRUCTIONS:\n"
92
+ f"{edit_instruction}"
93
+ ),
94
+ },
95
+ ])
96
+
97
+ if previous_error and failed_requests:
98
+ error_feedback = ERROR_FEEDBACK_PROMPT.format(
99
+ error=previous_error, failed_requests=json.dumps(failed_requests, indent=2)
100
+ )
101
+ messages.append({"role": "assistant", "content": "I failed to edit the document."})
102
+ messages.append({"role": "user", "content": error_feedback})
103
+
104
+ # Run OpenAI API call in thread pool to avoid blocking
105
+ loop = asyncio.get_event_loop()
106
+ completion = await loop.run_in_executor(
107
+ None,
108
+ lambda: openai_client.beta.chat.completions.parse(
109
+ model="gpt-5",
110
+ messages=messages, # type: ignore[arg-type]
111
+ response_format=edit_request_type.get_request_model(),
112
+ reasoning_effort="minimal", # type: ignore[arg-type]
113
+ ),
114
+ )
115
+
116
+ request = completion.choices[0].message.parsed
117
+ if request:
118
+ return {edit_request_type.value: request.model_dump(exclude_none=True)}
119
+ return None
120
+
121
+
122
+ async def generate_api_request_for_step(
123
+ openai_client: OpenAI,
124
+ docmd: DocMD,
125
+ step: Step,
126
+ previous_error: str | None = None,
127
+ failed_requests: list[dict] | None = None,
128
+ ) -> list[dict]:
129
+ """Generate edit requests for a single step.
130
+
131
+ Returns a list of request dictionaries for the Google batchUpdate endpoint
132
+ """
133
+ if not step.edit_items:
134
+ return []
135
+
136
+ # Generate requests for all edit items in the step in parallel
137
+ tasks = []
138
+ for edit_item in step.edit_items:
139
+ task = _generate_api_request_for_edit_item(
140
+ openai_client,
141
+ docmd,
142
+ edit_item,
143
+ previous_error,
144
+ failed_requests,
145
+ )
146
+ tasks.append(task)
147
+
148
+ results = await asyncio.gather(*tasks)
149
+
150
+ return [result for result in results if result is not None]
@@ -0,0 +1,21 @@
1
+ from typing import Any
2
+
3
+ from arcade_google_docs.docmd import DocMD, build_docmd
4
+ from arcade_google_docs.models.document import Document
5
+
6
+
7
+ def get_docmd(google_service: Any, document_id: str) -> DocMD:
8
+ """
9
+ Helper function to get a Google Doc and convert it to DocMD format.
10
+
11
+ Args:
12
+ google_service: The authenticated Google Docs service
13
+ document_id: The ID of the document to fetch
14
+
15
+ Returns:
16
+ DocMD object
17
+ """
18
+ google_get_response = google_service.documents().get(documentId=document_id).execute()
19
+ document = Document(**google_get_response)
20
+ docmd = build_docmd(document)
21
+ return docmd
@@ -3,6 +3,8 @@ from typing import Annotated
3
3
  from arcade_tdk import ToolContext, tool
4
4
  from arcade_tdk.auth import Google
5
5
 
6
+ from arcade_google_docs.docmd import build_docmd
7
+ from arcade_google_docs.models.document import Document
6
8
  from arcade_google_docs.utils import build_docs_service
7
9
 
8
10
 
@@ -21,6 +23,7 @@ async def get_document_by_id(
21
23
  document_id: Annotated[str, "The ID of the document to retrieve."],
22
24
  ) -> Annotated[dict, "The document contents as a dictionary"]:
23
25
  """
26
+ DEPRECATED DO NOT USE THIS TOOL
24
27
  Get the latest version of the specified Google Docs document.
25
28
  """
26
29
  service = build_docs_service(context.get_auth_token_or_empty())
@@ -30,3 +33,26 @@ async def get_document_by_id(
30
33
  request = service.documents().get(documentId=document_id)
31
34
  response = request.execute()
32
35
  return dict(response)
36
+
37
+
38
+ @tool(
39
+ requires_auth=Google(
40
+ scopes=[
41
+ "https://www.googleapis.com/auth/drive.file",
42
+ ],
43
+ ),
44
+ )
45
+ async def get_document_as_docmd(
46
+ context: ToolContext,
47
+ document_id: Annotated[str, "The ID of the document to retrieve."],
48
+ ) -> Annotated[str, "The document contents as DocMD"]:
49
+ """
50
+ Get the latest version of the specified Google Docs document as DocMD.
51
+ The DocMD output will include tags that can be used to annotate the document with location
52
+ information, the type of block, block IDs, and other metadata.
53
+ """
54
+ service = build_docs_service(context.get_auth_token_or_empty())
55
+
56
+ request = service.documents().get(documentId=document_id)
57
+ response = request.execute()
58
+ return build_docmd(Document(**response)).to_string()
@@ -5,7 +5,9 @@ from arcade_tdk.auth import Google
5
5
 
6
6
  from arcade_google_docs.doc_to_html import convert_document_to_html
7
7
  from arcade_google_docs.doc_to_markdown import convert_document_to_markdown
8
+ from arcade_google_docs.docmd import build_docmd
8
9
  from arcade_google_docs.enum import DocumentFormat, OrderBy
10
+ from arcade_google_docs.models.document import Document
9
11
  from arcade_google_docs.tools import get_document_by_id
10
12
  from arcade_google_docs.utils import (
11
13
  build_drive_service,
@@ -189,7 +191,9 @@ async def search_and_retrieve_documents(
189
191
  for item in response["documents"]:
190
192
  document = await get_document_by_id(context, document_id=item["id"])
191
193
 
192
- if return_format == DocumentFormat.MARKDOWN:
194
+ if return_format == DocumentFormat.DOCMD:
195
+ document = build_docmd(Document(**document)).to_string()
196
+ elif return_format == DocumentFormat.MARKDOWN:
193
197
  document = convert_document_to_markdown(document)
194
198
  elif return_format == DocumentFormat.HTML:
195
199
  document = convert_document_to_html(document)
@@ -0,0 +1,36 @@
1
+ from typing import Annotated, Any
2
+
3
+ from arcade_tdk import ToolContext, tool
4
+ from arcade_tdk.auth import Google
5
+
6
+ from arcade_google_docs.utils import build_docs_service
7
+ from arcade_google_docs.who_am_i_util import build_who_am_i_response
8
+
9
+
10
+ @tool(
11
+ requires_auth=Google(
12
+ scopes=[
13
+ "https://www.googleapis.com/auth/drive.file",
14
+ "https://www.googleapis.com/auth/userinfo.profile",
15
+ "https://www.googleapis.com/auth/userinfo.email",
16
+ ]
17
+ )
18
+ )
19
+ async def who_am_i(
20
+ context: ToolContext,
21
+ ) -> Annotated[
22
+ dict[str, Any],
23
+ "Get comprehensive user profile and Google Docs environment information.",
24
+ ]:
25
+ """
26
+ Get comprehensive user profile and Google Docs environment information.
27
+
28
+ This tool provides detailed information about the authenticated user including
29
+ their name, email, profile picture, Google Docs access permissions, and other
30
+ important profile details from Google services.
31
+ """
32
+
33
+ docs_service = build_docs_service(context.get_auth_token_or_empty())
34
+ user_info = build_who_am_i_response(context, docs_service)
35
+
36
+ return dict(user_info)
@@ -0,0 +1,86 @@
1
+ from typing import Any, TypedDict, cast
2
+
3
+ from google.oauth2.credentials import Credentials
4
+ from googleapiclient.discovery import build
5
+
6
+
7
+ class WhoAmIResponse(TypedDict, total=False):
8
+ my_email_address: str
9
+ display_name: str
10
+ given_name: str
11
+ family_name: str
12
+ formatted_name: str
13
+ profile_picture_url: str
14
+ google_docs_access: bool
15
+
16
+
17
+ def build_who_am_i_response(context: Any, docs_service: Any) -> WhoAmIResponse:
18
+ """Build complete who_am_i response from Google Docs and People APIs."""
19
+ credentials = Credentials(
20
+ context.authorization.token if context.authorization and context.authorization.token else ""
21
+ )
22
+ people_service = _build_people_service(credentials)
23
+ person = _get_people_api_data(people_service)
24
+
25
+ user_info = _extract_profile_data(person)
26
+ user_info.update(_extract_google_docs_info(docs_service))
27
+
28
+ return cast(WhoAmIResponse, user_info)
29
+
30
+
31
+ def _extract_profile_data(person: dict[str, Any]) -> dict[str, Any]:
32
+ """Extract user profile data from People API response."""
33
+ profile_data = {}
34
+
35
+ names = person.get("names", [])
36
+ if names:
37
+ primary_name = names[0]
38
+ profile_data.update({
39
+ "display_name": primary_name.get("displayName"),
40
+ "given_name": primary_name.get("givenName"),
41
+ "family_name": primary_name.get("familyName"),
42
+ "formatted_name": primary_name.get("displayNameLastFirst"),
43
+ })
44
+
45
+ photos = person.get("photos", [])
46
+ if photos:
47
+ profile_data["profile_picture_url"] = photos[0].get("url")
48
+
49
+ email_addresses = person.get("emailAddresses", [])
50
+ if email_addresses:
51
+ primary_emails = [
52
+ email for email in email_addresses if email.get("metadata", {}).get("primary")
53
+ ]
54
+ if primary_emails:
55
+ profile_data["my_email_address"] = primary_emails[0].get("value")
56
+
57
+ return profile_data
58
+
59
+
60
+ def _extract_google_docs_info(docs_service: Any) -> dict[str, Any]:
61
+ """Extract Google Docs specific information."""
62
+ docs_info = {}
63
+
64
+ try:
65
+ # Test Google Docs access by checking if we can access the service
66
+ # Since there's no direct way to test docs access, we'll assume it's available
67
+ # if we have the service (the scope validation happens at auth time)
68
+ docs_info["google_docs_access"] = True
69
+ except Exception:
70
+ docs_info["google_docs_access"] = False
71
+
72
+ return docs_info
73
+
74
+
75
+ def _build_people_service(credentials: Credentials) -> Any:
76
+ """Build and return the People API service client."""
77
+ return build("people", "v1", credentials=credentials)
78
+
79
+
80
+ def _get_people_api_data(people_service: Any) -> dict[str, Any]:
81
+ """Get user profile information from People API."""
82
+ person_fields = "names,emailAddresses,photos"
83
+ return cast(
84
+ dict[str, Any],
85
+ people_service.people().get(resourceName="people/me", personFields=person_fields).execute(),
86
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arcade_google_docs
3
- Version: 4.0.0
3
+ Version: 4.2.0
4
4
  Summary: Arcade.dev LLM tools for Google Docs
5
5
  Author-email: Arcade <dev@arcade.dev>
6
6
  License: Proprietary - Arcade Software License Agreement v1.0
@@ -11,6 +11,7 @@ Requires-Dist: google-api-python-client<3.0.0,>=2.137.0
11
11
  Requires-Dist: google-auth-httplib2<1.0.0,>=0.2.0
12
12
  Requires-Dist: google-auth<3.0.0,>=2.32.0
13
13
  Requires-Dist: googleapis-common-protos<2.0.0,>=1.63.2
14
+ Requires-Dist: openai==1.82.1
14
15
  Provides-Extra: dev
15
16
  Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.4; extra == 'dev'
16
17
  Requires-Dist: arcade-serve<3.0.0,>=2.0.0; extra == 'dev'
@@ -0,0 +1,30 @@
1
+ arcade_google_docs/__init__.py,sha256=nZoMwH9HWZLpfNMIioUXLDlZ7QDR-E4C1_8hySt_CkU,604
2
+ arcade_google_docs/doc_to_html.py,sha256=6RTpzRSrazNa6AndLZhA20wgVDzZuHUqpu3WAkAsbjQ,3146
3
+ arcade_google_docs/doc_to_markdown.py,sha256=eT-sc6ruxN8nEtUm9mBHFOWXajEBTTXkxsn6XsLHIxo,2020
4
+ arcade_google_docs/docmd.py,sha256=rgxUt0k2Nes0GWLlPR49qU0nnM8e6HCiqd61Z7zNfTk,20173
5
+ arcade_google_docs/enum.py,sha256=kuXlsHcMYbN28Qg-Dwp4viz-CZ8z85_WVjQVZj2EsEY,3441
6
+ arcade_google_docs/templates.py,sha256=pxbdMj57eV3-ImW3CixDWscpVKS94Z8nTNyTxDhUfGY,283
7
+ arcade_google_docs/utils.py,sha256=XMgKcWPWKy8SbH3y2eCil01hssHhUgYxVrwXyvHEU4A,3748
8
+ arcade_google_docs/who_am_i_util.py,sha256=NGQpQ0CuE2N86mGpkIdujXBRjcPBJ5SJ9BYzlb3mGFI,2925
9
+ arcade_google_docs/models/document.py,sha256=0RvZ2_dfpz6ZoF1aUucYWOkRYWy_K_hiChSzoQtwhTc,30419
10
+ arcade_google_docs/models/document_writables.py,sha256=DMBT5A05y7o7_PYlBB6O3KThma6-pm6hd5nxKGydT5Q,27575
11
+ arcade_google_docs/models/requests.py,sha256=8Cga7QECmQWNFhM2QiGudvnQcgA_THi7ThNsUb7uavg,52176
12
+ arcade_google_docs/tools/__init__.py,sha256=kQChYFQ6y1LJg7q6fngzCOPtaDH1-CeWUOzqxfR_oGY,904
13
+ arcade_google_docs/tools/comment.py,sha256=Qm5NHdNHONs3j4gqbZ7Fw9NrTVBb_mZ-th1X-z2IoLM,2836
14
+ arcade_google_docs/tools/create.py,sha256=AuYy8yMGscrxAdLJQX0WiisGHCTufSlaRu_QGMMKQmM,2764
15
+ arcade_google_docs/tools/file_picker.py,sha256=Dqn-hfMoTsWyHM8QCakVgHr5TKrzL_1Lj-vYHVGtOW4,2342
16
+ arcade_google_docs/tools/get.py,sha256=Aby3iI1EzjfZTovGH4OdwpIhuazfV4SQinpXDOl87cg,2076
17
+ arcade_google_docs/tools/search.py,sha256=1k1cI-fKOo4SZU4ufCwZ4DysK3l9MhPScZggPfNCC5Y,8261
18
+ arcade_google_docs/tools/system_context.py,sha256=19HPSpNkLsb-MDWc-9CFgK_ha-rRzwaJr7hV6Us_1LI,1130
19
+ arcade_google_docs/tools/update.py,sha256=_dReYit0s7ykn2bYQEUwohl3D_63U5leF87egO4eEiQ,1836
20
+ arcade_google_docs/tools/edit_agent/edit_agent.py,sha256=1LIgKrQ70pDWzWNoaoy1st659perqa-ZW_ALmbVRbW0,2439
21
+ arcade_google_docs/tools/edit_agent/executor.py,sha256=YbJqNXF0fw4N-u59gTbLpOBwKfoL7N6KZIOzV0pTllc,3718
22
+ arcade_google_docs/tools/edit_agent/planner.py,sha256=38aslAnlPDEY3JEoVXtHqL3Oq_9RDEFxyETqoEBsJLk,4053
23
+ arcade_google_docs/tools/edit_agent/progress_tracker.py,sha256=eb69tk-yL3uhEEo4ggPoBFtZtHTA6OwgcPxELtvbeEs,1280
24
+ arcade_google_docs/tools/edit_agent/prompts.py,sha256=M_f-HsPJppd3FQPhRAw7pKpaArkVfz213mKVx8qHp8A,15149
25
+ arcade_google_docs/tools/edit_agent/request_generator.py,sha256=eVDmzJDsOmJ-S8yANmJATt_G51rgCcCga6ArX4DTShM,5399
26
+ arcade_google_docs/tools/edit_agent/utils.py,sha256=6lYmdQfX4CHfemP5PfxcXKSYpbdqFyEad997-gzCD48,639
27
+ arcade_google_docs/tools/edit_agent/models/planning.py,sha256=RWQFB_KHl3Pq-snv1rHzoRxVvTnHVLZEGRpdohSX7wc,2962
28
+ arcade_google_docs-4.2.0.dist-info/METADATA,sha256=nLo_FmuOXT86wVAbVVFk-fnxFqI04jI_pJI0B2spx3g,1127
29
+ arcade_google_docs-4.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
30
+ arcade_google_docs-4.2.0.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- arcade_google_docs/__init__.py,sha256=nZoMwH9HWZLpfNMIioUXLDlZ7QDR-E4C1_8hySt_CkU,604
2
- arcade_google_docs/doc_to_html.py,sha256=6RTpzRSrazNa6AndLZhA20wgVDzZuHUqpu3WAkAsbjQ,3146
3
- arcade_google_docs/doc_to_markdown.py,sha256=eT-sc6ruxN8nEtUm9mBHFOWXajEBTTXkxsn6XsLHIxo,2020
4
- arcade_google_docs/enum.py,sha256=vFJWPe1JPG6I9xqdVVvuaEeen4LvvtJxax1sDYeh4UU,3421
5
- arcade_google_docs/templates.py,sha256=pxbdMj57eV3-ImW3CixDWscpVKS94Z8nTNyTxDhUfGY,283
6
- arcade_google_docs/utils.py,sha256=XMgKcWPWKy8SbH3y2eCil01hssHhUgYxVrwXyvHEU4A,3748
7
- arcade_google_docs/tools/__init__.py,sha256=s0FS3z-uXE4lQH06YhnUKgIOT-Kq-v6rQMqXUGdPDnw,827
8
- arcade_google_docs/tools/comment.py,sha256=Qm5NHdNHONs3j4gqbZ7Fw9NrTVBb_mZ-th1X-z2IoLM,2836
9
- arcade_google_docs/tools/create.py,sha256=AuYy8yMGscrxAdLJQX0WiisGHCTufSlaRu_QGMMKQmM,2764
10
- arcade_google_docs/tools/file_picker.py,sha256=Dqn-hfMoTsWyHM8QCakVgHr5TKrzL_1Lj-vYHVGtOW4,2342
11
- arcade_google_docs/tools/get.py,sha256=tCByk1-C97Mdo9P3oDdr--6bXD1dIQ3FxsDxptCJaA8,1145
12
- arcade_google_docs/tools/search.py,sha256=JpaOvcn3a6MjJEAqtne4vfiq4SvlINCw6lKv6nWt3zc,8035
13
- arcade_google_docs/tools/update.py,sha256=_dReYit0s7ykn2bYQEUwohl3D_63U5leF87egO4eEiQ,1836
14
- arcade_google_docs-4.0.0.dist-info/METADATA,sha256=lQakZrQRnTWx8Qa_vGbSwGxFOiVAMebNYVaSghfAGsw,1097
15
- arcade_google_docs-4.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- arcade_google_docs-4.0.0.dist-info/RECORD,,