cortexflow-ai 2.0.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.
- cortexflow_ai/__init__.py +8 -0
- cortexflow_ai/agent/__init__.py +1 -0
- cortexflow_ai/agent/pipeline.py +194 -0
- cortexflow_ai/agent/runtime.py +467 -0
- cortexflow_ai/agent/session.py +168 -0
- cortexflow_ai/channels/__init__.py +1 -0
- cortexflow_ai/channels/base.py +99 -0
- cortexflow_ai/channels/discord_.py +145 -0
- cortexflow_ai/channels/email_.py +256 -0
- cortexflow_ai/channels/irc.py +261 -0
- cortexflow_ai/channels/mastodon_.py +235 -0
- cortexflow_ai/channels/matrix.py +196 -0
- cortexflow_ai/channels/mattermost.py +235 -0
- cortexflow_ai/channels/nextcloud.py +297 -0
- cortexflow_ai/channels/signal_.py +221 -0
- cortexflow_ai/channels/slack.py +214 -0
- cortexflow_ai/channels/sms.py +176 -0
- cortexflow_ai/channels/teams.py +214 -0
- cortexflow_ai/channels/telegram.py +151 -0
- cortexflow_ai/channels/webhook.py +201 -0
- cortexflow_ai/channels/whatsapp.py +218 -0
- cortexflow_ai/cli.py +805 -0
- cortexflow_ai/commands/__init__.py +17 -0
- cortexflow_ai/commands/handler.py +202 -0
- cortexflow_ai/config.py +180 -0
- cortexflow_ai/gateway/__init__.py +1 -0
- cortexflow_ai/gateway/main.py +110 -0
- cortexflow_ai/gateway/routes.py +295 -0
- cortexflow_ai/gateway/websocket.py +189 -0
- cortexflow_ai/init_wizard.py +261 -0
- cortexflow_ai/memory/__init__.py +1 -0
- cortexflow_ai/memory/archiver.py +119 -0
- cortexflow_ai/memory/compactor.py +188 -0
- cortexflow_ai/memory/long_term.py +382 -0
- cortexflow_ai/memory/retrieval.py +337 -0
- cortexflow_ai/memory/short_term.py +190 -0
- cortexflow_ai/memory/tagging.py +101 -0
- cortexflow_ai/models/__init__.py +1 -0
- cortexflow_ai/models/deepseek.py +180 -0
- cortexflow_ai/models/openai_.py +157 -0
- cortexflow_ai/models/router.py +451 -0
- cortexflow_ai/observability/__init__.py +1 -0
- cortexflow_ai/observability/logs.py +161 -0
- cortexflow_ai/observability/metrics.py +324 -0
- cortexflow_ai/plugins/__init__.py +1 -0
- cortexflow_ai/plugins/base.py +101 -0
- cortexflow_ai/plugins/registry.py +150 -0
- cortexflow_ai/reflection/__init__.py +1 -0
- cortexflow_ai/reflection/engine.py +214 -0
- cortexflow_ai/tools/__init__.py +1 -0
- cortexflow_ai/tools/base.py +114 -0
- cortexflow_ai/tools/file_ops.py +180 -0
- cortexflow_ai/tools/registry.py +160 -0
- cortexflow_ai/tools/web_search.py +140 -0
- cortexflow_ai/update_checker.py +58 -0
- cortexflow_ai/voice/__init__.py +1 -0
- cortexflow_ai/voice/stt.py +106 -0
- cortexflow_ai/voice/tts.py +230 -0
- cortexflow_ai/voice/wake_word.py +211 -0
- cortexflow_ai/workspace.py +158 -0
- cortexflow_ai-2.0.0.dist-info/METADATA +609 -0
- cortexflow_ai-2.0.0.dist-info/RECORD +66 -0
- cortexflow_ai-2.0.0.dist-info/WHEEL +5 -0
- cortexflow_ai-2.0.0.dist-info/entry_points.txt +2 -0
- cortexflow_ai-2.0.0.dist-info/licenses/LICENSE +105 -0
- cortexflow_ai-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""WhatsApp Cloud API channel adapter.
|
|
2
|
+
|
|
3
|
+
Uses Meta's official WhatsApp Business Cloud API — no browser automation,
|
|
4
|
+
no unofficial libraries, no QR code scanning. Works with any Meta Business
|
|
5
|
+
Account (free tier available with 1,000 conversations/month).
|
|
6
|
+
|
|
7
|
+
Setup:
|
|
8
|
+
1. Create a Meta Developer App at developers.facebook.com
|
|
9
|
+
2. Enable WhatsApp → set up a test phone number
|
|
10
|
+
3. Copy the Phone Number ID and Permanent Token
|
|
11
|
+
|
|
12
|
+
Required config:
|
|
13
|
+
channels.whatsapp.phone_number_id = "ENV:WA_PHONE_NUMBER_ID"
|
|
14
|
+
channels.whatsapp.access_token = "ENV:WA_ACCESS_TOKEN"
|
|
15
|
+
channels.whatsapp.verify_token = "ENV:WA_VERIFY_TOKEN" # webhook verify
|
|
16
|
+
|
|
17
|
+
The gateway exposes POST /webhook/whatsapp for incoming messages.
|
|
18
|
+
Register this URL in the Meta App Dashboard.
|
|
19
|
+
|
|
20
|
+
Docs: https://developers.facebook.com/docs/whatsapp/cloud-api
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
|
|
31
|
+
from cortexflow_ai.channels.base import Attachment, ChannelAdapter, InboundMessage
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
_API_BASE = "https://graph.facebook.com/v19.0"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WhatsAppAdapter(ChannelAdapter):
|
|
39
|
+
"""WhatsApp Cloud API adapter.
|
|
40
|
+
|
|
41
|
+
Inbound messages arrive via a webhook (POST /webhook/whatsapp).
|
|
42
|
+
The gateway must call ``adapter.handle_webhook(payload)`` for each
|
|
43
|
+
verified webhook event. Outbound messages use the Cloud API REST endpoint.
|
|
44
|
+
|
|
45
|
+
Because WhatsApp uses a push webhook model (not a long-poll), there is no
|
|
46
|
+
background task to start in ``connect()``; the adapter simply validates
|
|
47
|
+
its credentials and registers itself for webhook dispatch.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
channel_id = "whatsapp"
|
|
51
|
+
|
|
52
|
+
def __init__(self, config: dict[str, Any]) -> None:
|
|
53
|
+
super().__init__(config)
|
|
54
|
+
self._phone_number_id = self._resolve(config.get("phone_number_id", ""))
|
|
55
|
+
self._access_token = self._resolve(config.get("access_token", ""))
|
|
56
|
+
self._verify_token = self._resolve(config.get("verify_token", "cortexflow"))
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# Lifecycle
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
async def connect(self) -> None:
|
|
63
|
+
if not self._phone_number_id:
|
|
64
|
+
raise RuntimeError("WhatsApp phone_number_id not configured")
|
|
65
|
+
if not self._access_token:
|
|
66
|
+
raise RuntimeError("WhatsApp access_token not configured")
|
|
67
|
+
|
|
68
|
+
# Smoke-test the token by fetching our own phone number object
|
|
69
|
+
async with httpx.AsyncClient() as client:
|
|
70
|
+
resp = await client.get(
|
|
71
|
+
f"{_API_BASE}/{self._phone_number_id}",
|
|
72
|
+
headers=self._auth_headers(),
|
|
73
|
+
timeout=10.0,
|
|
74
|
+
)
|
|
75
|
+
if resp.status_code == 401:
|
|
76
|
+
raise RuntimeError("WhatsApp access_token is invalid")
|
|
77
|
+
resp.raise_for_status()
|
|
78
|
+
|
|
79
|
+
logger.info(
|
|
80
|
+
"WhatsAppAdapter connected phone_number_id=%s", self._phone_number_id
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
async def disconnect(self) -> None:
|
|
84
|
+
logger.info("WhatsAppAdapter disconnected")
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# Send
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
async def send(
|
|
91
|
+
self,
|
|
92
|
+
target: str,
|
|
93
|
+
text: str,
|
|
94
|
+
*,
|
|
95
|
+
reply_to: str | None = None,
|
|
96
|
+
attachments: list[Attachment] | None = None,
|
|
97
|
+
) -> str | None:
|
|
98
|
+
payload: dict[str, Any] = {
|
|
99
|
+
"messaging_product": "whatsapp",
|
|
100
|
+
"recipient_type": "individual",
|
|
101
|
+
"to": target,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if reply_to:
|
|
105
|
+
payload["context"] = {"message_id": reply_to}
|
|
106
|
+
|
|
107
|
+
if attachments:
|
|
108
|
+
att = attachments[0]
|
|
109
|
+
media_type = att.type # "image" | "audio" | "video" | "document"
|
|
110
|
+
if media_type in ("image", "audio", "video", "document"):
|
|
111
|
+
payload["type"] = media_type
|
|
112
|
+
payload[media_type] = {"link": att.url, "caption": text if media_type == "image" else ""}
|
|
113
|
+
else:
|
|
114
|
+
payload["type"] = "text"
|
|
115
|
+
payload["text"] = {"body": text, "preview_url": False}
|
|
116
|
+
else:
|
|
117
|
+
payload["type"] = "text"
|
|
118
|
+
payload["text"] = {"body": text, "preview_url": False}
|
|
119
|
+
|
|
120
|
+
async with httpx.AsyncClient() as client:
|
|
121
|
+
resp = await client.post(
|
|
122
|
+
f"{_API_BASE}/{self._phone_number_id}/messages",
|
|
123
|
+
headers=self._auth_headers(),
|
|
124
|
+
json=payload,
|
|
125
|
+
timeout=20.0,
|
|
126
|
+
)
|
|
127
|
+
resp.raise_for_status()
|
|
128
|
+
data = resp.json()
|
|
129
|
+
|
|
130
|
+
messages = data.get("messages", [])
|
|
131
|
+
return messages[0].get("id") if messages else None
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------------
|
|
134
|
+
# Webhook handling
|
|
135
|
+
# ------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def verify_webhook(self, mode: str, token: str, challenge: str) -> str | None:
|
|
138
|
+
"""Handle GET /webhook/whatsapp — return challenge if token matches."""
|
|
139
|
+
if mode == "subscribe" and token == self._verify_token:
|
|
140
|
+
return challenge
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
async def handle_webhook(self, payload: dict[str, Any]) -> None:
|
|
144
|
+
"""Process a verified POST /webhook/whatsapp payload."""
|
|
145
|
+
try:
|
|
146
|
+
for entry in payload.get("entry", []):
|
|
147
|
+
for change in entry.get("changes", []):
|
|
148
|
+
value = change.get("value", {})
|
|
149
|
+
await self._process_value(value)
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
logger.warning("WhatsApp webhook processing error: %s", exc)
|
|
152
|
+
|
|
153
|
+
async def _process_value(self, value: dict[str, Any]) -> None:
|
|
154
|
+
contacts = {c["wa_id"]: c.get("profile", {}).get("name", c["wa_id"]) for c in value.get("contacts", [])}
|
|
155
|
+
|
|
156
|
+
for msg in value.get("messages", []):
|
|
157
|
+
sender_id = msg.get("from", "")
|
|
158
|
+
sender_name = contacts.get(sender_id, sender_id)
|
|
159
|
+
msg_type = msg.get("type", "text")
|
|
160
|
+
text: str | None = None
|
|
161
|
+
attachments: list[Attachment] = []
|
|
162
|
+
|
|
163
|
+
if msg_type == "text":
|
|
164
|
+
text = msg.get("text", {}).get("body")
|
|
165
|
+
elif msg_type in ("image", "audio", "video", "document", "sticker"):
|
|
166
|
+
media = msg.get(msg_type, {})
|
|
167
|
+
attachments = [
|
|
168
|
+
Attachment(
|
|
169
|
+
type=msg_type if msg_type != "sticker" else "image",
|
|
170
|
+
url=None, # must be fetched separately via media ID
|
|
171
|
+
filename=media.get("filename"),
|
|
172
|
+
mime_type=media.get("mime_type"),
|
|
173
|
+
data=None,
|
|
174
|
+
)
|
|
175
|
+
]
|
|
176
|
+
text = media.get("caption")
|
|
177
|
+
elif msg_type == "interactive":
|
|
178
|
+
# Button reply or list reply
|
|
179
|
+
interactive = msg.get("interactive", {})
|
|
180
|
+
if interactive.get("type") == "button_reply":
|
|
181
|
+
text = interactive["button_reply"].get("title")
|
|
182
|
+
elif interactive.get("type") == "list_reply":
|
|
183
|
+
text = interactive["list_reply"].get("title")
|
|
184
|
+
|
|
185
|
+
inbound = InboundMessage(
|
|
186
|
+
channel=self.channel_id,
|
|
187
|
+
sender_id=sender_id,
|
|
188
|
+
sender_name=sender_name,
|
|
189
|
+
text=text,
|
|
190
|
+
attachments=attachments,
|
|
191
|
+
raw=msg,
|
|
192
|
+
)
|
|
193
|
+
await self._dispatch(inbound)
|
|
194
|
+
|
|
195
|
+
# ------------------------------------------------------------------
|
|
196
|
+
# Helpers
|
|
197
|
+
# ------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
def _auth_headers(self) -> dict[str, str]:
|
|
200
|
+
return {"Authorization": f"Bearer {self._access_token}"}
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _resolve(value: str) -> str:
|
|
204
|
+
if isinstance(value, str) and value.startswith("ENV:"):
|
|
205
|
+
return os.getenv(value[4:], "")
|
|
206
|
+
return value
|
|
207
|
+
|
|
208
|
+
def get_config_schema(self) -> dict[str, Any]:
|
|
209
|
+
return {
|
|
210
|
+
"type": "object",
|
|
211
|
+
"properties": {
|
|
212
|
+
"enabled": {"type": "boolean", "default": False},
|
|
213
|
+
"phone_number_id": {"type": "string", "description": "Meta WhatsApp Phone Number ID"},
|
|
214
|
+
"access_token": {"type": "string", "description": "Meta permanent user access token"},
|
|
215
|
+
"verify_token": {"type": "string", "description": "Webhook verify token (you choose)"},
|
|
216
|
+
},
|
|
217
|
+
"required": ["phone_number_id", "access_token"],
|
|
218
|
+
}
|