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,17 @@
|
|
|
1
|
+
from picux.portals.templates import (
|
|
2
|
+
applyPortalActionTemplate,
|
|
3
|
+
getPortalActionTemplate,
|
|
4
|
+
getPortalRecoveryPlaybook,
|
|
5
|
+
instantiatePortalRecoveryPlaybook,
|
|
6
|
+
portalActionTemplates,
|
|
7
|
+
portalRecoveryPlaybooks,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"applyPortalActionTemplate",
|
|
12
|
+
"getPortalActionTemplate",
|
|
13
|
+
"getPortalRecoveryPlaybook",
|
|
14
|
+
"instantiatePortalRecoveryPlaybook",
|
|
15
|
+
"portalActionTemplates",
|
|
16
|
+
"portalRecoveryPlaybooks",
|
|
17
|
+
]
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from typing import Any
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
PORTAL_ACTION_TEMPLATES: tuple[dict[str, Any], ...] = (
|
|
9
|
+
{
|
|
10
|
+
"templateId": "checkStatus",
|
|
11
|
+
"name": "Check Account Status",
|
|
12
|
+
"kind": "checkStatus",
|
|
13
|
+
"description": "Open an authenticated portal status page and capture the latest case, order, claim, or ticket state.",
|
|
14
|
+
"targetPath": "/status",
|
|
15
|
+
"requiredInputs": ["recordRef"],
|
|
16
|
+
"steps": [
|
|
17
|
+
{"label": "Open status page", "action": "navigate", "proofRequired": False},
|
|
18
|
+
{"label": "Search record reference", "action": "fill", "selector": "[name='recordRef']", "valueKey": "recordRef", "proofRequired": False},
|
|
19
|
+
{"label": "Capture status proof", "action": "capture", "proofRequired": True},
|
|
20
|
+
],
|
|
21
|
+
"proofRequirements": ["domSnapshot", "confirmationOrStatus"],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"templateId": "retrieveRecord",
|
|
25
|
+
"name": "Retrieve Portal Record",
|
|
26
|
+
"kind": "retrieveRecord",
|
|
27
|
+
"description": "Find and capture a receipt, account record, policy document, order, or claim document from a portal.",
|
|
28
|
+
"targetPath": "/records",
|
|
29
|
+
"requiredInputs": ["recordRef"],
|
|
30
|
+
"steps": [
|
|
31
|
+
{"label": "Open records page", "action": "navigate", "proofRequired": False},
|
|
32
|
+
{"label": "Find record", "action": "fill", "selector": "[name='recordRef']", "valueKey": "recordRef", "proofRequired": False},
|
|
33
|
+
{"label": "Download or capture record", "action": "capture", "proofRequired": True},
|
|
34
|
+
],
|
|
35
|
+
"proofRequirements": ["downloadedRecordOrDomSnapshot"],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"templateId": "submitClaim",
|
|
39
|
+
"name": "Submit Claim Form",
|
|
40
|
+
"kind": "submitClaim",
|
|
41
|
+
"description": "Submit a merchant, carrier, utility, or provider claim form and capture a confirmation proof.",
|
|
42
|
+
"targetPath": "/claims/new",
|
|
43
|
+
"requiredInputs": ["claimText"],
|
|
44
|
+
"steps": [
|
|
45
|
+
{"label": "Open claim form", "action": "navigate", "proofRequired": False},
|
|
46
|
+
{"label": "Enter claim details", "action": "fill", "selector": "[name='claim']", "valueKey": "claimText", "proofRequired": False},
|
|
47
|
+
{"label": "Attach evidence", "action": "upload", "selector": "input[type='file']", "valueKey": "attachmentRefs", "proofRequired": False},
|
|
48
|
+
{"label": "Submit claim", "action": "click", "selector": "[type='submit']", "proofRequired": True},
|
|
49
|
+
{"label": "Capture confirmation", "action": "capture", "proofRequired": True},
|
|
50
|
+
],
|
|
51
|
+
"proofRequirements": ["confirmationNumber", "domSnapshot"],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"templateId": "uploadDocument",
|
|
55
|
+
"name": "Upload Document",
|
|
56
|
+
"kind": "uploadDocument",
|
|
57
|
+
"description": "Upload one or more evidence documents to an authenticated portal and capture upload proof.",
|
|
58
|
+
"targetPath": "/documents/upload",
|
|
59
|
+
"requiredInputs": ["attachmentRefs"],
|
|
60
|
+
"steps": [
|
|
61
|
+
{"label": "Open upload page", "action": "navigate", "proofRequired": False},
|
|
62
|
+
{"label": "Upload document", "action": "upload", "selector": "input[type='file']", "valueKey": "attachmentRefs", "proofRequired": False},
|
|
63
|
+
{"label": "Submit upload", "action": "click", "selector": "[type='submit']", "proofRequired": True},
|
|
64
|
+
{"label": "Capture upload proof", "action": "capture", "proofRequired": True},
|
|
65
|
+
],
|
|
66
|
+
"proofRequirements": ["uploadConfirmation", "domSnapshot"],
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
PORTAL_RECOVERY_PLAYBOOKS: tuple[dict[str, Any], ...] = (
|
|
71
|
+
{
|
|
72
|
+
"playbookId": "merchantDamageClaim",
|
|
73
|
+
"name": "Merchant Damage Claim",
|
|
74
|
+
"description": "Retrieve the purchase record, submit a merchant damage claim, and check claim status.",
|
|
75
|
+
"providerTypes": ["merchant", "retailer", "marketplace"],
|
|
76
|
+
"requiredInputs": ["recordRef", "claimText"],
|
|
77
|
+
"optionalInputs": ["attachmentRefs"],
|
|
78
|
+
"actions": [
|
|
79
|
+
{"ref": "retrieveOrderRecord", "templateId": "retrieveRecord", "label": "Retrieve order or receipt record", "targetPath": "/orders", "inputMap": {"recordRef": "recordRef"}},
|
|
80
|
+
{"ref": "submitDamageClaim", "templateId": "submitClaim", "label": "Submit damage claim", "targetPath": "/claims/new", "inputMap": {"claimText": "claimText", "attachmentRefs": "attachmentRefs"}},
|
|
81
|
+
{"ref": "checkClaimStatus", "templateId": "checkStatus", "label": "Check claim status", "targetPath": "/claims/status", "inputMap": {"recordRef": "recordRef"}},
|
|
82
|
+
],
|
|
83
|
+
"proofRequirements": ["purchaseRecord", "claimConfirmation", "statusSnapshot"],
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"playbookId": "carrierDamageClaim",
|
|
87
|
+
"name": "Carrier Damage Claim",
|
|
88
|
+
"description": "Retrieve shipment records, submit a carrier damage claim, upload evidence, and check claim status.",
|
|
89
|
+
"providerTypes": ["carrier", "logistics", "shipping"],
|
|
90
|
+
"requiredInputs": ["trackingNumber", "claimText", "attachmentRefs"],
|
|
91
|
+
"optionalInputs": ["recordRef"],
|
|
92
|
+
"actions": [
|
|
93
|
+
{"ref": "retrieveShipmentRecord", "templateId": "retrieveRecord", "label": "Retrieve shipment record", "targetPath": "/shipments", "inputMap": {"recordRef": "trackingNumber"}},
|
|
94
|
+
{"ref": "submitCarrierClaim", "templateId": "submitClaim", "label": "Submit carrier damage claim", "targetPath": "/claims/damage/new", "inputMap": {"claimText": "claimText", "attachmentRefs": "attachmentRefs"}},
|
|
95
|
+
{"ref": "uploadDamageEvidence", "templateId": "uploadDocument", "label": "Upload damage evidence", "targetPath": "/claims/documents/upload", "inputMap": {"attachmentRefs": "attachmentRefs"}},
|
|
96
|
+
{"ref": "checkCarrierClaimStatus", "templateId": "checkStatus", "label": "Check carrier claim status", "targetPath": "/claims/status", "inputMap": {"recordRef": "trackingNumber"}},
|
|
97
|
+
],
|
|
98
|
+
"proofRequirements": ["shipmentRecord", "claimConfirmation", "uploadConfirmation", "statusSnapshot"],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"playbookId": "utilityBillingDispute",
|
|
102
|
+
"name": "Utility Billing Dispute",
|
|
103
|
+
"description": "Retrieve billing records, submit a utility dispute, upload supporting documents, and check case status.",
|
|
104
|
+
"providerTypes": ["utility", "serviceProvider", "telecom"],
|
|
105
|
+
"requiredInputs": ["accountNumber", "claimText", "attachmentRefs"],
|
|
106
|
+
"optionalInputs": ["recordRef"],
|
|
107
|
+
"actions": [
|
|
108
|
+
{"ref": "retrieveBillingRecord", "templateId": "retrieveRecord", "label": "Retrieve billing record", "targetPath": "/billing/records", "inputMap": {"recordRef": "accountNumber"}},
|
|
109
|
+
{"ref": "submitBillingDispute", "templateId": "submitClaim", "label": "Submit billing dispute", "targetPath": "/billing/disputes/new", "inputMap": {"claimText": "claimText", "attachmentRefs": "attachmentRefs"}},
|
|
110
|
+
{"ref": "uploadDisputeDocuments", "templateId": "uploadDocument", "label": "Upload dispute documents", "targetPath": "/billing/documents/upload", "inputMap": {"attachmentRefs": "attachmentRefs"}},
|
|
111
|
+
{"ref": "checkDisputeStatus", "templateId": "checkStatus", "label": "Check dispute status", "targetPath": "/billing/disputes/status", "inputMap": {"recordRef": "accountNumber"}},
|
|
112
|
+
],
|
|
113
|
+
"proofRequirements": ["billingRecord", "disputeConfirmation", "uploadConfirmation", "statusSnapshot"],
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def portalActionTemplates(filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
119
|
+
filters = filters or {}
|
|
120
|
+
kind = str(filters.get("kind", "") or "")
|
|
121
|
+
records = [_templateRecord(item) for item in PORTAL_ACTION_TEMPLATES]
|
|
122
|
+
if kind:
|
|
123
|
+
records = [item for item in records if item.get("kind") == kind]
|
|
124
|
+
return {"ok": True, "templates": records, "count": len(records)}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def getPortalActionTemplate(templateId: str) -> dict[str, Any]:
|
|
128
|
+
requested = str(templateId or "")
|
|
129
|
+
for template in PORTAL_ACTION_TEMPLATES:
|
|
130
|
+
if template["templateId"] == requested:
|
|
131
|
+
return {"ok": True, "template": _templateRecord(template)}
|
|
132
|
+
return {"ok": False, "error": "portalTemplateNotFound", "templateId": requested}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def portalRecoveryPlaybooks(filters: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
136
|
+
filters = filters or {}
|
|
137
|
+
providerType = str(filters.get("providerType", filters.get("provider", "")) or "")
|
|
138
|
+
records = [_playbookRecord(item) for item in PORTAL_RECOVERY_PLAYBOOKS]
|
|
139
|
+
if providerType:
|
|
140
|
+
records = [item for item in records if providerType in item.get("providerTypes", [])]
|
|
141
|
+
return {"ok": True, "playbooks": records, "count": len(records)}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def getPortalRecoveryPlaybook(playbookId: str) -> dict[str, Any]:
|
|
145
|
+
requested = str(playbookId or "")
|
|
146
|
+
for playbook in PORTAL_RECOVERY_PLAYBOOKS:
|
|
147
|
+
if playbook["playbookId"] == requested:
|
|
148
|
+
return {"ok": True, "playbook": _playbookRecord(playbook)}
|
|
149
|
+
return {"ok": False, "error": "portalPlaybookNotFound", "playbookId": requested}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def instantiatePortalRecoveryPlaybook(playbookId: str, payload: dict[str, Any], *, session: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
153
|
+
found = getPortalRecoveryPlaybook(playbookId)
|
|
154
|
+
if not found.get("ok"):
|
|
155
|
+
return found
|
|
156
|
+
playbook = found["playbook"]
|
|
157
|
+
session = session or {}
|
|
158
|
+
inputs = payload.get("inputs", {}) if isinstance(payload.get("inputs"), dict) else {}
|
|
159
|
+
missing = _missingPlaybookInputs(playbook, inputs)
|
|
160
|
+
if missing:
|
|
161
|
+
return {"ok": False, "status": "needsInput", "playbook": playbook, "needsInput": missing}
|
|
162
|
+
baseUrl = str(payload.get("baseUrl", session.get("baseUrl", "")) or "")
|
|
163
|
+
targetUrls = payload.get("targetUrls", {}) if isinstance(payload.get("targetUrls"), dict) else {}
|
|
164
|
+
actions = []
|
|
165
|
+
for idx, item in enumerate(playbook.get("actions", [])):
|
|
166
|
+
if not isinstance(item, dict):
|
|
167
|
+
continue
|
|
168
|
+
ref = str(item.get("ref", f"action{idx + 1}") or f"action{idx + 1}")
|
|
169
|
+
templateId = str(item.get("templateId", "") or "")
|
|
170
|
+
targetUrl = str(targetUrls.get(ref, targetUrls.get(templateId, "")) or "")
|
|
171
|
+
if not targetUrl and baseUrl:
|
|
172
|
+
targetUrl = urljoin(baseUrl.rstrip("/") + "/", str(item.get("targetPath", "") or "").lstrip("/"))
|
|
173
|
+
actionInputs = _mappedInputs(item.get("inputMap", {}), inputs)
|
|
174
|
+
actions.append(
|
|
175
|
+
{
|
|
176
|
+
"portalSessionId": str(payload.get("portalSessionId", session.get("portalSessionId", payload.get("sessionId", ""))) or ""),
|
|
177
|
+
"templateId": templateId,
|
|
178
|
+
"caseId": str(payload.get("caseId", session.get("caseId", "")) or ""),
|
|
179
|
+
"conversationId": str(payload.get("conversationId", session.get("conversationId", "")) or ""),
|
|
180
|
+
"connectorId": str(payload.get("connectorId", session.get("connectorId", "")) or ""),
|
|
181
|
+
"provider": str(payload.get("provider", session.get("provider", "")) or ""),
|
|
182
|
+
"targetUrl": targetUrl,
|
|
183
|
+
"inputs": actionInputs,
|
|
184
|
+
"meta": {
|
|
185
|
+
"playbookId": playbookId,
|
|
186
|
+
"playbookActionRef": ref,
|
|
187
|
+
"playbookActionIndex": idx,
|
|
188
|
+
"playbookActionLabel": str(item.get("label", "") or ""),
|
|
189
|
+
"targetPath": str(item.get("targetPath", "") or ""),
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
return {"ok": True, "playbook": playbook, "actions": actions, "count": len(actions)}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def applyPortalActionTemplate(payload: dict[str, Any], *, session: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
197
|
+
templateId = str(payload.get("templateId", "") or "")
|
|
198
|
+
if not templateId:
|
|
199
|
+
return {"ok": True, "action": copy.deepcopy(payload), "template": {}}
|
|
200
|
+
found = getPortalActionTemplate(templateId)
|
|
201
|
+
if not found.get("ok"):
|
|
202
|
+
return found
|
|
203
|
+
template = found["template"]
|
|
204
|
+
session = session or {}
|
|
205
|
+
currentMeta = payload.get("meta", {}) if isinstance(payload.get("meta"), dict) else {}
|
|
206
|
+
targetUrl = str(payload.get("targetUrl", payload.get("url", "")) or "")
|
|
207
|
+
baseUrl = str(session.get("baseUrl", "") or "")
|
|
208
|
+
if not targetUrl and baseUrl:
|
|
209
|
+
targetUrl = urljoin(baseUrl.rstrip("/") + "/", str(template.get("targetPath", "") or "").lstrip("/"))
|
|
210
|
+
action = {
|
|
211
|
+
**payload,
|
|
212
|
+
"templateId": templateId,
|
|
213
|
+
"kind": str(payload.get("kind", "") or template.get("kind", "submitForm")),
|
|
214
|
+
"targetUrl": targetUrl,
|
|
215
|
+
"steps": payload.get("steps") if isinstance(payload.get("steps"), list) and payload.get("steps") else copy.deepcopy(template.get("steps", [])),
|
|
216
|
+
"meta": {
|
|
217
|
+
**currentMeta,
|
|
218
|
+
"templateId": templateId,
|
|
219
|
+
"templateName": template.get("name", ""),
|
|
220
|
+
"requiredInputs": template.get("requiredInputs", []),
|
|
221
|
+
"proofRequirements": template.get("proofRequirements", []),
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
return {"ok": True, "action": action, "template": template}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _templateRecord(template: dict[str, Any]) -> dict[str, Any]:
|
|
228
|
+
return {
|
|
229
|
+
"templateId": str(template.get("templateId", "") or ""),
|
|
230
|
+
"name": str(template.get("name", "") or ""),
|
|
231
|
+
"kind": str(template.get("kind", "") or ""),
|
|
232
|
+
"description": str(template.get("description", "") or ""),
|
|
233
|
+
"targetPath": str(template.get("targetPath", "") or ""),
|
|
234
|
+
"requiredInputs": [str(item) for item in template.get("requiredInputs", []) if str(item)],
|
|
235
|
+
"steps": [copy.deepcopy(item) for item in template.get("steps", []) if isinstance(item, dict)],
|
|
236
|
+
"proofRequirements": [str(item) for item in template.get("proofRequirements", []) if str(item)],
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _playbookRecord(playbook: dict[str, Any]) -> dict[str, Any]:
|
|
241
|
+
return {
|
|
242
|
+
"playbookId": str(playbook.get("playbookId", "") or ""),
|
|
243
|
+
"name": str(playbook.get("name", "") or ""),
|
|
244
|
+
"description": str(playbook.get("description", "") or ""),
|
|
245
|
+
"providerTypes": [str(item) for item in playbook.get("providerTypes", []) if str(item)],
|
|
246
|
+
"requiredInputs": [str(item) for item in playbook.get("requiredInputs", []) if str(item)],
|
|
247
|
+
"optionalInputs": [str(item) for item in playbook.get("optionalInputs", []) if str(item)],
|
|
248
|
+
"actions": [copy.deepcopy(item) for item in playbook.get("actions", []) if isinstance(item, dict)],
|
|
249
|
+
"proofRequirements": [str(item) for item in playbook.get("proofRequirements", []) if str(item)],
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _missingPlaybookInputs(playbook: dict[str, Any], inputs: dict[str, Any]) -> list[dict[str, Any]]:
|
|
254
|
+
missing = []
|
|
255
|
+
for key in playbook.get("requiredInputs", []):
|
|
256
|
+
clean = str(key or "")
|
|
257
|
+
value = inputs.get(clean)
|
|
258
|
+
if clean and (clean not in inputs or value is None or value == "" or value == []):
|
|
259
|
+
missing.append({"key": clean, "label": clean, "secret": False})
|
|
260
|
+
return missing
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _mappedInputs(inputMap: Any, inputs: dict[str, Any]) -> dict[str, Any]:
|
|
264
|
+
if not isinstance(inputMap, dict):
|
|
265
|
+
return dict(inputs)
|
|
266
|
+
mapped = {}
|
|
267
|
+
for targetKey, sourceKey in inputMap.items():
|
|
268
|
+
target = str(targetKey or "")
|
|
269
|
+
source = str(sourceKey or "")
|
|
270
|
+
if target and source in inputs:
|
|
271
|
+
mapped[target] = inputs[source]
|
|
272
|
+
return mapped
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Picux protocol transports and machine-readable envelopes."""
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib import request
|
|
7
|
+
|
|
8
|
+
from picux.config import resolve_env
|
|
9
|
+
from picux.protocols.a2a.envelope import createEnvelope
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class AgentMeshClient:
|
|
14
|
+
"""Minimal A2A delegation client using Picux env names with legacy fallback."""
|
|
15
|
+
|
|
16
|
+
base_url: str = ""
|
|
17
|
+
token: str = ""
|
|
18
|
+
timeout_seconds: int = 20
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def fromEnv(cls) -> "AgentMeshClient":
|
|
22
|
+
return cls(
|
|
23
|
+
base_url=resolve_env("PICUX_A2A_BASE_URL").value.strip().rstrip("/"),
|
|
24
|
+
token=resolve_env("PICUX_A2A_TOKEN").value.strip(),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def enabled(self) -> bool:
|
|
29
|
+
return bool(self.base_url)
|
|
30
|
+
|
|
31
|
+
def delegate(self, *, target_agent: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
32
|
+
if not self.enabled:
|
|
33
|
+
raise RuntimeError("a2a_client_disabled")
|
|
34
|
+
envelope = createEnvelope({**payload, "toAgent": target_agent})
|
|
35
|
+
if not envelope["ok"]:
|
|
36
|
+
raise RuntimeError(f"a2a_invalid_envelope:{','.join(envelope['errors'])}")
|
|
37
|
+
headers = {"Content-Type": "application/json"}
|
|
38
|
+
if self.token:
|
|
39
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
40
|
+
url = f"{self.base_url}/agents/{target_agent}/invoke"
|
|
41
|
+
req = request.Request(
|
|
42
|
+
url,
|
|
43
|
+
data=json.dumps({"envelope": envelope["envelope"], "payload": payload}, ensure_ascii=True).encode("utf-8"),
|
|
44
|
+
headers=headers,
|
|
45
|
+
method="POST",
|
|
46
|
+
)
|
|
47
|
+
with request.urlopen(req, timeout=max(5, self.timeout_seconds)) as response:
|
|
48
|
+
body = json.loads(response.read().decode("utf-8"))
|
|
49
|
+
if not isinstance(body, dict):
|
|
50
|
+
raise RuntimeError("a2a_invalid_response")
|
|
51
|
+
return body
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from picux import __version__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Clock = Callable[[], datetime | str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class A2AEnvelope:
|
|
18
|
+
msgId: str
|
|
19
|
+
kind: str
|
|
20
|
+
fromAgent: str
|
|
21
|
+
toAgent: str
|
|
22
|
+
task: dict[str, Any]
|
|
23
|
+
caps: tuple[str, ...] = ()
|
|
24
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
25
|
+
traceId: str = ""
|
|
26
|
+
createdAt: str = ""
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def fromPayload(cls, payload: dict[str, Any], *, clock: Clock | None = None) -> "A2AEnvelope":
|
|
30
|
+
now = _time(clock or (lambda: datetime.now(timezone.utc)))
|
|
31
|
+
task = payload.get("task") if isinstance(payload.get("task"), dict) else {}
|
|
32
|
+
body = payload.get("payload") if isinstance(payload.get("payload"), dict) else {}
|
|
33
|
+
fromAgent = str(payload.get("fromAgent", "picux") or "picux")
|
|
34
|
+
toAgent = str(payload.get("toAgent", payload.get("agentId", payload.get("targetAgent", ""))) or "")
|
|
35
|
+
kind = str(payload.get("kind", "delegate") or "delegate")
|
|
36
|
+
caps = tuple(_clean(payload.get("caps", payload.get("capabilities", []))))
|
|
37
|
+
traceId = str(payload.get("traceId", task.get("taskId", "")) or "")
|
|
38
|
+
base = {
|
|
39
|
+
"kind": kind,
|
|
40
|
+
"fromAgent": fromAgent,
|
|
41
|
+
"toAgent": toAgent,
|
|
42
|
+
"task": task,
|
|
43
|
+
"caps": list(caps),
|
|
44
|
+
"payload": body,
|
|
45
|
+
"traceId": traceId,
|
|
46
|
+
"createdAt": now,
|
|
47
|
+
}
|
|
48
|
+
msgId = "msg_" + hashlib.sha256(_json(base).encode("utf-8")).hexdigest()[:24]
|
|
49
|
+
return cls(
|
|
50
|
+
msgId=msgId,
|
|
51
|
+
kind=kind,
|
|
52
|
+
fromAgent=fromAgent,
|
|
53
|
+
toAgent=toAgent,
|
|
54
|
+
task=task,
|
|
55
|
+
caps=caps,
|
|
56
|
+
payload=body,
|
|
57
|
+
traceId=traceId,
|
|
58
|
+
createdAt=now,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def toMap(self) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"msgId": self.msgId,
|
|
64
|
+
"kind": self.kind,
|
|
65
|
+
"fromAgent": self.fromAgent,
|
|
66
|
+
"toAgent": self.toAgent,
|
|
67
|
+
"task": self.task,
|
|
68
|
+
"caps": list(self.caps),
|
|
69
|
+
"payload": self.payload,
|
|
70
|
+
"traceId": self.traceId,
|
|
71
|
+
"createdAt": self.createdAt,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def createEnvelope(payload: dict[str, Any], *, clock: Clock | None = None) -> dict[str, Any]:
|
|
76
|
+
envelope = A2AEnvelope.fromPayload(payload, clock=clock).toMap()
|
|
77
|
+
errors = validateEnvelope(envelope)
|
|
78
|
+
return {"ok": not errors, "errors": errors, "envelope": envelope}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validateEnvelope(envelope: dict[str, Any]) -> list[str]:
|
|
82
|
+
errors: list[str] = []
|
|
83
|
+
if not str(envelope.get("msgId", "") or "").startswith("msg_"):
|
|
84
|
+
errors.append("invalid:msgId")
|
|
85
|
+
for key in ("kind", "fromAgent", "toAgent", "createdAt"):
|
|
86
|
+
if not str(envelope.get(key, "") or ""):
|
|
87
|
+
errors.append(f"missing:{key}")
|
|
88
|
+
if not isinstance(envelope.get("task"), dict):
|
|
89
|
+
errors.append("invalid:task")
|
|
90
|
+
if not isinstance(envelope.get("payload"), dict):
|
|
91
|
+
errors.append("invalid:payload")
|
|
92
|
+
return errors
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def a2aContract() -> dict[str, Any]:
|
|
96
|
+
return {
|
|
97
|
+
"ok": True,
|
|
98
|
+
"protocol": "picux-a2a",
|
|
99
|
+
"version": __version__,
|
|
100
|
+
"transport": "external",
|
|
101
|
+
"envelope": {
|
|
102
|
+
"required": ["msgId", "kind", "fromAgent", "toAgent", "task", "createdAt"],
|
|
103
|
+
"optional": ["caps", "payload", "traceId"],
|
|
104
|
+
"schema": "schemas/protocols/a2a-envelope.schema.json",
|
|
105
|
+
},
|
|
106
|
+
"endpoints": {
|
|
107
|
+
"contract": "/v1/a2a/contract",
|
|
108
|
+
"createEnvelope": "/v1/a2a/envelopes",
|
|
109
|
+
"listEnvelopes": "/v1/a2a/envelopes",
|
|
110
|
+
"getEnvelope": "/v1/a2a/envelopes/{msgId}",
|
|
111
|
+
"updateEnvelope": "/v1/a2a/envelopes/{msgId}/update",
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _clean(value: Any) -> list[str]:
|
|
117
|
+
if isinstance(value, str):
|
|
118
|
+
value = [value]
|
|
119
|
+
if not isinstance(value, (list, tuple, set)):
|
|
120
|
+
return []
|
|
121
|
+
return [str(item).strip().lower() for item in value if str(item).strip()]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _time(clock: Clock) -> str:
|
|
125
|
+
value = clock()
|
|
126
|
+
if isinstance(value, datetime):
|
|
127
|
+
return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
128
|
+
return str(value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _json(payload: dict[str, Any]) -> str:
|
|
132
|
+
return json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib import request
|
|
8
|
+
|
|
9
|
+
from picux.config import resolve_env
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class MCPClient:
|
|
14
|
+
"""Minimal MCP JSON-RPC client using Picux env names with legacy fallback."""
|
|
15
|
+
|
|
16
|
+
base_url: str = ""
|
|
17
|
+
token: str = ""
|
|
18
|
+
timeout_seconds: int = 20
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def fromEnv(cls) -> "MCPClient":
|
|
22
|
+
return cls(
|
|
23
|
+
base_url=resolve_env("PICUX_MCP_BASE_URL").value.strip().rstrip("/"),
|
|
24
|
+
token=resolve_env("PICUX_MCP_TOKEN").value.strip(),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def enabled(self) -> bool:
|
|
29
|
+
return bool(self.base_url)
|
|
30
|
+
|
|
31
|
+
def endpoint(self) -> str:
|
|
32
|
+
if not self.base_url:
|
|
33
|
+
raise RuntimeError("mcp_client_disabled")
|
|
34
|
+
if self.base_url.rstrip("/").endswith("/mcp"):
|
|
35
|
+
return self.base_url.rstrip("/")
|
|
36
|
+
return f"{self.base_url.rstrip('/')}/mcp"
|
|
37
|
+
|
|
38
|
+
def call(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
39
|
+
payload = {
|
|
40
|
+
"jsonrpc": "2.0",
|
|
41
|
+
"id": f"picux-mcp-{int(time.time() * 1000)}",
|
|
42
|
+
"method": method,
|
|
43
|
+
"params": params or {},
|
|
44
|
+
}
|
|
45
|
+
headers = {"Content-Type": "application/json"}
|
|
46
|
+
if self.token:
|
|
47
|
+
headers["Authorization"] = f"Bearer {self.token}"
|
|
48
|
+
req = request.Request(
|
|
49
|
+
self.endpoint(),
|
|
50
|
+
data=json.dumps(payload, ensure_ascii=True).encode("utf-8"),
|
|
51
|
+
headers=headers,
|
|
52
|
+
method="POST",
|
|
53
|
+
)
|
|
54
|
+
with request.urlopen(req, timeout=max(5, self.timeout_seconds)) as response:
|
|
55
|
+
body = json.loads(response.read().decode("utf-8"))
|
|
56
|
+
if not isinstance(body, dict):
|
|
57
|
+
raise RuntimeError("mcp_invalid_response")
|
|
58
|
+
if "error" in body:
|
|
59
|
+
raise RuntimeError(f"mcp_error:{body['error']}")
|
|
60
|
+
result = body.get("result")
|
|
61
|
+
if not isinstance(result, dict):
|
|
62
|
+
raise RuntimeError("mcp_invalid_result")
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
def listTools(self) -> dict[str, Any]:
|
|
66
|
+
return self.call("tools/list")
|
|
67
|
+
|
|
68
|
+
def callTool(self, name: str, args: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
69
|
+
return self.call("tools/call", {"name": name, "arguments": args or {}})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from picux.api import PicuxApiService
|
|
6
|
+
from picux.contracts import CONTRACT_ENDPOINTS, mcpTools
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PicuxMCPContract:
|
|
10
|
+
"""MCP-facing contract for external apps, services, and agents."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, *, service: PicuxApiService | None = None) -> None:
|
|
13
|
+
self.service = service or PicuxApiService()
|
|
14
|
+
self._tools = {endpoint.mcpTool: endpoint for endpoint in CONTRACT_ENDPOINTS if endpoint.mcpTool}
|
|
15
|
+
|
|
16
|
+
def listTools(self) -> dict[str, Any]:
|
|
17
|
+
return {"tools": mcpTools()}
|
|
18
|
+
|
|
19
|
+
def callTool(self, name: str, args: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
20
|
+
if name not in self._tools:
|
|
21
|
+
return {"ok": False, "error": "toolNotFound", "tool": name}
|
|
22
|
+
args = args or {}
|
|
23
|
+
endpoint = self._tools[name]
|
|
24
|
+
path = self._path(endpoint.path, args)
|
|
25
|
+
status, payload = self.service.handle(endpoint.method, path, args, internal=True)
|
|
26
|
+
if status >= 400:
|
|
27
|
+
return {"ok": False, "status": status, "payload": payload}
|
|
28
|
+
return {"ok": True, "status": status, "payload": payload}
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _path(template: str, args: dict[str, Any]) -> str:
|
|
32
|
+
path = template
|
|
33
|
+
for key in (
|
|
34
|
+
"taskId",
|
|
35
|
+
"mandateId",
|
|
36
|
+
"receiptId",
|
|
37
|
+
"artifactId",
|
|
38
|
+
"povId",
|
|
39
|
+
"packId",
|
|
40
|
+
"agentId",
|
|
41
|
+
"connectorId",
|
|
42
|
+
"proxyId",
|
|
43
|
+
"escrowId",
|
|
44
|
+
"railId",
|
|
45
|
+
"challengeId",
|
|
46
|
+
"resourceId",
|
|
47
|
+
"adapterId",
|
|
48
|
+
"cardId",
|
|
49
|
+
"schemaId",
|
|
50
|
+
"msgId",
|
|
51
|
+
"signalId",
|
|
52
|
+
"eventId",
|
|
53
|
+
"subId",
|
|
54
|
+
"deliveryId",
|
|
55
|
+
"templateId",
|
|
56
|
+
"playbookId",
|
|
57
|
+
"sessionId",
|
|
58
|
+
"threadId",
|
|
59
|
+
"touchpointId",
|
|
60
|
+
"actionId",
|
|
61
|
+
"caseId",
|
|
62
|
+
"portalSessionId",
|
|
63
|
+
"portalActionId",
|
|
64
|
+
):
|
|
65
|
+
if "{" + key + "}" in path:
|
|
66
|
+
path = path.replace("{" + key + "}", str(args.get(key, "")))
|
|
67
|
+
return path
|