centive-sdk 2.2.0.dev6__py3-none-any.whl → 2.2.0.dev8__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.
- centive_sdk/config.py +6 -0
- centive_sdk/resources/websocket_server.py +102 -4
- {centive_sdk-2.2.0.dev6.dist-info → centive_sdk-2.2.0.dev8.dist-info}/METADATA +1 -1
- {centive_sdk-2.2.0.dev6.dist-info → centive_sdk-2.2.0.dev8.dist-info}/RECORD +6 -6
- {centive_sdk-2.2.0.dev6.dist-info → centive_sdk-2.2.0.dev8.dist-info}/WHEEL +0 -0
- {centive_sdk-2.2.0.dev6.dist-info → centive_sdk-2.2.0.dev8.dist-info}/licenses/LICENSE +0 -0
centive_sdk/config.py
CHANGED
|
@@ -83,6 +83,12 @@ class ClientConfig(BaseModel):
|
|
|
83
83
|
max_session_id_chars: int = 200
|
|
84
84
|
# Concurrent websocket connections accepted per user.
|
|
85
85
|
max_connections_per_user: int = 5
|
|
86
|
+
# How long the connect-time session trigger waits for the browser SDK to
|
|
87
|
+
# ask for a session itself before firing. Browser SDKs since 1.0 ask the
|
|
88
|
+
# moment the socket opens, and their request is the one that carries the
|
|
89
|
+
# page the user has open; firing first meant two Centive calls and two Anam
|
|
90
|
+
# tokens per connection. 0 restores the old immediate trigger.
|
|
91
|
+
session_trigger_grace_seconds: float = 0.5
|
|
86
92
|
|
|
87
93
|
# Page-telemetry frames (type="telemetry") from the FE SDK. These mirror the
|
|
88
94
|
# aria-sdk producer's own caps (MAX_EVENTS_PER_FRAME, MAX_FRAME_BYTES,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import inspect
|
|
2
3
|
import json
|
|
3
4
|
import secrets
|
|
@@ -82,6 +83,14 @@ class WebSocketServer:
|
|
|
82
83
|
# current_page when the FE's user_trigger frame carries no page of its
|
|
83
84
|
# own (older browser SDKs). Bounded by the number of live connections.
|
|
84
85
|
self._connection_last_page: Dict[WebSocketServerProtocol, dict] = {}
|
|
86
|
+
# One Centive trigger-session call per connection start, not two: the
|
|
87
|
+
# connect-time automatic trigger is held back for a grace period and
|
|
88
|
+
# stands down when the client asked itself (_auto_trigger_after_grace);
|
|
89
|
+
# a request that lands while another trigger for the same connection is
|
|
90
|
+
# in flight waits for it and receives the same frame.
|
|
91
|
+
self._connection_trigger_tasks: Dict[WebSocketServerProtocol, asyncio.Task] = {}
|
|
92
|
+
self._connection_triggered: set = set()
|
|
93
|
+
self._connection_trigger_inflight: Dict[WebSocketServerProtocol, asyncio.Future] = {}
|
|
85
94
|
# Every session key touched by a connection, so a disconnect saves all of
|
|
86
95
|
# them rather than only the most recent one.
|
|
87
96
|
self._connection_sessions: Dict[WebSocketServerProtocol, Set[str]] = {}
|
|
@@ -569,7 +578,9 @@ class WebSocketServer:
|
|
|
569
578
|
{"client": client_info, "user_id": user_id},
|
|
570
579
|
)
|
|
571
580
|
|
|
572
|
-
|
|
581
|
+
self._connection_trigger_tasks[websocket] = asyncio.create_task(
|
|
582
|
+
self._auto_trigger_after_grace(websocket, user_id, client_info)
|
|
583
|
+
)
|
|
573
584
|
|
|
574
585
|
try:
|
|
575
586
|
async for message in websocket:
|
|
@@ -599,6 +610,11 @@ class WebSocketServer:
|
|
|
599
610
|
self._clients.discard(websocket)
|
|
600
611
|
self._connection_users.pop(websocket, None)
|
|
601
612
|
self._connection_last_page.pop(websocket, None)
|
|
613
|
+
self._connection_triggered.discard(websocket)
|
|
614
|
+
self._connection_trigger_inflight.pop(websocket, None)
|
|
615
|
+
pending_trigger = self._connection_trigger_tasks.pop(websocket, None)
|
|
616
|
+
if pending_trigger is not None:
|
|
617
|
+
pending_trigger.cancel()
|
|
602
618
|
self._telemetry_rate.forget(websocket)
|
|
603
619
|
session_keys = self._connection_sessions.pop(websocket, set())
|
|
604
620
|
|
|
@@ -1408,10 +1424,47 @@ class WebSocketServer:
|
|
|
1408
1424
|
# batcher. The frame's own page wins; the last page_view seen on this
|
|
1409
1425
|
# connection stands in for browser SDKs that predate the field.
|
|
1410
1426
|
current_page = self._coerce_page(data.get("page")) or self._connection_last_page.get(websocket)
|
|
1427
|
+
# The client asked itself, so the connect-time automatic trigger, if it
|
|
1428
|
+
# is still waiting out its grace period, has nothing left to do.
|
|
1429
|
+
self._connection_triggered.add(websocket)
|
|
1411
1430
|
await self._trigger_and_emit(
|
|
1412
1431
|
websocket, user_id, user_trigger, client_info, current_page=current_page
|
|
1413
1432
|
)
|
|
1414
1433
|
|
|
1434
|
+
async def _auto_trigger_after_grace(
|
|
1435
|
+
self, websocket: WebSocketServerProtocol, user_id: str, client_info: str
|
|
1436
|
+
) -> None:
|
|
1437
|
+
"""The connect-time session trigger, held back briefly.
|
|
1438
|
+
|
|
1439
|
+
Browser SDKs since 1.0 ask for a token themselves the moment the socket
|
|
1440
|
+
opens, and that request is the one that carries the page the user has
|
|
1441
|
+
open. Firing the automatic trigger first (and before the message loop
|
|
1442
|
+
even started reading) meant two Centive calls and two Anam tokens per
|
|
1443
|
+
connection, with the opening prompt built from the page-less one. Now
|
|
1444
|
+
the automatic trigger waits config.session_trigger_grace_seconds and
|
|
1445
|
+
stands down when the client has asked in the meantime. A client that
|
|
1446
|
+
never asks (older SDKs, custom frontends) still gets its session, half
|
|
1447
|
+
a second later.
|
|
1448
|
+
"""
|
|
1449
|
+
try:
|
|
1450
|
+
grace = max(0.0, float(self._config.session_trigger_grace_seconds or 0))
|
|
1451
|
+
if grace:
|
|
1452
|
+
await asyncio.sleep(grace)
|
|
1453
|
+
if websocket in self._connection_triggered or not self._is_live(websocket):
|
|
1454
|
+
safe_log(
|
|
1455
|
+
self._logger,
|
|
1456
|
+
"info",
|
|
1457
|
+
"automatic session trigger stood down",
|
|
1458
|
+
{"client": client_info, "user_id": user_id,
|
|
1459
|
+
"reason": "client asked" if websocket in self._connection_triggered else "connection gone"},
|
|
1460
|
+
)
|
|
1461
|
+
return
|
|
1462
|
+
await self._trigger_and_emit(websocket, user_id, None, client_info)
|
|
1463
|
+
except asyncio.CancelledError:
|
|
1464
|
+
return
|
|
1465
|
+
finally:
|
|
1466
|
+
self._connection_trigger_tasks.pop(websocket, None)
|
|
1467
|
+
|
|
1415
1468
|
def _remember_page(self, websocket: WebSocketServerProtocol, events: Any) -> None:
|
|
1416
1469
|
"""Keeps the newest page_view's page for this connection (see _handle_user_trigger)."""
|
|
1417
1470
|
if not isinstance(events, list):
|
|
@@ -1444,7 +1497,50 @@ class WebSocketServer:
|
|
|
1444
1497
|
client_info: str,
|
|
1445
1498
|
current_page: Optional[dict] = None,
|
|
1446
1499
|
) -> None:
|
|
1447
|
-
"""calls trigger_session API and emits response to FE.
|
|
1500
|
+
"""calls trigger_session API and emits response to FE.
|
|
1501
|
+
|
|
1502
|
+
Two triggers for the same connection at once (the automatic one mid
|
|
1503
|
+
call when the client's own request lands) share one Centive call: the
|
|
1504
|
+
second waits for the first and re-emits its frame.
|
|
1505
|
+
"""
|
|
1506
|
+
inflight = self._connection_trigger_inflight.get(websocket)
|
|
1507
|
+
if inflight is not None:
|
|
1508
|
+
safe_log(
|
|
1509
|
+
self._logger,
|
|
1510
|
+
"info",
|
|
1511
|
+
"session trigger coalesced with one in flight",
|
|
1512
|
+
{"client": client_info, "user_id": user_id},
|
|
1513
|
+
)
|
|
1514
|
+
try:
|
|
1515
|
+
frame = await asyncio.shield(inflight)
|
|
1516
|
+
except Exception: # noqa: BLE001 - the first caller already reported it
|
|
1517
|
+
frame = None
|
|
1518
|
+
if frame is not None:
|
|
1519
|
+
try:
|
|
1520
|
+
await websocket.send(frame)
|
|
1521
|
+
except Exception: # noqa: BLE001
|
|
1522
|
+
pass
|
|
1523
|
+
return
|
|
1524
|
+
|
|
1525
|
+
done: asyncio.Future = asyncio.get_running_loop().create_future()
|
|
1526
|
+
self._connection_trigger_inflight[websocket] = done
|
|
1527
|
+
emitted: Optional[str] = None
|
|
1528
|
+
try:
|
|
1529
|
+
emitted = await self._trigger_once(websocket, user_id, user_trigger, client_info, current_page)
|
|
1530
|
+
finally:
|
|
1531
|
+
self._connection_trigger_inflight.pop(websocket, None)
|
|
1532
|
+
if not done.done():
|
|
1533
|
+
done.set_result(emitted)
|
|
1534
|
+
|
|
1535
|
+
async def _trigger_once(
|
|
1536
|
+
self,
|
|
1537
|
+
websocket: WebSocketServerProtocol,
|
|
1538
|
+
user_id: str,
|
|
1539
|
+
user_trigger: Optional[bool],
|
|
1540
|
+
client_info: str,
|
|
1541
|
+
current_page: Optional[dict],
|
|
1542
|
+
) -> Optional[str]:
|
|
1543
|
+
"""One trigger-session call. Returns the success frame it sent, if any."""
|
|
1448
1544
|
safe_log(
|
|
1449
1545
|
self._logger,
|
|
1450
1546
|
"info",
|
|
@@ -1481,7 +1577,7 @@ class WebSocketServer:
|
|
|
1481
1577
|
"message": "failed to trigger session",
|
|
1482
1578
|
"error": "UPSTREAM_TRIGGER_FAILED",
|
|
1483
1579
|
}))
|
|
1484
|
-
return
|
|
1580
|
+
return None
|
|
1485
1581
|
|
|
1486
1582
|
response_data = {
|
|
1487
1583
|
"status": response.status,
|
|
@@ -1490,7 +1586,8 @@ class WebSocketServer:
|
|
|
1490
1586
|
"time_taken": response.time_taken,
|
|
1491
1587
|
}
|
|
1492
1588
|
|
|
1493
|
-
|
|
1589
|
+
frame = json.dumps(response_data)
|
|
1590
|
+
await websocket.send(frame)
|
|
1494
1591
|
|
|
1495
1592
|
safe_log(
|
|
1496
1593
|
self._logger,
|
|
@@ -1503,6 +1600,7 @@ class WebSocketServer:
|
|
|
1503
1600
|
"has_session_data": response.session_data is not None,
|
|
1504
1601
|
},
|
|
1505
1602
|
)
|
|
1603
|
+
return frame
|
|
1506
1604
|
|
|
1507
1605
|
except Exception as e:
|
|
1508
1606
|
# Generic on the wire, detailed in the server's own logs: upstream
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: centive-sdk
|
|
3
|
-
Version: 2.2.0.
|
|
3
|
+
Version: 2.2.0.dev8
|
|
4
4
|
Summary: Python SDK for Centive backend integration
|
|
5
5
|
Project-URL: Homepage, https://github.com/TheAgenticAI/centive-backend-sdk
|
|
6
6
|
Project-URL: Repository, https://github.com/TheAgenticAI/centive-backend-sdk
|
|
@@ -4,7 +4,7 @@ centive_sdk/_logging.py,sha256=z8Kdi4vJdko0nkMeySGSHwPkEB2E7ucklc9FeJCbyOw,3518
|
|
|
4
4
|
centive_sdk/_retry.py,sha256=Q1cIFZyimj0RS4x14FEOseqpORCSydvJyzGiIPfDn7E,1755
|
|
5
5
|
centive_sdk/async_client.py,sha256=RRoCJrvdeLwQz5UJv2EjmlgTq7vntSV9ELMWHyNwJ5I,9355
|
|
6
6
|
centive_sdk/client.py,sha256=Evtsng8cqqRSoV54MI2IKbEW1q0vPOkxlz9zM0evH7k,3249
|
|
7
|
-
centive_sdk/config.py,sha256=
|
|
7
|
+
centive_sdk/config.py,sha256=1GoHkwJebL1HknSa-y4oFhxDAaG0boq0JY0csNV-pfE,6612
|
|
8
8
|
centive_sdk/exceptions.py,sha256=wFy-hU0TJ0dHa6TEDI7FIQu9Lbg5aMeWxW9E-IMMUbI,1841
|
|
9
9
|
centive_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
centive_sdk/models/__init__.py,sha256=-ledNpbOi1Q14ELhvlDbGPF37N3yv5XJ-QsH2I6UMVQ,598
|
|
@@ -16,8 +16,8 @@ centive_sdk/resources/async_telemetry.py,sha256=wT12g9rG2dp_ml5YpAYCxobLlhfcsyy-
|
|
|
16
16
|
centive_sdk/resources/message_accumulator.py,sha256=Or3sMGkXG9upeQ9sm8YWL_V5vvsxtnE9tu52VBUUGgY,19261
|
|
17
17
|
centive_sdk/resources/sessions.py,sha256=Ipg4bpmpejA5WoALbT49ah0c3I7x4OgPn9tSmfW8JAw,16741
|
|
18
18
|
centive_sdk/resources/telemetry_batcher.py,sha256=OkOv2Iir6E_9mdtAKvelol3RaveGSlH-ckIoE2cQYCc,14629
|
|
19
|
-
centive_sdk/resources/websocket_server.py,sha256=
|
|
20
|
-
centive_sdk-2.2.0.
|
|
21
|
-
centive_sdk-2.2.0.
|
|
22
|
-
centive_sdk-2.2.0.
|
|
23
|
-
centive_sdk-2.2.0.
|
|
19
|
+
centive_sdk/resources/websocket_server.py,sha256=RTkM1YyD7afNWs4KpoboCo-NI2TFgk8IkzaH0KAKR5A,66518
|
|
20
|
+
centive_sdk-2.2.0.dev8.dist-info/METADATA,sha256=5DM9IFShg7CIqzt5jQ8goRocZXmOCJtELTUWVUgVSZY,36427
|
|
21
|
+
centive_sdk-2.2.0.dev8.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
22
|
+
centive_sdk-2.2.0.dev8.dist-info/licenses/LICENSE,sha256=9dFibPr7z9srhWwd35lQL2MX1Kx2qivEZdVQb9L8Q-Q,1064
|
|
23
|
+
centive_sdk-2.2.0.dev8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|