patchr 0.1.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 (116) hide show
  1. apps/__init__.py +2 -0
  2. apps/api/__init__.py +2 -0
  3. apps/api/main.py +652 -0
  4. apps/benchmarks/__init__.py +1 -0
  5. apps/benchmarks/main.py +20 -0
  6. apps/sandbox/__init__.py +1 -0
  7. apps/sandbox/main.py +20 -0
  8. apps/worker/__init__.py +2 -0
  9. apps/worker/main.py +15 -0
  10. apps/worker/verify.py +14 -0
  11. patchr/__init__.py +12 -0
  12. patchr/sdk/__init__.py +20 -0
  13. patchr/sdk/client.py +12 -0
  14. patchr-0.1.0.dist-info/METADATA +137 -0
  15. patchr-0.1.0.dist-info/RECORD +116 -0
  16. patchr-0.1.0.dist-info/WHEEL +5 -0
  17. patchr-0.1.0.dist-info/entry_points.txt +5 -0
  18. patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
  19. patchr-0.1.0.dist-info/top_level.txt +3 -0
  20. picux/__init__.py +6 -0
  21. picux/agents/__init__.py +5 -0
  22. picux/agents/registry.py +204 -0
  23. picux/api/__init__.py +5 -0
  24. picux/api/service.py +5075 -0
  25. picux/audit/__init__.py +31 -0
  26. picux/audit/activity.py +97 -0
  27. picux/audit/observability.py +55 -0
  28. picux/audit/verification/__init__.py +21 -0
  29. picux/audit/verification/ledger.py +633 -0
  30. picux/benchmarks/__init__.py +5 -0
  31. picux/benchmarks/local.py +286 -0
  32. picux/config.py +140 -0
  33. picux/contracts/__init__.py +22 -0
  34. picux/contracts/handshake.py +122 -0
  35. picux/contracts/integration.py +385 -0
  36. picux/contracts/openapi.py +187 -0
  37. picux/contracts/protocol_map.py +152 -0
  38. picux/contracts/routes.py +980 -0
  39. picux/contracts/schema_catalog.py +125 -0
  40. picux/core/__init__.py +17 -0
  41. picux/core/models.py +148 -0
  42. picux/core/router.py +131 -0
  43. picux/core/runtime.py +42 -0
  44. picux/core/state_machine.py +38 -0
  45. picux/domains/__init__.py +2 -0
  46. picux/domains/bridge/HostRun.py +1104 -0
  47. picux/domains/bridge/__init__.py +6 -0
  48. picux/domains/bridge/engine.py +345 -0
  49. picux/domains/hunt/__init__.py +6 -0
  50. picux/domains/hunt/engine.py +307 -0
  51. picux/domains/hunt/models.py +88 -0
  52. picux/domains/pay/__init__.py +16 -0
  53. picux/domains/pay/adapters.py +607 -0
  54. picux/domains/pay/engine.py +950 -0
  55. picux/domains/pay/models.py +95 -0
  56. picux/domains/proxy/__init__.py +5 -0
  57. picux/domains/proxy/engine.py +466 -0
  58. picux/domains/resolve/__init__.py +5 -0
  59. picux/domains/resolve/engine.py +546 -0
  60. picux/orchestrator/__init__.py +3 -0
  61. picux/orchestrator/engine.py +2840 -0
  62. picux/portals/__init__.py +17 -0
  63. picux/portals/templates.py +272 -0
  64. picux/protocols/__init__.py +1 -0
  65. picux/protocols/a2a/__init__.py +6 -0
  66. picux/protocols/a2a/client.py +51 -0
  67. picux/protocols/a2a/envelope.py +132 -0
  68. picux/protocols/mcp/__init__.py +7 -0
  69. picux/protocols/mcp/client.py +69 -0
  70. picux/protocols/mcp/contract.py +67 -0
  71. picux/protocols/mcp/server.py +76 -0
  72. picux/sandbox/__init__.py +6 -0
  73. picux/sandbox/midnight_arbitrage.py +215 -0
  74. picux/sandbox/models.py +90 -0
  75. picux/sdk/__init__.py +13 -0
  76. picux/sdk/client.py +768 -0
  77. picux/sdk/external.py +245 -0
  78. picux/security/__init__.py +18 -0
  79. picux/security/auth.py +86 -0
  80. picux/security/config_validator.py +58 -0
  81. picux/security/policy.py +158 -0
  82. picux/security/secrets.py +144 -0
  83. picux/signals/__init__.py +1 -0
  84. picux/signals/community/__init__.py +24 -0
  85. picux/signals/community/adapters/__init__.py +7 -0
  86. picux/signals/community/adapters/reddit.py +37 -0
  87. picux/signals/community/adapters/shopify.py +23 -0
  88. picux/signals/community/adapters/web.py +23 -0
  89. picux/signals/community/disambiguation.py +51 -0
  90. picux/signals/community/intake.py +227 -0
  91. picux/signals/community/models.py +102 -0
  92. picux/signals/community/rules.py +91 -0
  93. picux/signals/community/scoring.py +64 -0
  94. picux/storage/__init__.py +41 -0
  95. picux/storage/agents.py +50 -0
  96. picux/storage/cases.py +440 -0
  97. picux/storage/channels.py +476 -0
  98. picux/storage/connectors.py +411 -0
  99. picux/storage/envelopes.py +137 -0
  100. picux/storage/escrows.py +168 -0
  101. picux/storage/events.py +989 -0
  102. picux/storage/keyspace.py +60 -0
  103. picux/storage/mandates.py +107 -0
  104. picux/storage/portals.py +222 -0
  105. picux/storage/postgres.py +2049 -0
  106. picux/storage/providers.py +148 -0
  107. picux/storage/proxy.py +231 -0
  108. picux/storage/receipts.py +131 -0
  109. picux/storage/signals.py +147 -0
  110. picux/storage/tasks.py +179 -0
  111. picux/tools/__init__.py +11 -0
  112. picux/tools/shared.py +2048 -0
  113. picux/verification/__init__.py +5 -0
  114. picux/verification/rollout.py +183 -0
  115. picux/workflows/__init__.py +5 -0
  116. picux/workflows/templates.py +74 -0
