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
shopping_cli/api/app.py
ADDED
|
@@ -0,0 +1,1353 @@
|
|
|
1
|
+
"""Marketplace API app factory.
|
|
2
|
+
|
|
3
|
+
FastAPI is used when installed. The lightweight fallback keeps route metadata
|
|
4
|
+
available for local tests in environments where optional API dependencies have
|
|
5
|
+
not been installed yet.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from types import SimpleNamespace
|
|
16
|
+
from typing import Any, Callable
|
|
17
|
+
|
|
18
|
+
from shopping_cli import VERSION
|
|
19
|
+
from shopping_cli.api import auth as api_auth
|
|
20
|
+
from shopping_cli.api.fallback_asgi import MarketplaceASGIApp
|
|
21
|
+
from shopping_cli.api.handlers import agents as agent_handlers
|
|
22
|
+
from shopping_cli.api.handlers import audit as audit_handlers
|
|
23
|
+
from shopping_cli.api.handlers import buyer as buyer_handlers
|
|
24
|
+
from shopping_cli.api.handlers import catalog as catalog_handlers
|
|
25
|
+
from shopping_cli.api.handlers import conversations as conversation_handlers
|
|
26
|
+
from shopping_cli.api.handlers import erp as erp_handlers
|
|
27
|
+
from shopping_cli.api.handlers import listings_projection as listings_projection_handlers
|
|
28
|
+
from shopping_cli.api.handlers import human_review as human_review_handlers
|
|
29
|
+
from shopping_cli.api.handlers import negotiation as negotiation_handlers
|
|
30
|
+
from shopping_cli.api.handlers.common import DEFAULT_RESULT_LIMIT
|
|
31
|
+
from shopping_cli.api.limits import max_request_body_bytes, validate_payload
|
|
32
|
+
from shopping_cli.core.errors import (
|
|
33
|
+
AuthError,
|
|
34
|
+
ConflictError,
|
|
35
|
+
IdempotencyConflict,
|
|
36
|
+
MethodNotAllowedError,
|
|
37
|
+
NotFoundError,
|
|
38
|
+
PermissionDenied,
|
|
39
|
+
RateLimitError,
|
|
40
|
+
PayloadTooLargeError,
|
|
41
|
+
ShoppingCliError,
|
|
42
|
+
ValidationError,
|
|
43
|
+
)
|
|
44
|
+
from shopping_cli.services import tokens as token_service
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger("shopping-cli")
|
|
47
|
+
|
|
48
|
+
try: # pragma: no cover - exercised when optional dependency is installed
|
|
49
|
+
from fastapi import FastAPI, Header, Request, Response
|
|
50
|
+
from fastapi.exceptions import RequestValidationError
|
|
51
|
+
from fastapi.responses import JSONResponse
|
|
52
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
53
|
+
except ModuleNotFoundError: # pragma: no cover - local CI currently has no fastapi
|
|
54
|
+
FastAPI = None # type: ignore[misc,assignment]
|
|
55
|
+
Header = None # type: ignore[assignment]
|
|
56
|
+
Response = None # type: ignore[misc,assignment]
|
|
57
|
+
JSONResponse = None # type: ignore[misc,assignment]
|
|
58
|
+
RequestValidationError = None # type: ignore[misc,assignment]
|
|
59
|
+
Request = None # type: ignore[misc,assignment]
|
|
60
|
+
StarletteHTTPException = None # type: ignore[misc,assignment]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Route handlers return the JSON response body (a dict).
|
|
64
|
+
Handler = Callable[..., Any]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class RouteEntry:
|
|
69
|
+
methods: set[str]
|
|
70
|
+
path_template: str
|
|
71
|
+
handler: Handler
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _RequestBodyLimitMiddleware:
|
|
75
|
+
"""Reject oversized HTTP bodies before FastAPI attempts to parse them."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, app: Any) -> None:
|
|
78
|
+
self.app = app
|
|
79
|
+
|
|
80
|
+
async def __call__(self, scope: dict[str, Any], receive: Any, send: Any) -> None:
|
|
81
|
+
if scope.get("type") != "http":
|
|
82
|
+
await self.app(scope, receive, send)
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
maximum = max_request_body_bytes()
|
|
86
|
+
headers = {key.lower(): value for key, value in scope.get("headers", [])}
|
|
87
|
+
try:
|
|
88
|
+
content_length = int(headers.get(b"content-length", b"0") or b"0")
|
|
89
|
+
except ValueError:
|
|
90
|
+
content_length = 0
|
|
91
|
+
if content_length > maximum:
|
|
92
|
+
await self._send_too_large(send)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
messages: list[dict[str, Any]] = []
|
|
96
|
+
received = 0
|
|
97
|
+
while True:
|
|
98
|
+
message = await receive()
|
|
99
|
+
messages.append(message)
|
|
100
|
+
if message.get("type") != "http.request":
|
|
101
|
+
break
|
|
102
|
+
received += len(message.get("body", b""))
|
|
103
|
+
if received > maximum:
|
|
104
|
+
await self._send_too_large(send)
|
|
105
|
+
return
|
|
106
|
+
if not message.get("more_body", False):
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
message_index = 0
|
|
110
|
+
|
|
111
|
+
async def replay_receive() -> dict[str, Any]:
|
|
112
|
+
nonlocal message_index
|
|
113
|
+
if message_index < len(messages):
|
|
114
|
+
message = messages[message_index]
|
|
115
|
+
message_index += 1
|
|
116
|
+
return message
|
|
117
|
+
return {"type": "http.request", "body": b"", "more_body": False}
|
|
118
|
+
|
|
119
|
+
await self.app(scope, replay_receive, send)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
async def _send_too_large(send: Any) -> None:
|
|
123
|
+
body = json.dumps({"ok": False, "error": "request body is too large"}).encode("utf-8")
|
|
124
|
+
await send(
|
|
125
|
+
{
|
|
126
|
+
"type": "http.response.start",
|
|
127
|
+
"status": 413,
|
|
128
|
+
"headers": [
|
|
129
|
+
(b"content-type", b"application/json"),
|
|
130
|
+
(b"content-length", str(len(body)).encode("ascii")),
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
)
|
|
134
|
+
await send({"type": "http.response.body", "body": body})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _json_error_response(status_code: int, error: str) -> Any:
|
|
138
|
+
payload = {"ok": False, "error": error}
|
|
139
|
+
if JSONResponse is not None: # pragma: no cover - exercised with fastapi installed
|
|
140
|
+
return JSONResponse(status_code=status_code, content=payload)
|
|
141
|
+
return SimpleNamespace(status_code=status_code, body=json.dumps(payload, ensure_ascii=False).encode("utf-8"))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _auth_header_default() -> Any:
|
|
145
|
+
if Header is None:
|
|
146
|
+
return ""
|
|
147
|
+
return Header(default="")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
AUTHORIZATION_HEADER = _auth_header_default()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _idempotency_key_header_default() -> Any:
|
|
154
|
+
if Header is None:
|
|
155
|
+
return ""
|
|
156
|
+
return Header(default="", alias="Idempotency-Key")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
IDEMPOTENCY_KEY_HEADER = _idempotency_key_header_default()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _require_merchant_token(conn: Any, merchant_id: str, payload: dict[str, Any]) -> None:
|
|
163
|
+
token_service.require_merchant_token(conn, merchant_id, api_auth.payload_token(payload))
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _resolve_agent_token(conn: Any, merchant_id: str, token: Any = "", token_prefix: Any = "") -> str:
|
|
167
|
+
return token_service.resolve_agent_token(conn, merchant_id, token, token_prefix)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _health(db_path: str | Path) -> dict[str, Any]:
|
|
171
|
+
return catalog_handlers.health(db_path)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _create_merchant(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
175
|
+
return catalog_handlers.create_merchant(db_path, payload, api_auth.require_admin_token)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _update_merchant(db_path: str | Path, merchant_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
179
|
+
return catalog_handlers.update_merchant(db_path, merchant_id, payload, _require_merchant_token)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _rotate_merchant_token(db_path: str | Path, merchant_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
183
|
+
return catalog_handlers.rotate_merchant_token(db_path, merchant_id, payload, api_auth.require_admin_token)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _revoke_merchant_tokens(db_path: str | Path, merchant_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
187
|
+
return catalog_handlers.revoke_merchant_tokens(db_path, merchant_id, payload, api_auth.require_admin_token)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _recover_merchant_token(db_path: str | Path, merchant_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
191
|
+
return catalog_handlers.recover_merchant_token(db_path, merchant_id, payload, api_auth.require_admin_token)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _get_merchant(db_path: str | Path, merchant_id: str) -> dict[str, Any]:
|
|
195
|
+
return catalog_handlers.get_merchant(db_path, merchant_id)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _get_merchant_private_config(
|
|
199
|
+
db_path: str | Path,
|
|
200
|
+
merchant_id: str,
|
|
201
|
+
payload: dict[str, Any],
|
|
202
|
+
) -> dict[str, Any]:
|
|
203
|
+
return catalog_handlers.get_merchant_private_config(db_path, merchant_id, payload)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _list_merchants(db_path: str | Path, query: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
207
|
+
return catalog_handlers.list_merchants(db_path, query)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _create_product(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
211
|
+
return catalog_handlers.create_product(db_path, payload, _require_merchant_token)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _update_product(db_path: str | Path, sku: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
215
|
+
return catalog_handlers.update_product(db_path, sku, payload, _require_merchant_token)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _get_product(db_path: str | Path, sku: str) -> dict[str, Any]:
|
|
219
|
+
return catalog_handlers.get_product(db_path, sku)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _list_listing_projections(db_path: str | Path, query: dict[str, Any]) -> dict[str, Any]:
|
|
223
|
+
return listings_projection_handlers.list_listing_projections(db_path, query)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _sync_erp(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
227
|
+
return erp_handlers.sync_erp(db_path, payload)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _get_listing_projection(db_path: str | Path, sku: str) -> dict[str, Any]:
|
|
231
|
+
return listings_projection_handlers.get_listing_projection(db_path, sku)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _search_products(db_path: str | Path, query: dict[str, Any]) -> dict[str, Any]:
|
|
235
|
+
return catalog_handlers.search_products(db_path, query)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _search_merchants(db_path: str | Path, query: dict[str, Any]) -> dict[str, Any]:
|
|
239
|
+
return catalog_handlers.search_merchants(db_path, query)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _buyer_ask(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
243
|
+
return buyer_handlers.buyer_ask(db_path, payload)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _ingest_channel_message(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
247
|
+
return buyer_handlers.ingest_channel_message(db_path, payload)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _get_conversation(db_path: str | Path, conversation_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
251
|
+
return conversation_handlers.get_conversation(db_path, conversation_id, payload)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _create_conversation(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
255
|
+
return buyer_handlers.create_conversation(db_path, payload)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _append_conversation_message(db_path: str | Path, conversation_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
259
|
+
return conversation_handlers.append_conversation_message(db_path, conversation_id, payload)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _close_conversation(db_path: str | Path, conversation_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
263
|
+
return conversation_handlers.close_conversation(db_path, conversation_id, payload)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _agent_heartbeat(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
267
|
+
return agent_handlers.agent_heartbeat(db_path, payload)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _create_agent_token(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
271
|
+
return agent_handlers.create_agent_token(db_path, payload)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _list_agent_tokens(
|
|
275
|
+
db_path: str | Path,
|
|
276
|
+
payload: dict[str, Any],
|
|
277
|
+
merchant_id: str = "",
|
|
278
|
+
limit: Any = DEFAULT_RESULT_LIMIT,
|
|
279
|
+
offset: Any = 0,
|
|
280
|
+
) -> dict[str, Any]:
|
|
281
|
+
return agent_handlers.list_agent_tokens(db_path, payload, merchant_id=merchant_id, limit=limit, offset=offset)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _revoke_agent_token(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
285
|
+
return agent_handlers.revoke_agent_token(db_path, payload)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _rotate_agent_token(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
289
|
+
return agent_handlers.rotate_agent_token(db_path, payload)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _claim_agent_message(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
293
|
+
return agent_handlers.claim_message(db_path, payload)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _complete_agent_message(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
297
|
+
return agent_handlers.complete_message(db_path, payload)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _fail_agent_message(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
301
|
+
return agent_handlers.fail_message(db_path, payload)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _abandon_agent_message(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
305
|
+
return agent_handlers.abandon_message(db_path, payload)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _abandon_stale_agent_messages(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
309
|
+
return agent_handlers.abandon_stale_messages(db_path, payload)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _list_agents(
|
|
313
|
+
db_path: str | Path,
|
|
314
|
+
payload: dict[str, Any],
|
|
315
|
+
owner_id: str = "",
|
|
316
|
+
limit: Any = DEFAULT_RESULT_LIMIT,
|
|
317
|
+
offset: Any = 0,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
return agent_handlers.list_agents(db_path, payload, owner_id=owner_id, limit=limit, offset=offset)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _get_agent(db_path: str | Path, agent_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
323
|
+
return agent_handlers.get_agent(db_path, agent_id, payload)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _conversation_list(
|
|
327
|
+
db_path: str | Path,
|
|
328
|
+
filters: dict[str, Any],
|
|
329
|
+
payload: dict[str, Any],
|
|
330
|
+
owner_kind: str,
|
|
331
|
+
owner_id: str,
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
return conversation_handlers.conversation_list(db_path, filters, payload, owner_kind, owner_id)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _merchant_conversations(
|
|
337
|
+
db_path: str | Path,
|
|
338
|
+
merchant_id: str,
|
|
339
|
+
payload: dict[str, Any],
|
|
340
|
+
status: str = "",
|
|
341
|
+
limit: Any = DEFAULT_RESULT_LIMIT,
|
|
342
|
+
offset: Any = 0,
|
|
343
|
+
include: Any = "",
|
|
344
|
+
) -> dict[str, Any]:
|
|
345
|
+
return conversation_handlers.merchant_conversations(
|
|
346
|
+
db_path,
|
|
347
|
+
merchant_id,
|
|
348
|
+
payload,
|
|
349
|
+
status=status,
|
|
350
|
+
limit=limit,
|
|
351
|
+
offset=offset,
|
|
352
|
+
include=include,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _human_review_queue(
|
|
357
|
+
db_path: str | Path,
|
|
358
|
+
payload: dict[str, Any],
|
|
359
|
+
merchant_id: str = "",
|
|
360
|
+
limit: Any = DEFAULT_RESULT_LIMIT,
|
|
361
|
+
offset: Any = 0,
|
|
362
|
+
) -> dict[str, Any]:
|
|
363
|
+
return human_review_handlers.human_review_queue(
|
|
364
|
+
db_path,
|
|
365
|
+
payload,
|
|
366
|
+
merchant_id=merchant_id,
|
|
367
|
+
limit=limit,
|
|
368
|
+
offset=offset,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _record_tool_call_audit(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
373
|
+
return audit_handlers.record_tool_call_audit(db_path, payload)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _audit_events(
|
|
377
|
+
db_path: str | Path,
|
|
378
|
+
payload: dict[str, Any],
|
|
379
|
+
merchant_id: str = "",
|
|
380
|
+
event: str = "",
|
|
381
|
+
limit: Any = 50,
|
|
382
|
+
offset: Any = 0,
|
|
383
|
+
) -> dict[str, Any]:
|
|
384
|
+
return audit_handlers.audit_events(
|
|
385
|
+
db_path,
|
|
386
|
+
payload,
|
|
387
|
+
merchant_id=merchant_id,
|
|
388
|
+
event=event,
|
|
389
|
+
limit=limit,
|
|
390
|
+
offset=offset,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _get_human_review(db_path: str | Path, review_id: str | int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
395
|
+
return human_review_handlers.get_human_review(db_path, review_id, payload)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _create_human_review(db_path: str | Path, conversation_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
399
|
+
return human_review_handlers.create_human_review(db_path, conversation_id, payload)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _resolve_human_review_item(db_path: str | Path, review_id: str | int, payload: dict[str, Any]) -> dict[str, Any]:
|
|
403
|
+
return human_review_handlers.resolve_human_review_item(db_path, review_id, payload)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _resolve_human_review(db_path: str | Path, conversation_id: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
407
|
+
return human_review_handlers.resolve_human_review(db_path, conversation_id, payload)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _capabilities(db_path: str | Path) -> dict[str, Any]:
|
|
411
|
+
return negotiation_handlers.capabilities(db_path)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _negotiation_pending_messages(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
415
|
+
return negotiation_handlers.pending_messages(db_path, payload)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _negotiation_claim(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
419
|
+
return negotiation_handlers.claim_message(db_path, payload)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _negotiation_complete_claim(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
423
|
+
return negotiation_handlers.complete_claim(db_path, payload)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _negotiation_fail_claim(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
427
|
+
return negotiation_handlers.fail_claim(db_path, payload)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _negotiation_abandon_claim(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
431
|
+
return negotiation_handlers.abandon_claim(db_path, payload)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _negotiation_heartbeat_claims(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
435
|
+
return negotiation_handlers.heartbeat_claims(db_path, payload)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _negotiation_abandon_stale_claims(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
439
|
+
return negotiation_handlers.abandon_stale_claims(db_path, payload)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _negotiation_snapshot(db_path: str | Path, payload: dict[str, Any], query: dict[str, Any]) -> dict[str, Any]:
|
|
443
|
+
return negotiation_handlers.get_snapshot(db_path, payload, query)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _negotiation_submit_decision(db_path: str | Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
447
|
+
return negotiation_handlers.submit_decision(db_path, payload)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _match_path(template: str, path: str) -> dict[str, str] | None:
|
|
451
|
+
parts = template.split("/")
|
|
452
|
+
regex_parts = []
|
|
453
|
+
for part in parts:
|
|
454
|
+
if part.startswith("{") and part.endswith("}"):
|
|
455
|
+
param_name = part[1:-1]
|
|
456
|
+
regex_parts.append(f"(?P<{param_name}>[^/]+)")
|
|
457
|
+
else:
|
|
458
|
+
regex_parts.append(re.escape(part))
|
|
459
|
+
match = re.match("^" + "/".join(regex_parts) + "$", path)
|
|
460
|
+
return match.groupdict() if match else None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _buyer_conversation_list(
|
|
464
|
+
db_path: str | Path, payload: dict[str, Any], query: dict[str, Any], buyer_id: str
|
|
465
|
+
) -> dict[str, Any]:
|
|
466
|
+
filters = dict(query)
|
|
467
|
+
filters["buyer_id"] = buyer_id
|
|
468
|
+
return _conversation_list(db_path, filters, payload, owner_kind="buyer", owner_id=buyer_id)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _merchant_conversation_list(
|
|
472
|
+
db_path: str | Path, payload: dict[str, Any], query: dict[str, Any], merchant_id: str
|
|
473
|
+
) -> dict[str, Any]:
|
|
474
|
+
filters = dict(query)
|
|
475
|
+
filters["merchant_id"] = merchant_id
|
|
476
|
+
return _conversation_list(db_path, filters, payload, owner_kind="merchant", owner_id=merchant_id)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _merchant_human_review_list(
|
|
480
|
+
db_path: str | Path, payload: dict[str, Any], query: dict[str, Any], merchant_id: str
|
|
481
|
+
) -> dict[str, Any]:
|
|
482
|
+
return _merchant_conversations(
|
|
483
|
+
db_path,
|
|
484
|
+
merchant_id,
|
|
485
|
+
payload,
|
|
486
|
+
status="human_required",
|
|
487
|
+
limit=query.get("limit"),
|
|
488
|
+
offset=query.get("offset"),
|
|
489
|
+
include=query.get("include"),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
_ROUTE_TABLE: tuple[RouteEntry, ...] = (
|
|
494
|
+
RouteEntry({"GET"}, "/health", lambda db_path, payload, query, **kw: _health(db_path)),
|
|
495
|
+
RouteEntry({"GET"}, "/merchants", lambda db_path, payload, query, **kw: _list_merchants(db_path, query)),
|
|
496
|
+
RouteEntry({"POST"}, "/merchants", lambda db_path, payload, query, **kw: _create_merchant(db_path, payload)),
|
|
497
|
+
RouteEntry(
|
|
498
|
+
{"GET"},
|
|
499
|
+
"/merchants/{merchant_id}",
|
|
500
|
+
lambda db_path, payload, query, merchant_id: _get_merchant(db_path, merchant_id),
|
|
501
|
+
),
|
|
502
|
+
RouteEntry(
|
|
503
|
+
{"GET"},
|
|
504
|
+
"/merchants/{merchant_id}/private-config",
|
|
505
|
+
lambda db_path, payload, query, merchant_id: _get_merchant_private_config(db_path, merchant_id, payload),
|
|
506
|
+
),
|
|
507
|
+
RouteEntry(
|
|
508
|
+
{"POST"},
|
|
509
|
+
"/merchants/{merchant_id}/token/rotate",
|
|
510
|
+
lambda db_path, payload, query, merchant_id: _rotate_merchant_token(db_path, merchant_id, payload),
|
|
511
|
+
),
|
|
512
|
+
RouteEntry(
|
|
513
|
+
{"POST"},
|
|
514
|
+
"/merchants/{merchant_id}/token/revoke",
|
|
515
|
+
lambda db_path, payload, query, merchant_id: _revoke_merchant_tokens(db_path, merchant_id, payload),
|
|
516
|
+
),
|
|
517
|
+
RouteEntry(
|
|
518
|
+
{"POST"},
|
|
519
|
+
"/merchants/{merchant_id}/token/recover",
|
|
520
|
+
lambda db_path, payload, query, merchant_id: _recover_merchant_token(db_path, merchant_id, payload),
|
|
521
|
+
),
|
|
522
|
+
RouteEntry(
|
|
523
|
+
{"PATCH"},
|
|
524
|
+
"/merchants/{merchant_id}",
|
|
525
|
+
lambda db_path, payload, query, merchant_id: _update_merchant(db_path, merchant_id, payload),
|
|
526
|
+
),
|
|
527
|
+
RouteEntry({"GET"}, "/merchants/{merchant_id}/conversations", _merchant_conversation_list),
|
|
528
|
+
RouteEntry({"GET"}, "/merchants/{merchant_id}/human-review", _merchant_human_review_list),
|
|
529
|
+
RouteEntry(
|
|
530
|
+
{"GET"},
|
|
531
|
+
"/merchants/{merchant_id}/agents",
|
|
532
|
+
lambda db_path, payload, query, merchant_id: _list_agents(
|
|
533
|
+
db_path,
|
|
534
|
+
payload,
|
|
535
|
+
owner_id=merchant_id,
|
|
536
|
+
limit=query.get("limit"),
|
|
537
|
+
offset=query.get("offset"),
|
|
538
|
+
),
|
|
539
|
+
),
|
|
540
|
+
RouteEntry({"POST"}, "/products", lambda db_path, payload, query, **kw: _create_product(db_path, payload)),
|
|
541
|
+
RouteEntry({"GET"}, "/products/{sku}", lambda db_path, payload, query, sku: _get_product(db_path, sku)),
|
|
542
|
+
RouteEntry(
|
|
543
|
+
{"PATCH"}, "/products/{sku}", lambda db_path, payload, query, sku: _update_product(db_path, sku, payload)
|
|
544
|
+
),
|
|
545
|
+
RouteEntry({"GET"}, "/search/products", lambda db_path, payload, query, **kw: _search_products(db_path, query)),
|
|
546
|
+
RouteEntry(
|
|
547
|
+
{"GET"},
|
|
548
|
+
"/v1/merchant/listings/projections",
|
|
549
|
+
lambda db_path, payload, query, **kw: _list_listing_projections(db_path, query or {}),
|
|
550
|
+
),
|
|
551
|
+
RouteEntry(
|
|
552
|
+
{"GET"},
|
|
553
|
+
"/v1/merchant/listings/{sku}/projection",
|
|
554
|
+
lambda db_path, payload, query, sku: _get_listing_projection(db_path, sku),
|
|
555
|
+
),
|
|
556
|
+
RouteEntry(
|
|
557
|
+
{"POST"},
|
|
558
|
+
"/v1/merchant/erp/sync",
|
|
559
|
+
lambda db_path, payload, query, **kw: _sync_erp(db_path, payload),
|
|
560
|
+
),
|
|
561
|
+
RouteEntry({"GET"}, "/search/merchants", lambda db_path, payload, query, **kw: _search_merchants(db_path, query)),
|
|
562
|
+
RouteEntry(
|
|
563
|
+
{"POST"}, "/channels/messages", lambda db_path, payload, query, **kw: _ingest_channel_message(db_path, payload)
|
|
564
|
+
),
|
|
565
|
+
RouteEntry({"POST"}, "/buyer/ask", lambda db_path, payload, query, **kw: _buyer_ask(db_path, payload)),
|
|
566
|
+
RouteEntry(
|
|
567
|
+
{"POST"}, "/conversations", lambda db_path, payload, query, **kw: _create_conversation(db_path, payload)
|
|
568
|
+
),
|
|
569
|
+
RouteEntry(
|
|
570
|
+
{"GET"},
|
|
571
|
+
"/conversations/{conversation_id}",
|
|
572
|
+
lambda db_path, payload, query, conversation_id: _get_conversation(db_path, conversation_id, payload),
|
|
573
|
+
),
|
|
574
|
+
RouteEntry(
|
|
575
|
+
{"POST"},
|
|
576
|
+
"/conversations/{conversation_id}/messages",
|
|
577
|
+
lambda db_path, payload, query, conversation_id: _append_conversation_message(
|
|
578
|
+
db_path, conversation_id, payload
|
|
579
|
+
),
|
|
580
|
+
),
|
|
581
|
+
RouteEntry(
|
|
582
|
+
{"POST"},
|
|
583
|
+
"/conversations/{conversation_id}/close",
|
|
584
|
+
lambda db_path, payload, query, conversation_id: _close_conversation(db_path, conversation_id, payload),
|
|
585
|
+
),
|
|
586
|
+
RouteEntry(
|
|
587
|
+
{"POST"},
|
|
588
|
+
"/conversations/{conversation_id}/human-review",
|
|
589
|
+
lambda db_path, payload, query, conversation_id: _create_human_review(db_path, conversation_id, payload),
|
|
590
|
+
),
|
|
591
|
+
RouteEntry(
|
|
592
|
+
{"POST"},
|
|
593
|
+
"/conversations/{conversation_id}/human-review/resolve",
|
|
594
|
+
lambda db_path, payload, query, conversation_id: _resolve_human_review(db_path, conversation_id, payload),
|
|
595
|
+
),
|
|
596
|
+
RouteEntry({"GET"}, "/buyers/{buyer_id}/conversations", _buyer_conversation_list),
|
|
597
|
+
RouteEntry({"POST"}, "/agents/heartbeat", lambda db_path, payload, query, **kw: _agent_heartbeat(db_path, payload)),
|
|
598
|
+
RouteEntry(
|
|
599
|
+
{"GET"},
|
|
600
|
+
"/agents/tokens",
|
|
601
|
+
lambda db_path, payload, query, **kw: _list_agent_tokens(
|
|
602
|
+
db_path,
|
|
603
|
+
payload,
|
|
604
|
+
merchant_id=str(query.get("merchant_id") or ""),
|
|
605
|
+
limit=query.get("limit"),
|
|
606
|
+
offset=query.get("offset"),
|
|
607
|
+
),
|
|
608
|
+
),
|
|
609
|
+
RouteEntry({"POST"}, "/agents/tokens", lambda db_path, payload, query, **kw: _create_agent_token(db_path, payload)),
|
|
610
|
+
RouteEntry(
|
|
611
|
+
{"POST"}, "/agents/tokens/revoke", lambda db_path, payload, query, **kw: _revoke_agent_token(db_path, payload)
|
|
612
|
+
),
|
|
613
|
+
RouteEntry(
|
|
614
|
+
{"POST"}, "/agents/tokens/rotate", lambda db_path, payload, query, **kw: _rotate_agent_token(db_path, payload)
|
|
615
|
+
),
|
|
616
|
+
RouteEntry(
|
|
617
|
+
{"POST"}, "/agents/messages/claim", lambda db_path, payload, query, **kw: _claim_agent_message(db_path, payload)
|
|
618
|
+
),
|
|
619
|
+
RouteEntry(
|
|
620
|
+
{"POST"},
|
|
621
|
+
"/agents/messages/complete",
|
|
622
|
+
lambda db_path, payload, query, **kw: _complete_agent_message(db_path, payload),
|
|
623
|
+
),
|
|
624
|
+
RouteEntry(
|
|
625
|
+
{"POST"}, "/agents/messages/fail", lambda db_path, payload, query, **kw: _fail_agent_message(db_path, payload)
|
|
626
|
+
),
|
|
627
|
+
RouteEntry(
|
|
628
|
+
{"POST"},
|
|
629
|
+
"/agents/messages/abandon",
|
|
630
|
+
lambda db_path, payload, query, **kw: _abandon_agent_message(db_path, payload),
|
|
631
|
+
),
|
|
632
|
+
RouteEntry(
|
|
633
|
+
{"POST"},
|
|
634
|
+
"/agents/messages/abandon-stale",
|
|
635
|
+
lambda db_path, payload, query, **kw: _abandon_stale_agent_messages(db_path, payload),
|
|
636
|
+
),
|
|
637
|
+
RouteEntry(
|
|
638
|
+
{"GET"},
|
|
639
|
+
"/agents",
|
|
640
|
+
lambda db_path, payload, query, **kw: _list_agents(
|
|
641
|
+
db_path,
|
|
642
|
+
payload,
|
|
643
|
+
limit=query.get("limit"),
|
|
644
|
+
offset=query.get("offset"),
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
RouteEntry(
|
|
648
|
+
{"GET"}, "/agents/{agent_id}", lambda db_path, payload, query, agent_id: _get_agent(db_path, agent_id, payload)
|
|
649
|
+
),
|
|
650
|
+
RouteEntry(
|
|
651
|
+
{"POST"}, "/audit/tool-calls", lambda db_path, payload, query, **kw: _record_tool_call_audit(db_path, payload)
|
|
652
|
+
),
|
|
653
|
+
RouteEntry(
|
|
654
|
+
{"GET"},
|
|
655
|
+
"/audit/events",
|
|
656
|
+
lambda db_path, payload, query, **kw: _audit_events(
|
|
657
|
+
db_path,
|
|
658
|
+
payload,
|
|
659
|
+
merchant_id=str(query.get("merchant_id") or ""),
|
|
660
|
+
event=str(query.get("event") or ""),
|
|
661
|
+
limit=query.get("limit") or 50,
|
|
662
|
+
offset=query.get("offset"),
|
|
663
|
+
),
|
|
664
|
+
),
|
|
665
|
+
RouteEntry(
|
|
666
|
+
{"GET"},
|
|
667
|
+
"/human-review/queue",
|
|
668
|
+
lambda db_path, payload, query, **kw: _human_review_queue(
|
|
669
|
+
db_path,
|
|
670
|
+
payload,
|
|
671
|
+
merchant_id=str(query.get("merchant_id") or ""),
|
|
672
|
+
limit=query.get("limit"),
|
|
673
|
+
offset=query.get("offset"),
|
|
674
|
+
),
|
|
675
|
+
),
|
|
676
|
+
RouteEntry(
|
|
677
|
+
{"GET"},
|
|
678
|
+
"/human-review/{review_id}",
|
|
679
|
+
lambda db_path, payload, query, review_id: _get_human_review(db_path, review_id, payload),
|
|
680
|
+
),
|
|
681
|
+
RouteEntry(
|
|
682
|
+
{"POST"},
|
|
683
|
+
"/human-review/{review_id}/resolve",
|
|
684
|
+
lambda db_path, payload, query, review_id: _resolve_human_review_item(db_path, review_id, payload),
|
|
685
|
+
),
|
|
686
|
+
RouteEntry({"GET"}, "/capabilities", lambda db_path, payload, query, **kw: _capabilities(db_path)),
|
|
687
|
+
RouteEntry(
|
|
688
|
+
{"GET"},
|
|
689
|
+
"/negotiation/pending-messages",
|
|
690
|
+
lambda db_path, payload, query, **kw: _negotiation_pending_messages(db_path, payload),
|
|
691
|
+
),
|
|
692
|
+
RouteEntry(
|
|
693
|
+
{"POST"}, "/negotiation/claims", lambda db_path, payload, query, **kw: _negotiation_claim(db_path, payload)
|
|
694
|
+
),
|
|
695
|
+
RouteEntry(
|
|
696
|
+
{"POST"},
|
|
697
|
+
"/negotiation/claims/complete",
|
|
698
|
+
lambda db_path, payload, query, **kw: _negotiation_complete_claim(db_path, payload),
|
|
699
|
+
),
|
|
700
|
+
RouteEntry(
|
|
701
|
+
{"POST"},
|
|
702
|
+
"/negotiation/claims/fail",
|
|
703
|
+
lambda db_path, payload, query, **kw: _negotiation_fail_claim(db_path, payload),
|
|
704
|
+
),
|
|
705
|
+
RouteEntry(
|
|
706
|
+
{"POST"},
|
|
707
|
+
"/negotiation/claims/abandon",
|
|
708
|
+
lambda db_path, payload, query, **kw: _negotiation_abandon_claim(db_path, payload),
|
|
709
|
+
),
|
|
710
|
+
RouteEntry(
|
|
711
|
+
{"POST"},
|
|
712
|
+
"/negotiation/claims/heartbeat",
|
|
713
|
+
lambda db_path, payload, query, **kw: _negotiation_heartbeat_claims(db_path, payload),
|
|
714
|
+
),
|
|
715
|
+
RouteEntry(
|
|
716
|
+
{"POST"},
|
|
717
|
+
"/negotiation/claims/abandon-stale",
|
|
718
|
+
lambda db_path, payload, query, **kw: _negotiation_abandon_stale_claims(db_path, payload),
|
|
719
|
+
),
|
|
720
|
+
RouteEntry(
|
|
721
|
+
{"GET"},
|
|
722
|
+
"/negotiation/snapshot",
|
|
723
|
+
lambda db_path, payload, query, **kw: _negotiation_snapshot(db_path, payload, query),
|
|
724
|
+
),
|
|
725
|
+
RouteEntry(
|
|
726
|
+
{"POST"},
|
|
727
|
+
"/negotiation/decisions",
|
|
728
|
+
lambda db_path, payload, query, **kw: _negotiation_submit_decision(db_path, payload),
|
|
729
|
+
),
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def resolve_route(
|
|
734
|
+
method: str, path: str, routes: tuple[RouteEntry, ...] | list[Any] | None = None
|
|
735
|
+
) -> tuple[bool, bool]:
|
|
736
|
+
"""Return (path_known, method_allowed) without parsing the request body.
|
|
737
|
+
|
|
738
|
+
*routes* defaults to the full ``_ROUTE_TABLE``; the parameter is kept for
|
|
739
|
+
API compatibility (previously used by the kiwi-catalog standalone service).
|
|
740
|
+
"""
|
|
741
|
+
table = _ROUTE_TABLE if routes is None else tuple(routes)
|
|
742
|
+
path_known = False
|
|
743
|
+
for route in table:
|
|
744
|
+
template = getattr(route, "path_template", None) or getattr(route, "path", "")
|
|
745
|
+
if _match_path(template, path) is None:
|
|
746
|
+
continue
|
|
747
|
+
path_known = True
|
|
748
|
+
if method.upper() in route.methods:
|
|
749
|
+
return True, True
|
|
750
|
+
return path_known, False
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def handle_request(
|
|
754
|
+
db_path: str | Path,
|
|
755
|
+
method: str,
|
|
756
|
+
path: str,
|
|
757
|
+
payload: dict[str, Any] | None = None,
|
|
758
|
+
query: dict[str, Any] | None = None,
|
|
759
|
+
) -> tuple[int, dict[str, Any]]:
|
|
760
|
+
payload = payload or {}
|
|
761
|
+
query = query or {}
|
|
762
|
+
try:
|
|
763
|
+
validate_payload(payload)
|
|
764
|
+
path_matched = False
|
|
765
|
+
for route in _ROUTE_TABLE:
|
|
766
|
+
path_params = _match_path(route.path_template, path)
|
|
767
|
+
if path_params is None:
|
|
768
|
+
continue
|
|
769
|
+
path_matched = True
|
|
770
|
+
if method.upper() in route.methods:
|
|
771
|
+
result = route.handler(db_path, payload, query, **path_params)
|
|
772
|
+
return 200, result
|
|
773
|
+
if path_matched:
|
|
774
|
+
raise MethodNotAllowedError(f"Method not allowed for {method} {path}")
|
|
775
|
+
raise NotFoundError(f"No route for {method} {path}")
|
|
776
|
+
except AuthError as exc:
|
|
777
|
+
return 403, {"ok": False, "error": str(exc)}
|
|
778
|
+
except PermissionDenied as exc:
|
|
779
|
+
return 403, {"ok": False, "error": str(exc)}
|
|
780
|
+
except IdempotencyConflict as exc:
|
|
781
|
+
return 409, {"ok": False, "error": str(exc)}
|
|
782
|
+
except ConflictError as exc:
|
|
783
|
+
return 409, {"ok": False, "error": str(exc)}
|
|
784
|
+
except NotFoundError as exc:
|
|
785
|
+
return 404, {"ok": False, "error": str(exc)}
|
|
786
|
+
except RateLimitError as exc:
|
|
787
|
+
return 429, {"ok": False, "error": str(exc)}
|
|
788
|
+
except PayloadTooLargeError as exc:
|
|
789
|
+
return 413, {"ok": False, "error": str(exc)}
|
|
790
|
+
except MethodNotAllowedError as exc:
|
|
791
|
+
return 405, {"ok": False, "error": str(exc)}
|
|
792
|
+
except ValidationError as exc:
|
|
793
|
+
return 400, {"ok": False, "error": str(exc)}
|
|
794
|
+
except ShoppingCliError as exc:
|
|
795
|
+
return 400, {"ok": False, "error": str(exc)}
|
|
796
|
+
except Exception:
|
|
797
|
+
logger.exception("unhandled error handling %s %s", method, path)
|
|
798
|
+
return 500, {"ok": False, "error": "internal server error"}
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def create_app(db_path: str | Path = "shopping-cli.sqlite") -> Any:
|
|
802
|
+
from shopping_cli.api.route_registry import route_info
|
|
803
|
+
|
|
804
|
+
routes = route_info()
|
|
805
|
+
if FastAPI is None:
|
|
806
|
+
return MarketplaceASGIApp(
|
|
807
|
+
db_path,
|
|
808
|
+
route_provider=lambda: routes,
|
|
809
|
+
route_resolver=lambda method, path: resolve_route(method, path, routes=routes),
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
app = FastAPI(
|
|
813
|
+
title="shopping-cli Marketplace API",
|
|
814
|
+
version=VERSION,
|
|
815
|
+
docs_url=None,
|
|
816
|
+
redoc_url=None,
|
|
817
|
+
openapi_url=None,
|
|
818
|
+
)
|
|
819
|
+
app.state.db_path = str(db_path)
|
|
820
|
+
app.state.fastapi_available = True
|
|
821
|
+
|
|
822
|
+
if hasattr(app, "add_middleware"):
|
|
823
|
+
app.add_middleware(_RequestBodyLimitMiddleware)
|
|
824
|
+
|
|
825
|
+
@app.exception_handler(AuthError)
|
|
826
|
+
def auth_error_handler(_request: Any, exc: AuthError) -> Any:
|
|
827
|
+
return _json_error_response(403, str(exc))
|
|
828
|
+
|
|
829
|
+
@app.exception_handler(PermissionDenied)
|
|
830
|
+
def permission_denied_handler(_request: Any, exc: PermissionDenied) -> Any:
|
|
831
|
+
return _json_error_response(403, str(exc))
|
|
832
|
+
|
|
833
|
+
@app.exception_handler(IdempotencyConflict)
|
|
834
|
+
def idempotency_conflict_handler(_request: Any, exc: IdempotencyConflict) -> Any:
|
|
835
|
+
return _json_error_response(409, str(exc))
|
|
836
|
+
|
|
837
|
+
@app.exception_handler(ConflictError)
|
|
838
|
+
def conflict_error_handler(_request: Any, exc: ConflictError) -> Any:
|
|
839
|
+
return _json_error_response(409, str(exc))
|
|
840
|
+
|
|
841
|
+
@app.exception_handler(NotFoundError)
|
|
842
|
+
def not_found_error_handler(_request: Any, exc: NotFoundError) -> Any:
|
|
843
|
+
return _json_error_response(404, str(exc))
|
|
844
|
+
|
|
845
|
+
@app.exception_handler(RateLimitError)
|
|
846
|
+
def rate_limit_error_handler(_request: Any, exc: RateLimitError) -> Any:
|
|
847
|
+
return _json_error_response(429, str(exc))
|
|
848
|
+
|
|
849
|
+
@app.exception_handler(PayloadTooLargeError)
|
|
850
|
+
def payload_too_large_handler(_request: Any, exc: PayloadTooLargeError) -> Any:
|
|
851
|
+
return _json_error_response(413, str(exc))
|
|
852
|
+
|
|
853
|
+
@app.exception_handler(MethodNotAllowedError)
|
|
854
|
+
def method_not_allowed_handler(_request: Any, exc: MethodNotAllowedError) -> Any:
|
|
855
|
+
return _json_error_response(405, str(exc))
|
|
856
|
+
|
|
857
|
+
@app.exception_handler(ValidationError)
|
|
858
|
+
def validation_error_handler(_request: Any, exc: ValidationError) -> Any:
|
|
859
|
+
return _json_error_response(400, str(exc))
|
|
860
|
+
|
|
861
|
+
@app.exception_handler(ShoppingCliError)
|
|
862
|
+
def shopping_cli_error_handler(_request: Any, exc: ShoppingCliError) -> Any:
|
|
863
|
+
return _json_error_response(400, str(exc))
|
|
864
|
+
|
|
865
|
+
if RequestValidationError is not None: # pragma: no cover - exercised with fastapi installed
|
|
866
|
+
|
|
867
|
+
@app.exception_handler(RequestValidationError)
|
|
868
|
+
def request_validation_error_handler(request: Any, exc: Exception) -> Any:
|
|
869
|
+
# 不回显 str(exc):包含 schema 内部结构并回显调用方输入。
|
|
870
|
+
logger.warning("request validation failed on %s: %s", getattr(request, "url", "?"), exc)
|
|
871
|
+
return _json_error_response(400, "invalid request body")
|
|
872
|
+
|
|
873
|
+
if StarletteHTTPException is not None: # pragma: no cover - exercised with fastapi installed
|
|
874
|
+
|
|
875
|
+
@app.exception_handler(StarletteHTTPException)
|
|
876
|
+
def http_exception_handler(_request: Any, exc: Any) -> Any:
|
|
877
|
+
status = int(exc.status_code)
|
|
878
|
+
message = "not found" if status == 404 else "method not allowed" if status == 405 else str(exc.detail)
|
|
879
|
+
return _json_error_response(status, message)
|
|
880
|
+
|
|
881
|
+
@app.exception_handler(Exception)
|
|
882
|
+
def unexpected_error_handler(request: Any, exc: Exception) -> Any:
|
|
883
|
+
logger.exception("unhandled error on %s %s", getattr(request, "method", "?"), getattr(request, "url", "?"))
|
|
884
|
+
return _json_error_response(500, "internal server error")
|
|
885
|
+
|
|
886
|
+
@app.get("/health")
|
|
887
|
+
def health() -> dict[str, Any]:
|
|
888
|
+
return _health(db_path)
|
|
889
|
+
|
|
890
|
+
@app.get("/merchants")
|
|
891
|
+
def list_merchants(limit: str = "", offset: str = "") -> dict[str, Any]:
|
|
892
|
+
return _list_merchants(db_path, {"limit": limit, "offset": offset})
|
|
893
|
+
|
|
894
|
+
@app.post("/merchants")
|
|
895
|
+
def create_merchant(
|
|
896
|
+
payload: dict[str, Any],
|
|
897
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
898
|
+
idempotency_key: str = IDEMPOTENCY_KEY_HEADER,
|
|
899
|
+
) -> dict[str, Any]:
|
|
900
|
+
return _create_merchant(
|
|
901
|
+
db_path,
|
|
902
|
+
api_auth.payload_with_auth(payload, authorization, idempotency_key),
|
|
903
|
+
)
|
|
904
|
+
|
|
905
|
+
@app.get("/merchants/{merchant_id}")
|
|
906
|
+
def get_merchant(merchant_id: str) -> dict[str, Any]:
|
|
907
|
+
return _get_merchant(db_path, merchant_id)
|
|
908
|
+
|
|
909
|
+
@app.get("/merchants/{merchant_id}/private-config")
|
|
910
|
+
def get_merchant_private_config(
|
|
911
|
+
merchant_id: str,
|
|
912
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
913
|
+
) -> dict[str, Any]:
|
|
914
|
+
return _get_merchant_private_config(
|
|
915
|
+
db_path,
|
|
916
|
+
merchant_id,
|
|
917
|
+
api_auth.payload_with_auth({}, authorization),
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
@app.post("/merchants/{merchant_id}/token/rotate")
|
|
921
|
+
def rotate_merchant_token(
|
|
922
|
+
merchant_id: str,
|
|
923
|
+
payload: dict[str, Any],
|
|
924
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
925
|
+
) -> dict[str, Any]:
|
|
926
|
+
return _rotate_merchant_token(
|
|
927
|
+
db_path,
|
|
928
|
+
merchant_id,
|
|
929
|
+
api_auth.payload_with_auth(payload, authorization),
|
|
930
|
+
)
|
|
931
|
+
|
|
932
|
+
@app.post("/merchants/{merchant_id}/token/revoke")
|
|
933
|
+
def revoke_merchant_tokens(
|
|
934
|
+
merchant_id: str,
|
|
935
|
+
payload: dict[str, Any],
|
|
936
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
937
|
+
) -> dict[str, Any]:
|
|
938
|
+
return _revoke_merchant_tokens(
|
|
939
|
+
db_path,
|
|
940
|
+
merchant_id,
|
|
941
|
+
api_auth.payload_with_auth(payload, authorization),
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
@app.post("/merchants/{merchant_id}/token/recover")
|
|
945
|
+
def recover_merchant_token(
|
|
946
|
+
merchant_id: str,
|
|
947
|
+
payload: dict[str, Any],
|
|
948
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
949
|
+
) -> dict[str, Any]:
|
|
950
|
+
return _recover_merchant_token(
|
|
951
|
+
db_path,
|
|
952
|
+
merchant_id,
|
|
953
|
+
api_auth.payload_with_auth(payload, authorization),
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
@app.patch("/merchants/{merchant_id}")
|
|
957
|
+
def update_merchant(
|
|
958
|
+
merchant_id: str,
|
|
959
|
+
payload: dict[str, Any],
|
|
960
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
961
|
+
) -> dict[str, Any]:
|
|
962
|
+
return _update_merchant(db_path, merchant_id, api_auth.payload_with_auth(payload, authorization))
|
|
963
|
+
|
|
964
|
+
@app.post("/products")
|
|
965
|
+
def create_product(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
966
|
+
return _create_product(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
967
|
+
|
|
968
|
+
@app.get("/products/{sku}")
|
|
969
|
+
def get_product(sku: str) -> dict[str, Any]:
|
|
970
|
+
return _get_product(db_path, sku)
|
|
971
|
+
|
|
972
|
+
@app.patch("/products/{sku}")
|
|
973
|
+
def update_product(
|
|
974
|
+
sku: str,
|
|
975
|
+
payload: dict[str, Any],
|
|
976
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
977
|
+
) -> dict[str, Any]:
|
|
978
|
+
return _update_product(db_path, sku, api_auth.payload_with_auth(payload, authorization))
|
|
979
|
+
|
|
980
|
+
@app.get("/v1/merchant/listings/projections")
|
|
981
|
+
def merchant_listing_projections(merchant_id: str = "") -> dict[str, Any]:
|
|
982
|
+
return _list_listing_projections(db_path, {"merchant_id": merchant_id})
|
|
983
|
+
|
|
984
|
+
@app.get("/v1/merchant/listings/{sku}/projection")
|
|
985
|
+
def merchant_listing_projection(sku: str) -> dict[str, Any]:
|
|
986
|
+
return _get_listing_projection(db_path, sku)
|
|
987
|
+
|
|
988
|
+
@app.post("/v1/merchant/erp/sync")
|
|
989
|
+
def merchant_erp_sync(
|
|
990
|
+
payload: dict[str, Any],
|
|
991
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
992
|
+
) -> dict[str, Any]:
|
|
993
|
+
return _sync_erp(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
994
|
+
|
|
995
|
+
@app.get("/search/products")
|
|
996
|
+
def search_products(
|
|
997
|
+
query: str = "",
|
|
998
|
+
city: str = "",
|
|
999
|
+
area: str = "",
|
|
1000
|
+
max_price: str = "",
|
|
1001
|
+
include_out_of_stock: str = "",
|
|
1002
|
+
limit: str = "",
|
|
1003
|
+
offset: str = "",
|
|
1004
|
+
) -> dict[str, Any]:
|
|
1005
|
+
return _search_products(
|
|
1006
|
+
db_path,
|
|
1007
|
+
{
|
|
1008
|
+
"query": query,
|
|
1009
|
+
"city": city,
|
|
1010
|
+
"area": area,
|
|
1011
|
+
"max_price": max_price,
|
|
1012
|
+
"include_out_of_stock": include_out_of_stock,
|
|
1013
|
+
"limit": limit,
|
|
1014
|
+
"offset": offset,
|
|
1015
|
+
},
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
@app.get("/search/merchants")
|
|
1019
|
+
def search_merchants(query: str = "", city: str = "", limit: str = "", offset: str = "") -> dict[str, Any]:
|
|
1020
|
+
return _search_merchants(db_path, {"query": query, "city": city, "limit": limit, "offset": offset})
|
|
1021
|
+
|
|
1022
|
+
@app.post("/channels/messages")
|
|
1023
|
+
def ingest_channel_message(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1024
|
+
return _ingest_channel_message(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1025
|
+
|
|
1026
|
+
@app.post("/buyer/ask")
|
|
1027
|
+
def buyer_ask(
|
|
1028
|
+
payload: dict[str, Any],
|
|
1029
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1030
|
+
idempotency_key: str = IDEMPOTENCY_KEY_HEADER,
|
|
1031
|
+
) -> dict[str, Any]:
|
|
1032
|
+
return _buyer_ask(db_path, api_auth.payload_with_auth(payload, authorization, idempotency_key))
|
|
1033
|
+
|
|
1034
|
+
@app.post("/conversations")
|
|
1035
|
+
def create_conversation(
|
|
1036
|
+
payload: dict[str, Any],
|
|
1037
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1038
|
+
idempotency_key: str = IDEMPOTENCY_KEY_HEADER,
|
|
1039
|
+
) -> dict[str, Any]:
|
|
1040
|
+
return _create_conversation(db_path, api_auth.payload_with_auth(payload, authorization, idempotency_key))
|
|
1041
|
+
|
|
1042
|
+
@app.get("/buyers/{buyer_id}/conversations")
|
|
1043
|
+
def get_buyer_conversations(
|
|
1044
|
+
buyer_id: str,
|
|
1045
|
+
status: str = "",
|
|
1046
|
+
merchant_id: str = "",
|
|
1047
|
+
sku: str = "",
|
|
1048
|
+
updated_since: str = "",
|
|
1049
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1050
|
+
limit: str = "",
|
|
1051
|
+
offset: str = "",
|
|
1052
|
+
include: str = "",
|
|
1053
|
+
) -> dict[str, Any]:
|
|
1054
|
+
return _conversation_list(
|
|
1055
|
+
db_path,
|
|
1056
|
+
{
|
|
1057
|
+
"buyer_id": buyer_id,
|
|
1058
|
+
"status": status,
|
|
1059
|
+
"merchant_id": merchant_id,
|
|
1060
|
+
"sku": sku,
|
|
1061
|
+
"updated_since": updated_since,
|
|
1062
|
+
"limit": limit,
|
|
1063
|
+
"offset": offset,
|
|
1064
|
+
"include": include,
|
|
1065
|
+
},
|
|
1066
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1067
|
+
owner_kind="buyer",
|
|
1068
|
+
owner_id=buyer_id,
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
@app.get("/conversations/{conversation_id}")
|
|
1072
|
+
def get_conversation(conversation_id: str, authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1073
|
+
return _get_conversation(db_path, conversation_id, api_auth.payload_with_auth({}, authorization))
|
|
1074
|
+
|
|
1075
|
+
@app.post("/conversations/{conversation_id}/messages")
|
|
1076
|
+
def add_message(
|
|
1077
|
+
conversation_id: str,
|
|
1078
|
+
payload: dict[str, Any],
|
|
1079
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1080
|
+
) -> dict[str, Any]:
|
|
1081
|
+
return _append_conversation_message(
|
|
1082
|
+
db_path, conversation_id, api_auth.payload_with_auth(payload, authorization)
|
|
1083
|
+
)
|
|
1084
|
+
|
|
1085
|
+
@app.post("/conversations/{conversation_id}/close")
|
|
1086
|
+
def close_conversation(
|
|
1087
|
+
conversation_id: str,
|
|
1088
|
+
payload: dict[str, Any],
|
|
1089
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1090
|
+
) -> dict[str, Any]:
|
|
1091
|
+
return _close_conversation(db_path, conversation_id, api_auth.payload_with_auth(payload, authorization))
|
|
1092
|
+
|
|
1093
|
+
@app.post("/agents/heartbeat")
|
|
1094
|
+
def agent_heartbeat(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1095
|
+
return _agent_heartbeat(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1096
|
+
|
|
1097
|
+
@app.post("/agents/tokens")
|
|
1098
|
+
def create_agent_token(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1099
|
+
return _create_agent_token(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1100
|
+
|
|
1101
|
+
@app.get("/agents/tokens")
|
|
1102
|
+
def list_agent_tokens(
|
|
1103
|
+
merchant_id: str = "",
|
|
1104
|
+
limit: str = "",
|
|
1105
|
+
offset: str = "",
|
|
1106
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1107
|
+
) -> dict[str, Any]:
|
|
1108
|
+
return _list_agent_tokens(
|
|
1109
|
+
db_path,
|
|
1110
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1111
|
+
merchant_id=merchant_id,
|
|
1112
|
+
limit=limit,
|
|
1113
|
+
offset=offset,
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
@app.post("/agents/tokens/revoke")
|
|
1117
|
+
def revoke_agent_token(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1118
|
+
return _revoke_agent_token(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1119
|
+
|
|
1120
|
+
@app.post("/agents/tokens/rotate")
|
|
1121
|
+
def rotate_agent_token(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1122
|
+
return _rotate_agent_token(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1123
|
+
|
|
1124
|
+
@app.post("/agents/messages/claim")
|
|
1125
|
+
def claim_agent_message_route(
|
|
1126
|
+
payload: dict[str, Any],
|
|
1127
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1128
|
+
) -> dict[str, Any]:
|
|
1129
|
+
return _claim_agent_message(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1130
|
+
|
|
1131
|
+
@app.post("/agents/messages/complete")
|
|
1132
|
+
def complete_agent_message_route(
|
|
1133
|
+
payload: dict[str, Any],
|
|
1134
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1135
|
+
) -> dict[str, Any]:
|
|
1136
|
+
return _complete_agent_message(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1137
|
+
|
|
1138
|
+
@app.post("/agents/messages/fail")
|
|
1139
|
+
def fail_agent_message_route(
|
|
1140
|
+
payload: dict[str, Any],
|
|
1141
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1142
|
+
) -> dict[str, Any]:
|
|
1143
|
+
return _fail_agent_message(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1144
|
+
|
|
1145
|
+
@app.post("/agents/messages/abandon")
|
|
1146
|
+
def abandon_agent_message_route(
|
|
1147
|
+
payload: dict[str, Any],
|
|
1148
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1149
|
+
) -> dict[str, Any]:
|
|
1150
|
+
return _abandon_agent_message(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1151
|
+
|
|
1152
|
+
@app.post("/agents/messages/abandon-stale")
|
|
1153
|
+
def abandon_stale_agent_messages_route(
|
|
1154
|
+
payload: dict[str, Any],
|
|
1155
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1156
|
+
) -> dict[str, Any]:
|
|
1157
|
+
return _abandon_stale_agent_messages(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1158
|
+
|
|
1159
|
+
@app.get("/agents")
|
|
1160
|
+
def list_agents(
|
|
1161
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1162
|
+
limit: str = "",
|
|
1163
|
+
offset: str = "",
|
|
1164
|
+
) -> dict[str, Any]:
|
|
1165
|
+
return _list_agents(db_path, api_auth.payload_with_auth({}, authorization), limit=limit, offset=offset)
|
|
1166
|
+
|
|
1167
|
+
@app.get("/agents/{agent_id}")
|
|
1168
|
+
def get_agent(agent_id: str, authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1169
|
+
return _get_agent(db_path, agent_id, api_auth.payload_with_auth({}, authorization))
|
|
1170
|
+
|
|
1171
|
+
@app.get("/merchants/{merchant_id}/agents")
|
|
1172
|
+
def get_merchant_agents(
|
|
1173
|
+
merchant_id: str,
|
|
1174
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1175
|
+
limit: str = "",
|
|
1176
|
+
offset: str = "",
|
|
1177
|
+
) -> dict[str, Any]:
|
|
1178
|
+
return _list_agents(
|
|
1179
|
+
db_path,
|
|
1180
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1181
|
+
owner_id=merchant_id,
|
|
1182
|
+
limit=limit,
|
|
1183
|
+
offset=offset,
|
|
1184
|
+
)
|
|
1185
|
+
|
|
1186
|
+
@app.post("/audit/tool-calls")
|
|
1187
|
+
def record_tool_call_audit(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1188
|
+
return _record_tool_call_audit(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1189
|
+
|
|
1190
|
+
@app.get("/audit/events")
|
|
1191
|
+
def get_audit_events(
|
|
1192
|
+
merchant_id: str = "",
|
|
1193
|
+
event: str = "",
|
|
1194
|
+
limit: str = "",
|
|
1195
|
+
offset: str = "",
|
|
1196
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1197
|
+
) -> dict[str, Any]:
|
|
1198
|
+
return _audit_events(
|
|
1199
|
+
db_path,
|
|
1200
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1201
|
+
merchant_id=merchant_id,
|
|
1202
|
+
event=event,
|
|
1203
|
+
limit=limit,
|
|
1204
|
+
offset=offset,
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
@app.get("/human-review/queue")
|
|
1208
|
+
def human_review_queue(
|
|
1209
|
+
merchant_id: str = "",
|
|
1210
|
+
limit: str = "",
|
|
1211
|
+
offset: str = "",
|
|
1212
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1213
|
+
) -> dict[str, Any]:
|
|
1214
|
+
return _human_review_queue(
|
|
1215
|
+
db_path,
|
|
1216
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1217
|
+
merchant_id=merchant_id,
|
|
1218
|
+
limit=limit,
|
|
1219
|
+
offset=offset,
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
@app.get("/human-review/{review_id}")
|
|
1223
|
+
def get_human_review(review_id: str, authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1224
|
+
return _get_human_review(db_path, review_id, api_auth.payload_with_auth({}, authorization))
|
|
1225
|
+
|
|
1226
|
+
@app.post("/human-review/{review_id}/resolve")
|
|
1227
|
+
def resolve_human_review_item(
|
|
1228
|
+
review_id: str,
|
|
1229
|
+
payload: dict[str, Any],
|
|
1230
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1231
|
+
) -> dict[str, Any]:
|
|
1232
|
+
return _resolve_human_review_item(db_path, review_id, api_auth.payload_with_auth(payload, authorization))
|
|
1233
|
+
|
|
1234
|
+
@app.get("/merchants/{merchant_id}/conversations")
|
|
1235
|
+
def get_merchant_conversations(
|
|
1236
|
+
merchant_id: str,
|
|
1237
|
+
status: str = "",
|
|
1238
|
+
buyer_id: str = "",
|
|
1239
|
+
sku: str = "",
|
|
1240
|
+
updated_since: str = "",
|
|
1241
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1242
|
+
limit: str = "",
|
|
1243
|
+
offset: str = "",
|
|
1244
|
+
include: str = "",
|
|
1245
|
+
) -> dict[str, Any]:
|
|
1246
|
+
return _conversation_list(
|
|
1247
|
+
db_path,
|
|
1248
|
+
{
|
|
1249
|
+
"merchant_id": merchant_id,
|
|
1250
|
+
"status": status,
|
|
1251
|
+
"buyer_id": buyer_id,
|
|
1252
|
+
"sku": sku,
|
|
1253
|
+
"updated_since": updated_since,
|
|
1254
|
+
"limit": limit,
|
|
1255
|
+
"offset": offset,
|
|
1256
|
+
"include": include,
|
|
1257
|
+
},
|
|
1258
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1259
|
+
owner_kind="merchant",
|
|
1260
|
+
owner_id=merchant_id,
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
@app.get("/merchants/{merchant_id}/human-review")
|
|
1264
|
+
def human_review(
|
|
1265
|
+
merchant_id: str,
|
|
1266
|
+
limit: str = "",
|
|
1267
|
+
offset: str = "",
|
|
1268
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1269
|
+
include: str = "",
|
|
1270
|
+
) -> dict[str, Any]:
|
|
1271
|
+
return _merchant_conversations(
|
|
1272
|
+
db_path,
|
|
1273
|
+
merchant_id,
|
|
1274
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1275
|
+
status="human_required",
|
|
1276
|
+
limit=limit,
|
|
1277
|
+
offset=offset,
|
|
1278
|
+
include=include,
|
|
1279
|
+
)
|
|
1280
|
+
|
|
1281
|
+
@app.post("/conversations/{conversation_id}/human-review")
|
|
1282
|
+
def create_human_review(
|
|
1283
|
+
conversation_id: str,
|
|
1284
|
+
payload: dict[str, Any],
|
|
1285
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1286
|
+
) -> dict[str, Any]:
|
|
1287
|
+
return _create_human_review(db_path, conversation_id, api_auth.payload_with_auth(payload, authorization))
|
|
1288
|
+
|
|
1289
|
+
@app.post("/conversations/{conversation_id}/human-review/resolve")
|
|
1290
|
+
def resolve_human_review(
|
|
1291
|
+
conversation_id: str,
|
|
1292
|
+
payload: dict[str, Any],
|
|
1293
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1294
|
+
) -> dict[str, Any]:
|
|
1295
|
+
return _resolve_human_review(db_path, conversation_id, api_auth.payload_with_auth(payload, authorization))
|
|
1296
|
+
|
|
1297
|
+
@app.get("/capabilities")
|
|
1298
|
+
def capabilities() -> dict[str, Any]:
|
|
1299
|
+
return _capabilities(db_path)
|
|
1300
|
+
|
|
1301
|
+
@app.get("/negotiation/pending-messages")
|
|
1302
|
+
def negotiation_pending_messages(authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1303
|
+
return _negotiation_pending_messages(db_path, api_auth.payload_with_auth({}, authorization))
|
|
1304
|
+
|
|
1305
|
+
@app.post("/negotiation/claims")
|
|
1306
|
+
def negotiation_claim(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1307
|
+
return _negotiation_claim(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1308
|
+
|
|
1309
|
+
@app.post("/negotiation/claims/complete")
|
|
1310
|
+
def negotiation_complete_claim(
|
|
1311
|
+
payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER
|
|
1312
|
+
) -> dict[str, Any]:
|
|
1313
|
+
return _negotiation_complete_claim(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1314
|
+
|
|
1315
|
+
@app.post("/negotiation/claims/fail")
|
|
1316
|
+
def negotiation_fail_claim(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1317
|
+
return _negotiation_fail_claim(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1318
|
+
|
|
1319
|
+
@app.post("/negotiation/claims/abandon")
|
|
1320
|
+
def negotiation_abandon_claim(payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER) -> dict[str, Any]:
|
|
1321
|
+
return _negotiation_abandon_claim(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1322
|
+
|
|
1323
|
+
@app.post("/negotiation/claims/heartbeat")
|
|
1324
|
+
def negotiation_heartbeat_claims(
|
|
1325
|
+
payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER
|
|
1326
|
+
) -> dict[str, Any]:
|
|
1327
|
+
return _negotiation_heartbeat_claims(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1328
|
+
|
|
1329
|
+
@app.post("/negotiation/claims/abandon-stale")
|
|
1330
|
+
def negotiation_abandon_stale_claims(
|
|
1331
|
+
payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER
|
|
1332
|
+
) -> dict[str, Any]:
|
|
1333
|
+
return _negotiation_abandon_stale_claims(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1334
|
+
|
|
1335
|
+
@app.get("/negotiation/snapshot")
|
|
1336
|
+
def negotiation_snapshot(
|
|
1337
|
+
conversation_id: str = "",
|
|
1338
|
+
message_id: str = "",
|
|
1339
|
+
authorization: str = AUTHORIZATION_HEADER,
|
|
1340
|
+
) -> dict[str, Any]:
|
|
1341
|
+
return _negotiation_snapshot(
|
|
1342
|
+
db_path,
|
|
1343
|
+
api_auth.payload_with_auth({}, authorization),
|
|
1344
|
+
{"conversation_id": conversation_id, "message_id": message_id},
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
@app.post("/negotiation/decisions")
|
|
1348
|
+
def negotiation_submit_decision(
|
|
1349
|
+
payload: dict[str, Any], authorization: str = AUTHORIZATION_HEADER
|
|
1350
|
+
) -> dict[str, Any]:
|
|
1351
|
+
return _negotiation_submit_decision(db_path, api_auth.payload_with_auth(payload, authorization))
|
|
1352
|
+
|
|
1353
|
+
return app
|