hyperagents 1.2.3rc5.post3__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.
- agents/__init__.py +31 -0
- agents/background_monitor.py +453 -0
- agents/learning.py +460 -0
- agents/planner.py +421 -0
- agents/transcriber.py +341 -0
- civilization/__init__.py +51 -0
- civilization/api/__init__.py +7 -0
- civilization/api/civilization_api.py +361 -0
- civilization/graph/__init__.py +19 -0
- civilization/graph/knowledge_linker.py +348 -0
- civilization/graph/org_graph.py +348 -0
- civilization/graph/process_graph.py +386 -0
- civilization/ingestion/__init__.py +20 -0
- civilization/ingestion/chunker.py +273 -0
- civilization/ingestion/document_ingestor.py +358 -0
- civilization/ingestion/embedder.py +262 -0
- civilization/ingestion/extractor.py +224 -0
- civilization/interview/__init__.py +19 -0
- civilization/interview/gap_detector.py +380 -0
- civilization/interview/interviewer.py +323 -0
- civilization/interview/questioner.py +363 -0
- civilization/retrieval/__init__.py +17 -0
- civilization/retrieval/civilization_rag.py +316 -0
- civilization/retrieval/context_injector.py +342 -0
- civilization/retrieval/relevance_ranker.py +287 -0
- civilization/schema.py +299 -0
- civilization/store.py +177 -0
- civilization/sync/__init__.py +16 -0
- civilization/sync/confluence_sync.py +317 -0
- civilization/sync/gdrive_sync.py +262 -0
- civilization/sync/notion_sync.py +237 -0
- civilization/versioning/__init__.py +19 -0
- civilization/versioning/diff_engine.py +351 -0
- civilization/versioning/staleness_detector.py +303 -0
- civilization/versioning/version_manager.py +293 -0
- cli/__init__.py +1 -0
- cli/doctor.py +334 -0
- cli/hyperclaw.py +1357 -0
- cli/onboarding.py +469 -0
- core/__init__.py +1 -0
- core/connectors/__init__.py +119 -0
- core/connectors/infrastructure.py +772 -0
- core/connectors/messaging.py +586 -0
- core/connectors/productivity.py +764 -0
- core/connectors/smarthome.py +528 -0
- core/connectors/social.py +730 -0
- core/hyperrouter/__init__.py +23 -0
- core/hyperrouter/bandit.py +218 -0
- core/hyperrouter/fast_loop.py +68 -0
- core/hyperrouter/slow_loop.py +143 -0
- core/hyperstate/__init__.py +24 -0
- core/hyperstate/certifier.py +154 -0
- core/hyperstate/schema.py +160 -0
- core/hyperstate/state_manager.py +103 -0
- core/hyperstate/store.py +163 -0
- core/providers/__init__.py +121 -0
- core/providers/embeddings.py +389 -0
- core/providers/image.py +534 -0
- core/providers/llm.py +1935 -0
- core/providers/search.py +608 -0
- core/providers/voice.py +474 -0
- hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
- hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
- hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
- hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
- hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
- hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
- hyperclaw/__init__.py +67 -0
- hyperclaw/__main__.py +220 -0
- hyperclaw/agent.py +775 -0
- hyperclaw/agent_coordinator.py +577 -0
- hyperclaw/anthropic_params.py +225 -0
- hyperclaw/api_utils.py +85 -0
- hyperclaw/approval_telegram.py +209 -0
- hyperclaw/approval_terminal.py +80 -0
- hyperclaw/attachments.py +177 -0
- hyperclaw/branding.py +55 -0
- hyperclaw/brief/__init__.py +11 -0
- hyperclaw/brief/cli.py +204 -0
- hyperclaw/brief/collect.py +146 -0
- hyperclaw/brief/compose.py +333 -0
- hyperclaw/brief/diff.py +110 -0
- hyperclaw/brief/errors.py +99 -0
- hyperclaw/brief/google.py +269 -0
- hyperclaw/brief/google_auth.py +294 -0
- hyperclaw/brief/links.py +124 -0
- hyperclaw/brief/service.py +424 -0
- hyperclaw/brief/store.py +205 -0
- hyperclaw/brief/tasks.py +200 -0
- hyperclaw/chat_scope.py +115 -0
- hyperclaw/cli_tui.py +497 -0
- hyperclaw/client_guard.py +717 -0
- hyperclaw/connection_check.py +113 -0
- hyperclaw/cost_guard.py +1728 -0
- hyperclaw/default_config/agents.yaml +534 -0
- hyperclaw/default_config/hyperclaw.yaml +27 -0
- hyperclaw/default_config/models.yaml +161 -0
- hyperclaw/deployment.py +39 -0
- hyperclaw/digest/__init__.py +9 -0
- hyperclaw/digest/cli.py +117 -0
- hyperclaw/digest/errors.py +100 -0
- hyperclaw/digest/gitlab.py +187 -0
- hyperclaw/digest/http.py +113 -0
- hyperclaw/digest/redact.py +32 -0
- hyperclaw/digest/service.py +271 -0
- hyperclaw/digest/slack.py +223 -0
- hyperclaw/digest/state.py +124 -0
- hyperclaw/digest/summarize.py +198 -0
- hyperclaw/digest/tokens.py +143 -0
- hyperclaw/distribution.py +46 -0
- hyperclaw/documents.py +351 -0
- hyperclaw/feeds.py +339 -0
- hyperclaw/google_workspace/__init__.py +5 -0
- hyperclaw/google_workspace/api.py +151 -0
- hyperclaw/google_workspace/auth.py +397 -0
- hyperclaw/google_workspace/calendar.py +157 -0
- hyperclaw/google_workspace/cli.py +72 -0
- hyperclaw/google_workspace/drive.py +236 -0
- hyperclaw/google_workspace/errors.py +58 -0
- hyperclaw/google_workspace/gmail.py +295 -0
- hyperclaw/google_workspace/meet.py +122 -0
- hyperclaw/google_workspace/tools.py +319 -0
- hyperclaw/hyperspeed.py +57 -0
- hyperclaw/integrations_layer.py +1427 -0
- hyperclaw/live_state.py +188 -0
- hyperclaw/managed_config.py +245 -0
- hyperclaw/media_hub.py +179 -0
- hyperclaw/memory_manager.py +584 -0
- hyperclaw/model_catalog.py +551 -0
- hyperclaw/model_policy.py +211 -0
- hyperclaw/model_router.py +595 -0
- hyperclaw/model_selector.py +72 -0
- hyperclaw/naming.py +284 -0
- hyperclaw/notices.py +65 -0
- hyperclaw/oai_stream.py +106 -0
- hyperclaw/orchestrator.py +591 -0
- hyperclaw/outbox.py +82 -0
- hyperclaw/persona.py +367 -0
- hyperclaw/prometheus.py +219 -0
- hyperclaw/prompt_assembly.py +157 -0
- hyperclaw/provider_setup.py +146 -0
- hyperclaw/providers.py +340 -0
- hyperclaw/qualification.py +341 -0
- hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
- hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
- hyperclaw/reasoning_display.py +35 -0
- hyperclaw/scheduler.py +454 -0
- hyperclaw/server.py +847 -0
- hyperclaw/setup.py +627 -0
- hyperclaw/skills.py +717 -0
- hyperclaw/solomon.py +442 -0
- hyperclaw/swarm.py +533 -0
- hyperclaw/taint.py +832 -0
- hyperclaw/team/__init__.py +28 -0
- hyperclaw/team/dispatcher.py +431 -0
- hyperclaw/team/genesis.py +106 -0
- hyperclaw/team/lead.py +110 -0
- hyperclaw/team/model.py +135 -0
- hyperclaw/team/morning.py +238 -0
- hyperclaw/team/repos.py +66 -0
- hyperclaw/team/roster.py +264 -0
- hyperclaw/team/specialist.py +235 -0
- hyperclaw/team/status.py +163 -0
- hyperclaw/team/watchers.py +112 -0
- hyperclaw/teamsetup/__init__.py +17 -0
- hyperclaw/teamsetup/checks.py +208 -0
- hyperclaw/teamsetup/item.py +391 -0
- hyperclaw/teamsetup/oauth_loopback.py +84 -0
- hyperclaw/teamsetup/onboard.py +550 -0
- hyperclaw/teamsetup/profile.py +82 -0
- hyperclaw/teamsetup/secrets.py +313 -0
- hyperclaw/teamsetup/selfcheck.py +295 -0
- hyperclaw/teamsetup/service.py +495 -0
- hyperclaw/teamsetup/slack_oauth.py +442 -0
- hyperclaw/teamsetup/tty.py +62 -0
- hyperclaw/telegram_bot.py +1106 -0
- hyperclaw/telegram_intake.py +126 -0
- hyperclaw/telegram_pairing.py +360 -0
- hyperclaw/terminal_provider.py +323 -0
- hyperclaw/tools/__init__.py +1 -0
- hyperclaw/tools/approval.py +949 -0
- hyperclaw/tools/args.py +212 -0
- hyperclaw/tools/catalog.py +244 -0
- hyperclaw/tools/classify.py +266 -0
- hyperclaw/tools/effects.py +724 -0
- hyperclaw/tools/egress.py +210 -0
- hyperclaw/tools/envelope.py +188 -0
- hyperclaw/tools/guard.py +367 -0
- hyperclaw/tools/ledger.py +339 -0
- hyperclaw/tools/receipts.py +70 -0
- hyperclaw/tools/registry.py +318 -0
- hyperclaw/tools/service_guard.py +151 -0
- hyperclaw/tools/surfaces.py +54 -0
- hyperclaw/tools/workspace.py +87 -0
- hyperclaw/trading/__init__.py +77 -0
- hyperclaw/truth_guard.py +791 -0
- hyperclaw/tts.py +124 -0
- hyperclaw/tui.py +4255 -0
- hyperclaw/tui_bridge.py +938 -0
- hyperclaw/unattended.py +60 -0
- hyperclaw/updates/__init__.py +54 -0
- hyperclaw/updates/check.py +131 -0
- hyperclaw/updates/index.py +178 -0
- hyperclaw/updates/install.py +110 -0
- hyperclaw/updates/managed.py +161 -0
- hyperclaw/updates/manifest.py +138 -0
- hyperclaw/updates/notices.py +86 -0
- hyperclaw/updates/paths.py +53 -0
- hyperclaw/updates/release.py +146 -0
- hyperclaw/updates/upgrade.py +253 -0
- hyperclaw/updates/versioning.py +104 -0
- hyperclaw/userconfig.py +119 -0
- hyperclaw/xai.py +83 -0
- integrations/__init__.py +31 -0
- integrations/automation/__init__.py +1 -0
- integrations/automation/make.py +87 -0
- integrations/automation/zapier.py +87 -0
- integrations/base.py +476 -0
- integrations/communication/__init__.py +1 -0
- integrations/communication/twilio.py +279 -0
- integrations/communication/zoom.py +290 -0
- integrations/data/__init__.py +1 -0
- integrations/data/google_analytics.py +135 -0
- integrations/data/postgres.py +182 -0
- integrations/data/supabase_connector.py +202 -0
- integrations/data/weather_feeds.py +450 -0
- integrations/developer/__init__.py +9 -0
- integrations/developer/github.py +406 -0
- integrations/developer/gitlab.py +298 -0
- integrations/enterprise/__init__.py +13 -0
- integrations/enterprise/confluence.py +266 -0
- integrations/enterprise/hubspot.py +293 -0
- integrations/enterprise/jira.py +314 -0
- integrations/enterprise/salesforce.py +286 -0
- integrations/finance/__init__.py +1 -0
- integrations/finance/polymarket_client.py +247 -0
- integrations/finance/quickbooks.py +281 -0
- integrations/finance/stripe.py +261 -0
- integrations/gateway.py +238 -0
- integrations/messaging/__init__.py +32 -0
- integrations/messaging/discord.py +159 -0
- integrations/messaging/email.py +401 -0
- integrations/messaging/imessage.py +443 -0
- integrations/messaging/signal.py +175 -0
- integrations/messaging/slack.py +217 -0
- integrations/messaging/sms.py +175 -0
- integrations/messaging/teams.py +208 -0
- integrations/messaging/telegram.py +260 -0
- integrations/messaging/whatsapp.py +203 -0
- integrations/productivity/__init__.py +17 -0
- integrations/productivity/airtable.py +217 -0
- integrations/productivity/asana.py +248 -0
- integrations/productivity/google/__init__.py +19 -0
- integrations/productivity/google/base.py +89 -0
- integrations/productivity/google/calendar.py +272 -0
- integrations/productivity/google/docs.py +202 -0
- integrations/productivity/google/drive.py +268 -0
- integrations/productivity/google/gmail.py +279 -0
- integrations/productivity/google/meet.py +218 -0
- integrations/productivity/google/sheets.py +222 -0
- integrations/productivity/google/tasks.py +199 -0
- integrations/productivity/linear.py +279 -0
- integrations/productivity/microsoft/__init__.py +13 -0
- integrations/productivity/microsoft/base.py +93 -0
- integrations/productivity/microsoft/calendar.py +207 -0
- integrations/productivity/microsoft/onedrive.py +222 -0
- integrations/productivity/microsoft/outlook.py +241 -0
- integrations/productivity/microsoft/sharepoint.py +211 -0
- integrations/productivity/notion.py +243 -0
- integrations/productivity/todoist.py +250 -0
- integrations/productivity/trello.py +244 -0
- integrations/storage/__init__.py +1 -0
- integrations/storage/box.py +279 -0
- integrations/storage/dropbox.py +215 -0
- integrations/telegram_inbound.py +179 -0
- integrations/telegram_poller.py +151 -0
- memory/__init__.py +1 -0
- memory/causal_graph.py +125 -0
- memory/impact_tracker.py +88 -0
- memory/vector_memory.py +436 -0
- models/__init__.py +12 -0
- models/chatjimmy_client.py +210 -0
- models/claude_client.py +235 -0
- models/claude_code_subagent.py +192 -0
- models/openai_client.py +120 -0
- models/router.py +162 -0
- recursive/discovery_loop.py +118 -0
- recursive/optimization_loop.py +135 -0
- recursive/validation_loop.py +38 -0
- security/__init__.py +1 -0
- security/audit_logger.py +106 -0
- security/filesystem_guard.py +100 -0
- security/hypershield.py +99 -0
- security/network_guard.py +97 -0
- security/policy_engine.py +198 -0
- swarm/__init__.py +1 -0
- swarm/agents/__init__.py +0 -0
- swarm/agents/base.py +230 -0
- swarm/agents/business/__init__.py +0 -0
- swarm/agents/business/counsel.py +31 -0
- swarm/agents/business/herald.py +32 -0
- swarm/agents/business/ledger.py +26 -0
- swarm/agents/business/nexus.py +30 -0
- swarm/agents/business/ops.py +30 -0
- swarm/agents/business/pipeline.py +28 -0
- swarm/agents/business/revenue.py +30 -0
- swarm/agents/business/sovereign.py +31 -0
- swarm/agents/business/strategos.py +27 -0
- swarm/agents/business/talent.py +27 -0
- swarm/agents/business/venture.py +32 -0
- swarm/agents/comms/__init__.py +1 -0
- swarm/agents/comms/cipher.py +32 -0
- swarm/agents/comms/echo_comms.py +31 -0
- swarm/agents/comms/envoy.py +31 -0
- swarm/agents/comms/herald_scheduler.py +33 -0
- swarm/agents/comms/pulse.py +29 -0
- swarm/agents/creative/__init__.py +0 -0
- swarm/agents/creative/author.py +27 -0
- swarm/agents/creative/lens.py +25 -0
- swarm/agents/executive/__init__.py +1 -0
- swarm/agents/intelligence/__init__.py +1 -0
- swarm/agents/intelligence/arbiter.py +30 -0
- swarm/agents/intelligence/sentinel.py +30 -0
- swarm/agents/media/__init__.py +0 -0
- swarm/agents/personal/__init__.py +0 -0
- swarm/agents/personal/atlas.py +27 -0
- swarm/agents/personal/hearth.py +26 -0
- swarm/agents/personal/midas.py +27 -0
- swarm/agents/personal/navigator.py +27 -0
- swarm/agents/personal/nourish.py +31 -0
- swarm/agents/personal/vitals.py +33 -0
- swarm/agents/recursive/__init__.py +0 -0
- swarm/agents/recursive/alchemist.py +73 -0
- swarm/agents/recursive/calibrator.py +45 -0
- swarm/agents/recursive/scout.py +97 -0
- swarm/agents/scientific/__init__.py +0 -0
- swarm/agents/scientific/cosmos.py +36 -0
- swarm/agents/scientific/gaia.py +26 -0
- swarm/agents/scientific/medicus.py +33 -0
- swarm/agents/scientific/oracle_agent.py +39 -0
- swarm/agents/scientific/scribe.py +39 -0
- swarm/agents/talent/__init__.py +0 -0
- swarm/agents/talent/deal.py +28 -0
- swarm/agents/talent/roster.py +29 -0
- swarm/agents/talent/scout.py +29 -0
- swarm/agents/talent/stage.py +27 -0
- swarm/agents/tech/__init__.py +0 -0
- swarm/agents/tech/aegis.py +28 -0
- swarm/agents/tech/bridge.py +28 -0
- swarm/agents/tech/forge.py +28 -0
- swarm/agents/trading/__init__.py +1 -0
- swarm/agents/trading/global_prediction_engine.py +611 -0
- swarm/agents/trading/polymarket_trader.py +165 -0
- swarm/agents/trading/prediction_strategist.py +218 -0
- swarm/autogen_bridge.py +68 -0
- swarm/bid_protocol.py +196 -0
- swarm/dispatcher.py +527 -0
- swarm/nexus.py +181 -0
- swarm/registry.py +112 -0
- telegram_listener.py +403 -0
- ui/tui/app.py +214 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""GoogleDocsConnector — Google Docs API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from integrations.base import (
|
|
10
|
+
BaseConnector,
|
|
11
|
+
ConnectorCapability,
|
|
12
|
+
ConnectorInfo,
|
|
13
|
+
)
|
|
14
|
+
from integrations.productivity.google.base import GoogleOAuthBase
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("hyperclaw.integrations.google_docs")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GoogleDocsConnector(BaseConnector, GoogleOAuthBase):
|
|
20
|
+
"""
|
|
21
|
+
Google Docs API connector.
|
|
22
|
+
|
|
23
|
+
Required config (via GoogleOAuthBase):
|
|
24
|
+
- access_token or credentials_file
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
BASE_URL = "https://docs.googleapis.com/v1"
|
|
28
|
+
DRIVE_URL = "https://www.googleapis.com/drive/v3"
|
|
29
|
+
|
|
30
|
+
def __init__(self, config: dict) -> None:
|
|
31
|
+
self.config = config
|
|
32
|
+
self._init_google_auth(config)
|
|
33
|
+
self._validate_google_config(config)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def info(self) -> ConnectorInfo:
|
|
37
|
+
return ConnectorInfo(
|
|
38
|
+
connector_id="google_docs",
|
|
39
|
+
platform="google_docs",
|
|
40
|
+
category="productivity",
|
|
41
|
+
capabilities=frozenset(
|
|
42
|
+
[
|
|
43
|
+
ConnectorCapability.READ_DATA,
|
|
44
|
+
ConnectorCapability.WRITE_DATA,
|
|
45
|
+
ConnectorCapability.ACTION,
|
|
46
|
+
]
|
|
47
|
+
),
|
|
48
|
+
auth_type="oauth2",
|
|
49
|
+
rate_limit_per_minute=60,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
53
|
+
return await self._get_google_client()
|
|
54
|
+
|
|
55
|
+
async def health(self) -> bool:
|
|
56
|
+
"""Check if Docs API is accessible."""
|
|
57
|
+
try:
|
|
58
|
+
client = await self._get_client()
|
|
59
|
+
# Use Drive API to check access since Docs doesn't have a simple endpoint
|
|
60
|
+
response = await client.get(
|
|
61
|
+
f"{self.DRIVE_URL}/about",
|
|
62
|
+
params={"fields": "user"},
|
|
63
|
+
)
|
|
64
|
+
return response.status_code == 200
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Google Docs health check failed: {e}")
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
async def get_document(self, document_id: str) -> dict:
|
|
70
|
+
"""Get a document's content and metadata."""
|
|
71
|
+
client = await self._get_client()
|
|
72
|
+
|
|
73
|
+
response = await client.get(f"{self.BASE_URL}/documents/{document_id}")
|
|
74
|
+
|
|
75
|
+
if response.status_code != 200:
|
|
76
|
+
raise Exception(f"Docs get document failed: {response.text}")
|
|
77
|
+
|
|
78
|
+
return response.json()
|
|
79
|
+
|
|
80
|
+
async def create_document(
|
|
81
|
+
self,
|
|
82
|
+
title: str,
|
|
83
|
+
content: str = "",
|
|
84
|
+
) -> dict:
|
|
85
|
+
"""Create a new document."""
|
|
86
|
+
client = await self._get_client()
|
|
87
|
+
|
|
88
|
+
# Create empty document
|
|
89
|
+
response = await client.post(
|
|
90
|
+
f"{self.BASE_URL}/documents",
|
|
91
|
+
json={"title": title},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if response.status_code not in (200, 201):
|
|
95
|
+
raise Exception(f"Docs create document failed: {response.text}")
|
|
96
|
+
|
|
97
|
+
doc = response.json()
|
|
98
|
+
|
|
99
|
+
# If content provided, append it
|
|
100
|
+
if content:
|
|
101
|
+
await self.append_text(doc["documentId"], content)
|
|
102
|
+
|
|
103
|
+
return doc
|
|
104
|
+
|
|
105
|
+
async def append_text(
|
|
106
|
+
self,
|
|
107
|
+
document_id: str,
|
|
108
|
+
text: str,
|
|
109
|
+
) -> dict:
|
|
110
|
+
"""Append text to a document."""
|
|
111
|
+
client = await self._get_client()
|
|
112
|
+
|
|
113
|
+
requests = [
|
|
114
|
+
{
|
|
115
|
+
"insertText": {
|
|
116
|
+
"location": {"index": 1},
|
|
117
|
+
"text": text,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
response = await client.post(
|
|
123
|
+
f"{self.BASE_URL}/documents/{document_id}:batchUpdate",
|
|
124
|
+
json={"requests": requests},
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if response.status_code != 200:
|
|
128
|
+
raise Exception(f"Docs append text failed: {response.text}")
|
|
129
|
+
|
|
130
|
+
return response.json()
|
|
131
|
+
|
|
132
|
+
async def replace_text(
|
|
133
|
+
self,
|
|
134
|
+
document_id: str,
|
|
135
|
+
find: str,
|
|
136
|
+
replace: str,
|
|
137
|
+
) -> dict:
|
|
138
|
+
"""Replace text in a document."""
|
|
139
|
+
client = await self._get_client()
|
|
140
|
+
|
|
141
|
+
requests = [
|
|
142
|
+
{
|
|
143
|
+
"replaceAllText": {
|
|
144
|
+
"containsText": {"text": find, "matchCase": True},
|
|
145
|
+
"replaceText": replace,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
response = await client.post(
|
|
151
|
+
f"{self.BASE_URL}/documents/{document_id}:batchUpdate",
|
|
152
|
+
json={"requests": requests},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if response.status_code != 200:
|
|
156
|
+
raise Exception(f"Docs replace text failed: {response.text}")
|
|
157
|
+
|
|
158
|
+
return response.json()
|
|
159
|
+
|
|
160
|
+
async def export_as_pdf(self, document_id: str) -> bytes:
|
|
161
|
+
"""Export document as PDF."""
|
|
162
|
+
client = await self._get_client()
|
|
163
|
+
|
|
164
|
+
response = await client.get(
|
|
165
|
+
f"{self.DRIVE_URL}/files/{document_id}/export",
|
|
166
|
+
params={"mimeType": "application/pdf"},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if response.status_code != 200:
|
|
170
|
+
raise Exception(f"Docs export as PDF failed: {response.text}")
|
|
171
|
+
|
|
172
|
+
return response.content
|
|
173
|
+
|
|
174
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
175
|
+
"""Read a document."""
|
|
176
|
+
return await self.get_document(resource_id)
|
|
177
|
+
|
|
178
|
+
async def _write_impl(self, resource_type: str, data: dict, **kwargs) -> dict:
|
|
179
|
+
"""Create a document."""
|
|
180
|
+
return await self.create_document(
|
|
181
|
+
title=data.get("title", "Untitled"),
|
|
182
|
+
content=data.get("content", ""),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def _action_impl(self, action_name: str, params: dict) -> dict:
|
|
186
|
+
"""Execute Docs-specific actions."""
|
|
187
|
+
if action_name == "append_text":
|
|
188
|
+
return await self.append_text(
|
|
189
|
+
params["document_id"],
|
|
190
|
+
params["text"],
|
|
191
|
+
)
|
|
192
|
+
elif action_name == "replace_text":
|
|
193
|
+
return await self.replace_text(
|
|
194
|
+
params["document_id"],
|
|
195
|
+
params["find"],
|
|
196
|
+
params["replace"],
|
|
197
|
+
)
|
|
198
|
+
elif action_name == "export_pdf":
|
|
199
|
+
pdf_bytes = await self.export_as_pdf(params["document_id"])
|
|
200
|
+
return {"size": len(pdf_bytes), "format": "pdf"}
|
|
201
|
+
|
|
202
|
+
raise ValueError(f"Unknown action: {action_name}")
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""GoogleDriveConnector — Google Drive API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import mimetypes
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from integrations.base import (
|
|
12
|
+
BaseConnector,
|
|
13
|
+
ConnectorCapability,
|
|
14
|
+
ConnectorInfo,
|
|
15
|
+
)
|
|
16
|
+
from integrations.productivity.google.base import GoogleOAuthBase
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("hyperclaw.integrations.google_drive")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GoogleDriveConnector(BaseConnector, GoogleOAuthBase):
|
|
22
|
+
"""
|
|
23
|
+
Google Drive API connector.
|
|
24
|
+
|
|
25
|
+
Required config (via GoogleOAuthBase):
|
|
26
|
+
- access_token or credentials_file
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
BASE_URL = "https://www.googleapis.com/drive/v3"
|
|
30
|
+
UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3"
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: dict) -> None:
|
|
33
|
+
self.config = config
|
|
34
|
+
self._init_google_auth(config)
|
|
35
|
+
self._validate_google_config(config)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def info(self) -> ConnectorInfo:
|
|
39
|
+
return ConnectorInfo(
|
|
40
|
+
connector_id="google_drive",
|
|
41
|
+
platform="google_drive",
|
|
42
|
+
category="productivity",
|
|
43
|
+
capabilities=frozenset(
|
|
44
|
+
[
|
|
45
|
+
ConnectorCapability.READ_DATA,
|
|
46
|
+
ConnectorCapability.WRITE_DATA,
|
|
47
|
+
ConnectorCapability.DELETE_DATA,
|
|
48
|
+
ConnectorCapability.LIST_DATA,
|
|
49
|
+
ConnectorCapability.SEARCH,
|
|
50
|
+
ConnectorCapability.FILE_UPLOAD,
|
|
51
|
+
ConnectorCapability.FILE_DOWNLOAD,
|
|
52
|
+
]
|
|
53
|
+
),
|
|
54
|
+
auth_type="oauth2",
|
|
55
|
+
rate_limit_per_minute=100,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
59
|
+
return await self._get_google_client()
|
|
60
|
+
|
|
61
|
+
async def health(self) -> bool:
|
|
62
|
+
"""Check if Drive API is accessible."""
|
|
63
|
+
try:
|
|
64
|
+
client = await self._get_client()
|
|
65
|
+
response = await client.get(f"{self.BASE_URL}/about", params={"fields": "user"})
|
|
66
|
+
return response.status_code == 200
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Google Drive health check failed: {e}")
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
async def upload_file(
|
|
72
|
+
self,
|
|
73
|
+
path: str,
|
|
74
|
+
folder_id: str | None = None,
|
|
75
|
+
mime_type: str | None = None,
|
|
76
|
+
) -> dict:
|
|
77
|
+
"""Upload a file to Google Drive."""
|
|
78
|
+
client = await self._get_client()
|
|
79
|
+
|
|
80
|
+
file_path = Path(path)
|
|
81
|
+
if not mime_type:
|
|
82
|
+
mime_type = mimetypes.guess_type(str(file_path))[0] or "application/octet-stream"
|
|
83
|
+
|
|
84
|
+
metadata = {"name": file_path.name}
|
|
85
|
+
if folder_id:
|
|
86
|
+
metadata["parents"] = [folder_id]
|
|
87
|
+
|
|
88
|
+
with open(path, "rb") as f:
|
|
89
|
+
content = f.read()
|
|
90
|
+
|
|
91
|
+
# Use multipart upload
|
|
92
|
+
response = await client.post(
|
|
93
|
+
f"{self.UPLOAD_URL}/files",
|
|
94
|
+
params={"uploadType": "multipart"},
|
|
95
|
+
files={
|
|
96
|
+
"metadata": ("metadata", str(metadata).encode(), "application/json"),
|
|
97
|
+
"file": (file_path.name, content, mime_type),
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if response.status_code not in (200, 201):
|
|
102
|
+
raise Exception(f"Drive upload failed: {response.text}")
|
|
103
|
+
|
|
104
|
+
return response.json()
|
|
105
|
+
|
|
106
|
+
async def download_file(
|
|
107
|
+
self,
|
|
108
|
+
file_id: str,
|
|
109
|
+
destination: str,
|
|
110
|
+
) -> str:
|
|
111
|
+
"""Download a file from Google Drive."""
|
|
112
|
+
client = await self._get_client()
|
|
113
|
+
|
|
114
|
+
response = await client.get(
|
|
115
|
+
f"{self.BASE_URL}/files/{file_id}",
|
|
116
|
+
params={"alt": "media"},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if response.status_code != 200:
|
|
120
|
+
raise Exception(f"Drive download failed: {response.text}")
|
|
121
|
+
|
|
122
|
+
with open(destination, "wb") as f:
|
|
123
|
+
f.write(response.content)
|
|
124
|
+
|
|
125
|
+
return destination
|
|
126
|
+
|
|
127
|
+
async def list_folder(
|
|
128
|
+
self,
|
|
129
|
+
folder_id: str = "root",
|
|
130
|
+
page_size: int = 100,
|
|
131
|
+
) -> list[dict]:
|
|
132
|
+
"""List files in a folder."""
|
|
133
|
+
client = await self._get_client()
|
|
134
|
+
|
|
135
|
+
query = f"'{folder_id}' in parents and trashed=false"
|
|
136
|
+
|
|
137
|
+
response = await client.get(
|
|
138
|
+
f"{self.BASE_URL}/files",
|
|
139
|
+
params={
|
|
140
|
+
"q": query,
|
|
141
|
+
"pageSize": page_size,
|
|
142
|
+
"fields": "files(id,name,mimeType,size,createdTime,modifiedTime)",
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if response.status_code != 200:
|
|
147
|
+
raise Exception(f"Drive list folder failed: {response.text}")
|
|
148
|
+
|
|
149
|
+
return response.json().get("files", [])
|
|
150
|
+
|
|
151
|
+
async def search_files(self, query: str) -> list[dict]:
|
|
152
|
+
"""Search for files."""
|
|
153
|
+
client = await self._get_client()
|
|
154
|
+
|
|
155
|
+
response = await client.get(
|
|
156
|
+
f"{self.BASE_URL}/files",
|
|
157
|
+
params={
|
|
158
|
+
"q": f"name contains '{query}' and trashed=false",
|
|
159
|
+
"pageSize": 50,
|
|
160
|
+
"fields": "files(id,name,mimeType,size,createdTime,modifiedTime)",
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if response.status_code != 200:
|
|
165
|
+
raise Exception(f"Drive search failed: {response.text}")
|
|
166
|
+
|
|
167
|
+
return response.json().get("files", [])
|
|
168
|
+
|
|
169
|
+
async def create_folder(
|
|
170
|
+
self,
|
|
171
|
+
name: str,
|
|
172
|
+
parent_id: str | None = None,
|
|
173
|
+
) -> dict:
|
|
174
|
+
"""Create a folder."""
|
|
175
|
+
client = await self._get_client()
|
|
176
|
+
|
|
177
|
+
metadata = {
|
|
178
|
+
"name": name,
|
|
179
|
+
"mimeType": "application/vnd.google-apps.folder",
|
|
180
|
+
}
|
|
181
|
+
if parent_id:
|
|
182
|
+
metadata["parents"] = [parent_id]
|
|
183
|
+
|
|
184
|
+
response = await client.post(
|
|
185
|
+
f"{self.BASE_URL}/files",
|
|
186
|
+
json=metadata,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if response.status_code not in (200, 201):
|
|
190
|
+
raise Exception(f"Drive create folder failed: {response.text}")
|
|
191
|
+
|
|
192
|
+
return response.json()
|
|
193
|
+
|
|
194
|
+
async def share_file(
|
|
195
|
+
self,
|
|
196
|
+
file_id: str,
|
|
197
|
+
email: str,
|
|
198
|
+
role: str = "reader",
|
|
199
|
+
) -> dict:
|
|
200
|
+
"""Share a file with a user."""
|
|
201
|
+
client = await self._get_client()
|
|
202
|
+
|
|
203
|
+
response = await client.post(
|
|
204
|
+
f"{self.BASE_URL}/files/{file_id}/permissions",
|
|
205
|
+
json={
|
|
206
|
+
"type": "user",
|
|
207
|
+
"role": role,
|
|
208
|
+
"emailAddress": email,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if response.status_code not in (200, 201):
|
|
213
|
+
raise Exception(f"Drive share failed: {response.text}")
|
|
214
|
+
|
|
215
|
+
return response.json()
|
|
216
|
+
|
|
217
|
+
async def get_file_metadata(self, file_id: str) -> dict:
|
|
218
|
+
"""Get file metadata."""
|
|
219
|
+
client = await self._get_client()
|
|
220
|
+
|
|
221
|
+
response = await client.get(
|
|
222
|
+
f"{self.BASE_URL}/files/{file_id}",
|
|
223
|
+
params={"fields": "id,name,mimeType,size,createdTime,modifiedTime,owners,permissions"},
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if response.status_code != 200:
|
|
227
|
+
raise Exception(f"Drive get metadata failed: {response.text}")
|
|
228
|
+
|
|
229
|
+
return response.json()
|
|
230
|
+
|
|
231
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
232
|
+
"""Read file metadata."""
|
|
233
|
+
return await self.get_file_metadata(resource_id)
|
|
234
|
+
|
|
235
|
+
async def _write_impl(self, resource_type: str, data: dict, **kwargs) -> dict:
|
|
236
|
+
"""Create a file or folder."""
|
|
237
|
+
if resource_type == "folder":
|
|
238
|
+
return await self.create_folder(
|
|
239
|
+
name=data.get("name", "Untitled"),
|
|
240
|
+
parent_id=data.get("parent_id"),
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
return await self.upload_file(
|
|
244
|
+
path=data.get("path"),
|
|
245
|
+
folder_id=data.get("folder_id"),
|
|
246
|
+
mime_type=data.get("mime_type"),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
async def _delete_impl(self, resource_id: str, **kwargs) -> bool:
|
|
250
|
+
"""Delete a file."""
|
|
251
|
+
client = await self._get_client()
|
|
252
|
+
|
|
253
|
+
response = await client.delete(f"{self.BASE_URL}/files/{resource_id}")
|
|
254
|
+
|
|
255
|
+
return response.status_code == 204
|
|
256
|
+
|
|
257
|
+
async def _list_impl(
|
|
258
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
259
|
+
) -> list[dict]:
|
|
260
|
+
"""List files."""
|
|
261
|
+
return await self.list_folder(
|
|
262
|
+
folder_id=filters.get("folder_id", "root"),
|
|
263
|
+
page_size=filters.get("page_size", 100),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
async def _search_impl(self, query: str, **kwargs) -> list[dict]:
|
|
267
|
+
"""Search files."""
|
|
268
|
+
return await self.search_files(query)
|