@@ -0,0 +1,476 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import time
7
+ from typing import Any
8
+
9
+
10
+ THREAD_STATUSES = {"active", "closed", "archived"}
11
+ TOUCHPOINT_DIRECTIONS = {"inbound", "outbound", "internal"}
12
+ TOUCHPOINT_STATUSES = {"received", "queued", "sent", "delivered", "failed", "needsInput", "pending"}
13
+ ACTION_STATUSES = {"queued", "running", "succeeded", "failed", "blocked", "needsInput"}
14
+
15
+
16
+ class ChannelBook:
17
+ """Persistent conversation state across channel providers and touchpoints."""
18
+
19
+ def __init__(self, *, backing: Any | None = None) -> None:
20
+ self.backing = backing
21
+ self._threads: dict[str, dict[str, Any]] = {}
22
+ self._touchpoints: dict[str, dict[str, Any]] = {}
23
+ self._actions: dict[str, dict[str, Any]] = {}
24
+ self._callbacks: dict[str, dict[str, Any]] = {}
25
+
26
+ def createThread(self, payload: dict[str, Any]) -> dict[str, Any]:
27
+ now = int(time.time())
28
+ threadId = str(payload.get("threadId", "") or _stableId("thread", _threadIdentity(payload)))
29
+ current = self.getThread(threadId)
30
+ createdAt = int(current.get("thread", {}).get("createdAt", now) if current.get("ok") else now)
31
+ record = _threadFromPayload({**payload, "threadId": threadId}, createdAt=createdAt, updatedAt=now)
32
+ self._saveThread(record)
33
+ return {"ok": True, "thread": copy.deepcopy(record)}
34
+
35
+ def getThread(self, threadId: str) -> dict[str, Any]:
36
+ threadId = str(threadId or "")
37
+ record = self._threads.get(threadId)
38
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchChannelThread"):
39
+ record = self.backing.fetchChannelThread(threadId)
40
+ if record:
41
+ self._threads[threadId] = copy.deepcopy(record)
42
+ if not record:
43
+ return {"ok": False, "error": "channelThreadNotFound", "threadId": threadId}
44
+ return {"ok": True, "thread": copy.deepcopy(record)}
45
+
46
+ def listThreads(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
47
+ self._loadThreads()
48
+ filters = filters or {}
49
+ records = list(self._threads.values())
50
+ for key in ("caseId", "conversationId", "channel", "provider", "connectorId", "status"):
51
+ value = str(filters.get(key, "") or "")
52
+ if value:
53
+ records = [record for record in records if str(record.get(key, "") or "") == value]
54
+ records.sort(key=lambda item: (int(item.get("updatedAt", 0) or 0), str(item.get("threadId", ""))), reverse=True)
55
+ limit = _limit(filters.get("limit", 100))
56
+ return {"ok": True, "threads": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
57
+
58
+ def updateThread(self, threadId: str, updates: dict[str, Any]) -> dict[str, Any]:
59
+ current = self.getThread(threadId)
60
+ if not current.get("ok"):
61
+ return current
62
+ merged = {**current["thread"], **updates, "threadId": threadId}
63
+ record = _threadFromPayload(
64
+ merged,
65
+ createdAt=int(current["thread"].get("createdAt", int(time.time())) or int(time.time())),
66
+ updatedAt=int(time.time()),
67
+ )
68
+ self._saveThread(record)
69
+ return {"ok": True, "thread": copy.deepcopy(record)}
70
+
71
+ def recordTouchpoint(self, payload: dict[str, Any]) -> dict[str, Any]:
72
+ now = int(time.time())
73
+ thread = self._ensureThread(payload)
74
+ threadRecord = thread.get("thread", {}) if thread.get("ok") else {}
75
+ touchpointId = str(payload.get("touchpointId", "") or _stableId("touch", {**payload, "threadId": threadRecord.get("threadId", "")}))
76
+ record = _touchpointFromPayload({**payload, "touchpointId": touchpointId, **_threadRefs(threadRecord, payload)}, createdAt=now)
77
+ self._saveTouchpoint(record)
78
+ if threadRecord:
79
+ self._updateThreadState(threadRecord, record)
80
+ return {"ok": True, "touchpoint": copy.deepcopy(record), "thread": self.getThread(record["threadId"]).get("thread", threadRecord)}
81
+
82
+ def getTouchpoint(self, touchpointId: str) -> dict[str, Any]:
83
+ touchpointId = str(touchpointId or "")
84
+ record = self._touchpoints.get(touchpointId)
85
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchTouchpoint"):
86
+ record = self.backing.fetchTouchpoint(touchpointId)
87
+ if record:
88
+ self._touchpoints[touchpointId] = copy.deepcopy(record)
89
+ if not record:
90
+ return {"ok": False, "error": "touchpointNotFound", "touchpointId": touchpointId}
91
+ return {"ok": True, "touchpoint": copy.deepcopy(record)}
92
+
93
+ def listTouchpoints(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
94
+ self._loadTouchpoints(filters or {})
95
+ filters = filters or {}
96
+ records = list(self._touchpoints.values())
97
+ for key in ("threadId", "caseId", "conversationId", "channel", "provider", "connectorId", "direction", "status"):
98
+ value = str(filters.get(key, "") or "")
99
+ if value:
100
+ records = [record for record in records if str(record.get(key, "") or "") == value]
101
+ records.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("touchpointId", ""))), reverse=True)
102
+ limit = _limit(filters.get("limit", 100))
103
+ return {"ok": True, "touchpoints": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
104
+
105
+ def createProviderAction(self, payload: dict[str, Any]) -> dict[str, Any]:
106
+ now = int(time.time())
107
+ actionId = str(payload.get("actionId", "") or _stableId("action", payload))
108
+ record = _actionFromPayload({**payload, "actionId": actionId}, createdAt=now, updatedAt=now)
109
+ self._saveAction(record)
110
+ touchpoint = {}
111
+ if record.get("threadId") or record.get("conversationId") or record.get("caseId"):
112
+ touchpoint = self.recordTouchpoint(
113
+ {
114
+ "threadId": record.get("threadId", ""),
115
+ "caseId": record.get("caseId", ""),
116
+ "conversationId": record.get("conversationId", ""),
117
+ "channel": record.get("channel", "bridge"),
118
+ "provider": record.get("provider", ""),
119
+ "connectorId": record.get("connectorId", ""),
120
+ "sessionId": record.get("sessionId", ""),
121
+ "direction": "outbound",
122
+ "status": "queued",
123
+ "text": str(record.get("action", "") or ""),
124
+ "meta": {"actionId": actionId, "resource": record.get("resource", "")},
125
+ }
126
+ ).get("touchpoint", {})
127
+ result = {"ok": True, "action": copy.deepcopy(record)}
128
+ if touchpoint:
129
+ result["touchpoint"] = touchpoint
130
+ return result
131
+
132
+ def getProviderAction(self, actionId: str) -> dict[str, Any]:
133
+ actionId = str(actionId or "")
134
+ record = self._actions.get(actionId)
135
+ if record is None and self._backingEnabled() and hasattr(self.backing, "fetchProviderAction"):
136
+ record = self.backing.fetchProviderAction(actionId)
137
+ if record:
138
+ self._actions[actionId] = copy.deepcopy(record)
139
+ if not record:
140
+ return {"ok": False, "error": "providerActionNotFound", "actionId": actionId}
141
+ return {"ok": True, "action": copy.deepcopy(record)}
142
+
143
+ def listProviderActions(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
144
+ self._loadActions()
145
+ filters = filters or {}
146
+ records = list(self._actions.values())
147
+ for key in ("caseId", "conversationId", "threadId", "connectorId", "sessionId", "status", "channel", "provider"):
148
+ value = str(filters.get(key, "") or "")
149
+ if value:
150
+ records = [record for record in records if str(record.get(key, "") or "") == value]
151
+ records.sort(key=lambda item: (int(item.get("updatedAt", 0) or 0), str(item.get("actionId", ""))), reverse=True)
152
+ limit = _limit(filters.get("limit", 100))
153
+ return {"ok": True, "actions": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
154
+
155
+ def updateProviderAction(self, actionId: str, updates: dict[str, Any]) -> dict[str, Any]:
156
+ current = self.getProviderAction(actionId)
157
+ if not current.get("ok"):
158
+ return current
159
+ record = _actionFromPayload(
160
+ {**current["action"], **updates, "actionId": actionId},
161
+ createdAt=int(current["action"].get("createdAt", int(time.time())) or int(time.time())),
162
+ updatedAt=int(time.time()),
163
+ )
164
+ self._saveAction(record)
165
+ return {"ok": True, "action": copy.deepcopy(record)}
166
+
167
+ def ingestProviderCallback(self, payload: dict[str, Any]) -> dict[str, Any]:
168
+ now = int(time.time())
169
+ callbackId = str(payload.get("callbackId", "") or _stableId("callback", payload))
170
+ callback = _callbackFromPayload({**payload, "callbackId": callbackId}, receivedAt=now)
171
+ self._saveCallback(callback)
172
+ touchPayload = _touchpointPayloadFromCallback(callback)
173
+ touchpoint = self.recordTouchpoint(touchPayload) if touchPayload else {}
174
+ result = {"ok": True, "callback": copy.deepcopy(callback)}
175
+ if touchpoint.get("ok"):
176
+ result["touchpoint"] = touchpoint["touchpoint"]
177
+ result["thread"] = touchpoint.get("thread", {})
178
+ return result
179
+
180
+ def listProviderCallbacks(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
181
+ self._loadCallbacks()
182
+ filters = filters or {}
183
+ records = list(self._callbacks.values())
184
+ for key in ("caseId", "conversationId", "threadId", "connectorId", "sessionId", "provider", "status"):
185
+ value = str(filters.get(key, "") or "")
186
+ if value:
187
+ records = [record for record in records if str(record.get(key, "") or "") == value]
188
+ records.sort(key=lambda item: (int(item.get("receivedAt", 0) or 0), str(item.get("callbackId", ""))), reverse=True)
189
+ limit = _limit(filters.get("limit", 100))
190
+ return {"ok": True, "callbacks": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
191
+
192
+ def _ensureThread(self, payload: dict[str, Any]) -> dict[str, Any]:
193
+ threadId = str(payload.get("threadId", "") or "")
194
+ if threadId:
195
+ found = self.getThread(threadId)
196
+ if found.get("ok"):
197
+ return found
198
+ refs = _threadIdentity(payload)
199
+ found = self.findThread(refs)
200
+ if found.get("ok"):
201
+ return found
202
+ return self.createThread(refs)
203
+
204
+ def findThread(self, refs: dict[str, Any]) -> dict[str, Any]:
205
+ self._loadThreads()
206
+ for record in self._threads.values():
207
+ if _threadMatches(record, refs):
208
+ return {"ok": True, "thread": copy.deepcopy(record)}
209
+ if self._backingEnabled() and hasattr(self.backing, "findChannelThread"):
210
+ record = self.backing.findChannelThread(refs)
211
+ if record:
212
+ self._threads[str(record.get("threadId", "") or "")] = copy.deepcopy(record)
213
+ return {"ok": True, "thread": copy.deepcopy(record)}
214
+ return {"ok": False, "error": "channelThreadNotFound"}
215
+
216
+ def _updateThreadState(self, thread: dict[str, Any], touchpoint: dict[str, Any]) -> None:
217
+ state = thread.get("state", {}) if isinstance(thread.get("state"), dict) else {}
218
+ count = int(state.get("touchpointCount", 0) or 0) + 1
219
+ now = int(touchpoint.get("createdAt", int(time.time())) or int(time.time()))
220
+ direction = str(touchpoint.get("direction", "") or "")
221
+ nextState = {
222
+ **state,
223
+ "touchpointCount": count,
224
+ "lastTouchpointId": str(touchpoint.get("touchpointId", "") or ""),
225
+ "lastStatus": str(touchpoint.get("status", "") or ""),
226
+ "lastAt": now,
227
+ }
228
+ if direction == "inbound":
229
+ nextState["lastInboundAt"] = now
230
+ if direction == "outbound":
231
+ nextState["lastOutboundAt"] = now
232
+ self.updateThread(str(thread.get("threadId", "") or ""), {"state": nextState})
233
+
234
+ def _saveThread(self, record: dict[str, Any]) -> None:
235
+ threadId = str(record.get("threadId", "") or "")
236
+ if not threadId:
237
+ return
238
+ self._threads[threadId] = copy.deepcopy(record)
239
+ if self._backingEnabled() and hasattr(self.backing, "upsertChannelThread"):
240
+ self.backing.upsertChannelThread(record)
241
+
242
+ def _saveTouchpoint(self, record: dict[str, Any]) -> None:
243
+ touchpointId = str(record.get("touchpointId", "") or "")
244
+ if not touchpointId:
245
+ return
246
+ self._touchpoints[touchpointId] = copy.deepcopy(record)
247
+ if self._backingEnabled() and hasattr(self.backing, "upsertTouchpoint"):
248
+ self.backing.upsertTouchpoint(record)
249
+
250
+ def _saveAction(self, record: dict[str, Any]) -> None:
251
+ actionId = str(record.get("actionId", "") or "")
252
+ if not actionId:
253
+ return
254
+ self._actions[actionId] = copy.deepcopy(record)
255
+ if self._backingEnabled() and hasattr(self.backing, "upsertProviderAction"):
256
+ self.backing.upsertProviderAction(record)
257
+
258
+ def _saveCallback(self, record: dict[str, Any]) -> None:
259
+ callbackId = str(record.get("callbackId", "") or "")
260
+ if not callbackId:
261
+ return
262
+ self._callbacks[callbackId] = copy.deepcopy(record)
263
+ if self._backingEnabled() and hasattr(self.backing, "upsertProviderCallback"):
264
+ self.backing.upsertProviderCallback(record)
265
+
266
+ def _loadThreads(self) -> None:
267
+ if self._backingEnabled() and hasattr(self.backing, "listChannelThreads"):
268
+ for record in self.backing.listChannelThreads():
269
+ if record.get("threadId"):
270
+ self._threads[str(record["threadId"])] = copy.deepcopy(record)
271
+
272
+ def _loadTouchpoints(self, filters: dict[str, Any]) -> None:
273
+ if self._backingEnabled() and hasattr(self.backing, "listTouchpoints"):
274
+ for record in self.backing.listTouchpoints(**_backingFilters(filters)):
275
+ if record.get("touchpointId"):
276
+ self._touchpoints[str(record["touchpointId"])] = copy.deepcopy(record)
277
+
278
+ def _loadActions(self) -> None:
279
+ if self._backingEnabled() and hasattr(self.backing, "listProviderActions"):
280
+ for record in self.backing.listProviderActions():
281
+ if record.get("actionId"):
282
+ self._actions[str(record["actionId"])] = copy.deepcopy(record)
283
+
284
+ def _loadCallbacks(self) -> None:
285
+ if self._backingEnabled() and hasattr(self.backing, "listProviderCallbacks"):
286
+ for record in self.backing.listProviderCallbacks():
287
+ if record.get("callbackId"):
288
+ self._callbacks[str(record["callbackId"])] = copy.deepcopy(record)
289
+
290
+ def _backingEnabled(self) -> bool:
291
+ return bool(self.backing is not None and getattr(self.backing, "enabled", True))
292
+
293
+
294
+ def _threadFromPayload(payload: dict[str, Any], *, createdAt: int, updatedAt: int) -> dict[str, Any]:
295
+ return {
296
+ "threadId": str(payload.get("threadId", "") or ""),
297
+ "caseId": str(payload.get("caseId", "") or ""),
298
+ "conversationId": str(payload.get("conversationId", "") or ""),
299
+ "channel": str(payload.get("channel", "api") or "api"),
300
+ "provider": str(payload.get("provider", "") or ""),
301
+ "providerThreadId": str(payload.get("providerThreadId", payload.get("threadRef", "")) or ""),
302
+ "connectorId": str(payload.get("connectorId", "") or ""),
303
+ "sessionId": str(payload.get("sessionId", "") or ""),
304
+ "subject": str(payload.get("subject", "") or ""),
305
+ "participants": _cleanList(payload.get("participants", [])),
306
+ "status": _choice(payload.get("status", "active"), THREAD_STATUSES, "active"),
307
+ "state": payload.get("state", {}) if isinstance(payload.get("state"), dict) else {},
308
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
309
+ "createdAt": int(createdAt),
310
+ "updatedAt": int(updatedAt),
311
+ }
312
+
313
+
314
+ def _touchpointFromPayload(payload: dict[str, Any], *, createdAt: int) -> dict[str, Any]:
315
+ return {
316
+ "touchpointId": str(payload.get("touchpointId", "") or ""),
317
+ "threadId": str(payload.get("threadId", "") or ""),
318
+ "caseId": str(payload.get("caseId", "") or ""),
319
+ "conversationId": str(payload.get("conversationId", "") or ""),
320
+ "direction": _choice(payload.get("direction", "inbound"), TOUCHPOINT_DIRECTIONS, "inbound"),
321
+ "channel": str(payload.get("channel", "api") or "api"),
322
+ "provider": str(payload.get("provider", "") or ""),
323
+ "connectorId": str(payload.get("connectorId", "") or ""),
324
+ "sessionId": str(payload.get("sessionId", "") or ""),
325
+ "providerMessageId": str(payload.get("providerMessageId", payload.get("messageId", "")) or ""),
326
+ "from": str(payload.get("from", payload.get("sender", "")) or ""),
327
+ "to": _cleanList(payload.get("to", payload.get("recipients", []))),
328
+ "text": str(payload.get("text", payload.get("body", payload.get("message", ""))) or ""),
329
+ "status": _choice(payload.get("status", "received"), TOUCHPOINT_STATUSES, "received"),
330
+ "artifactRefs": _cleanList(payload.get("artifactRefs", payload.get("artifacts", []))),
331
+ "transcript": payload.get("transcript", []) if isinstance(payload.get("transcript"), list) else [],
332
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
333
+ "createdAt": int(createdAt),
334
+ }
335
+
336
+
337
+ def _actionFromPayload(payload: dict[str, Any], *, createdAt: int, updatedAt: int) -> dict[str, Any]:
338
+ return {
339
+ "actionId": str(payload.get("actionId", "") or ""),
340
+ "caseId": str(payload.get("caseId", "") or ""),
341
+ "conversationId": str(payload.get("conversationId", "") or ""),
342
+ "threadId": str(payload.get("threadId", "") or ""),
343
+ "connectorId": str(payload.get("connectorId", "") or ""),
344
+ "sessionId": str(payload.get("sessionId", "") or ""),
345
+ "channel": str(payload.get("channel", "bridge") or "bridge"),
346
+ "provider": str(payload.get("provider", "") or ""),
347
+ "action": str(payload.get("action", payload.get("kind", "send")) or "send"),
348
+ "resource": str(payload.get("resource", "") or ""),
349
+ "status": _choice(payload.get("status", "queued"), ACTION_STATUSES, "queued"),
350
+ "request": payload.get("request", {}) if isinstance(payload.get("request"), dict) else {},
351
+ "response": payload.get("response", {}) if isinstance(payload.get("response"), dict) else {},
352
+ "error": str(payload.get("error", "") or ""),
353
+ "proofRefs": _cleanList(payload.get("proofRefs", [])),
354
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
355
+ "createdAt": int(createdAt),
356
+ "updatedAt": int(updatedAt),
357
+ }
358
+
359
+
360
+ def _callbackFromPayload(payload: dict[str, Any], *, receivedAt: int) -> dict[str, Any]:
361
+ raw = payload.get("payload", payload.get("body", {}))
362
+ return {
363
+ "callbackId": str(payload.get("callbackId", "") or ""),
364
+ "caseId": str(payload.get("caseId", "") or ""),
365
+ "conversationId": str(payload.get("conversationId", "") or ""),
366
+ "threadId": str(payload.get("threadId", "") or ""),
367
+ "connectorId": str(payload.get("connectorId", "") or ""),
368
+ "sessionId": str(payload.get("sessionId", "") or ""),
369
+ "provider": str(payload.get("provider", "") or ""),
370
+ "providerEventId": str(payload.get("providerEventId", payload.get("eventId", "")) or ""),
371
+ "status": str(payload.get("status", "received") or "received"),
372
+ "payload": raw if isinstance(raw, dict) else {"value": raw},
373
+ "receivedAt": int(receivedAt),
374
+ }
375
+
376
+
377
+ def _touchpointPayloadFromCallback(callback: dict[str, Any]) -> dict[str, Any]:
378
+ payload = callback.get("payload", {}) if isinstance(callback.get("payload"), dict) else {}
379
+ text = str(payload.get("text", payload.get("body", payload.get("message", ""))) or "")
380
+ providerThreadId = str(payload.get("threadId", payload.get("providerThreadId", "")) or "")
381
+ return {
382
+ "threadId": str(callback.get("threadId", "") or ""),
383
+ "caseId": str(callback.get("caseId", "") or payload.get("caseId", "") or ""),
384
+ "conversationId": str(callback.get("conversationId", "") or payload.get("conversationId", "") or ""),
385
+ "channel": str(payload.get("channel", payload.get("type", "callback")) or "callback"),
386
+ "provider": str(callback.get("provider", payload.get("provider", "")) or ""),
387
+ "providerThreadId": providerThreadId,
388
+ "connectorId": str(callback.get("connectorId", "") or ""),
389
+ "sessionId": str(callback.get("sessionId", "") or ""),
390
+ "providerMessageId": str(payload.get("messageId", payload.get("id", callback.get("providerEventId", ""))) or ""),
391
+ "direction": str(payload.get("direction", "inbound") or "inbound"),
392
+ "status": str(payload.get("status", "received") or "received"),
393
+ "from": str(payload.get("from", payload.get("sender", "")) or ""),
394
+ "to": payload.get("to", payload.get("recipients", [])),
395
+ "text": text,
396
+ "artifactRefs": payload.get("artifactRefs", []),
397
+ "transcript": payload.get("transcript", []),
398
+ "meta": {"callbackId": callback.get("callbackId", ""), "providerEventId": callback.get("providerEventId", "")},
399
+ }
400
+
401
+
402
+ def _threadIdentity(payload: dict[str, Any]) -> dict[str, Any]:
403
+ return {
404
+ "caseId": str(payload.get("caseId", "") or ""),
405
+ "conversationId": str(payload.get("conversationId", "") or ""),
406
+ "channel": str(payload.get("channel", "api") or "api"),
407
+ "provider": str(payload.get("provider", "") or ""),
408
+ "providerThreadId": str(payload.get("providerThreadId", payload.get("threadRef", "")) or ""),
409
+ "connectorId": str(payload.get("connectorId", "") or ""),
410
+ "sessionId": str(payload.get("sessionId", "") or ""),
411
+ "subject": str(payload.get("subject", "") or ""),
412
+ "participants": _cleanList(payload.get("participants", [])),
413
+ "meta": payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {},
414
+ }
415
+
416
+
417
+ def _threadRefs(thread: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
418
+ return {
419
+ "threadId": str(thread.get("threadId", payload.get("threadId", "")) or ""),
420
+ "caseId": str(payload.get("caseId", thread.get("caseId", "")) or ""),
421
+ "conversationId": str(payload.get("conversationId", thread.get("conversationId", "")) or ""),
422
+ "channel": str(payload.get("channel", thread.get("channel", "api")) or "api"),
423
+ "provider": str(payload.get("provider", thread.get("provider", "")) or ""),
424
+ "connectorId": str(payload.get("connectorId", thread.get("connectorId", "")) or ""),
425
+ "sessionId": str(payload.get("sessionId", thread.get("sessionId", "")) or ""),
426
+ }
427
+
428
+
429
+ def _threadMatches(record: dict[str, Any], refs: dict[str, Any]) -> bool:
430
+ providerThreadId = str(refs.get("providerThreadId", "") or "")
431
+ if providerThreadId and str(record.get("providerThreadId", "") or "") == providerThreadId:
432
+ return True
433
+ conversationId = str(refs.get("conversationId", "") or "")
434
+ channel = str(refs.get("channel", "") or "")
435
+ provider = str(refs.get("provider", "") or "")
436
+ if conversationId and conversationId == str(record.get("conversationId", "") or ""):
437
+ if not channel or channel == str(record.get("channel", "") or ""):
438
+ if not provider or provider == str(record.get("provider", "") or ""):
439
+ return True
440
+ caseId = str(refs.get("caseId", "") or "")
441
+ if caseId and caseId == str(record.get("caseId", "") or "") and channel and channel == str(record.get("channel", "") or ""):
442
+ return True
443
+ return False
444
+
445
+
446
+ def _choice(value: Any, allowed: set[str], default: str) -> str:
447
+ clean = str(value or default)
448
+ return clean if clean in allowed else default
449
+
450
+
451
+ def _cleanList(value: Any) -> list[Any]:
452
+ if isinstance(value, list):
453
+ return [item for item in value if item not in {"", None}]
454
+ if value in {"", None}:
455
+ return []
456
+ return [value]
457
+
458
+
459
+ def _limit(value: Any) -> int:
460
+ try:
461
+ return max(1, min(int(value or 100), 500))
462
+ except Exception:
463
+ return 100
464
+
465
+
466
+ def _stableId(prefix: str, payload: dict[str, Any]) -> str:
467
+ digest = hashlib.sha256(json.dumps(payload, ensure_ascii=True, sort_keys=True).encode("utf-8")).hexdigest()
468
+ return f"{prefix}_{digest[:24]}"
469
+
470
+
471
+ def _backingFilters(filters: dict[str, Any]) -> dict[str, Any]:
472
+ out: dict[str, Any] = {}
473
+ for key in ("threadId", "caseId", "conversationId", "limit"):
474
+ if filters.get(key):
475
+ out[key] = filters[key]
476
+ return out