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.
Files changed (66) hide show
  1. cortexflow_ai/__init__.py +8 -0
  2. cortexflow_ai/agent/__init__.py +1 -0
  3. cortexflow_ai/agent/pipeline.py +194 -0
  4. cortexflow_ai/agent/runtime.py +467 -0
  5. cortexflow_ai/agent/session.py +168 -0
  6. cortexflow_ai/channels/__init__.py +1 -0
  7. cortexflow_ai/channels/base.py +99 -0
  8. cortexflow_ai/channels/discord_.py +145 -0
  9. cortexflow_ai/channels/email_.py +256 -0
  10. cortexflow_ai/channels/irc.py +261 -0
  11. cortexflow_ai/channels/mastodon_.py +235 -0
  12. cortexflow_ai/channels/matrix.py +196 -0
  13. cortexflow_ai/channels/mattermost.py +235 -0
  14. cortexflow_ai/channels/nextcloud.py +297 -0
  15. cortexflow_ai/channels/signal_.py +221 -0
  16. cortexflow_ai/channels/slack.py +214 -0
  17. cortexflow_ai/channels/sms.py +176 -0
  18. cortexflow_ai/channels/teams.py +214 -0
  19. cortexflow_ai/channels/telegram.py +151 -0
  20. cortexflow_ai/channels/webhook.py +201 -0
  21. cortexflow_ai/channels/whatsapp.py +218 -0
  22. cortexflow_ai/cli.py +805 -0
  23. cortexflow_ai/commands/__init__.py +17 -0
  24. cortexflow_ai/commands/handler.py +202 -0
  25. cortexflow_ai/config.py +180 -0
  26. cortexflow_ai/gateway/__init__.py +1 -0
  27. cortexflow_ai/gateway/main.py +110 -0
  28. cortexflow_ai/gateway/routes.py +295 -0
  29. cortexflow_ai/gateway/websocket.py +189 -0
  30. cortexflow_ai/init_wizard.py +261 -0
  31. cortexflow_ai/memory/__init__.py +1 -0
  32. cortexflow_ai/memory/archiver.py +119 -0
  33. cortexflow_ai/memory/compactor.py +188 -0
  34. cortexflow_ai/memory/long_term.py +382 -0
  35. cortexflow_ai/memory/retrieval.py +337 -0
  36. cortexflow_ai/memory/short_term.py +190 -0
  37. cortexflow_ai/memory/tagging.py +101 -0
  38. cortexflow_ai/models/__init__.py +1 -0
  39. cortexflow_ai/models/deepseek.py +180 -0
  40. cortexflow_ai/models/openai_.py +157 -0
  41. cortexflow_ai/models/router.py +451 -0
  42. cortexflow_ai/observability/__init__.py +1 -0
  43. cortexflow_ai/observability/logs.py +161 -0
  44. cortexflow_ai/observability/metrics.py +324 -0
  45. cortexflow_ai/plugins/__init__.py +1 -0
  46. cortexflow_ai/plugins/base.py +101 -0
  47. cortexflow_ai/plugins/registry.py +150 -0
  48. cortexflow_ai/reflection/__init__.py +1 -0
  49. cortexflow_ai/reflection/engine.py +214 -0
  50. cortexflow_ai/tools/__init__.py +1 -0
  51. cortexflow_ai/tools/base.py +114 -0
  52. cortexflow_ai/tools/file_ops.py +180 -0
  53. cortexflow_ai/tools/registry.py +160 -0
  54. cortexflow_ai/tools/web_search.py +140 -0
  55. cortexflow_ai/update_checker.py +58 -0
  56. cortexflow_ai/voice/__init__.py +1 -0
  57. cortexflow_ai/voice/stt.py +106 -0
  58. cortexflow_ai/voice/tts.py +230 -0
  59. cortexflow_ai/voice/wake_word.py +211 -0
  60. cortexflow_ai/workspace.py +158 -0
  61. cortexflow_ai-2.0.0.dist-info/METADATA +609 -0
  62. cortexflow_ai-2.0.0.dist-info/RECORD +66 -0
  63. cortexflow_ai-2.0.0.dist-info/WHEEL +5 -0
  64. cortexflow_ai-2.0.0.dist-info/entry_points.txt +2 -0
  65. cortexflow_ai-2.0.0.dist-info/licenses/LICENSE +105 -0
  66. 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
+ }