shopping-cli 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shopping_cli/__init__.py +3 -0
- shopping_cli/agents/__init__.py +1 -0
- shopping_cli/agents/agent_cli.py +31 -0
- shopping_cli/agents/buyer_cli.py +163 -0
- shopping_cli/agents/merchant_agent.py +263 -0
- shopping_cli/agents/merchant_daemon.py +872 -0
- shopping_cli/agents/tools.py +488 -0
- shopping_cli/api/__init__.py +1 -0
- shopping_cli/api/app.py +1353 -0
- shopping_cli/api/auth.py +125 -0
- shopping_cli/api/fallback_asgi.py +219 -0
- shopping_cli/api/handlers/__init__.py +1 -0
- shopping_cli/api/handlers/agents.py +277 -0
- shopping_cli/api/handlers/audit.py +97 -0
- shopping_cli/api/handlers/buyer.py +291 -0
- shopping_cli/api/handlers/catalog.py +392 -0
- shopping_cli/api/handlers/common.py +76 -0
- shopping_cli/api/handlers/conversations.py +172 -0
- shopping_cli/api/handlers/erp.py +127 -0
- shopping_cli/api/handlers/human_review.py +293 -0
- shopping_cli/api/handlers/listings_projection.py +42 -0
- shopping_cli/api/handlers/negotiation.py +121 -0
- shopping_cli/api/idempotency.py +291 -0
- shopping_cli/api/limits.py +59 -0
- shopping_cli/api/route_registry.py +105 -0
- shopping_cli/api/routes_agents.py +13 -0
- shopping_cli/api/routes_conversations.py +13 -0
- shopping_cli/api/routes_marketplace.py +13 -0
- shopping_cli/api/routes_merchants.py +13 -0
- shopping_cli/api/server.py +31 -0
- shopping_cli/cli.py +897 -0
- shopping_cli/cli_agent_commands.py +386 -0
- shopping_cli/cli_buyer_commands.py +231 -0
- shopping_cli/cli_catalog_commands.py +281 -0
- shopping_cli/cli_common.py +135 -0
- shopping_cli/cli_conversation_commands.py +167 -0
- shopping_cli/cli_erp_commands.py +60 -0
- shopping_cli/cli_listing_commands.py +35 -0
- shopping_cli/commerce/__init__.py +6 -0
- shopping_cli/commerce/adapters.py +310 -0
- shopping_cli/commerce/data_source.py +95 -0
- shopping_cli/config.py +262 -0
- shopping_cli/contracts/shopping.negotiation/0.1/capabilities.schema.json +47 -0
- shopping_cli/contracts/shopping.negotiation/0.1/decision.schema.json +120 -0
- shopping_cli/contracts/shopping.negotiation/0.1/policy-result.schema.json +52 -0
- shopping_cli/contracts/shopping.negotiation/0.1/snapshot.schema.json +172 -0
- shopping_cli/core/__init__.py +1 -0
- shopping_cli/core/catalog.py +1281 -0
- shopping_cli/core/channels.py +329 -0
- shopping_cli/core/conversations.py +502 -0
- shopping_cli/core/delivery.py +7 -0
- shopping_cli/core/errors.py +43 -0
- shopping_cli/core/harness.py +345 -0
- shopping_cli/core/limits.py +137 -0
- shopping_cli/core/negotiation.py +273 -0
- shopping_cli/core/policies.py +356 -0
- shopping_cli/core/risk.py +101 -0
- shopping_cli/core/tokens.py +33 -0
- shopping_cli/data_sources/__init__.py +12 -0
- shopping_cli/data_sources/erp_source.py +440 -0
- shopping_cli/db/__init__.py +1 -0
- shopping_cli/db/migrations.py +415 -0
- shopping_cli/db/models.py +325 -0
- shopping_cli/db/provenance.py +21 -0
- shopping_cli/db/session.py +108 -0
- shopping_cli/http_client.py +149 -0
- shopping_cli/listings/__init__.py +4 -0
- shopping_cli/listings/projection.py +148 -0
- shopping_cli/services/__init__.py +1 -0
- shopping_cli/services/agents.py +244 -0
- shopping_cli/services/audit.py +34 -0
- shopping_cli/services/buyer_bootstrap.py +23 -0
- shopping_cli/services/conversations.py +209 -0
- shopping_cli/services/human_review.py +165 -0
- shopping_cli/services/negotiation.py +929 -0
- shopping_cli/services/tokens.py +318 -0
- shopping_cli-3.0.0.dist-info/METADATA +219 -0
- shopping_cli-3.0.0.dist-info/RECORD +82 -0
- shopping_cli-3.0.0.dist-info/WHEEL +5 -0
- shopping_cli-3.0.0.dist-info/entry_points.txt +4 -0
- shopping_cli-3.0.0.dist-info/licenses/LICENSE +201 -0
- shopping_cli-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""Typed marketplace tools used by resident merchant agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import sqlite3
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from typing import Any, Protocol
|
|
9
|
+
|
|
10
|
+
from shopping_cli import VERSION
|
|
11
|
+
from shopping_cli.core.catalog import product_summary, require_merchant
|
|
12
|
+
from shopping_cli.core.conversations import add_flag, append_message, waiting_merchant_conversations
|
|
13
|
+
from shopping_cli.core.errors import ValidationError
|
|
14
|
+
from shopping_cli.core.harness import abandon_agent_message, abandon_stale_agent_messages, claim_agent_message, complete_agent_message, fail_agent_message
|
|
15
|
+
from shopping_cli.db.session import encode_json, now_iso
|
|
16
|
+
from shopping_cli.http_client import MarketplaceHTTPClient, MarketplaceHTTPError
|
|
17
|
+
|
|
18
|
+
DEFAULT_CAPABILITIES = ["catalog", "inventory", "delivery", "consultation"]
|
|
19
|
+
AGENT_STATUSES = {"online", "away", "human_required"}
|
|
20
|
+
MAX_SQLITE_INTEGER = 2**63 - 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _non_negative_whole_int(value: Any, field_name: str, default: int = 0) -> int:
|
|
24
|
+
if value in (None, ""):
|
|
25
|
+
return default
|
|
26
|
+
if isinstance(value, bool):
|
|
27
|
+
raise ValueError(f"{field_name} must be a whole number")
|
|
28
|
+
if isinstance(value, int):
|
|
29
|
+
number = value
|
|
30
|
+
elif isinstance(value, float):
|
|
31
|
+
if not math.isfinite(value) or not value.is_integer():
|
|
32
|
+
raise ValueError(f"{field_name} must be a whole number")
|
|
33
|
+
number = int(value)
|
|
34
|
+
else:
|
|
35
|
+
try:
|
|
36
|
+
number = int(str(value).strip())
|
|
37
|
+
except ValueError as exc:
|
|
38
|
+
raise ValueError(f"{field_name} must be a whole number") from exc
|
|
39
|
+
if number < 0:
|
|
40
|
+
raise ValueError(f"{field_name} must be non-negative")
|
|
41
|
+
if number > MAX_SQLITE_INTEGER:
|
|
42
|
+
raise ValueError(f"{field_name} must be <= {MAX_SQLITE_INTEGER}")
|
|
43
|
+
return number
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _positive_whole_int(value: Any, field_name: str) -> int:
|
|
47
|
+
number = _non_negative_whole_int(value, field_name)
|
|
48
|
+
if number <= 0:
|
|
49
|
+
raise ValueError(f"{field_name} must be greater than 0")
|
|
50
|
+
return number
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _normalize_agent_status(value: Any) -> str:
|
|
54
|
+
status = str(value or "").strip() or "online"
|
|
55
|
+
if status not in AGENT_STATUSES:
|
|
56
|
+
raise ValidationError(f"Unknown agent status: {status}")
|
|
57
|
+
return status
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize_capabilities(value: Any) -> list[str]:
|
|
61
|
+
if value is None:
|
|
62
|
+
return list(DEFAULT_CAPABILITIES)
|
|
63
|
+
if not isinstance(value, list):
|
|
64
|
+
raise ValidationError("agent capabilities must be a list of strings")
|
|
65
|
+
normalized: list[str] = []
|
|
66
|
+
for item in value:
|
|
67
|
+
if not isinstance(item, str):
|
|
68
|
+
raise ValidationError("agent capabilities must be a list of strings")
|
|
69
|
+
text = item.strip()
|
|
70
|
+
if text:
|
|
71
|
+
normalized.append(text)
|
|
72
|
+
return normalized or list(DEFAULT_CAPABILITIES)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MerchantAgentTools(Protocol):
|
|
76
|
+
def heartbeat(
|
|
77
|
+
self,
|
|
78
|
+
merchant_id: str,
|
|
79
|
+
status: str = "online",
|
|
80
|
+
last_error: str = "",
|
|
81
|
+
checked_count: int = 0,
|
|
82
|
+
replied_count: int = 0,
|
|
83
|
+
) -> dict[str, Any]:
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
def waiting_merchant_conversations(self, merchant_id: str) -> list[dict[str, Any]]:
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
def product_summary(self, sku: str) -> dict[str, Any]:
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
def append_message(
|
|
93
|
+
self,
|
|
94
|
+
conversation_id: str,
|
|
95
|
+
sender: str,
|
|
96
|
+
intent: str,
|
|
97
|
+
text: str,
|
|
98
|
+
structured_payload: dict[str, Any],
|
|
99
|
+
status: str,
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
def add_flag(self, conversation_id: str, reason: str, sku: str = "") -> dict[str, Any]:
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
def claim_message(self, agent_id: str, conversation_id: str, message_id: int, idempotency_key: str) -> dict[str, Any]:
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
def complete_message(self, agent_id: str, message_id: int) -> dict[str, Any]:
|
|
110
|
+
...
|
|
111
|
+
|
|
112
|
+
def fail_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
|
|
113
|
+
...
|
|
114
|
+
|
|
115
|
+
def abandon_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
def abandon_stale_messages(self, agent_id: str, stale_after_seconds: int = 300) -> list[dict[str, Any]]:
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def record_heartbeat(
|
|
123
|
+
conn: sqlite3.Connection,
|
|
124
|
+
merchant_id: str,
|
|
125
|
+
status: str = "online",
|
|
126
|
+
capabilities: list[str] | None = None,
|
|
127
|
+
pid: int = 0,
|
|
128
|
+
version: str = VERSION,
|
|
129
|
+
last_error: str = "",
|
|
130
|
+
checked_count: int = 0,
|
|
131
|
+
replied_count: int = 0,
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
merchant_id = str(merchant_id or "").strip()
|
|
134
|
+
status = _normalize_agent_status(status)
|
|
135
|
+
pid = _non_negative_whole_int(pid, "pid")
|
|
136
|
+
checked_count = _non_negative_whole_int(checked_count, "checked_count")
|
|
137
|
+
replied_count = _non_negative_whole_int(replied_count, "replied_count")
|
|
138
|
+
capabilities = _normalize_capabilities(capabilities)
|
|
139
|
+
require_merchant(conn, merchant_id)
|
|
140
|
+
agent_id = f"shopping-cli-merchant-agent:{merchant_id}"
|
|
141
|
+
now = now_iso()
|
|
142
|
+
conn.execute(
|
|
143
|
+
"""
|
|
144
|
+
insert into agents(
|
|
145
|
+
id, type, owner_id, status, capabilities_json, last_seen_at,
|
|
146
|
+
pid, version, last_error, checked_count, replied_count
|
|
147
|
+
)
|
|
148
|
+
values (?, 'merchant', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
149
|
+
on conflict(id) do update set
|
|
150
|
+
status = excluded.status,
|
|
151
|
+
capabilities_json = excluded.capabilities_json,
|
|
152
|
+
last_seen_at = excluded.last_seen_at,
|
|
153
|
+
pid = excluded.pid,
|
|
154
|
+
version = excluded.version,
|
|
155
|
+
last_error = excluded.last_error,
|
|
156
|
+
checked_count = excluded.checked_count,
|
|
157
|
+
replied_count = excluded.replied_count
|
|
158
|
+
""",
|
|
159
|
+
(
|
|
160
|
+
agent_id,
|
|
161
|
+
merchant_id,
|
|
162
|
+
status,
|
|
163
|
+
encode_json(capabilities),
|
|
164
|
+
now,
|
|
165
|
+
pid,
|
|
166
|
+
version or VERSION,
|
|
167
|
+
last_error or "",
|
|
168
|
+
checked_count,
|
|
169
|
+
replied_count,
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
return {
|
|
173
|
+
"id": agent_id,
|
|
174
|
+
"type": "merchant",
|
|
175
|
+
"owner_id": merchant_id,
|
|
176
|
+
"status": status,
|
|
177
|
+
"capabilities": capabilities,
|
|
178
|
+
"last_seen_at": now,
|
|
179
|
+
"pid": pid,
|
|
180
|
+
"version": version or VERSION,
|
|
181
|
+
"last_error": last_error or "",
|
|
182
|
+
"checked_count": checked_count,
|
|
183
|
+
"replied_count": replied_count,
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class SQLiteMerchantAgentTools:
|
|
188
|
+
def __init__(self, conn: sqlite3.Connection):
|
|
189
|
+
self.conn = conn
|
|
190
|
+
|
|
191
|
+
def heartbeat(
|
|
192
|
+
self,
|
|
193
|
+
merchant_id: str,
|
|
194
|
+
status: str = "online",
|
|
195
|
+
last_error: str = "",
|
|
196
|
+
checked_count: int = 0,
|
|
197
|
+
replied_count: int = 0,
|
|
198
|
+
) -> dict[str, Any]:
|
|
199
|
+
return record_heartbeat(
|
|
200
|
+
self.conn,
|
|
201
|
+
merchant_id,
|
|
202
|
+
status=status,
|
|
203
|
+
last_error=last_error,
|
|
204
|
+
checked_count=checked_count,
|
|
205
|
+
replied_count=replied_count,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def waiting_merchant_conversations(self, merchant_id: str) -> list[dict[str, Any]]:
|
|
209
|
+
return waiting_merchant_conversations(self.conn, merchant_id)
|
|
210
|
+
|
|
211
|
+
def product_summary(self, sku: str) -> dict[str, Any]:
|
|
212
|
+
return product_summary(self.conn, sku)
|
|
213
|
+
|
|
214
|
+
def append_message(
|
|
215
|
+
self,
|
|
216
|
+
conversation_id: str,
|
|
217
|
+
sender: str,
|
|
218
|
+
intent: str,
|
|
219
|
+
text: str,
|
|
220
|
+
structured_payload: dict[str, Any],
|
|
221
|
+
status: str,
|
|
222
|
+
) -> dict[str, Any]:
|
|
223
|
+
return append_message(
|
|
224
|
+
self.conn,
|
|
225
|
+
conversation_id,
|
|
226
|
+
sender,
|
|
227
|
+
intent,
|
|
228
|
+
text,
|
|
229
|
+
structured_payload=structured_payload,
|
|
230
|
+
status=status,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def add_flag(self, conversation_id: str, reason: str, sku: str = "") -> dict[str, Any]:
|
|
234
|
+
return add_flag(self.conn, conversation_id, reason, sku=sku)
|
|
235
|
+
|
|
236
|
+
def claim_message(self, agent_id: str, conversation_id: str, message_id: int, idempotency_key: str) -> dict[str, Any]:
|
|
237
|
+
return claim_agent_message(self.conn, agent_id, conversation_id, message_id, idempotency_key)
|
|
238
|
+
|
|
239
|
+
def complete_message(self, agent_id: str, message_id: int) -> dict[str, Any]:
|
|
240
|
+
return complete_agent_message(self.conn, agent_id, message_id)
|
|
241
|
+
|
|
242
|
+
def fail_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
|
|
243
|
+
return fail_agent_message(self.conn, agent_id, message_id, error)
|
|
244
|
+
|
|
245
|
+
def abandon_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
|
|
246
|
+
return abandon_agent_message(self.conn, agent_id, message_id, error)
|
|
247
|
+
|
|
248
|
+
def abandon_stale_messages(self, agent_id: str, stale_after_seconds: int = 300) -> list[dict[str, Any]]:
|
|
249
|
+
return abandon_stale_agent_messages(self.conn, agent_id, stale_after_seconds=stale_after_seconds)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
HTTPMarketplaceError = MarketplaceHTTPError
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class HTTPMerchantAgentTools:
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
base_url: str,
|
|
259
|
+
merchant_id: str,
|
|
260
|
+
merchant_token: str,
|
|
261
|
+
timeout: float = 10.0,
|
|
262
|
+
opener: Any | None = None,
|
|
263
|
+
host: str = "",
|
|
264
|
+
session_id: str = "",
|
|
265
|
+
allow_insecure_http: bool = False,
|
|
266
|
+
):
|
|
267
|
+
self.merchant_id = str(merchant_id or "").strip()
|
|
268
|
+
if not self.merchant_id:
|
|
269
|
+
raise ValueError("merchant_id is required")
|
|
270
|
+
self.merchant_token = str(merchant_token or "").strip()
|
|
271
|
+
if not self.merchant_token:
|
|
272
|
+
raise ValueError("merchant_token is required")
|
|
273
|
+
self.http = MarketplaceHTTPClient(
|
|
274
|
+
base_url,
|
|
275
|
+
self.merchant_token,
|
|
276
|
+
timeout=timeout,
|
|
277
|
+
opener=opener,
|
|
278
|
+
allow_insecure_http=allow_insecure_http,
|
|
279
|
+
)
|
|
280
|
+
self.base_url = self.http.base_url
|
|
281
|
+
self.timeout = self.http.timeout
|
|
282
|
+
self.host = str(host or "")
|
|
283
|
+
self.session_id = str(session_id or "")
|
|
284
|
+
self._message_created_review_flags: dict[tuple[str, str], dict[str, Any]] = {}
|
|
285
|
+
|
|
286
|
+
def _merchant_payload(self, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
287
|
+
merged = dict(payload or {})
|
|
288
|
+
merged["merchant_id"] = self.merchant_id
|
|
289
|
+
merged["merchant_token"] = self.merchant_token
|
|
290
|
+
return merged
|
|
291
|
+
|
|
292
|
+
def _token_payload(self, payload: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
293
|
+
merged = dict(payload or {})
|
|
294
|
+
merged["merchant_token"] = self.merchant_token
|
|
295
|
+
return merged
|
|
296
|
+
|
|
297
|
+
def _check_merchant(self, merchant_id: str) -> None:
|
|
298
|
+
if merchant_id != self.merchant_id:
|
|
299
|
+
raise ValueError(f"HTTP tools are scoped to merchant {self.merchant_id}, not {merchant_id}")
|
|
300
|
+
|
|
301
|
+
def _should_record_tool_calls(self) -> bool:
|
|
302
|
+
return bool(self.host or self.session_id)
|
|
303
|
+
|
|
304
|
+
def _record_tool_call(
|
|
305
|
+
self,
|
|
306
|
+
tool: str,
|
|
307
|
+
conversation_id: str = "",
|
|
308
|
+
status: str = "ok",
|
|
309
|
+
error: str = "",
|
|
310
|
+
) -> None:
|
|
311
|
+
if not self._should_record_tool_calls():
|
|
312
|
+
return
|
|
313
|
+
payload = self._token_payload(
|
|
314
|
+
{
|
|
315
|
+
"merchant_id": self.merchant_id,
|
|
316
|
+
"tool": tool,
|
|
317
|
+
"status": status,
|
|
318
|
+
"host": self.host,
|
|
319
|
+
"session_id": self.session_id,
|
|
320
|
+
"actor": f"shopping-cli-merchant-agent:{self.merchant_id}",
|
|
321
|
+
"source_id": f"shopping-cli-merchant-agent:{self.merchant_id}",
|
|
322
|
+
"token_scope": "merchant_agent",
|
|
323
|
+
"error": error,
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
if conversation_id:
|
|
327
|
+
payload["conversation_id"] = conversation_id
|
|
328
|
+
try:
|
|
329
|
+
self._request("POST", "/audit/tool-calls", payload)
|
|
330
|
+
except HTTPMarketplaceError:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
def _request(
|
|
334
|
+
self,
|
|
335
|
+
method: str,
|
|
336
|
+
path: str,
|
|
337
|
+
payload: dict[str, Any] | None = None,
|
|
338
|
+
query: dict[str, Any] | None = None,
|
|
339
|
+
) -> dict[str, Any]:
|
|
340
|
+
return self.http.request(method, path, payload, query)
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
def _response_object(result: dict[str, Any], key: str) -> dict[str, Any]:
|
|
344
|
+
return MarketplaceHTTPClient.response_object(result, key)
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def _response_list(result: dict[str, Any], key: str) -> list[Any]:
|
|
348
|
+
return MarketplaceHTTPClient.response_list(result, key)
|
|
349
|
+
|
|
350
|
+
def heartbeat(
|
|
351
|
+
self,
|
|
352
|
+
merchant_id: str,
|
|
353
|
+
status: str = "online",
|
|
354
|
+
last_error: str = "",
|
|
355
|
+
checked_count: int = 0,
|
|
356
|
+
replied_count: int = 0,
|
|
357
|
+
) -> dict[str, Any]:
|
|
358
|
+
self._check_merchant(merchant_id)
|
|
359
|
+
result = self._request(
|
|
360
|
+
"POST",
|
|
361
|
+
"/agents/heartbeat",
|
|
362
|
+
self._merchant_payload(
|
|
363
|
+
{
|
|
364
|
+
"status": status,
|
|
365
|
+
"last_error": last_error,
|
|
366
|
+
"checked_count": _non_negative_whole_int(checked_count, "checked_count"),
|
|
367
|
+
"replied_count": _non_negative_whole_int(replied_count, "replied_count"),
|
|
368
|
+
}
|
|
369
|
+
),
|
|
370
|
+
)
|
|
371
|
+
self._record_tool_call("agent_heartbeat")
|
|
372
|
+
return self._response_object(result, "agent")
|
|
373
|
+
|
|
374
|
+
def waiting_merchant_conversations(self, merchant_id: str) -> list[dict[str, Any]]:
|
|
375
|
+
self._check_merchant(merchant_id)
|
|
376
|
+
path = f"/merchants/{urllib.parse.quote(merchant_id, safe='')}/conversations"
|
|
377
|
+
result = self._request("GET", path, query={"status": "waiting_merchant", "include": "details", "limit": 100})
|
|
378
|
+
return self._response_list(result, "conversations")
|
|
379
|
+
|
|
380
|
+
def product_summary(self, sku: str) -> dict[str, Any]:
|
|
381
|
+
result = self._request("GET", f"/products/{urllib.parse.quote(str(sku), safe='')}")
|
|
382
|
+
return self._response_object(result, "product")
|
|
383
|
+
|
|
384
|
+
def append_message(
|
|
385
|
+
self,
|
|
386
|
+
conversation_id: str,
|
|
387
|
+
sender: str,
|
|
388
|
+
intent: str,
|
|
389
|
+
text: str,
|
|
390
|
+
structured_payload: dict[str, Any],
|
|
391
|
+
status: str,
|
|
392
|
+
) -> dict[str, Any]:
|
|
393
|
+
result = self._request(
|
|
394
|
+
"POST",
|
|
395
|
+
f"/conversations/{urllib.parse.quote(conversation_id, safe='')}/messages",
|
|
396
|
+
self._token_payload(
|
|
397
|
+
{
|
|
398
|
+
"sender": sender,
|
|
399
|
+
"intent": intent,
|
|
400
|
+
"text": text,
|
|
401
|
+
"structured_payload": structured_payload,
|
|
402
|
+
"status": status,
|
|
403
|
+
}
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
self._record_tool_call("conversation_message", conversation_id=conversation_id)
|
|
407
|
+
conversation = result.get("conversation")
|
|
408
|
+
if isinstance(conversation, dict):
|
|
409
|
+
for flag in conversation.get("flags") or []:
|
|
410
|
+
if isinstance(flag, dict) and flag.get("reason") and not flag.get("resolved_at"):
|
|
411
|
+
self._message_created_review_flags[(conversation_id, str(flag["reason"]))] = dict(flag)
|
|
412
|
+
return self._response_object(result, "message")
|
|
413
|
+
|
|
414
|
+
def add_flag(self, conversation_id: str, reason: str, sku: str = "") -> dict[str, Any]:
|
|
415
|
+
cached = self._message_created_review_flags.pop((conversation_id, reason), None)
|
|
416
|
+
if cached is not None:
|
|
417
|
+
self._record_tool_call("human_review_flag", conversation_id=conversation_id)
|
|
418
|
+
return cached
|
|
419
|
+
result = self._request(
|
|
420
|
+
"POST",
|
|
421
|
+
f"/conversations/{urllib.parse.quote(conversation_id, safe='')}/human-review",
|
|
422
|
+
self._merchant_payload({"reason": reason, "sku": sku, "source_id": f"shopping-cli-merchant-agent:{self.merchant_id}"}),
|
|
423
|
+
)
|
|
424
|
+
self._record_tool_call("human_review_flag", conversation_id=conversation_id)
|
|
425
|
+
return self._response_object(result, "review")
|
|
426
|
+
|
|
427
|
+
def claim_message(self, agent_id: str, conversation_id: str, message_id: int, idempotency_key: str) -> dict[str, Any]:
|
|
428
|
+
result = self._request(
|
|
429
|
+
"POST",
|
|
430
|
+
"/agents/messages/claim",
|
|
431
|
+
self._merchant_payload(
|
|
432
|
+
{
|
|
433
|
+
"agent_id": agent_id,
|
|
434
|
+
"conversation_id": conversation_id,
|
|
435
|
+
"message_id": _positive_whole_int(message_id, "message_id"),
|
|
436
|
+
"idempotency_key": idempotency_key,
|
|
437
|
+
}
|
|
438
|
+
),
|
|
439
|
+
)
|
|
440
|
+
self._record_tool_call("agent_message_claim", conversation_id=conversation_id)
|
|
441
|
+
return self._response_object(result, "claim")
|
|
442
|
+
|
|
443
|
+
def complete_message(self, agent_id: str, message_id: int) -> dict[str, Any]:
|
|
444
|
+
result = self._request(
|
|
445
|
+
"POST",
|
|
446
|
+
"/agents/messages/complete",
|
|
447
|
+
self._merchant_payload(
|
|
448
|
+
{"agent_id": agent_id, "message_id": _positive_whole_int(message_id, "message_id")}
|
|
449
|
+
),
|
|
450
|
+
)
|
|
451
|
+
self._record_tool_call("agent_message_complete")
|
|
452
|
+
return self._response_object(result, "process")
|
|
453
|
+
|
|
454
|
+
def fail_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
|
|
455
|
+
result = self._request(
|
|
456
|
+
"POST",
|
|
457
|
+
"/agents/messages/fail",
|
|
458
|
+
self._merchant_payload(
|
|
459
|
+
{"agent_id": agent_id, "message_id": _positive_whole_int(message_id, "message_id"), "error": error}
|
|
460
|
+
),
|
|
461
|
+
)
|
|
462
|
+
self._record_tool_call("agent_message_fail", status="failed", error=error)
|
|
463
|
+
return self._response_object(result, "process")
|
|
464
|
+
|
|
465
|
+
def abandon_message(self, agent_id: str, message_id: int, error: str) -> dict[str, Any]:
|
|
466
|
+
result = self._request(
|
|
467
|
+
"POST",
|
|
468
|
+
"/agents/messages/abandon",
|
|
469
|
+
self._merchant_payload(
|
|
470
|
+
{"agent_id": agent_id, "message_id": _positive_whole_int(message_id, "message_id"), "error": error}
|
|
471
|
+
),
|
|
472
|
+
)
|
|
473
|
+
self._record_tool_call("agent_message_abandon", error=error)
|
|
474
|
+
return self._response_object(result, "process")
|
|
475
|
+
|
|
476
|
+
def abandon_stale_messages(self, agent_id: str, stale_after_seconds: int = 300) -> list[dict[str, Any]]:
|
|
477
|
+
result = self._request(
|
|
478
|
+
"POST",
|
|
479
|
+
"/agents/messages/abandon-stale",
|
|
480
|
+
self._merchant_payload(
|
|
481
|
+
{
|
|
482
|
+
"agent_id": agent_id,
|
|
483
|
+
"stale_after_seconds": _positive_whole_int(stale_after_seconds, "stale_after_seconds"),
|
|
484
|
+
}
|
|
485
|
+
),
|
|
486
|
+
)
|
|
487
|
+
self._record_tool_call("agent_message_abandon_stale")
|
|
488
|
+
return self._response_list(result, "abandoned")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Marketplace API package."""
|