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,260 @@
|
|
|
1
|
+
"""TelegramConnector — Telegram Bot API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import AsyncIterator
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from integrations.base import (
|
|
11
|
+
BaseConnector,
|
|
12
|
+
ConnectorCapability,
|
|
13
|
+
ConnectorInfo,
|
|
14
|
+
InboundMessage,
|
|
15
|
+
OutboundMessage,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("hyperclaw.integrations.telegram")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
from hyperclaw.tools.egress import telegram_payload # noqa: E402 (link previews off)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TelegramConnector(BaseConnector):
|
|
25
|
+
"""
|
|
26
|
+
Telegram Bot API connector.
|
|
27
|
+
|
|
28
|
+
Required config:
|
|
29
|
+
- bot_token: Telegram Bot API token
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
BASE_URL = "https://api.telegram.org"
|
|
33
|
+
|
|
34
|
+
def __init__(self, config: dict) -> None:
|
|
35
|
+
self.config = config
|
|
36
|
+
self.bot_token = config.get("bot_token", "")
|
|
37
|
+
|
|
38
|
+
if config.get("enabled", False) and not self.bot_token:
|
|
39
|
+
raise ValueError("TelegramConnector requires bot_token")
|
|
40
|
+
|
|
41
|
+
self._last_update_id = 0
|
|
42
|
+
self._client: httpx.AsyncClient | None = None
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def info(self) -> ConnectorInfo:
|
|
46
|
+
return ConnectorInfo(
|
|
47
|
+
connector_id="telegram",
|
|
48
|
+
platform="telegram",
|
|
49
|
+
category="messaging",
|
|
50
|
+
capabilities=frozenset(
|
|
51
|
+
[
|
|
52
|
+
ConnectorCapability.SEND_MESSAGE,
|
|
53
|
+
ConnectorCapability.RECEIVE_MESSAGE,
|
|
54
|
+
ConnectorCapability.FILE_UPLOAD,
|
|
55
|
+
ConnectorCapability.FILE_DOWNLOAD,
|
|
56
|
+
ConnectorCapability.WEBHOOKS,
|
|
57
|
+
ConnectorCapability.NOTIFY,
|
|
58
|
+
ConnectorCapability.ACTION,
|
|
59
|
+
]
|
|
60
|
+
),
|
|
61
|
+
auth_type="api_key",
|
|
62
|
+
rate_limit_per_minute=30,
|
|
63
|
+
supports_threads=True,
|
|
64
|
+
supports_attachments=True,
|
|
65
|
+
supports_reactions=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
69
|
+
if self._client is None:
|
|
70
|
+
self._client = httpx.AsyncClient(timeout=30.0)
|
|
71
|
+
return self._client
|
|
72
|
+
|
|
73
|
+
def _api_url(self, method: str) -> str:
|
|
74
|
+
return f"{self.BASE_URL}/bot{self.bot_token}/{method}"
|
|
75
|
+
|
|
76
|
+
async def health(self) -> bool:
|
|
77
|
+
"""Check if bot token is valid."""
|
|
78
|
+
try:
|
|
79
|
+
client = await self._get_client()
|
|
80
|
+
response = await client.get(self._api_url("getMe"))
|
|
81
|
+
data = response.json()
|
|
82
|
+
return data.get("ok", False)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(f"Telegram health check failed: {e}")
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
async def _send_impl(self, message: OutboundMessage) -> str:
|
|
88
|
+
"""Send a message via Telegram."""
|
|
89
|
+
client = await self._get_client()
|
|
90
|
+
|
|
91
|
+
payload = {
|
|
92
|
+
"chat_id": message.recipient_id,
|
|
93
|
+
"text": message.content,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if message.thread_id:
|
|
97
|
+
payload["reply_to_message_id"] = message.thread_id
|
|
98
|
+
|
|
99
|
+
if message.format == "markdown":
|
|
100
|
+
payload["parse_mode"] = "MarkdownV2"
|
|
101
|
+
elif message.format == "html":
|
|
102
|
+
payload["parse_mode"] = "HTML"
|
|
103
|
+
|
|
104
|
+
response = await client.post(self._api_url("sendMessage"), json=telegram_payload(payload))
|
|
105
|
+
data = response.json()
|
|
106
|
+
|
|
107
|
+
if not data.get("ok"):
|
|
108
|
+
raise Exception(f"Telegram send failed: {data.get('description')}")
|
|
109
|
+
|
|
110
|
+
return str(data["result"]["message_id"])
|
|
111
|
+
|
|
112
|
+
async def _receive_impl(self) -> AsyncIterator[InboundMessage]:
|
|
113
|
+
"""Poll for updates using long polling."""
|
|
114
|
+
client = await self._get_client()
|
|
115
|
+
|
|
116
|
+
params = {
|
|
117
|
+
"offset": self._last_update_id + 1,
|
|
118
|
+
"timeout": 30,
|
|
119
|
+
"allowed_updates": ["message"],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
response = await client.get(self._api_url("getUpdates"), params=params)
|
|
123
|
+
data = response.json()
|
|
124
|
+
|
|
125
|
+
if not data.get("ok"):
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
for update in data.get("result", []):
|
|
129
|
+
self._last_update_id = update["update_id"]
|
|
130
|
+
|
|
131
|
+
msg = update.get("message", {})
|
|
132
|
+
if not msg:
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
sender = msg.get("from", {})
|
|
136
|
+
attachments = []
|
|
137
|
+
|
|
138
|
+
# Handle attachments
|
|
139
|
+
for attach_type in ["photo", "document", "video", "audio", "voice"]:
|
|
140
|
+
if attach_type in msg:
|
|
141
|
+
attach_data = msg[attach_type]
|
|
142
|
+
if isinstance(attach_data, list):
|
|
143
|
+
attach_data = attach_data[-1] # Get largest photo
|
|
144
|
+
attachments.append({"type": attach_type, "data": attach_data})
|
|
145
|
+
|
|
146
|
+
yield InboundMessage(
|
|
147
|
+
message_id=str(msg.get("message_id", "")),
|
|
148
|
+
platform="telegram",
|
|
149
|
+
sender_id=str(sender.get("id", "")),
|
|
150
|
+
sender_name=sender.get("first_name", "")
|
|
151
|
+
+ " "
|
|
152
|
+
+ sender.get("last_name", ""),
|
|
153
|
+
content=msg.get("text", "") or msg.get("caption", ""),
|
|
154
|
+
thread_id=str(msg.get("reply_to_message", {}).get("message_id", ""))
|
|
155
|
+
or None,
|
|
156
|
+
channel_id=str(msg.get("chat", {}).get("id", "")),
|
|
157
|
+
attachments=attachments,
|
|
158
|
+
raw=msg,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def send_photo(
|
|
162
|
+
self,
|
|
163
|
+
chat_id: str,
|
|
164
|
+
photo_url: str,
|
|
165
|
+
caption: str = "",
|
|
166
|
+
) -> str:
|
|
167
|
+
"""Send a photo to a chat."""
|
|
168
|
+
client = await self._get_client()
|
|
169
|
+
|
|
170
|
+
payload = {
|
|
171
|
+
"chat_id": chat_id,
|
|
172
|
+
"photo": photo_url,
|
|
173
|
+
}
|
|
174
|
+
if caption:
|
|
175
|
+
payload["caption"] = caption
|
|
176
|
+
|
|
177
|
+
response = await client.post(self._api_url("sendPhoto"), json=payload)
|
|
178
|
+
data = response.json()
|
|
179
|
+
|
|
180
|
+
if not data.get("ok"):
|
|
181
|
+
raise Exception(f"Telegram sendPhoto failed: {data.get('description')}")
|
|
182
|
+
|
|
183
|
+
return str(data["result"]["message_id"])
|
|
184
|
+
|
|
185
|
+
async def send_document(
|
|
186
|
+
self,
|
|
187
|
+
chat_id: str,
|
|
188
|
+
file_path: str,
|
|
189
|
+
caption: str = "",
|
|
190
|
+
) -> str:
|
|
191
|
+
"""Send a document to a chat."""
|
|
192
|
+
client = await self._get_client()
|
|
193
|
+
|
|
194
|
+
with open(file_path, "rb") as f:
|
|
195
|
+
files = {"document": f}
|
|
196
|
+
data = {"chat_id": chat_id}
|
|
197
|
+
if caption:
|
|
198
|
+
data["caption"] = caption
|
|
199
|
+
|
|
200
|
+
response = await client.post(
|
|
201
|
+
self._api_url("sendDocument"),
|
|
202
|
+
data=data,
|
|
203
|
+
files=files,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
result = response.json()
|
|
207
|
+
|
|
208
|
+
if not result.get("ok"):
|
|
209
|
+
raise Exception(f"Telegram sendDocument failed: {result.get('description')}")
|
|
210
|
+
|
|
211
|
+
return str(result["result"]["message_id"])
|
|
212
|
+
|
|
213
|
+
async def send_keyboard(
|
|
214
|
+
self,
|
|
215
|
+
chat_id: str,
|
|
216
|
+
text: str,
|
|
217
|
+
buttons: list[list[str]],
|
|
218
|
+
) -> str:
|
|
219
|
+
"""Send a message with an inline keyboard."""
|
|
220
|
+
client = await self._get_client()
|
|
221
|
+
|
|
222
|
+
keyboard = {
|
|
223
|
+
"inline_keyboard": [
|
|
224
|
+
[{"text": btn, "callback_data": btn} for btn in row] for row in buttons
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
payload = {
|
|
229
|
+
"chat_id": chat_id,
|
|
230
|
+
"text": text,
|
|
231
|
+
"reply_markup": keyboard,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
response = await client.post(self._api_url("sendMessage"), json=telegram_payload(payload))
|
|
235
|
+
data = response.json()
|
|
236
|
+
|
|
237
|
+
if not data.get("ok"):
|
|
238
|
+
raise Exception(f"Telegram sendKeyboard failed: {data.get('description')}")
|
|
239
|
+
|
|
240
|
+
return str(data["result"]["message_id"])
|
|
241
|
+
|
|
242
|
+
async def _action_impl(self, action_name: str, params: dict) -> dict:
|
|
243
|
+
"""Execute Telegram-specific actions."""
|
|
244
|
+
client = await self._get_client()
|
|
245
|
+
|
|
246
|
+
if action_name == "send_chat_action":
|
|
247
|
+
response = await client.post(
|
|
248
|
+
self._api_url("sendChatAction"),
|
|
249
|
+
json={"chat_id": params["chat_id"], "action": params.get("action", "typing")},
|
|
250
|
+
)
|
|
251
|
+
return response.json()
|
|
252
|
+
|
|
253
|
+
elif action_name == "get_chat":
|
|
254
|
+
response = await client.get(
|
|
255
|
+
self._api_url("getChat"),
|
|
256
|
+
params={"chat_id": params["chat_id"]},
|
|
257
|
+
)
|
|
258
|
+
return response.json()
|
|
259
|
+
|
|
260
|
+
raise ValueError(f"Unknown action: {action_name}")
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""WhatsAppConnector — Meta WhatsApp Business API integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import AsyncIterator
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from integrations.base import (
|
|
11
|
+
BaseConnector,
|
|
12
|
+
ConnectorCapability,
|
|
13
|
+
ConnectorInfo,
|
|
14
|
+
InboundMessage,
|
|
15
|
+
OutboundMessage,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("hyperclaw.integrations.whatsapp")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WhatsAppConnector(BaseConnector):
|
|
22
|
+
"""
|
|
23
|
+
WhatsApp Business API connector via Meta Graph API.
|
|
24
|
+
|
|
25
|
+
Required config:
|
|
26
|
+
- access_token: Meta Graph API access token
|
|
27
|
+
- phone_number_id: WhatsApp Business phone number ID
|
|
28
|
+
|
|
29
|
+
Note: This connector handles health data and sets hypershield_context accordingly.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
BASE_URL = "https://graph.facebook.com/v17.0"
|
|
33
|
+
|
|
34
|
+
# HyperShield context for health data protection
|
|
35
|
+
hypershield_context = "health_data"
|
|
36
|
+
|
|
37
|
+
def __init__(self, config: dict) -> None:
|
|
38
|
+
self.config = config
|
|
39
|
+
self.access_token = config.get("access_token", "")
|
|
40
|
+
self.phone_number_id = config.get("phone_number_id", "")
|
|
41
|
+
|
|
42
|
+
if config.get("enabled", False):
|
|
43
|
+
if not self.access_token:
|
|
44
|
+
raise ValueError("WhatsAppConnector requires access_token")
|
|
45
|
+
if not self.phone_number_id:
|
|
46
|
+
raise ValueError("WhatsAppConnector requires phone_number_id")
|
|
47
|
+
|
|
48
|
+
self._client: httpx.AsyncClient | None = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def info(self) -> ConnectorInfo:
|
|
52
|
+
return ConnectorInfo(
|
|
53
|
+
connector_id="whatsapp",
|
|
54
|
+
platform="whatsapp",
|
|
55
|
+
category="messaging",
|
|
56
|
+
capabilities=frozenset(
|
|
57
|
+
[
|
|
58
|
+
ConnectorCapability.SEND_MESSAGE,
|
|
59
|
+
ConnectorCapability.RECEIVE_MESSAGE,
|
|
60
|
+
ConnectorCapability.FILE_UPLOAD,
|
|
61
|
+
ConnectorCapability.FILE_DOWNLOAD,
|
|
62
|
+
ConnectorCapability.WEBHOOKS,
|
|
63
|
+
]
|
|
64
|
+
),
|
|
65
|
+
auth_type="bearer",
|
|
66
|
+
rate_limit_per_minute=80,
|
|
67
|
+
supports_threads=False,
|
|
68
|
+
supports_attachments=True,
|
|
69
|
+
supports_reactions=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
73
|
+
if self._client is None:
|
|
74
|
+
self._client = httpx.AsyncClient(
|
|
75
|
+
timeout=30.0,
|
|
76
|
+
headers={
|
|
77
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
return self._client
|
|
82
|
+
|
|
83
|
+
async def health(self) -> bool:
|
|
84
|
+
"""Check if credentials are valid."""
|
|
85
|
+
try:
|
|
86
|
+
client = await self._get_client()
|
|
87
|
+
response = await client.get(
|
|
88
|
+
f"{self.BASE_URL}/{self.phone_number_id}",
|
|
89
|
+
)
|
|
90
|
+
return response.status_code == 200
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(f"WhatsApp health check failed: {e}")
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
async def _send_impl(self, message: OutboundMessage) -> str:
|
|
96
|
+
"""Send a text message via WhatsApp."""
|
|
97
|
+
client = await self._get_client()
|
|
98
|
+
|
|
99
|
+
payload = {
|
|
100
|
+
"messaging_product": "whatsapp",
|
|
101
|
+
"recipient_type": "individual",
|
|
102
|
+
"to": message.recipient_id,
|
|
103
|
+
"type": "text",
|
|
104
|
+
"text": {"body": message.content},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
response = await client.post(
|
|
108
|
+
f"{self.BASE_URL}/{self.phone_number_id}/messages",
|
|
109
|
+
json=payload,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if response.status_code not in (200, 201):
|
|
113
|
+
raise Exception(f"WhatsApp send failed: {response.text}")
|
|
114
|
+
|
|
115
|
+
data = response.json()
|
|
116
|
+
messages = data.get("messages", [])
|
|
117
|
+
return messages[0].get("id", "") if messages else ""
|
|
118
|
+
|
|
119
|
+
async def _receive_impl(self) -> AsyncIterator[InboundMessage]:
|
|
120
|
+
"""
|
|
121
|
+
Receive messages. WhatsApp uses webhooks for incoming messages.
|
|
122
|
+
This is a stub; implement webhook handler separately.
|
|
123
|
+
"""
|
|
124
|
+
return
|
|
125
|
+
yield
|
|
126
|
+
|
|
127
|
+
async def send_template(
|
|
128
|
+
self,
|
|
129
|
+
to: str,
|
|
130
|
+
template_name: str,
|
|
131
|
+
language_code: str = "en",
|
|
132
|
+
components: list[dict] | None = None,
|
|
133
|
+
) -> str:
|
|
134
|
+
"""Send a pre-approved message template."""
|
|
135
|
+
client = await self._get_client()
|
|
136
|
+
|
|
137
|
+
payload = {
|
|
138
|
+
"messaging_product": "whatsapp",
|
|
139
|
+
"recipient_type": "individual",
|
|
140
|
+
"to": to,
|
|
141
|
+
"type": "template",
|
|
142
|
+
"template": {
|
|
143
|
+
"name": template_name,
|
|
144
|
+
"language": {"code": language_code},
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if components:
|
|
149
|
+
payload["template"]["components"] = components
|
|
150
|
+
|
|
151
|
+
response = await client.post(
|
|
152
|
+
f"{self.BASE_URL}/{self.phone_number_id}/messages",
|
|
153
|
+
json=payload,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if response.status_code not in (200, 201):
|
|
157
|
+
raise Exception(f"WhatsApp send_template failed: {response.text}")
|
|
158
|
+
|
|
159
|
+
data = response.json()
|
|
160
|
+
messages = data.get("messages", [])
|
|
161
|
+
return messages[0].get("id", "") if messages else ""
|
|
162
|
+
|
|
163
|
+
async def send_media(
|
|
164
|
+
self,
|
|
165
|
+
to: str,
|
|
166
|
+
media_type: str,
|
|
167
|
+
url: str,
|
|
168
|
+
caption: str = "",
|
|
169
|
+
) -> str:
|
|
170
|
+
"""
|
|
171
|
+
Send media (image, video, audio, document).
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
to: Recipient phone number
|
|
175
|
+
media_type: One of "image", "video", "audio", "document"
|
|
176
|
+
url: Public URL of the media
|
|
177
|
+
caption: Optional caption (for image/video/document)
|
|
178
|
+
"""
|
|
179
|
+
client = await self._get_client()
|
|
180
|
+
|
|
181
|
+
media_object = {"link": url}
|
|
182
|
+
if caption and media_type in ("image", "video", "document"):
|
|
183
|
+
media_object["caption"] = caption
|
|
184
|
+
|
|
185
|
+
payload = {
|
|
186
|
+
"messaging_product": "whatsapp",
|
|
187
|
+
"recipient_type": "individual",
|
|
188
|
+
"to": to,
|
|
189
|
+
"type": media_type,
|
|
190
|
+
media_type: media_object,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
response = await client.post(
|
|
194
|
+
f"{self.BASE_URL}/{self.phone_number_id}/messages",
|
|
195
|
+
json=payload,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if response.status_code not in (200, 201):
|
|
199
|
+
raise Exception(f"WhatsApp send_media failed: {response.text}")
|
|
200
|
+
|
|
201
|
+
data = response.json()
|
|
202
|
+
messages = data.get("messages", [])
|
|
203
|
+
return messages[0].get("id", "") if messages else ""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""HyperClaw Productivity Connectors."""
|
|
2
|
+
|
|
3
|
+
from integrations.productivity.notion import NotionConnector
|
|
4
|
+
from integrations.productivity.airtable import AirtableConnector
|
|
5
|
+
from integrations.productivity.trello import TrelloConnector
|
|
6
|
+
from integrations.productivity.asana import AsanaConnector
|
|
7
|
+
from integrations.productivity.linear import LinearConnector
|
|
8
|
+
from integrations.productivity.todoist import TodoistConnector
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"NotionConnector",
|
|
12
|
+
"AirtableConnector",
|
|
13
|
+
"TrelloConnector",
|
|
14
|
+
"AsanaConnector",
|
|
15
|
+
"LinearConnector",
|
|
16
|
+
"TodoistConnector",
|
|
17
|
+
]
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""AirtableConnector — Airtable 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
|
+
|
|
15
|
+
logger = logging.getLogger("hyperclaw.integrations.airtable")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AirtableConnector(BaseConnector):
|
|
19
|
+
"""
|
|
20
|
+
Airtable API connector.
|
|
21
|
+
|
|
22
|
+
Required config:
|
|
23
|
+
- api_key: Airtable API key or Personal Access Token
|
|
24
|
+
- base_id: Airtable base ID
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
BASE_URL = "https://api.airtable.com/v0"
|
|
28
|
+
|
|
29
|
+
def __init__(self, config: dict) -> None:
|
|
30
|
+
self.config = config
|
|
31
|
+
self.api_key = config.get("api_key", "")
|
|
32
|
+
self.base_id = config.get("base_id", "")
|
|
33
|
+
|
|
34
|
+
if config.get("enabled", False):
|
|
35
|
+
if not self.api_key:
|
|
36
|
+
raise ValueError("AirtableConnector requires api_key")
|
|
37
|
+
if not self.base_id:
|
|
38
|
+
raise ValueError("AirtableConnector requires base_id")
|
|
39
|
+
|
|
40
|
+
self._client: httpx.AsyncClient | None = None
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def info(self) -> ConnectorInfo:
|
|
44
|
+
return ConnectorInfo(
|
|
45
|
+
connector_id="airtable",
|
|
46
|
+
platform="airtable",
|
|
47
|
+
category="productivity",
|
|
48
|
+
capabilities=frozenset(
|
|
49
|
+
[
|
|
50
|
+
ConnectorCapability.READ_DATA,
|
|
51
|
+
ConnectorCapability.WRITE_DATA,
|
|
52
|
+
ConnectorCapability.DELETE_DATA,
|
|
53
|
+
ConnectorCapability.LIST_DATA,
|
|
54
|
+
ConnectorCapability.SEARCH,
|
|
55
|
+
]
|
|
56
|
+
),
|
|
57
|
+
auth_type="bearer",
|
|
58
|
+
rate_limit_per_minute=100,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def _get_client(self) -> httpx.AsyncClient:
|
|
62
|
+
if self._client is None:
|
|
63
|
+
self._client = httpx.AsyncClient(
|
|
64
|
+
timeout=30.0,
|
|
65
|
+
headers={
|
|
66
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
},
|
|
69
|
+
)
|
|
70
|
+
return self._client
|
|
71
|
+
|
|
72
|
+
async def health(self) -> bool:
|
|
73
|
+
"""Check if Airtable API is accessible."""
|
|
74
|
+
try:
|
|
75
|
+
client = await self._get_client()
|
|
76
|
+
response = await client.get(f"{self.BASE_URL}/meta/bases")
|
|
77
|
+
return response.status_code == 200
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(f"Airtable health check failed: {e}")
|
|
80
|
+
return False
|
|
81
|
+
|
|
82
|
+
async def list_records(
|
|
83
|
+
self,
|
|
84
|
+
table_name: str,
|
|
85
|
+
filter_formula: str | None = None,
|
|
86
|
+
sort: list[dict] | None = None,
|
|
87
|
+
max_records: int = 100,
|
|
88
|
+
) -> list[dict]:
|
|
89
|
+
"""List records in a table."""
|
|
90
|
+
client = await self._get_client()
|
|
91
|
+
|
|
92
|
+
params = {"maxRecords": max_records}
|
|
93
|
+
if filter_formula:
|
|
94
|
+
params["filterByFormula"] = filter_formula
|
|
95
|
+
if sort:
|
|
96
|
+
for i, s in enumerate(sort):
|
|
97
|
+
params[f"sort[{i}][field]"] = s.get("field")
|
|
98
|
+
params[f"sort[{i}][direction]"] = s.get("direction", "asc")
|
|
99
|
+
|
|
100
|
+
response = await client.get(
|
|
101
|
+
f"{self.BASE_URL}/{self.base_id}/{table_name}",
|
|
102
|
+
params=params,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if response.status_code != 200:
|
|
106
|
+
raise Exception(f"Airtable list records failed: {response.text}")
|
|
107
|
+
|
|
108
|
+
return response.json().get("records", [])
|
|
109
|
+
|
|
110
|
+
async def get_record(
|
|
111
|
+
self,
|
|
112
|
+
table_name: str,
|
|
113
|
+
record_id: str,
|
|
114
|
+
) -> dict:
|
|
115
|
+
"""Get a single record."""
|
|
116
|
+
client = await self._get_client()
|
|
117
|
+
|
|
118
|
+
response = await client.get(
|
|
119
|
+
f"{self.BASE_URL}/{self.base_id}/{table_name}/{record_id}"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if response.status_code != 200:
|
|
123
|
+
raise Exception(f"Airtable get record failed: {response.text}")
|
|
124
|
+
|
|
125
|
+
return response.json()
|
|
126
|
+
|
|
127
|
+
async def create_record(
|
|
128
|
+
self,
|
|
129
|
+
table_name: str,
|
|
130
|
+
fields: dict,
|
|
131
|
+
) -> dict:
|
|
132
|
+
"""Create a new record."""
|
|
133
|
+
client = await self._get_client()
|
|
134
|
+
|
|
135
|
+
response = await client.post(
|
|
136
|
+
f"{self.BASE_URL}/{self.base_id}/{table_name}",
|
|
137
|
+
json={"fields": fields},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if response.status_code not in (200, 201):
|
|
141
|
+
raise Exception(f"Airtable create record failed: {response.text}")
|
|
142
|
+
|
|
143
|
+
return response.json()
|
|
144
|
+
|
|
145
|
+
async def update_record(
|
|
146
|
+
self,
|
|
147
|
+
table_name: str,
|
|
148
|
+
record_id: str,
|
|
149
|
+
fields: dict,
|
|
150
|
+
) -> dict:
|
|
151
|
+
"""Update a record."""
|
|
152
|
+
client = await self._get_client()
|
|
153
|
+
|
|
154
|
+
response = await client.patch(
|
|
155
|
+
f"{self.BASE_URL}/{self.base_id}/{table_name}/{record_id}",
|
|
156
|
+
json={"fields": fields},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if response.status_code != 200:
|
|
160
|
+
raise Exception(f"Airtable update record failed: {response.text}")
|
|
161
|
+
|
|
162
|
+
return response.json()
|
|
163
|
+
|
|
164
|
+
async def delete_record(
|
|
165
|
+
self,
|
|
166
|
+
table_name: str,
|
|
167
|
+
record_id: str,
|
|
168
|
+
) -> bool:
|
|
169
|
+
"""Delete a record."""
|
|
170
|
+
client = await self._get_client()
|
|
171
|
+
|
|
172
|
+
response = await client.delete(
|
|
173
|
+
f"{self.BASE_URL}/{self.base_id}/{table_name}/{record_id}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return response.status_code == 200
|
|
177
|
+
|
|
178
|
+
async def _read_impl(self, resource_id: str, **kwargs) -> dict:
|
|
179
|
+
"""Read a record."""
|
|
180
|
+
return await self.get_record(
|
|
181
|
+
table_name=kwargs.get("table_name"),
|
|
182
|
+
record_id=resource_id,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def _write_impl(self, resource_type: str, data: dict, **kwargs) -> dict:
|
|
186
|
+
"""Create a record."""
|
|
187
|
+
return await self.create_record(
|
|
188
|
+
table_name=data.get("table_name"),
|
|
189
|
+
fields=data.get("fields", {}),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def _delete_impl(self, resource_id: str, **kwargs) -> bool:
|
|
193
|
+
"""Delete a record."""
|
|
194
|
+
return await self.delete_record(
|
|
195
|
+
table_name=kwargs.get("table_name"),
|
|
196
|
+
record_id=resource_id,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
async def _list_impl(
|
|
200
|
+
self, resource_type: str, filters: dict, **kwargs
|
|
201
|
+
) -> list[dict]:
|
|
202
|
+
"""List records."""
|
|
203
|
+
return await self.list_records(
|
|
204
|
+
table_name=filters.get("table_name"),
|
|
205
|
+
filter_formula=filters.get("filter_formula"),
|
|
206
|
+
sort=filters.get("sort"),
|
|
207
|
+
max_records=filters.get("max_records", 100),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def _search_impl(self, query: str, **kwargs) -> list[dict]:
|
|
211
|
+
"""Search records using filter formula."""
|
|
212
|
+
table_name = kwargs.get("table_name")
|
|
213
|
+
field = kwargs.get("field", "Name")
|
|
214
|
+
return await self.list_records(
|
|
215
|
+
table_name=table_name,
|
|
216
|
+
filter_formula=f"SEARCH('{query}', {{{field}}})",
|
|
217
|
+
)
|