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.
- apps/__init__.py +2 -0
- apps/api/__init__.py +2 -0
- apps/api/main.py +652 -0
- apps/benchmarks/__init__.py +1 -0
- apps/benchmarks/main.py +20 -0
- apps/sandbox/__init__.py +1 -0
- apps/sandbox/main.py +20 -0
- apps/worker/__init__.py +2 -0
- apps/worker/main.py +15 -0
- apps/worker/verify.py +14 -0
- patchr/__init__.py +12 -0
- patchr/sdk/__init__.py +20 -0
- patchr/sdk/client.py +12 -0
- patchr-0.1.0.dist-info/METADATA +137 -0
- patchr-0.1.0.dist-info/RECORD +116 -0
- patchr-0.1.0.dist-info/WHEEL +5 -0
- patchr-0.1.0.dist-info/entry_points.txt +5 -0
- patchr-0.1.0.dist-info/licenses/LICENSE +17 -0
- patchr-0.1.0.dist-info/top_level.txt +3 -0
- picux/__init__.py +6 -0
- picux/agents/__init__.py +5 -0
- picux/agents/registry.py +204 -0
- picux/api/__init__.py +5 -0
- picux/api/service.py +5075 -0
- picux/audit/__init__.py +31 -0
- picux/audit/activity.py +97 -0
- picux/audit/observability.py +55 -0
- picux/audit/verification/__init__.py +21 -0
- picux/audit/verification/ledger.py +633 -0
- picux/benchmarks/__init__.py +5 -0
- picux/benchmarks/local.py +286 -0
- picux/config.py +140 -0
- picux/contracts/__init__.py +22 -0
- picux/contracts/handshake.py +122 -0
- picux/contracts/integration.py +385 -0
- picux/contracts/openapi.py +187 -0
- picux/contracts/protocol_map.py +152 -0
- picux/contracts/routes.py +980 -0
- picux/contracts/schema_catalog.py +125 -0
- picux/core/__init__.py +17 -0
- picux/core/models.py +148 -0
- picux/core/router.py +131 -0
- picux/core/runtime.py +42 -0
- picux/core/state_machine.py +38 -0
- picux/domains/__init__.py +2 -0
- picux/domains/bridge/HostRun.py +1104 -0
- picux/domains/bridge/__init__.py +6 -0
- picux/domains/bridge/engine.py +345 -0
- picux/domains/hunt/__init__.py +6 -0
- picux/domains/hunt/engine.py +307 -0
- picux/domains/hunt/models.py +88 -0
- picux/domains/pay/__init__.py +16 -0
- picux/domains/pay/adapters.py +607 -0
- picux/domains/pay/engine.py +950 -0
- picux/domains/pay/models.py +95 -0
- picux/domains/proxy/__init__.py +5 -0
- picux/domains/proxy/engine.py +466 -0
- picux/domains/resolve/__init__.py +5 -0
- picux/domains/resolve/engine.py +546 -0
- picux/orchestrator/__init__.py +3 -0
- picux/orchestrator/engine.py +2840 -0
- picux/portals/__init__.py +17 -0
- picux/portals/templates.py +272 -0
- picux/protocols/__init__.py +1 -0
- picux/protocols/a2a/__init__.py +6 -0
- picux/protocols/a2a/client.py +51 -0
- picux/protocols/a2a/envelope.py +132 -0
- picux/protocols/mcp/__init__.py +7 -0
- picux/protocols/mcp/client.py +69 -0
- picux/protocols/mcp/contract.py +67 -0
- picux/protocols/mcp/server.py +76 -0
- picux/sandbox/__init__.py +6 -0
- picux/sandbox/midnight_arbitrage.py +215 -0
- picux/sandbox/models.py +90 -0
- picux/sdk/__init__.py +13 -0
- picux/sdk/client.py +768 -0
- picux/sdk/external.py +245 -0
- picux/security/__init__.py +18 -0
- picux/security/auth.py +86 -0
- picux/security/config_validator.py +58 -0
- picux/security/policy.py +158 -0
- picux/security/secrets.py +144 -0
- picux/signals/__init__.py +1 -0
- picux/signals/community/__init__.py +24 -0
- picux/signals/community/adapters/__init__.py +7 -0
- picux/signals/community/adapters/reddit.py +37 -0
- picux/signals/community/adapters/shopify.py +23 -0
- picux/signals/community/adapters/web.py +23 -0
- picux/signals/community/disambiguation.py +51 -0
- picux/signals/community/intake.py +227 -0
- picux/signals/community/models.py +102 -0
- picux/signals/community/rules.py +91 -0
- picux/signals/community/scoring.py +64 -0
- picux/storage/__init__.py +41 -0
- picux/storage/agents.py +50 -0
- picux/storage/cases.py +440 -0
- picux/storage/channels.py +476 -0
- picux/storage/connectors.py +411 -0
- picux/storage/envelopes.py +137 -0
- picux/storage/escrows.py +168 -0
- picux/storage/events.py +989 -0
- picux/storage/keyspace.py +60 -0
- picux/storage/mandates.py +107 -0
- picux/storage/portals.py +222 -0
- picux/storage/postgres.py +2049 -0
- picux/storage/providers.py +148 -0
- picux/storage/proxy.py +231 -0
- picux/storage/receipts.py +131 -0
- picux/storage/signals.py +147 -0
- picux/storage/tasks.py +179 -0
- picux/tools/__init__.py +11 -0
- picux/tools/shared.py +2048 -0
- picux/verification/__init__.py +5 -0
- picux/verification/rollout.py +183 -0
- picux/workflows/__init__.py +5 -0
- picux/workflows/templates.py +74 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LocalProviderBook:
|
|
12
|
+
"""Private local-service contact cache for HUNT -> PROXY workflows."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, *, backing: Any | None = None) -> None:
|
|
15
|
+
self.backing = backing
|
|
16
|
+
self._records: dict[str, dict[str, Any]] = {}
|
|
17
|
+
|
|
18
|
+
def saveLead(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
19
|
+
return self._save(payload, status=str(payload.get("status", "lead") or "lead"))
|
|
20
|
+
|
|
21
|
+
def saveVerified(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
22
|
+
return self._save(payload, status="verified")
|
|
23
|
+
|
|
24
|
+
def getProvider(self, providerId: str) -> dict[str, Any]:
|
|
25
|
+
providerId = str(providerId or "").strip()
|
|
26
|
+
record = self._records.get(providerId)
|
|
27
|
+
if record is None and self._backingEnabled() and hasattr(self.backing, "fetchLocalProvider"):
|
|
28
|
+
record = self.backing.fetchLocalProvider(providerId)
|
|
29
|
+
if record:
|
|
30
|
+
self._records[providerId] = copy.deepcopy(record)
|
|
31
|
+
if not record:
|
|
32
|
+
return {"ok": False, "error": "providerNotFound", "providerId": providerId}
|
|
33
|
+
return {"ok": True, "provider": copy.deepcopy(record)}
|
|
34
|
+
|
|
35
|
+
def find(self, *, service: str = "", location: str = "", verifiedOnly: bool = True, limit: int = 10) -> dict[str, Any]:
|
|
36
|
+
self._loadBacking()
|
|
37
|
+
serviceKey = _norm(service)
|
|
38
|
+
locationKey = _norm(location)
|
|
39
|
+
records = list(self._records.values())
|
|
40
|
+
if verifiedOnly:
|
|
41
|
+
records = [item for item in records if item.get("status") == "verified"]
|
|
42
|
+
if serviceKey:
|
|
43
|
+
records = [item for item in records if serviceKey in _norm(item.get("service", "")) or serviceKey in _norm(item.get("entity", ""))]
|
|
44
|
+
if locationKey:
|
|
45
|
+
records = [item for item in records if _locationMatch(locationKey, _norm(" ".join(str(item.get(key, "")) for key in ("location", "address", "area"))))]
|
|
46
|
+
records.sort(key=lambda item: (item.get("status") == "verified", float(item.get("rating", 0.0) or 0.0), int(item.get("updatedAt", 0) or 0)), reverse=True)
|
|
47
|
+
limited = records[: max(1, min(int(limit or 10), 50))]
|
|
48
|
+
return {"ok": True, "providers": [copy.deepcopy(record) for record in limited], "count": len(limited)}
|
|
49
|
+
|
|
50
|
+
def toPlaces(self, *, service: str = "", location: str = "", limit: int = 10) -> list[dict[str, Any]]:
|
|
51
|
+
found = self.find(service=service, location=location, verifiedOnly=True, limit=limit)
|
|
52
|
+
places = []
|
|
53
|
+
for record in found.get("providers", []):
|
|
54
|
+
places.append(
|
|
55
|
+
{
|
|
56
|
+
"placeId": str(record.get("providerId", "") or record.get("contactRef", "")),
|
|
57
|
+
"name": str(record.get("name", "") or ""),
|
|
58
|
+
"category": str(record.get("category", service) or service),
|
|
59
|
+
"address": str(record.get("address", "") or ""),
|
|
60
|
+
"phone": str(record.get("phone", "") or ""),
|
|
61
|
+
"website": str(record.get("website", "") or ""),
|
|
62
|
+
"rating": record.get("rating", 0),
|
|
63
|
+
"lat": record.get("lat", 0),
|
|
64
|
+
"lng": record.get("lng", 0),
|
|
65
|
+
"distanceKm": record.get("distanceKm", 0),
|
|
66
|
+
"source": "verifiedProviders",
|
|
67
|
+
"sourceUrl": str(record.get("sourceUrl", "") or ""),
|
|
68
|
+
"sourceBound": True,
|
|
69
|
+
"entity": str(record.get("service", service) or service),
|
|
70
|
+
"contactRef": str(record.get("contactRef", record.get("providerId", "")) or ""),
|
|
71
|
+
"verified": True,
|
|
72
|
+
"availability": record.get("availability", {}) if isinstance(record.get("availability"), dict) else {},
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
return places
|
|
76
|
+
|
|
77
|
+
def _save(self, payload: dict[str, Any], *, status: str) -> dict[str, Any]:
|
|
78
|
+
if not isinstance(payload, dict):
|
|
79
|
+
return {"ok": False, "error": "invalid:provider"}
|
|
80
|
+
providerId = str(payload.get("providerId", payload.get("contactRef", "")) or "").strip()
|
|
81
|
+
if not providerId:
|
|
82
|
+
providerId = "provider_" + hashlib.sha256(json.dumps(payload, sort_keys=True, default=str).encode("utf-8")).hexdigest()[:24]
|
|
83
|
+
now = int(time.time())
|
|
84
|
+
current = self._records.get(providerId, {})
|
|
85
|
+
createdAt = int(current.get("createdAt", now) or now)
|
|
86
|
+
record = {
|
|
87
|
+
**copy.deepcopy(current),
|
|
88
|
+
"providerId": providerId,
|
|
89
|
+
"contactRef": str(payload.get("contactRef", current.get("contactRef", providerId)) or providerId),
|
|
90
|
+
"status": status,
|
|
91
|
+
"service": str(payload.get("service", payload.get("entity", current.get("service", ""))) or ""),
|
|
92
|
+
"entity": str(payload.get("entity", payload.get("service", current.get("entity", ""))) or ""),
|
|
93
|
+
"location": str(payload.get("location", payload.get("area", current.get("location", ""))) or ""),
|
|
94
|
+
"area": str(payload.get("area", payload.get("location", current.get("area", ""))) or ""),
|
|
95
|
+
"name": str(payload.get("name", current.get("name", "")) or ""),
|
|
96
|
+
"category": str(payload.get("category", current.get("category", "")) or ""),
|
|
97
|
+
"address": str(payload.get("address", current.get("address", "")) or ""),
|
|
98
|
+
"phone": str(payload.get("phone", current.get("phone", "")) or ""),
|
|
99
|
+
"website": str(payload.get("website", current.get("website", "")) or ""),
|
|
100
|
+
"rating": _float(payload.get("rating", current.get("rating", 0))),
|
|
101
|
+
"lat": _float(payload.get("lat", current.get("lat", 0))),
|
|
102
|
+
"lng": _float(payload.get("lng", current.get("lng", 0))),
|
|
103
|
+
"distanceKm": _float(payload.get("distanceKm", current.get("distanceKm", 0))),
|
|
104
|
+
"source": str(payload.get("source", current.get("source", "")) or ""),
|
|
105
|
+
"sourceUrl": str(payload.get("sourceUrl", current.get("sourceUrl", "")) or ""),
|
|
106
|
+
"availability": payload.get("availability", current.get("availability", {})) if isinstance(payload.get("availability", current.get("availability", {})), dict) else {},
|
|
107
|
+
"proof": payload.get("proof", current.get("proof", {})) if isinstance(payload.get("proof", current.get("proof", {})), dict) else {},
|
|
108
|
+
"meta": payload.get("meta", current.get("meta", {})) if isinstance(payload.get("meta", current.get("meta", {})), dict) else {},
|
|
109
|
+
"createdAt": createdAt,
|
|
110
|
+
"updatedAt": now,
|
|
111
|
+
}
|
|
112
|
+
self._records[providerId] = copy.deepcopy(record)
|
|
113
|
+
if self._backingEnabled() and hasattr(self.backing, "upsertLocalProvider"):
|
|
114
|
+
self.backing.upsertLocalProvider(record)
|
|
115
|
+
return {"ok": True, "provider": copy.deepcopy(record)}
|
|
116
|
+
|
|
117
|
+
def _loadBacking(self) -> None:
|
|
118
|
+
if not self._backingEnabled() or not hasattr(self.backing, "listLocalProviders"):
|
|
119
|
+
return
|
|
120
|
+
for record in self.backing.listLocalProviders():
|
|
121
|
+
if isinstance(record, dict) and str(record.get("providerId", "") or ""):
|
|
122
|
+
self._records[str(record["providerId"])] = copy.deepcopy(record)
|
|
123
|
+
|
|
124
|
+
def _backingEnabled(self) -> bool:
|
|
125
|
+
if self.backing is None:
|
|
126
|
+
return False
|
|
127
|
+
return bool(getattr(self.backing, "enabled", True))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _norm(value: Any) -> str:
|
|
131
|
+
text = re.sub(r"[^a-z0-9]+", " ", str(value or "").lower())
|
|
132
|
+
return re.sub(r"\s+", " ", text).strip()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _locationMatch(needle: str, haystack: str) -> bool:
|
|
136
|
+
if not needle:
|
|
137
|
+
return True
|
|
138
|
+
if needle in haystack:
|
|
139
|
+
return True
|
|
140
|
+
needleParts = [part for part in needle.split() if len(part) > 2]
|
|
141
|
+
return bool(needleParts) and all(part in haystack for part in needleParts[:3])
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _float(value: Any) -> float:
|
|
145
|
+
try:
|
|
146
|
+
return float(value or 0.0)
|
|
147
|
+
except Exception:
|
|
148
|
+
return 0.0
|
picux/storage/proxy.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
PROXY_STATUSES = {
|
|
9
|
+
"requested",
|
|
10
|
+
"awaitingProxy",
|
|
11
|
+
"notified",
|
|
12
|
+
"inProgress",
|
|
13
|
+
"submitted",
|
|
14
|
+
"pending",
|
|
15
|
+
"confirmedSuccess",
|
|
16
|
+
"confirmedFailure",
|
|
17
|
+
"canceled",
|
|
18
|
+
"overdue",
|
|
19
|
+
}
|
|
20
|
+
PROXY_KINDS = {"review", "logistics", "call", "message", "approval", "custom"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProxyBook:
|
|
24
|
+
"""Proxy mission storage with callback reference indexes."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, *, backing: Any | None = None) -> None:
|
|
27
|
+
self.backing = backing
|
|
28
|
+
self._records: dict[str, dict[str, Any]] = {}
|
|
29
|
+
self._refs: dict[str, str] = {}
|
|
30
|
+
|
|
31
|
+
def saveMission(self, payload: dict[str, Any], *, status: str = "awaitingProxy") -> dict[str, Any]:
|
|
32
|
+
proxyId = str(payload.get("proxyId", payload.get("missionId", "")) or "")
|
|
33
|
+
if not proxyId:
|
|
34
|
+
return {"ok": False, "error": "missing:proxyId"}
|
|
35
|
+
errors = _missionErrors(payload)
|
|
36
|
+
if errors:
|
|
37
|
+
return {"ok": False, "error": "invalid:proxyMission", "errors": errors}
|
|
38
|
+
current = self.getMission(proxyId)
|
|
39
|
+
now = int(time.time())
|
|
40
|
+
createdAt = current.get("mission", {}).get("createdAt", now) if current.get("ok") else now
|
|
41
|
+
record = _recordFromPayload({**payload, "proxyId": proxyId}, status=status, createdAt=createdAt, updatedAt=now)
|
|
42
|
+
self._saveRecord(record)
|
|
43
|
+
return {"ok": True, "mission": copy.deepcopy(record)}
|
|
44
|
+
|
|
45
|
+
def getMission(self, proxyId: str) -> dict[str, Any]:
|
|
46
|
+
proxyId = str(proxyId or "")
|
|
47
|
+
record = self._records.get(proxyId)
|
|
48
|
+
if record is None and self._backingEnabled() and hasattr(self.backing, "fetchProxyMission"):
|
|
49
|
+
record = self.backing.fetchProxyMission(proxyId)
|
|
50
|
+
if record:
|
|
51
|
+
self._saveRecord(record, persist=False)
|
|
52
|
+
if not record:
|
|
53
|
+
return {"ok": False, "error": "proxyMissionNotFound", "proxyId": proxyId}
|
|
54
|
+
return {"ok": True, "mission": copy.deepcopy(record)}
|
|
55
|
+
|
|
56
|
+
def getMissionByRef(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
57
|
+
proxyId = self.resolveProxyId(payload)
|
|
58
|
+
return self.getMission(proxyId) if proxyId else {"ok": False, "error": "proxyMissionNotFound", "proxyId": ""}
|
|
59
|
+
|
|
60
|
+
def resolveProxyId(self, payload: dict[str, Any]) -> str:
|
|
61
|
+
for key in ("proxyId", "missionId"):
|
|
62
|
+
value = str(payload.get(key, "") or "")
|
|
63
|
+
if value:
|
|
64
|
+
return value
|
|
65
|
+
refs = payload.get("refs", {}) if isinstance(payload.get("refs"), dict) else {}
|
|
66
|
+
for key in ("requestId", "providerTaskId", "externalRef", "connectorRunId"):
|
|
67
|
+
value = str(payload.get(key, refs.get(key, "")) or "")
|
|
68
|
+
if not value:
|
|
69
|
+
continue
|
|
70
|
+
found = self._refs.get(f"{key}:{value}")
|
|
71
|
+
if found:
|
|
72
|
+
return found
|
|
73
|
+
if self._backingEnabled() and hasattr(self.backing, "fetchProxyMissionByRef"):
|
|
74
|
+
record = self.backing.fetchProxyMissionByRef(key, value)
|
|
75
|
+
if record:
|
|
76
|
+
self._saveRecord(record, persist=False)
|
|
77
|
+
proxyId = str(record.get("proxyId", "") or "")
|
|
78
|
+
if proxyId:
|
|
79
|
+
return proxyId
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
def listMissions(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
83
|
+
self._loadBacking()
|
|
84
|
+
filters = filters or {}
|
|
85
|
+
records = list(self._records.values())
|
|
86
|
+
for key in ("taskId", "userId", "status", "kind"):
|
|
87
|
+
value = str(filters.get(key, "") or "")
|
|
88
|
+
if value:
|
|
89
|
+
records = [record for record in records if str(record.get(key, "") or "") == value]
|
|
90
|
+
records.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("proxyId", ""))), reverse=True)
|
|
91
|
+
limit = _limit(filters.get("limit", 100))
|
|
92
|
+
return {"ok": True, "missions": [copy.deepcopy(record) for record in records[:limit]], "count": min(len(records), limit)}
|
|
93
|
+
|
|
94
|
+
def submitOutcome(self, proxyId: str, payload: dict[str, Any], outcome: dict[str, Any]) -> dict[str, Any]:
|
|
95
|
+
current = self.getMission(proxyId)
|
|
96
|
+
if not current.get("ok"):
|
|
97
|
+
return current
|
|
98
|
+
now = int(time.time())
|
|
99
|
+
mission = current["mission"]
|
|
100
|
+
status = _status(outcome.get("status", payload.get("status", "submitted")))
|
|
101
|
+
merged = {
|
|
102
|
+
**mission,
|
|
103
|
+
"status": status,
|
|
104
|
+
"outcome": copy.deepcopy(outcome),
|
|
105
|
+
"resume": copy.deepcopy(outcome.get("resume", {}) if isinstance(outcome.get("resume"), dict) else {}),
|
|
106
|
+
"updatedAt": int(payload.get("updatedAt", now) or now),
|
|
107
|
+
}
|
|
108
|
+
refs = _refs({**mission, **payload})
|
|
109
|
+
if refs:
|
|
110
|
+
merged["refs"] = refs
|
|
111
|
+
self._saveRecord(merged)
|
|
112
|
+
return {"ok": True, "mission": copy.deepcopy(merged), "outcome": copy.deepcopy(outcome)}
|
|
113
|
+
|
|
114
|
+
def submitOutcomeByRef(self, payload: dict[str, Any], outcome: dict[str, Any]) -> dict[str, Any]:
|
|
115
|
+
proxyId = self.resolveProxyId(payload)
|
|
116
|
+
if not proxyId:
|
|
117
|
+
return {"ok": False, "error": "proxyMissionNotFound", "proxyId": ""}
|
|
118
|
+
return self.submitOutcome(proxyId, payload, outcome)
|
|
119
|
+
|
|
120
|
+
def _saveRecord(self, record: dict[str, Any], *, persist: bool = True) -> None:
|
|
121
|
+
proxyId = str(record.get("proxyId", "") or "")
|
|
122
|
+
if not proxyId:
|
|
123
|
+
return
|
|
124
|
+
self._records[proxyId] = copy.deepcopy(record)
|
|
125
|
+
for key, value in record.get("refs", {}).items():
|
|
126
|
+
clean = str(value or "")
|
|
127
|
+
if clean:
|
|
128
|
+
self._refs[f"{key}:{clean}"] = proxyId
|
|
129
|
+
if persist and self._backingEnabled() and hasattr(self.backing, "upsertProxyMission"):
|
|
130
|
+
self.backing.upsertProxyMission(record)
|
|
131
|
+
|
|
132
|
+
def _loadBacking(self) -> None:
|
|
133
|
+
if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listProxyMissions"):
|
|
134
|
+
return
|
|
135
|
+
for record in self.backing.listProxyMissions():
|
|
136
|
+
proxyId = str(record.get("proxyId", "") or "")
|
|
137
|
+
if proxyId:
|
|
138
|
+
self._saveRecord(record, persist=False)
|
|
139
|
+
|
|
140
|
+
def _backingEnabled(self) -> bool:
|
|
141
|
+
if self.backing is None:
|
|
142
|
+
return False
|
|
143
|
+
return bool(getattr(self.backing, "enabled", True))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _recordFromPayload(payload: dict[str, Any], *, status: str, createdAt: int, updatedAt: int) -> dict[str, Any]:
|
|
147
|
+
brief = _dict(payload.get("brief", {}))
|
|
148
|
+
context = _dict(payload.get("context", {}))
|
|
149
|
+
return {
|
|
150
|
+
"proxyId": str(payload.get("proxyId", "") or ""),
|
|
151
|
+
"status": _status(payload.get("status", status)),
|
|
152
|
+
"domain": "proxy",
|
|
153
|
+
"kind": _kind(payload.get("kind", payload.get("missionType", brief.get("missionType", "custom")))),
|
|
154
|
+
"taskId": str(payload.get("taskId", context.get("taskId", "")) or ""),
|
|
155
|
+
"userId": str(payload.get("userId", context.get("userId", "")) or ""),
|
|
156
|
+
"resolveId": str(payload.get("resolveId", context.get("resolveId", "")) or ""),
|
|
157
|
+
"reason": str(payload.get("reason", brief.get("reason", "humanIntervention")) or "humanIntervention"),
|
|
158
|
+
"brief": brief,
|
|
159
|
+
"context": context,
|
|
160
|
+
"decisionFork": _dict(payload.get("decisionFork", brief.get("decisionFork", {}))),
|
|
161
|
+
"logistics": _dict(payload.get("logistics", brief.get("logistics", {}))),
|
|
162
|
+
"proofReq": _cleanList(payload.get("proofReq", brief.get("proofReq", []))),
|
|
163
|
+
"caseOpsTimeline": _cleanDicts(payload.get("caseOpsTimeline", brief.get("caseOpsTimeline", []))),
|
|
164
|
+
"route": _dict(payload.get("route", {})),
|
|
165
|
+
"channelMsg": _dict(payload.get("channelMsg", brief.get("channelMsg", {}))),
|
|
166
|
+
"voiceCall": _dict(payload.get("voiceCall", brief.get("voiceCall", {}))),
|
|
167
|
+
"refs": _refs(payload),
|
|
168
|
+
"bridgeAction": _dict(payload.get("bridgeAction", {})),
|
|
169
|
+
"bridgeRun": _dict(payload.get("bridgeRun", {})),
|
|
170
|
+
"outcome": _dict(payload.get("outcome", {})),
|
|
171
|
+
"resume": _dict(payload.get("resume", {})),
|
|
172
|
+
"createdAt": int(createdAt or updatedAt),
|
|
173
|
+
"updatedAt": int(updatedAt or createdAt),
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _missionErrors(payload: dict[str, Any]) -> list[str]:
|
|
178
|
+
errors: list[str] = []
|
|
179
|
+
if _kind(payload.get("kind", payload.get("missionType", "custom"))) not in PROXY_KINDS:
|
|
180
|
+
errors.append("invalid:kind")
|
|
181
|
+
if not _dict(payload.get("brief", {})) and not _dict(payload.get("context", {})) and not str(payload.get("task", payload.get("goal", "")) or ""):
|
|
182
|
+
errors.append("missing:context")
|
|
183
|
+
return errors
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _refs(payload: dict[str, Any]) -> dict[str, str]:
|
|
187
|
+
raw = payload.get("refs", {}) if isinstance(payload.get("refs"), dict) else {}
|
|
188
|
+
refs: dict[str, str] = {}
|
|
189
|
+
for key in ("requestId", "providerTaskId", "externalRef", "connectorRunId"):
|
|
190
|
+
value = str(payload.get(key, raw.get(key, "")) or "")
|
|
191
|
+
if value:
|
|
192
|
+
refs[key] = value
|
|
193
|
+
return refs
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _dict(value: Any) -> dict[str, Any]:
|
|
197
|
+
return copy.deepcopy(value) if isinstance(value, dict) else {}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _cleanList(value: Any) -> list[str]:
|
|
201
|
+
if not isinstance(value, list):
|
|
202
|
+
return []
|
|
203
|
+
out = []
|
|
204
|
+
for item in value:
|
|
205
|
+
clean = str(item or "").strip()
|
|
206
|
+
if clean and clean not in out:
|
|
207
|
+
out.append(clean)
|
|
208
|
+
return out
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _cleanDicts(value: Any) -> list[dict[str, Any]]:
|
|
212
|
+
if not isinstance(value, list):
|
|
213
|
+
return []
|
|
214
|
+
return [copy.deepcopy(item) for item in value if isinstance(item, dict)]
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _kind(value: Any) -> str:
|
|
218
|
+
kind = str(value or "custom")
|
|
219
|
+
return kind if kind in PROXY_KINDS else kind
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _status(value: Any) -> str:
|
|
223
|
+
status = str(value or "awaitingProxy")
|
|
224
|
+
return status if status in PROXY_STATUSES else "submitted"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _limit(value: Any) -> int:
|
|
228
|
+
try:
|
|
229
|
+
return max(1, min(500, int(value)))
|
|
230
|
+
except Exception:
|
|
231
|
+
return 100
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ReceiptBook:
|
|
8
|
+
"""Receipt and proof export boundary with optional durable backing."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, *, backing: Any | None = None) -> None:
|
|
11
|
+
self.backing = backing
|
|
12
|
+
self._receipts: dict[str, dict[str, Any]] = {}
|
|
13
|
+
self._pov: dict[str, dict[str, Any]] = {}
|
|
14
|
+
|
|
15
|
+
def saveReceipt(self, receipt: dict[str, Any]) -> dict[str, Any]:
|
|
16
|
+
receiptId = str(receipt.get("receiptId", "") or "")
|
|
17
|
+
if not receiptId:
|
|
18
|
+
return {"ok": False, "error": "missing:receiptId"}
|
|
19
|
+
stored = copy.deepcopy(receipt)
|
|
20
|
+
self._receipts[receiptId] = stored
|
|
21
|
+
if self._backingEnabled() and hasattr(self.backing, "insertReceipt"):
|
|
22
|
+
self.backing.insertReceipt(stored)
|
|
23
|
+
return {"ok": True, "receiptId": receiptId}
|
|
24
|
+
|
|
25
|
+
def getReceipt(self, receiptId: str) -> dict[str, Any]:
|
|
26
|
+
receiptId = str(receiptId or "")
|
|
27
|
+
receipt = self._receipts.get(receiptId)
|
|
28
|
+
if receipt is None and self._backingEnabled() and hasattr(self.backing, "fetchReceipt"):
|
|
29
|
+
receipt = self.backing.fetchReceipt(receiptId)
|
|
30
|
+
if receipt:
|
|
31
|
+
self._receipts[receiptId] = copy.deepcopy(receipt)
|
|
32
|
+
if not receipt:
|
|
33
|
+
return {"ok": False, "error": "receiptNotFound", "receiptId": receiptId}
|
|
34
|
+
return {"ok": True, "receipt": copy.deepcopy(receipt)}
|
|
35
|
+
|
|
36
|
+
def listReceipts(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
37
|
+
self._loadReceipts()
|
|
38
|
+
filters = filters or {}
|
|
39
|
+
status = str(filters.get("status", "") or "")
|
|
40
|
+
mandateId = str(filters.get("mandateId", "") or "")
|
|
41
|
+
taskId = str(filters.get("taskId", "") or "")
|
|
42
|
+
records = list(self._receipts.values())
|
|
43
|
+
if status:
|
|
44
|
+
records = [record for record in records if record.get("status") == status]
|
|
45
|
+
if mandateId:
|
|
46
|
+
records = [record for record in records if record.get("mandateId") == mandateId]
|
|
47
|
+
if taskId:
|
|
48
|
+
records = [record for record in records if record.get("taskId") == taskId]
|
|
49
|
+
records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("receiptId", ""))), reverse=True)
|
|
50
|
+
limit = _limit(filters.get("limit", 100))
|
|
51
|
+
return {
|
|
52
|
+
"ok": True,
|
|
53
|
+
"receipts": [copy.deepcopy(record) for record in records[:limit]],
|
|
54
|
+
"count": min(len(records), limit),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def savePov(self, pov: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
povId = str(pov.get("povId", "") or "")
|
|
59
|
+
if not povId:
|
|
60
|
+
return {"ok": False, "error": "missing:povId"}
|
|
61
|
+
stored = copy.deepcopy(pov)
|
|
62
|
+
self._pov[povId] = stored
|
|
63
|
+
if self._backingEnabled() and hasattr(self.backing, "insertPov"):
|
|
64
|
+
self.backing.insertPov(stored)
|
|
65
|
+
return {"ok": True, "povId": povId}
|
|
66
|
+
|
|
67
|
+
def getPov(self, povId: str) -> dict[str, Any]:
|
|
68
|
+
povId = str(povId or "")
|
|
69
|
+
pov = self._pov.get(povId)
|
|
70
|
+
if pov is None and self._backingEnabled() and hasattr(self.backing, "fetchPov"):
|
|
71
|
+
pov = self.backing.fetchPov(povId)
|
|
72
|
+
if pov:
|
|
73
|
+
self._pov[povId] = copy.deepcopy(pov)
|
|
74
|
+
if not pov:
|
|
75
|
+
return {"ok": False, "error": "povNotFound", "povId": povId}
|
|
76
|
+
return {"ok": True, "pov": copy.deepcopy(pov)}
|
|
77
|
+
|
|
78
|
+
def listPov(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
79
|
+
self._loadPov()
|
|
80
|
+
filters = filters or {}
|
|
81
|
+
taskId = str(filters.get("taskId", "") or "")
|
|
82
|
+
domain = str(filters.get("domain", "") or "")
|
|
83
|
+
userId = str(filters.get("userId", "") or "")
|
|
84
|
+
records = list(self._pov.values())
|
|
85
|
+
if taskId:
|
|
86
|
+
records = [record for record in records if record.get("taskId") == taskId]
|
|
87
|
+
if domain:
|
|
88
|
+
records = [record for record in records if record.get("domain") == domain]
|
|
89
|
+
if userId:
|
|
90
|
+
records = [record for record in records if _povUserId(record) == userId]
|
|
91
|
+
records.sort(key=lambda item: (str(item.get("createdAt", "")), str(item.get("povId", ""))), reverse=True)
|
|
92
|
+
limit = _limit(filters.get("limit", 100))
|
|
93
|
+
return {
|
|
94
|
+
"ok": True,
|
|
95
|
+
"pov": [copy.deepcopy(record) for record in records[:limit]],
|
|
96
|
+
"count": min(len(records), limit),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def _loadReceipts(self) -> None:
|
|
100
|
+
if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listReceipts"):
|
|
101
|
+
return
|
|
102
|
+
for receipt in self.backing.listReceipts():
|
|
103
|
+
receiptId = str(receipt.get("receiptId", "") or "")
|
|
104
|
+
if receiptId:
|
|
105
|
+
self._receipts[receiptId] = copy.deepcopy(receipt)
|
|
106
|
+
|
|
107
|
+
def _loadPov(self) -> None:
|
|
108
|
+
if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listPov"):
|
|
109
|
+
return
|
|
110
|
+
for pov in self.backing.listPov():
|
|
111
|
+
povId = str(pov.get("povId", "") or "")
|
|
112
|
+
if povId:
|
|
113
|
+
self._pov[povId] = copy.deepcopy(pov)
|
|
114
|
+
|
|
115
|
+
def _backingEnabled(self) -> bool:
|
|
116
|
+
if self.backing is None:
|
|
117
|
+
return False
|
|
118
|
+
return bool(getattr(self.backing, "enabled", True))
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _povUserId(record: dict[str, Any]) -> str:
|
|
122
|
+
meta = record.get("meta", {}) if isinstance(record.get("meta"), dict) else {}
|
|
123
|
+
return str(record.get("userId", meta.get("userId", "")) or "")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _limit(value: Any) -> int:
|
|
127
|
+
try:
|
|
128
|
+
parsed = int(value)
|
|
129
|
+
except (TypeError, ValueError):
|
|
130
|
+
return 100
|
|
131
|
+
return max(1, min(parsed, 500))
|
picux/storage/signals.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import time
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class SignalBook:
|
|
10
|
+
"""Community signal records with optional durable backing."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, *, backing: Any | None = None) -> None:
|
|
13
|
+
self.backing = backing
|
|
14
|
+
self._records: dict[str, dict[str, Any]] = {}
|
|
15
|
+
|
|
16
|
+
def saveSignal(self, signal: Any, analysis: Any) -> dict[str, Any]:
|
|
17
|
+
signalMap = _toMap(signal)
|
|
18
|
+
analysisMap = _toMap(analysis)
|
|
19
|
+
signalId = str(signalMap.get("signalId", analysisMap.get("signalId", "")) or "")
|
|
20
|
+
if not signalId:
|
|
21
|
+
return {"ok": False, "error": "missing:signalId"}
|
|
22
|
+
|
|
23
|
+
current = self.getSignal(signalId)
|
|
24
|
+
observedAt = str(signalMap.get("observedAt", "") or "")
|
|
25
|
+
now = _epoch(observedAt) or int(time.time())
|
|
26
|
+
createdAt = current.get("record", {}).get("createdAt", now) if current.get("ok") else now
|
|
27
|
+
record = {
|
|
28
|
+
"signalId": signalId,
|
|
29
|
+
"entity": str(analysisMap.get("entity", signalMap.get("entity", "unknown")) or "unknown"),
|
|
30
|
+
"confidence": _confidence(analysisMap.get("confidence", signalMap.get("confidence", 0.0))),
|
|
31
|
+
"sourcePlatform": _sourcePlatform(signalMap, analysisMap),
|
|
32
|
+
"query": str(analysisMap.get("query", signalMap.get("query", "")) or ""),
|
|
33
|
+
"signal": copy.deepcopy(signalMap),
|
|
34
|
+
"analysis": copy.deepcopy(analysisMap),
|
|
35
|
+
"createdAt": int(createdAt or now),
|
|
36
|
+
"updatedAt": now,
|
|
37
|
+
}
|
|
38
|
+
self._saveRecord(record)
|
|
39
|
+
return {"ok": True, "record": copy.deepcopy(record)}
|
|
40
|
+
|
|
41
|
+
def getSignal(self, signalId: str) -> dict[str, Any]:
|
|
42
|
+
signalId = str(signalId or "")
|
|
43
|
+
record = self._records.get(signalId)
|
|
44
|
+
if record is None and self._backingEnabled() and hasattr(self.backing, "fetchSignal"):
|
|
45
|
+
record = self.backing.fetchSignal(signalId)
|
|
46
|
+
if record:
|
|
47
|
+
self._records[signalId] = copy.deepcopy(record)
|
|
48
|
+
if not record:
|
|
49
|
+
return {"ok": False, "error": "signalNotFound", "signalId": signalId}
|
|
50
|
+
return {"ok": True, "record": copy.deepcopy(record)}
|
|
51
|
+
|
|
52
|
+
def listSignals(self, filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
53
|
+
self._loadBacking()
|
|
54
|
+
filters = filters or {}
|
|
55
|
+
entity = str(filters.get("entity", "") or "")
|
|
56
|
+
sourcePlatform = str(filters.get("sourcePlatform", "") or "")
|
|
57
|
+
query = str(filters.get("query", "") or "")
|
|
58
|
+
minConfidence = _optionalFloat(filters.get("minConfidence"))
|
|
59
|
+
records = list(self._records.values())
|
|
60
|
+
if entity:
|
|
61
|
+
records = [record for record in records if record.get("entity") == entity]
|
|
62
|
+
if sourcePlatform:
|
|
63
|
+
records = [record for record in records if record.get("sourcePlatform") == sourcePlatform]
|
|
64
|
+
if query:
|
|
65
|
+
records = [record for record in records if record.get("query") == query]
|
|
66
|
+
if minConfidence is not None:
|
|
67
|
+
records = [record for record in records if _confidence(record.get("confidence", 0.0)) >= minConfidence]
|
|
68
|
+
records.sort(key=lambda item: (int(item.get("createdAt", 0) or 0), str(item.get("signalId", ""))), reverse=True)
|
|
69
|
+
limit = _limit(filters.get("limit", 100))
|
|
70
|
+
return {
|
|
71
|
+
"ok": True,
|
|
72
|
+
"signals": [copy.deepcopy(record) for record in records[:limit]],
|
|
73
|
+
"count": min(len(records), limit),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def _saveRecord(self, record: dict[str, Any]) -> None:
|
|
77
|
+
signalId = str(record.get("signalId", "") or "")
|
|
78
|
+
if not signalId:
|
|
79
|
+
return
|
|
80
|
+
self._records[signalId] = copy.deepcopy(record)
|
|
81
|
+
if self._backingEnabled() and hasattr(self.backing, "upsertSignal"):
|
|
82
|
+
self.backing.upsertSignal(record)
|
|
83
|
+
|
|
84
|
+
def _loadBacking(self) -> None:
|
|
85
|
+
if self.backing is None or not self._backingEnabled() or not hasattr(self.backing, "listSignals"):
|
|
86
|
+
return
|
|
87
|
+
for record in self.backing.listSignals():
|
|
88
|
+
signalId = str(record.get("signalId", "") or "")
|
|
89
|
+
if signalId:
|
|
90
|
+
self._records[signalId] = copy.deepcopy(record)
|
|
91
|
+
|
|
92
|
+
def _backingEnabled(self) -> bool:
|
|
93
|
+
if self.backing is None:
|
|
94
|
+
return False
|
|
95
|
+
return bool(getattr(self.backing, "enabled", True))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _toMap(value: Any) -> dict[str, Any]:
|
|
99
|
+
if hasattr(value, "toMap"):
|
|
100
|
+
mapped = value.toMap()
|
|
101
|
+
return mapped if isinstance(mapped, dict) else {}
|
|
102
|
+
return copy.deepcopy(value) if isinstance(value, dict) else {}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _sourcePlatform(signal: dict[str, Any], analysis: dict[str, Any]) -> str:
|
|
106
|
+
source = signal.get("source", {}) if isinstance(signal.get("source"), dict) else {}
|
|
107
|
+
analysisSource = analysis.get("source", {}) if isinstance(analysis.get("source"), dict) else {}
|
|
108
|
+
return str(source.get("platform", analysisSource.get("platform", "")) or "")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _epoch(value: Any) -> int:
|
|
112
|
+
if isinstance(value, (int, float)):
|
|
113
|
+
return int(value or 0)
|
|
114
|
+
raw = str(value or "").strip()
|
|
115
|
+
if not raw:
|
|
116
|
+
return 0
|
|
117
|
+
if raw.endswith("Z"):
|
|
118
|
+
raw = raw[:-1] + "+00:00"
|
|
119
|
+
try:
|
|
120
|
+
parsed = datetime.fromisoformat(raw)
|
|
121
|
+
except Exception:
|
|
122
|
+
return 0
|
|
123
|
+
if parsed.tzinfo is None:
|
|
124
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
125
|
+
return int(parsed.timestamp())
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _confidence(value: Any) -> float:
|
|
129
|
+
try:
|
|
130
|
+
parsed = float(value or 0.0)
|
|
131
|
+
except (TypeError, ValueError):
|
|
132
|
+
return 0.0
|
|
133
|
+
return max(0.0, min(parsed, 1.0))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _optionalFloat(value: Any) -> float | None:
|
|
137
|
+
if value is None or value == "":
|
|
138
|
+
return None
|
|
139
|
+
return _confidence(value)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _limit(value: Any) -> int:
|
|
143
|
+
try:
|
|
144
|
+
parsed = int(value)
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
return 100
|
|
147
|
+
return max(1, min(parsed, 500))
|