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,235 @@
1
+ """Mattermost channel adapter — WebSocket real-time events + REST API.
2
+
3
+ Connects to a Mattermost server using a personal access token or bot token.
4
+ Inbound messages arrive via the Mattermost WebSocket API (``ws(s)://host/api/v4/websocket``).
5
+ Outbound messages use the ``POST /api/v4/posts`` REST endpoint.
6
+
7
+ Config keys:
8
+ url Base URL of the Mattermost server (default: http://localhost:8065)
9
+ token Personal access token or bot token (ENV:MATTERMOST_TOKEN)
10
+ team Team name or ID to filter messages (optional)
11
+ channel Channel name to listen on (default: town-square)
12
+
13
+ The adapter filters out its own bot messages to prevent echo loops.
14
+
15
+ WebSocket event format (Mattermost ≥ 5.x):
16
+ {
17
+ "event": "posted",
18
+ "data": {
19
+ "post": "{\"id\":\"...\",\"user_id\":\"...\",\"channel_id\":\"...\",\"message\":\"Hello\"}",
20
+ "sender_name": "@alice",
21
+ "team_id": "..."
22
+ }
23
+ }
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import asyncio
29
+ import json
30
+ import logging
31
+ import os
32
+ import time
33
+ from typing import Any
34
+
35
+ from cortexflow_ai.channels.base import ChannelAdapter, InboundMessage
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class MattermostAdapter(ChannelAdapter):
41
+ """Mattermost adapter using the native WebSocket + REST v4 API."""
42
+
43
+ channel_id = "mattermost"
44
+
45
+ def __init__(self, config: dict[str, Any]) -> None:
46
+ super().__init__(config)
47
+ self._url: str = config.get("url", "http://localhost:8065").rstrip("/")
48
+ self._token: str = self._resolve(config.get("token", ""))
49
+ self._team: str = config.get("team", "")
50
+ self._channel_name: str = config.get("channel", "town-square")
51
+ self._bot_user_id: str | None = None
52
+ self._ws_task: asyncio.Task | None = None
53
+ self._ws: Any = None
54
+
55
+ # ------------------------------------------------------------------
56
+ # Lifecycle
57
+ # ------------------------------------------------------------------
58
+
59
+ async def connect(self) -> None:
60
+ """Resolve bot identity, then start the WebSocket listener."""
61
+ self._bot_user_id = await self._fetch_bot_user_id()
62
+ self._ws_task = asyncio.create_task(self._ws_loop())
63
+ logger.info("mattermost.connected url=%s", self._url)
64
+
65
+ async def disconnect(self) -> None:
66
+ if self._ws_task:
67
+ self._ws_task.cancel()
68
+ try:
69
+ await self._ws_task
70
+ except asyncio.CancelledError:
71
+ pass
72
+ self._ws_task = None
73
+ if self._ws is not None:
74
+ try:
75
+ await self._ws.close()
76
+ except Exception:
77
+ pass
78
+ self._ws = None
79
+
80
+ # ------------------------------------------------------------------
81
+ # Outbound
82
+ # ------------------------------------------------------------------
83
+
84
+ async def send(
85
+ self,
86
+ target: str,
87
+ text: str,
88
+ *,
89
+ reply_to: str | None = None,
90
+ attachments=None,
91
+ ) -> str | None:
92
+ """Post a message to Mattermost channel *target* (channel_id).
93
+
94
+ Returns the new post ID on success, None on error.
95
+ """
96
+ if not self._token:
97
+ return None
98
+ try:
99
+ import httpx
100
+
101
+ payload: dict[str, Any] = {"channel_id": target, "message": text}
102
+ if reply_to:
103
+ payload["root_id"] = reply_to
104
+
105
+ async with httpx.AsyncClient() as client:
106
+ resp = await client.post(
107
+ f"{self._url}/api/v4/posts",
108
+ headers={
109
+ "Authorization": f"Bearer {self._token}",
110
+ "Content-Type": "application/json",
111
+ },
112
+ json=payload,
113
+ timeout=15.0,
114
+ )
115
+ resp.raise_for_status()
116
+ return resp.json().get("id")
117
+ except Exception as exc:
118
+ logger.error("mattermost.send failed: %s", exc)
119
+ return None
120
+
121
+ # ------------------------------------------------------------------
122
+ # WebSocket event loop
123
+ # ------------------------------------------------------------------
124
+
125
+ async def _ws_loop(self) -> None:
126
+ """Connect to the Mattermost WebSocket and dispatch posted events."""
127
+ ws_url = self._url.replace("http://", "ws://").replace("https://", "wss://")
128
+ ws_url += "/api/v4/websocket"
129
+
130
+ while True:
131
+ try:
132
+ await self._ws_connect_and_listen(ws_url)
133
+ except asyncio.CancelledError:
134
+ return
135
+ except Exception as exc:
136
+ logger.warning("mattermost.ws_error: %s — reconnecting in 5s", exc)
137
+ await asyncio.sleep(5)
138
+
139
+ async def _ws_connect_and_listen(self, ws_url: str) -> None:
140
+ try:
141
+ import websockets # type: ignore[import]
142
+ except ImportError:
143
+ raise RuntimeError("websockets package required: pip install websockets")
144
+
145
+ async with websockets.connect(
146
+ ws_url,
147
+ extra_headers={"Authorization": f"Bearer {self._token}"},
148
+ ) as ws:
149
+ self._ws = ws
150
+ # Authenticate via challenge message
151
+ await ws.send(json.dumps({
152
+ "seq": 1,
153
+ "action": "authentication_challenge",
154
+ "data": {"token": self._token},
155
+ }))
156
+ async for raw in ws:
157
+ event = json.loads(raw)
158
+ await self._process_event(event)
159
+
160
+ async def _process_event(self, event: dict[str, Any]) -> None:
161
+ if event.get("event") != "posted":
162
+ return
163
+
164
+ data = event.get("data", {})
165
+ post_raw = data.get("post", "{}")
166
+ try:
167
+ post = json.loads(post_raw) if isinstance(post_raw, str) else post_raw
168
+ except json.JSONDecodeError:
169
+ return
170
+
171
+ user_id = post.get("user_id", "")
172
+ if self._bot_user_id and user_id == self._bot_user_id:
173
+ return # skip own messages
174
+
175
+ text = (post.get("message") or "").strip()
176
+ if not text:
177
+ return
178
+
179
+ sender_name = data.get("sender_name", "")
180
+ msg = InboundMessage(
181
+ channel=self.channel_id,
182
+ sender_id=user_id,
183
+ sender_name=sender_name.lstrip("@"),
184
+ text=text,
185
+ thread_id=post.get("channel_id"),
186
+ timestamp=time.time(),
187
+ raw=post,
188
+ )
189
+ asyncio.create_task(self._dispatch(msg))
190
+
191
+ # ------------------------------------------------------------------
192
+ # REST helpers
193
+ # ------------------------------------------------------------------
194
+
195
+ async def _fetch_bot_user_id(self) -> str | None:
196
+ if not self._token:
197
+ return None
198
+ try:
199
+ import httpx
200
+
201
+ async with httpx.AsyncClient() as client:
202
+ resp = await client.get(
203
+ f"{self._url}/api/v4/users/me",
204
+ headers={"Authorization": f"Bearer {self._token}"},
205
+ timeout=10.0,
206
+ )
207
+ resp.raise_for_status()
208
+ return resp.json().get("id")
209
+ except Exception as exc:
210
+ logger.warning("mattermost.fetch_user_id failed: %s", exc)
211
+ return None
212
+
213
+ # ------------------------------------------------------------------
214
+ # Helpers
215
+ # ------------------------------------------------------------------
216
+
217
+ def _resolve(self, value: str) -> str:
218
+ if isinstance(value, str) and value.startswith("ENV:"):
219
+ return os.getenv(value[4:], "")
220
+ return value
221
+
222
+ def get_config_schema(self) -> dict[str, Any]:
223
+ return {
224
+ "type": "object",
225
+ "required": ["token"],
226
+ "properties": {
227
+ "url": {"type": "string", "default": "http://localhost:8065"},
228
+ "token": {
229
+ "type": "string",
230
+ "description": "Personal access token (ENV:MATTERMOST_TOKEN)",
231
+ },
232
+ "team": {"type": "string", "description": "Team name or ID"},
233
+ "channel": {"type": "string", "default": "town-square"},
234
+ },
235
+ }
@@ -0,0 +1,297 @@
1
+ """Nextcloud Talk channel adapter — OCS REST API polling.
2
+
3
+ CortexFlow connects to Nextcloud Talk rooms by long-polling the chat endpoint.
4
+ There is no persistent WebSocket in the Nextcloud Talk OCS API; the adapter
5
+ uses ``lastKnownMessageId`` to fetch only new messages on each poll cycle.
6
+
7
+ Config keys:
8
+ url Nextcloud base URL (e.g. https://cloud.example.com)
9
+ username Nextcloud username (ENV:NEXTCLOUD_USERNAME)
10
+ password App password or user password (ENV:NEXTCLOUD_PASSWORD)
11
+ room_token Talk room token to poll (e.g. "abc123de")
12
+ poll_interval Seconds between poll requests (default: 5)
13
+
14
+ OCS API endpoints used:
15
+ GET /ocs/v2.php/apps/spreed/api/v1/chat/{token}
16
+ Query params: lookIntoFuture=1, limit=100, lastKnownMessageId=<id>
17
+ POST /ocs/v2.php/apps/spreed/api/v1/chat/{token}
18
+ Body: {"message": "<text>"}
19
+
20
+ Authentication: HTTP Basic Auth (username + app password).
21
+ All requests include ``OCS-APIRequest: true`` and ``Accept: application/json``.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import asyncio
27
+ import logging
28
+ import os
29
+ import time
30
+ from typing import Any
31
+
32
+ from cortexflow_ai.channels.base import ChannelAdapter, InboundMessage
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+ _OCS_HEADERS = {
37
+ "OCS-APIRequest": "true",
38
+ "Accept": "application/json",
39
+ }
40
+
41
+
42
+ class NextcloudAdapter(ChannelAdapter):
43
+ """Nextcloud Talk adapter using the OCS v2 REST API with long-polling."""
44
+
45
+ channel_id = "nextcloud"
46
+
47
+ def __init__(self, config: dict[str, Any]) -> None:
48
+ super().__init__(config)
49
+ self._url: str = config.get("url", "https://localhost").rstrip("/")
50
+ self._username: str = self._resolve(config.get("username", ""))
51
+ self._password: str = self._resolve(config.get("password", ""))
52
+ self._room_token: str = config.get("room_token", "")
53
+ self._poll_interval: float = float(config.get("poll_interval", 5))
54
+ self._last_message_id: int = 0
55
+ self._own_user_id: str | None = None
56
+ self._poll_task: asyncio.Task | None = None
57
+
58
+ # ------------------------------------------------------------------
59
+ # Lifecycle
60
+ # ------------------------------------------------------------------
61
+
62
+ async def connect(self) -> None:
63
+ """Resolve own user identity, then start the polling loop."""
64
+ self._own_user_id = await self._fetch_own_user_id()
65
+ self._last_message_id = await self._fetch_last_message_id()
66
+ self._poll_task = asyncio.create_task(self._poll_loop())
67
+ logger.info(
68
+ "nextcloud.connected url=%s room=%s", self._url, self._room_token
69
+ )
70
+
71
+ async def disconnect(self) -> None:
72
+ if self._poll_task:
73
+ self._poll_task.cancel()
74
+ try:
75
+ await self._poll_task
76
+ except asyncio.CancelledError:
77
+ pass
78
+ self._poll_task = None
79
+
80
+ # ------------------------------------------------------------------
81
+ # Outbound
82
+ # ------------------------------------------------------------------
83
+
84
+ async def send(
85
+ self,
86
+ target: str,
87
+ text: str,
88
+ *,
89
+ reply_to: str | None = None,
90
+ attachments=None,
91
+ ) -> str | None:
92
+ """Post *text* to the room given by *target* (room token).
93
+
94
+ Returns the new message ID on success, None on error.
95
+ """
96
+ if not target:
97
+ target = self._room_token
98
+ if not target or not self._username or not self._password:
99
+ return None
100
+ try:
101
+ import httpx
102
+
103
+ endpoint = (
104
+ f"{self._url}/ocs/v2.php/apps/spreed/api/v1/chat/{target}"
105
+ )
106
+ payload: dict[str, Any] = {"message": text}
107
+ if reply_to:
108
+ payload["replyTo"] = int(reply_to)
109
+
110
+ async with httpx.AsyncClient() as client:
111
+ resp = await client.post(
112
+ endpoint,
113
+ headers=_OCS_HEADERS,
114
+ json=payload,
115
+ auth=(self._username, self._password),
116
+ timeout=15.0,
117
+ )
118
+ resp.raise_for_status()
119
+ data = resp.json()
120
+ msg_id = (
121
+ data.get("ocs", {})
122
+ .get("data", {})
123
+ .get("id")
124
+ )
125
+ return str(msg_id) if msg_id is not None else None
126
+ except Exception as exc:
127
+ logger.error("nextcloud.send failed: %s", exc)
128
+ return None
129
+
130
+ # ------------------------------------------------------------------
131
+ # Polling loop
132
+ # ------------------------------------------------------------------
133
+
134
+ async def _poll_loop(self) -> None:
135
+ while True:
136
+ try:
137
+ await self._poll_once()
138
+ except asyncio.CancelledError:
139
+ return
140
+ except Exception as exc:
141
+ logger.warning("nextcloud.poll_error: %s", exc)
142
+ await asyncio.sleep(self._poll_interval)
143
+
144
+ async def _poll_once(self) -> None:
145
+ if not self._room_token or not self._username or not self._password:
146
+ return
147
+ try:
148
+ import httpx
149
+
150
+ endpoint = (
151
+ f"{self._url}/ocs/v2.php/apps/spreed/api/v1/chat"
152
+ f"/{self._room_token}"
153
+ )
154
+ params: dict[str, Any] = {
155
+ "lookIntoFuture": 1,
156
+ "limit": 100,
157
+ "lastKnownMessageId": self._last_message_id,
158
+ }
159
+ async with httpx.AsyncClient() as client:
160
+ resp = await client.get(
161
+ endpoint,
162
+ headers=_OCS_HEADERS,
163
+ params=params,
164
+ auth=(self._username, self._password),
165
+ timeout=30.0,
166
+ )
167
+ if resp.status_code == 304:
168
+ return # no new messages
169
+ resp.raise_for_status()
170
+ messages = (
171
+ resp.json().get("ocs", {}).get("data", [])
172
+ )
173
+ for msg in messages:
174
+ await self._process_message(msg)
175
+ except Exception as exc:
176
+ logger.debug("nextcloud._poll_once error: %s", exc)
177
+ raise
178
+
179
+ async def _process_message(self, msg: dict[str, Any]) -> None:
180
+ msg_id = int(msg.get("id", 0))
181
+ if msg_id > self._last_message_id:
182
+ self._last_message_id = msg_id
183
+
184
+ # Only handle regular chat messages from other users
185
+ if msg.get("systemMessage") or msg.get("messageType") == "system":
186
+ return
187
+
188
+ actor_id = msg.get("actorId", "")
189
+ if self._own_user_id and actor_id == self._own_user_id:
190
+ return # skip own messages
191
+
192
+ text = (msg.get("message") or "").strip()
193
+ if not text:
194
+ return
195
+
196
+ inbound = InboundMessage(
197
+ channel=self.channel_id,
198
+ sender_id=actor_id,
199
+ sender_name=msg.get("actorDisplayName", ""),
200
+ text=text,
201
+ thread_id=self._room_token,
202
+ timestamp=float(msg.get("timestamp", time.time())),
203
+ raw=msg,
204
+ )
205
+ asyncio.create_task(self._dispatch(inbound))
206
+
207
+ # ------------------------------------------------------------------
208
+ # REST helpers
209
+ # ------------------------------------------------------------------
210
+
211
+ async def _fetch_own_user_id(self) -> str | None:
212
+ if not self._username or not self._password:
213
+ return None
214
+ try:
215
+ import httpx
216
+
217
+ async with httpx.AsyncClient() as client:
218
+ resp = await client.get(
219
+ f"{self._url}/ocs/v2.php/cloud/user",
220
+ headers=_OCS_HEADERS,
221
+ auth=(self._username, self._password),
222
+ timeout=10.0,
223
+ )
224
+ resp.raise_for_status()
225
+ return (
226
+ resp.json().get("ocs", {}).get("data", {}).get("id")
227
+ )
228
+ except Exception as exc:
229
+ logger.warning("nextcloud.fetch_user_id failed: %s", exc)
230
+ return None
231
+
232
+ async def _fetch_last_message_id(self) -> int:
233
+ """Fetch the most recent message ID to avoid replaying history on start."""
234
+ if not self._room_token or not self._username or not self._password:
235
+ return 0
236
+ try:
237
+ import httpx
238
+
239
+ endpoint = (
240
+ f"{self._url}/ocs/v2.php/apps/spreed/api/v1/chat"
241
+ f"/{self._room_token}"
242
+ )
243
+ async with httpx.AsyncClient() as client:
244
+ resp = await client.get(
245
+ endpoint,
246
+ headers=_OCS_HEADERS,
247
+ params={"limit": 1, "lookIntoFuture": 0},
248
+ auth=(self._username, self._password),
249
+ timeout=10.0,
250
+ )
251
+ resp.raise_for_status()
252
+ messages = (
253
+ resp.json().get("ocs", {}).get("data", [])
254
+ )
255
+ if messages:
256
+ return int(messages[-1].get("id", 0))
257
+ except Exception as exc:
258
+ logger.warning("nextcloud.fetch_last_id failed: %s", exc)
259
+ return 0
260
+
261
+ # ------------------------------------------------------------------
262
+ # Helpers
263
+ # ------------------------------------------------------------------
264
+
265
+ def _resolve(self, value: str) -> str:
266
+ if isinstance(value, str) and value.startswith("ENV:"):
267
+ return os.getenv(value[4:], "")
268
+ return value
269
+
270
+ def get_config_schema(self) -> dict[str, Any]:
271
+ return {
272
+ "type": "object",
273
+ "required": ["url", "username", "password", "room_token"],
274
+ "properties": {
275
+ "url": {
276
+ "type": "string",
277
+ "description": "Nextcloud base URL",
278
+ },
279
+ "username": {
280
+ "type": "string",
281
+ "description": "Nextcloud username (ENV:NEXTCLOUD_USERNAME)",
282
+ },
283
+ "password": {
284
+ "type": "string",
285
+ "description": "App password (ENV:NEXTCLOUD_PASSWORD)",
286
+ },
287
+ "room_token": {
288
+ "type": "string",
289
+ "description": "Talk room token",
290
+ },
291
+ "poll_interval": {
292
+ "type": "number",
293
+ "default": 5,
294
+ "description": "Seconds between polls",
295
+ },
296
+ },
297
+ }