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,546 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+
10
+ DEFAULT_RULES: tuple[dict[str, str], ...] = (
11
+ {
12
+ "id": "junkFee",
13
+ "pattern": r"(junk fee|processing fee|regulatory recovery|administrative fee|hidden fee)",
14
+ "feeName": "Undisclosed Fee",
15
+ "clause": "Consumer pricing transparency obligations",
16
+ },
17
+ {
18
+ "id": "shippingLeak",
19
+ "pattern": r"(shipping surcharge|residential surcharge|fuel adjustment)",
20
+ "feeName": "Shipping Surcharge",
21
+ "clause": "Carrier billing disclosure obligations",
22
+ },
23
+ {
24
+ "id": "refundGap",
25
+ "pattern": r"(refund due|credit due|overcharge|duplicate charge)",
26
+ "feeName": "Recoverable Overcharge",
27
+ "clause": "Provider refund and account-credit obligations",
28
+ },
29
+ {
30
+ "id": "damagedGoods",
31
+ "pattern": r"(broken in transit|damaged in transit|damaged tv|cracked panel|arrived broken|defective item|damaged goods)",
32
+ "feeName": "Damaged Goods Claim",
33
+ "clause": "Consumer purchase and transport-damage obligations",
34
+ },
35
+ )
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class ResolveDomain:
40
+ rules: tuple[dict[str, str], ...] = field(default_factory=lambda: DEFAULT_RULES)
41
+ policyVer: str = "picux-resolve-2026-05-09"
42
+
43
+ def redact(self, text: str) -> str:
44
+ masked = re.sub(r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", "[email]", str(text or ""), flags=re.IGNORECASE)
45
+ masked = re.sub(r"\+?\b(?:\d[\s().-]?){8,15}\d\b", "[phone]", masked)
46
+ masked = re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "***-**-****", masked)
47
+ masked = re.sub(r"\b(?:\d[ -]*?){13,19}\b", "**** **** **** ****", masked)
48
+ masked = re.sub(r"\b\d{9,12}\b", "**********", masked)
49
+ return masked
50
+
51
+ def auditText(self, text: str, *, provider: str = "") -> dict[str, Any]:
52
+ lowered = self.redact(text).lower()
53
+ findings: list[dict[str, Any]] = []
54
+ for rule in self.rules:
55
+ pattern = str(rule.get("pattern", ""))
56
+ if not pattern or not re.search(pattern, lowered):
57
+ continue
58
+ refund = self._nearestAmount(lowered, pattern)
59
+ findings.append(
60
+ {
61
+ "ruleId": str(rule.get("id", "")),
62
+ "feeName": str(rule.get("feeName", "Unknown Fee")),
63
+ "clause": str(rule.get("clause", "Consumer pricing protections")),
64
+ "refundAmount": refund,
65
+ "reason": f"pattern:{pattern}",
66
+ }
67
+ )
68
+
69
+ refundAmount = round(sum(max(0.0, float(item.get("refundAmount", 0.0))) for item in findings), 2)
70
+ top = findings[0] if findings else {}
71
+ return {
72
+ "provider": provider,
73
+ "violation": bool(findings),
74
+ "findings": findings,
75
+ "refundAmount": refundAmount,
76
+ "clause": str(top.get("clause", "")),
77
+ "feeName": str(top.get("feeName", "")),
78
+ "policyVer": self.policyVer,
79
+ "ruleCount": len(self.rules),
80
+ "auditedAt": int(time.time()),
81
+ }
82
+
83
+ def detectOutcome(self, *, providerText: str = "", payload: dict[str, Any] | None = None) -> dict[str, Any]:
84
+ payload = payload or {}
85
+ payloadText = ""
86
+ if payload:
87
+ try:
88
+ payloadText = json.dumps(payload, ensure_ascii=True, sort_keys=True).lower()
89
+ except Exception:
90
+ payloadText = str(payload).lower()
91
+ text = f"{str(providerText or '').lower()} {payloadText}".strip()
92
+ if not text:
93
+ return {"status": "pending", "reason": "missingProviderText", "type": "", "amount": 0.0}
94
+
95
+ positive = (
96
+ "refund issued",
97
+ "refund processed",
98
+ "credit issued",
99
+ "credit applied",
100
+ "credit posted",
101
+ "adjustment posted",
102
+ "charge reversed",
103
+ "fee removed",
104
+ "we have credited",
105
+ "replacement approved",
106
+ "exchange approved",
107
+ "replacement case approved",
108
+ "rma approved",
109
+ '"status":"credited"',
110
+ '"status":"refunded"',
111
+ '"decision":"approved"',
112
+ )
113
+ negative = (
114
+ "refund denied",
115
+ "credit denied",
116
+ "request denied",
117
+ "request rejected",
118
+ "not eligible",
119
+ "unable to provide a refund",
120
+ "no refund",
121
+ '"status":"denied"',
122
+ '"decision":"denied"',
123
+ )
124
+ pending = (
125
+ "under review",
126
+ "in progress",
127
+ "pending",
128
+ "awaiting review",
129
+ "processing request",
130
+ "we are investigating",
131
+ )
132
+
133
+ amount = self._firstAmount(text)
134
+ if any(marker in text for marker in negative):
135
+ return {"status": "confirmedFailure", "reason": "providerDenied", "type": "denied", "amount": amount}
136
+ if any(marker in text for marker in positive):
137
+ kind = "credit" if "credit" in text else "refund"
138
+ return {"status": "confirmedSuccess", "reason": "providerConfirmed", "type": kind, "amount": amount}
139
+ if any(marker in text for marker in pending):
140
+ return {"status": "pending", "reason": "providerPending", "type": "", "amount": amount}
141
+ return {"status": "pending", "reason": "outcomeUnclear", "type": "", "amount": amount}
142
+
143
+ def buildNotice(self, *, accountId: str, provider: str, userName: str, audit: dict[str, Any]) -> dict[str, str]:
144
+ refund = float(audit.get("refundAmount", 0.0) or 0.0)
145
+ feeName = str(audit.get("feeName", "Undisclosed Fee") or "Undisclosed Fee")
146
+ clause = str(audit.get("clause", "Consumer pricing protections") or "Consumer pricing protections")
147
+ subject = f"Formal Billing Dispute - Account {accountId}"
148
+ body = (
149
+ f"To the {provider} Billing Department,\n\n"
150
+ f"Picux Resolve identified a '{feeName}' charge of ${refund:.2f}. "
151
+ f"This appears to violate {clause}.\n\n"
152
+ "Required action: issue the applicable credit and confirm the fee has been removed from future invoices.\n\n"
153
+ f"Regards,\nPicux Resolve on behalf of {userName}"
154
+ )
155
+ return {"subject": subject, "body": body}
156
+
157
+ def contactPlan(self, *, request: dict[str, Any] | None = None, target: dict[str, Any] | None = None) -> dict[str, Any]:
158
+ request = request or {}
159
+ target = target or {}
160
+ entityType = str(target.get("type", target.get("entityType", "merchant")) or "merchant")
161
+ channels = target.get("channels", []) if isinstance(target.get("channels"), list) else []
162
+ if not channels:
163
+ channels = self._defaultChannels(entityType)
164
+ preferred = str(request.get("preferredChannel", target.get("preferredChannel", "")) or "")
165
+ primary = self._primaryChannel(channels, preferred)
166
+ phone = next((item for item in channels if isinstance(item, dict) and str(item.get("type", "")) == "phone"), {})
167
+ requiresProxy = bool(target.get("voiceRequired") or request.get("requiresVoice") or phone.get("requiresProxy"))
168
+ return {
169
+ "target": {
170
+ "entityId": str(target.get("entityId", request.get("provider", "")) or ""),
171
+ "name": str(target.get("name", request.get("provider", "")) or ""),
172
+ "type": entityType,
173
+ },
174
+ "channels": channels,
175
+ "primary": primary,
176
+ "requiresProxy": requiresProxy,
177
+ "proxyChannel": phone if requiresProxy else {},
178
+ }
179
+
180
+ def draftClaim(
181
+ self,
182
+ *,
183
+ request: dict[str, Any] | None = None,
184
+ audit: dict[str, Any] | None = None,
185
+ target: dict[str, Any] | None = None,
186
+ ) -> dict[str, Any]:
187
+ request = request or {}
188
+ audit = audit or {}
189
+ target = target or {}
190
+ plan = self.contactPlan(request=request, target=target)
191
+ targetName = plan["target"]["name"] or "target entity"
192
+ issue = str(request.get("issueType", audit.get("feeName", "consumer claim")) or "consumer claim")
193
+ location = str(request.get("location", "") or "")
194
+ evidence = self._attachmentRefs(request.get("attachments", []))
195
+ subject = f"Claim for {issue} with {targetName}".strip()
196
+ evidenceLine = ", ".join(item["label"] for item in evidence) or "attached evidence"
197
+ body = (
198
+ f"Hello {targetName},\n\n"
199
+ f"Picux Resolve is submitting a claim for {issue}. "
200
+ f"The request references {location or 'the relevant service location'} and includes {evidenceLine}. "
201
+ "Please confirm the remedy path, case reference, and next action.\n\n"
202
+ "Requested remedy: repair, replacement, refund, or formal case opening according to the applicable policy.\n\n"
203
+ "Regards,\nPicux Resolve"
204
+ )
205
+ chatText = f"Picux Resolve claim: {issue}. Evidence: {evidenceLine}. Please provide case reference and remedy path."
206
+ phoneScript = (
207
+ f"Call {targetName}. Explain that Picux Resolve is following up on {issue}. "
208
+ "Ask for the case reference, remedy path, expected timeline, and whether more evidence is required."
209
+ )
210
+ return {
211
+ "kind": "resolveClaimDraft",
212
+ "target": plan["target"],
213
+ "channelPlan": plan,
214
+ "evidence": evidence,
215
+ "drafts": {
216
+ "email": {
217
+ "to": str(plan["primary"].get("address", plan["primary"].get("to", "")) or ""),
218
+ "subject": subject,
219
+ "body": body,
220
+ },
221
+ "letter": {"subject": subject, "body": body},
222
+ "chat": {"channel": str(plan["primary"].get("type", "chat") or "chat"), "text": chatText},
223
+ "phone": {
224
+ "requiresProxy": bool(plan["requiresProxy"]),
225
+ "connectorId": str(plan["proxyChannel"].get("connectorId", "") or ""),
226
+ "phoneRef": str(plan["proxyChannel"].get("phoneRef", plan["proxyChannel"].get("phone", "")) or ""),
227
+ "script": phoneScript,
228
+ },
229
+ },
230
+ }
231
+
232
+ def triageClaim(self, payload: dict[str, Any]) -> dict[str, Any]:
233
+ text = self.redact(str(payload.get("claim", payload.get("text", payload.get("query", ""))) or ""))
234
+ docs = payload.get("documents", payload.get("attachments", []))
235
+ docs = docs if isinstance(docs, list) else []
236
+ agentClaims = payload.get("agentClaims", [])
237
+ agentClaims = agentClaims if isinstance(agentClaims, list) else []
238
+ profile = self._profile(payload, text)
239
+ evidenceMap = self._evidenceMap(docs)
240
+ contradictions = self._contradictions(text, docs, agentClaims)
241
+ sourceClasses = self._sourceClasses(text, docs)
242
+ typedEvidence = self._typedEvidence(text, docs, sourceClasses)
243
+ fraudIndicators = self._fraudIndicators(text, docs, agentClaims, sourceClasses)
244
+ missing = self._missingEvidence(text, docs, profile=profile, sourceClasses=sourceClasses, typedEvidence=typedEvidence)
245
+ risk = self._riskClass(text, contradictions, missing, agentClaims, profile=profile, fraudIndicators=fraudIndicators)
246
+ confidence = self._triageConfidence(evidenceMap, contradictions, missing)
247
+ openQuestions = [f"missing:{item}" for item in missing]
248
+ if contradictions:
249
+ openQuestions.append("contradictionsRequireReview")
250
+ if fraudIndicators:
251
+ openQuestions.append("fraudIndicatorsRequireReview")
252
+ escalation = {
253
+ "recommended": risk["severity"] in {"high", "critical"} or bool(openQuestions),
254
+ "routeDomain": "proxy" if risk["severity"] in {"high", "critical"} or bool(openQuestions) else "",
255
+ "reason": "humanReviewerNeeded" if risk["severity"] in {"high", "critical"} else ("missingEvidence" if openQuestions else ""),
256
+ }
257
+ return {
258
+ "kind": "resolveScamTriage",
259
+ "workflow": str(payload.get("workflow", "resolveForMobility") or "resolveForMobility"),
260
+ "profile": profile,
261
+ "claim": text,
262
+ "risk": risk,
263
+ "evidenceMap": evidenceMap,
264
+ "typedEvidence": typedEvidence,
265
+ "sourceClasses": sourceClasses,
266
+ "fraudIndicators": fraudIndicators,
267
+ "verificationStatus": self._verificationStatus(evidenceMap, sourceClasses, fraudIndicators, missing),
268
+ "caseOpsTimeline": self._caseOpsTimeline(profile, risk, sourceClasses, fraudIndicators, missing),
269
+ "contradictions": contradictions,
270
+ "missingEvidence": missing,
271
+ "openQuestions": openQuestions,
272
+ "confidence": confidence,
273
+ "recommendedRestraint": "escalateBeforeActing" if escalation["recommended"] else "safeToProceedWithCitations",
274
+ "nextAction": self._nextAction(risk, missing, contradictions, fraudIndicators),
275
+ "escalation": escalation,
276
+ }
277
+
278
+ @staticmethod
279
+ def _nearestAmount(text: str, pattern: str) -> float:
280
+ match = re.search(pattern, text)
281
+ if not match:
282
+ return 0.0
283
+ start = max(0, match.start() - 80)
284
+ end = min(len(text), match.end() + 80)
285
+ return ResolveDomain._firstAmount(text[start:end])
286
+
287
+ @staticmethod
288
+ def _firstAmount(text: str) -> float:
289
+ matches = re.findall(r"\$\s*([0-9]+(?:\.[0-9]{1,2})?)", str(text or ""))
290
+ if not matches:
291
+ return 0.0
292
+ try:
293
+ return float(matches[0])
294
+ except Exception:
295
+ return 0.0
296
+
297
+ @staticmethod
298
+ def _defaultChannels(entityType: str) -> list[dict[str, Any]]:
299
+ if entityType == "bank":
300
+ return [{"type": "secureMessage"}, {"type": "phone", "requiresProxy": True}, {"type": "letter"}]
301
+ if entityType == "serviceOps":
302
+ return [{"type": "ticket"}, {"type": "chat"}, {"type": "phone", "requiresProxy": True}]
303
+ return [{"type": "email"}, {"type": "chat"}, {"type": "phone", "requiresProxy": True}]
304
+
305
+ @staticmethod
306
+ def _primaryChannel(channels: list[Any], preferred: str) -> dict[str, Any]:
307
+ typed = [item for item in channels if isinstance(item, dict)]
308
+ if preferred:
309
+ match = next((item for item in typed if str(item.get("type", "")) == preferred), None)
310
+ if match:
311
+ return match
312
+ return next((item for item in typed if str(item.get("type", "")) != "phone"), typed[0] if typed else {})
313
+
314
+ @staticmethod
315
+ def _attachmentRefs(value: Any) -> list[dict[str, str]]:
316
+ if not isinstance(value, list):
317
+ return []
318
+ refs: list[dict[str, str]] = []
319
+ for item in value:
320
+ if not isinstance(item, dict):
321
+ continue
322
+ name = str(item.get("name", item.get("uri", item.get("kind", "attachment"))) or "attachment")
323
+ kind = str(item.get("kind", "attachment") or "attachment")
324
+ digest = str(item.get("sha256", item.get("hash", "")) or "")
325
+ refs.append(
326
+ {
327
+ "kind": kind,
328
+ "name": name,
329
+ "sha256": digest,
330
+ "mime": str(item.get("mime", "") or ""),
331
+ "size": str(item.get("size", "") or ""),
332
+ "label": f"{kind}:{name}",
333
+ }
334
+ )
335
+ return refs
336
+
337
+ @staticmethod
338
+ def _evidenceMap(docs: list[Any]) -> list[dict[str, Any]]:
339
+ out = []
340
+ for index, item in enumerate(docs, start=1):
341
+ doc = item if isinstance(item, dict) else {"label": str(item)}
342
+ digest = str(doc.get("sha256", doc.get("hash", "")) or "")
343
+ out.append(
344
+ {
345
+ "ref": str(doc.get("artifactId", doc.get("name", f"doc_{index}")) or f"doc_{index}"),
346
+ "kind": str(doc.get("kind", doc.get("type", "document")) or "document"),
347
+ "sourceBound": bool(digest or doc.get("source")),
348
+ "hash": digest,
349
+ "prevHash": str(doc.get("prevHash", "") or ""),
350
+ "label": str(doc.get("label", doc.get("name", "")) or ""),
351
+ }
352
+ )
353
+ return out
354
+
355
+ @staticmethod
356
+ def _profile(payload: dict[str, Any], text: str) -> str:
357
+ explicit = str(payload.get("profile", payload.get("useCase", "")) or "")
358
+ if explicit:
359
+ return explicit
360
+ lowered = text.lower()
361
+ if any(token in lowered for token in ("migration", "visa", "residence permit", "work permit", "admission letter", "offer letter", "recruiter", "school claim")):
362
+ return "migrationTrust"
363
+ return str(payload.get("workflow", "resolveForMobility") or "resolveForMobility")
364
+
365
+ @staticmethod
366
+ def _sourceClasses(text: str, docs: list[Any]) -> list[dict[str, str]]:
367
+ classes: list[dict[str, str]] = []
368
+ for index, item in enumerate(docs, start=1):
369
+ doc = item if isinstance(item, dict) else {"label": str(item)}
370
+ ref = str(doc.get("artifactId", doc.get("name", f"doc_{index}")) or f"doc_{index}")
371
+ source = str(doc.get("source", doc.get("uri", "")) or "").lower()
372
+ kind = str(doc.get("kind", doc.get("type", "")) or "").lower()
373
+ label = "userUpload"
374
+ if any(token in source for token in (".gov", "gov/", "migrationsverket", "udi.no", "immigration")) or "official" in kind:
375
+ label = "officialPortal"
376
+ elif any(token in f"{kind} {source}" for token in ("school", "university", "admission")):
377
+ label = "school"
378
+ elif "recruiter" in f"{kind} {source}":
379
+ label = "recruiter"
380
+ elif any(token in f"{kind} {source}" for token in ("agent", "consultant", "licensed")):
381
+ label = "licensedAgent"
382
+ elif any(token in source for token in ("reddit", "forum", "facebook", "whatsapp")):
383
+ label = "publicForum"
384
+ elif source and not source.startswith("upload://"):
385
+ label = "externalSource"
386
+ classes.append({"ref": ref, "sourceClass": label, "verificationStatus": "sourceBound" if label == "officialPortal" else "unverified"})
387
+ if not classes and any(token in text.lower() for token in ("migration", "visa", "recruiter", "school", "offer letter")):
388
+ classes.append({"ref": "claimText", "sourceClass": "userStatement", "verificationStatus": "unverified"})
389
+ return classes
390
+
391
+ @staticmethod
392
+ def _typedEvidence(text: str, docs: list[Any], sourceClasses: list[dict[str, str]]) -> list[dict[str, str]]:
393
+ typed: list[dict[str, str]] = []
394
+ for index, item in enumerate(docs, start=1):
395
+ doc = item if isinstance(item, dict) else {"label": str(item)}
396
+ ref = str(doc.get("artifactId", doc.get("name", f"doc_{index}")) or f"doc_{index}")
397
+ value = f"{doc.get('kind', '')} {doc.get('type', '')} {doc.get('name', '')} {doc.get('source', '')}".lower()
398
+ evidenceType = "sourceSnapshot"
399
+ if "offer" in value:
400
+ evidenceType = "offerLetter"
401
+ elif "recruiter" in value:
402
+ evidenceType = "recruiterClaim"
403
+ elif any(token in value for token in ("school", "university", "admission")):
404
+ evidenceType = "schoolClaim"
405
+ elif "visa" in value or "permit" in value:
406
+ evidenceType = "visaStep"
407
+ elif any(token in value for token in ("payment", "invoice", "bank", "fee")):
408
+ evidenceType = "paymentInstruction"
409
+ elif any(token in value for token in ("identity", "passport", "id")):
410
+ evidenceType = "identityDocument"
411
+ elif any(token in value for token in ("message", "email", "chat", "thread")):
412
+ evidenceType = "messageThread"
413
+ sourceClass = next((item.get("sourceClass", "") for item in sourceClasses if item.get("ref") == ref), "")
414
+ typed.append({"ref": ref, "evidenceType": evidenceType, "sourceClass": sourceClass, "verificationStatus": "sourceBound" if doc.get("sha256") or doc.get("hash") or doc.get("source") else "unverified"})
415
+ if not typed and any(token in text.lower() for token in ("migration", "visa", "recruiter", "school", "offer letter")):
416
+ typed.append({"ref": "claimText", "evidenceType": "sourceSnapshot", "sourceClass": "userStatement", "verificationStatus": "unverified"})
417
+ return typed
418
+
419
+ @staticmethod
420
+ def _fraudIndicators(text: str, docs: list[Any], agentClaims: list[Any], sourceClasses: list[dict[str, str]]) -> list[dict[str, str]]:
421
+ indicators: list[dict[str, str]] = []
422
+ lowered = text.lower()
423
+ checks = (
424
+ ("guaranteedVisa", "critical", ("guaranteed visa", "guaranteed approval", "100% visa")),
425
+ ("urgencyPressure", "high", ("urgent payment", "pay today", "limited slot", "act now")),
426
+ ("informalPaymentRail", "high", ("western union", "crypto", "bitcoin", "gift card", "bank transfer only")),
427
+ ("identityMismatch", "high", ("mismatch", "different name", "name does not match")),
428
+ ("forgedDocument", "critical", ("forged", "fake document", "fake offer", "fake admission")),
429
+ ("unofficialChannel", "medium", ("whatsapp only", "gmail.com", "telegram only")),
430
+ )
431
+ for indicator, severity, tokens in checks:
432
+ if any(token in lowered for token in tokens):
433
+ indicators.append({"indicator": indicator, "severity": severity, "reason": "claimText", "evidenceRef": "claimText"})
434
+ if sourceClasses and not any(item.get("sourceClass") == "officialPortal" for item in sourceClasses):
435
+ indicators.append({"indicator": "officialSourceMissing", "severity": "medium", "reason": "noOfficialPortalSource", "evidenceRef": sourceClasses[0].get("ref", "claimText")})
436
+ for claim in agentClaims:
437
+ if isinstance(claim, dict) and str(claim.get("status", "")).lower() in {"unverified", "contested"}:
438
+ indicators.append({"indicator": "contestedAgentClaim", "severity": "high", "reason": str(claim.get("claim", "contested agent claim") or ""), "evidenceRef": "agentClaims"})
439
+ return indicators
440
+
441
+ @staticmethod
442
+ def _contradictions(text: str, docs: list[Any], agentClaims: list[Any]) -> list[dict[str, str]]:
443
+ contradictions: list[dict[str, str]] = []
444
+ lowered = text.lower()
445
+ if "fake offer" in lowered or "too good to be true" in lowered:
446
+ contradictions.append({"kind": "offerCredibility", "detail": "claim alleges fake or unrealistic offer"})
447
+ if "different name" in lowered or "mismatch" in lowered:
448
+ contradictions.append({"kind": "identityMismatch", "detail": "claim alleges document or actor mismatch"})
449
+ for claim in agentClaims:
450
+ if isinstance(claim, dict) and str(claim.get("status", "")).lower() in {"unverified", "contested"}:
451
+ contradictions.append({"kind": "agentClaim", "detail": str(claim.get("claim", "contested agent claim") or "")})
452
+ hashes = [str(item.get("sha256", item.get("hash", "")) or "") for item in docs if isinstance(item, dict)]
453
+ if hashes and len([item for item in hashes if item]) != len(set(item for item in hashes if item)):
454
+ contradictions.append({"kind": "duplicateDocumentHash", "detail": "document chain contains repeated hashes"})
455
+ return contradictions
456
+
457
+ @staticmethod
458
+ def _missingEvidence(text: str, docs: list[Any], *, profile: str = "", sourceClasses: list[dict[str, str]] | None = None, typedEvidence: list[dict[str, str]] | None = None) -> list[str]:
459
+ lowered = text.lower()
460
+ missing: list[str] = []
461
+ if "offer" in lowered and not any(isinstance(item, dict) and str(item.get("kind", "")) == "offer" for item in docs):
462
+ missing.append("offerSnapshot")
463
+ if not docs:
464
+ missing.append("sourceDocuments")
465
+ if not any(isinstance(item, dict) and str(item.get("sha256", item.get("hash", "")) or "") for item in docs):
466
+ missing.append("documentHashes")
467
+ if profile == "migrationTrust":
468
+ sourceClasses = sourceClasses or []
469
+ typedEvidence = typedEvidence or []
470
+ if not any(item.get("sourceClass") == "officialPortal" for item in sourceClasses):
471
+ missing.append("officialSourceSnapshot")
472
+ if any(item.get("evidenceType") == "offerLetter" for item in typedEvidence) and not any(item.get("sourceClass") in {"officialPortal", "school"} for item in sourceClasses):
473
+ missing.append("issuingInstitutionVerification")
474
+ return list(dict.fromkeys(missing))
475
+
476
+ @staticmethod
477
+ def _riskClass(text: str, contradictions: list[dict[str, str]], missing: list[str], agentClaims: list[Any], *, profile: str = "", fraudIndicators: list[dict[str, str]] | None = None) -> dict[str, str]:
478
+ lowered = text.lower()
479
+ fraudIndicators = fraudIndicators or []
480
+ if profile == "migrationTrust":
481
+ if any(item.get("severity") == "critical" for item in fraudIndicators) or any(token in lowered for token in ("scam", "fake offer", "fraud", "forged")):
482
+ return {"label": "migrationTrustFraudRisk", "severity": "critical"}
483
+ if fraudIndicators or contradictions:
484
+ return {"label": "migrationTrustReview", "severity": "high"}
485
+ if missing:
486
+ return {"label": "migrationTrustNeedsEvidence", "severity": "medium"}
487
+ return {"label": "migrationTrustSourceBound", "severity": "low"}
488
+ if any(token in lowered for token in ("scam", "fake offer", "fraud", "forged")):
489
+ return {"label": "scamTriage", "severity": "critical"}
490
+ if contradictions:
491
+ return {"label": "contestedFacts", "severity": "high"}
492
+ if any(token in lowered for token in ("damaged", "broken", "defective", "in-transit", "in transit", "transit")):
493
+ if any(token in lowered for token in ("followup", "follow up", "follow-up", "did not follow", "no response", "customer representative", "customer represent")):
494
+ return {"label": "damagedGoodsEscalation", "severity": "high"}
495
+ return {"label": "damagedGoodsClaim", "severity": "medium"}
496
+ if missing or agentClaims:
497
+ return {"label": "needsEvidence", "severity": "medium"}
498
+ return {"label": "routineDispute", "severity": "low"}
499
+
500
+ @staticmethod
501
+ def _triageConfidence(evidenceMap: list[dict[str, Any]], contradictions: list[dict[str, str]], missing: list[str]) -> float:
502
+ confidence = 0.45
503
+ if evidenceMap:
504
+ confidence += 0.20
505
+ if any(item.get("sourceBound") for item in evidenceMap):
506
+ confidence += 0.20
507
+ if contradictions:
508
+ confidence -= 0.05
509
+ if missing:
510
+ confidence -= min(0.20, 0.05 * len(missing))
511
+ return round(max(0.0, min(1.0, confidence)), 3)
512
+
513
+ @staticmethod
514
+ def _verificationStatus(evidenceMap: list[dict[str, Any]], sourceClasses: list[dict[str, str]], fraudIndicators: list[dict[str, str]], missing: list[str]) -> str:
515
+ if fraudIndicators and any(item.get("severity") in {"high", "critical"} for item in fraudIndicators):
516
+ return "needsHumanReview"
517
+ if missing:
518
+ return "needsSourceVerification"
519
+ if evidenceMap and any(item.get("sourceClass") == "officialPortal" for item in sourceClasses):
520
+ return "sourceBound"
521
+ return "unverified"
522
+
523
+ @staticmethod
524
+ def _caseOpsTimeline(profile: str, risk: dict[str, str], sourceClasses: list[dict[str, str]], fraudIndicators: list[dict[str, str]], missing: list[str]) -> list[dict[str, str]]:
525
+ if profile != "migrationTrust":
526
+ return []
527
+ timeline = [
528
+ {"stepId": "case_intake", "status": "completed", "owner": "resolve", "label": "Claim captured"},
529
+ {"stepId": "source_classification", "status": "completed" if sourceClasses else "needsInput", "owner": "hunt", "label": "Source classes assigned"},
530
+ {"stepId": "fraud_screen", "status": "completed" if fraudIndicators else "noFinding", "owner": "resolve", "label": "Fraud indicators screened"},
531
+ ]
532
+ if missing:
533
+ timeline.append({"stepId": "request_missing_evidence", "status": "needsInput", "owner": "proxy", "label": "Request missing evidence"})
534
+ if risk.get("severity") in {"high", "critical"} or fraudIndicators:
535
+ timeline.append({"stepId": "human_review", "status": "awaitingProxy", "owner": "proxy", "label": "Route to human reviewer"})
536
+ return timeline
537
+
538
+ @staticmethod
539
+ def _nextAction(risk: dict[str, str], missing: list[str], contradictions: list[dict[str, str]], fraudIndicators: list[dict[str, str]] | None = None) -> str:
540
+ if fraudIndicators or risk.get("severity") in {"high", "critical"}:
541
+ return "escalateToHumanReviewer"
542
+ if missing:
543
+ return "requestMissingEvidence"
544
+ if contradictions or risk.get("severity") in {"high", "critical"}:
545
+ return "escalateToHumanReviewer"
546
+ return "prepareClaimOrAuditSummary"
@@ -0,0 +1,3 @@
1
+ from .engine import PicuxMultiAgentOrchestrator
2
+
3
+ __all__ = ["PicuxMultiAgentOrchestrator"]