soothe-client-python 1.0.1__tar.gz → 1.0.2__tar.gz
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.
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/PKG-INFO +1 -1
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/__init__.py +14 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/classifier.py +5 -3
- soothe_client_python-1.0.2/src/soothe_client/appkit/turn_boundary.py +127 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/turn_runner.py +24 -5
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/.gitignore +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/LICENSE +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/README.md +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/pyproject.toml +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/__init__.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/attachments.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/broadcaster.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/chunk_filter.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/daemon_session.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/events.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/managed_client.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/observability.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/pool.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/query_gate.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/session_store.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/thinking_step.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/turn.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/command_client.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/errors.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/helpers.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/intent_hints.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/protocol_params.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/py.typed +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/schemas.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/session.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/stream_terminal.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/turn_boundary.py +0 -0
- {soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: soothe-client-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: WebSocket client + appkit for soothe-daemon (Python)
|
|
5
5
|
Project-URL: Homepage, https://github.com/mirasoth/soothe-client-python
|
|
6
6
|
Project-URL: Documentation, https://soothe.readthedocs.io
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/__init__.py
RENAMED
|
@@ -42,6 +42,14 @@ from soothe_client.appkit.thinking_step import (
|
|
|
42
42
|
DEFAULT_THINKING_STEP_EVENTS,
|
|
43
43
|
extract_thinking_step,
|
|
44
44
|
)
|
|
45
|
+
from soothe_client.appkit.turn_boundary import (
|
|
46
|
+
TURN_END_IDLE,
|
|
47
|
+
TURN_END_STOPPED,
|
|
48
|
+
TURN_END_STREAM_END,
|
|
49
|
+
TurnBoundary,
|
|
50
|
+
TurnLifecycleGate,
|
|
51
|
+
is_daemon_turn_end_event,
|
|
52
|
+
)
|
|
45
53
|
from soothe_client.appkit.turn_runner import (
|
|
46
54
|
STREAM_CLOSE_FAIL,
|
|
47
55
|
STREAM_CLOSE_SOFT_COMPLETE,
|
|
@@ -88,10 +96,16 @@ __all__ = [
|
|
|
88
96
|
"SessionMessage",
|
|
89
97
|
"SessionStore",
|
|
90
98
|
"StreamClosePolicy",
|
|
99
|
+
"TURN_END_IDLE",
|
|
100
|
+
"TURN_END_STOPPED",
|
|
101
|
+
"TURN_END_STREAM_END",
|
|
91
102
|
"TimeoutPolicy",
|
|
103
|
+
"TurnBoundary",
|
|
92
104
|
"TurnConfig",
|
|
105
|
+
"TurnLifecycleGate",
|
|
93
106
|
"TurnRunner",
|
|
94
107
|
"compact_attachments",
|
|
108
|
+
"is_daemon_turn_end_event",
|
|
95
109
|
"compact_image_attachment",
|
|
96
110
|
"default_pool_config",
|
|
97
111
|
"extract_thinking_step",
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/classifier.py
RENAMED
|
@@ -49,9 +49,9 @@ class ClassifierConfig:
|
|
|
49
49
|
persisted as final (avoids finishing on stub ACKs). Defaults to 8.
|
|
50
50
|
thinking_step_events: Optional override of the default thinking-step
|
|
51
51
|
event allowlist.
|
|
52
|
-
treat_status_idle_as_complete:
|
|
53
|
-
``
|
|
54
|
-
|
|
52
|
+
treat_status_idle_as_complete: Standalone ``classify`` only. Prefer
|
|
53
|
+
``TurnRunner`` + ``TurnBoundary`` for turn end (DaemonSession
|
|
54
|
+
contract). Default false.
|
|
55
55
|
"""
|
|
56
56
|
|
|
57
57
|
deliverable_phases: frozenset[str] | set[str]
|
|
@@ -85,6 +85,8 @@ class EventClassifier:
|
|
|
85
85
|
return False
|
|
86
86
|
if event_type in (
|
|
87
87
|
"status.idle",
|
|
88
|
+
"status.stopped",
|
|
89
|
+
"soothe.stream.end",
|
|
88
90
|
"idle_timeout",
|
|
89
91
|
"query_timeout",
|
|
90
92
|
"stream_closed",
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""DaemonSession turn-end contract for the pool TurnRunner path.
|
|
2
|
+
|
|
3
|
+
TurnRunner owns one ``TurnBoundary`` per Execute. EventClassifier may
|
|
4
|
+
early-complete on deliverable phases for UX; it is not the sole terminator.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from soothe_client.stream_terminal import (
|
|
13
|
+
STREAM_END,
|
|
14
|
+
is_turn_end_custom_data,
|
|
15
|
+
is_turn_progress_chunk,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
TURN_END_STREAM_END = STREAM_END
|
|
19
|
+
TURN_END_IDLE = "status.idle"
|
|
20
|
+
TURN_END_STOPPED = "status.stopped"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TurnLifecycleGate:
|
|
25
|
+
"""Per-turn progress flags (DaemonSession parity; not shared across chats)."""
|
|
26
|
+
|
|
27
|
+
saw_running: bool = False
|
|
28
|
+
saw_stream_payload: bool = False
|
|
29
|
+
saw_turn_progress: bool = False
|
|
30
|
+
|
|
31
|
+
def observe(self, msg: Any) -> None:
|
|
32
|
+
frame = _normalize_frame(msg)
|
|
33
|
+
if frame is None:
|
|
34
|
+
return
|
|
35
|
+
typ = str(frame.get("type") or "")
|
|
36
|
+
if typ == "status":
|
|
37
|
+
if str(frame.get("state") or "").strip().lower() == "running":
|
|
38
|
+
self.saw_running = True
|
|
39
|
+
return
|
|
40
|
+
if typ == "event":
|
|
41
|
+
self.saw_stream_payload = True
|
|
42
|
+
mode = str(frame.get("mode") or "")
|
|
43
|
+
if is_turn_progress_chunk(mode, frame.get("data")):
|
|
44
|
+
self.saw_turn_progress = True
|
|
45
|
+
|
|
46
|
+
def allow_stream_end(self) -> bool:
|
|
47
|
+
return self.saw_running and self.saw_turn_progress
|
|
48
|
+
|
|
49
|
+
def allow_idle_complete(self) -> bool:
|
|
50
|
+
return self.saw_running and self.saw_stream_payload
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class TurnBoundary:
|
|
55
|
+
"""Applies DaemonSession end rules to pool decoded frames."""
|
|
56
|
+
|
|
57
|
+
gate: TurnLifecycleGate = field(default_factory=TurnLifecycleGate)
|
|
58
|
+
ended: bool = False
|
|
59
|
+
reason: str = ""
|
|
60
|
+
|
|
61
|
+
def feed(self, msg: Any) -> tuple[bool, str]:
|
|
62
|
+
if self.ended:
|
|
63
|
+
return True, self.reason
|
|
64
|
+
self.gate.observe(msg)
|
|
65
|
+
frame = _normalize_frame(msg)
|
|
66
|
+
if frame is None:
|
|
67
|
+
return False, ""
|
|
68
|
+
|
|
69
|
+
typ = str(frame.get("type") or "")
|
|
70
|
+
if typ == "status":
|
|
71
|
+
state = str(frame.get("state") or "").strip().lower()
|
|
72
|
+
if state == "stopped" and self.gate.saw_running:
|
|
73
|
+
return self._mark(TURN_END_STOPPED)
|
|
74
|
+
if state == "idle" and self.gate.allow_idle_complete():
|
|
75
|
+
return self._mark(TURN_END_IDLE)
|
|
76
|
+
return False, ""
|
|
77
|
+
|
|
78
|
+
if typ == "event":
|
|
79
|
+
mode = str(frame.get("mode") or "")
|
|
80
|
+
data = frame.get("data")
|
|
81
|
+
if mode == "custom" and is_turn_end_custom_data(data) and self.gate.allow_stream_end():
|
|
82
|
+
return self._mark(TURN_END_STREAM_END)
|
|
83
|
+
return False, ""
|
|
84
|
+
|
|
85
|
+
def _mark(self, reason: str) -> tuple[bool, str]:
|
|
86
|
+
self.ended = True
|
|
87
|
+
self.reason = reason
|
|
88
|
+
return True, reason
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def is_daemon_turn_end_event(completion_event: str) -> bool:
|
|
92
|
+
"""True for TurnBoundary completion_event values (not phase deliverables)."""
|
|
93
|
+
return (completion_event or "").strip() in {
|
|
94
|
+
TURN_END_STREAM_END,
|
|
95
|
+
TURN_END_IDLE,
|
|
96
|
+
TURN_END_STOPPED,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _normalize_frame(msg: Any) -> dict[str, Any] | None:
|
|
101
|
+
if not isinstance(msg, dict):
|
|
102
|
+
return None
|
|
103
|
+
typ = msg.get("type")
|
|
104
|
+
if typ == "next":
|
|
105
|
+
payload = msg.get("payload")
|
|
106
|
+
if not isinstance(payload, dict):
|
|
107
|
+
return None
|
|
108
|
+
inner = payload.get("data")
|
|
109
|
+
if isinstance(inner, dict) and inner.get("type") == "status":
|
|
110
|
+
return inner
|
|
111
|
+
if isinstance(inner, dict) and inner.get("mode"):
|
|
112
|
+
return {
|
|
113
|
+
"type": "event",
|
|
114
|
+
"mode": inner.get("mode"),
|
|
115
|
+
"data": inner.get("data"),
|
|
116
|
+
"namespace": inner.get("namespace") or payload.get("namespace"),
|
|
117
|
+
}
|
|
118
|
+
mode = payload.get("mode")
|
|
119
|
+
if mode:
|
|
120
|
+
return {
|
|
121
|
+
"type": "event",
|
|
122
|
+
"mode": mode,
|
|
123
|
+
"data": payload.get("data"),
|
|
124
|
+
"namespace": payload.get("namespace"),
|
|
125
|
+
}
|
|
126
|
+
return None
|
|
127
|
+
return msg
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/turn_runner.py
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Turn runner for appkit.
|
|
2
2
|
|
|
3
3
|
Executes one query turn end-to-end: acquire a pooled connection, enforce
|
|
4
|
-
single-flight, send loop_input, consume the event stream,
|
|
5
|
-
resolve the deliverable, persist the reply, and broadcast completion.
|
|
4
|
+
single-flight, send loop_input, consume the event stream, persist/broadcast.
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
Turn end is owned by ``TurnBoundary`` (DaemonSession.iter_turn_chunks contract:
|
|
7
|
+
gated ``stream.end`` / idle / stopped). ``EventClassifier`` selects user-visible
|
|
8
|
+
text and may early-complete on deliverable phases for UX.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
@@ -24,6 +24,7 @@ from soothe_client.appkit.classifier import ChatEventTerminal, EventClassifier
|
|
|
24
24
|
from soothe_client.appkit.pool import ConnectionPool, PooledConn
|
|
25
25
|
from soothe_client.appkit.query_gate import QueryGate
|
|
26
26
|
from soothe_client.appkit.session_store import SessionMessage, SessionStore
|
|
27
|
+
from soothe_client.appkit.turn_boundary import TurnBoundary, is_daemon_turn_end_event
|
|
27
28
|
from soothe_client.intent_hints import validate_loop_input_intent_hint
|
|
28
29
|
|
|
29
30
|
Attachment = dict[str, Any]
|
|
@@ -261,6 +262,7 @@ class TurnRunner:
|
|
|
261
262
|
|
|
262
263
|
assistant_content = ""
|
|
263
264
|
started_at = time.time()
|
|
265
|
+
boundary = TurnBoundary()
|
|
264
266
|
agen = event_stream.__aiter__()
|
|
265
267
|
arm_idle()
|
|
266
268
|
|
|
@@ -378,6 +380,7 @@ class TurnRunner:
|
|
|
378
380
|
|
|
379
381
|
arm_idle()
|
|
380
382
|
|
|
383
|
+
ended, end_reason = boundary.feed(msg)
|
|
381
384
|
event_result = self._classifier.classify(msg, assistant_content)
|
|
382
385
|
if (
|
|
383
386
|
event_result.err is not None
|
|
@@ -404,7 +407,9 @@ class TurnRunner:
|
|
|
404
407
|
event_result,
|
|
405
408
|
assistant_content,
|
|
406
409
|
)
|
|
407
|
-
if deliverable
|
|
410
|
+
if deliverable and not is_daemon_turn_end_event(
|
|
411
|
+
event_result.completion_event or ""
|
|
412
|
+
):
|
|
408
413
|
await self._complete_turn(
|
|
409
414
|
session_id,
|
|
410
415
|
loop_id,
|
|
@@ -413,6 +418,20 @@ class TurnRunner:
|
|
|
413
418
|
event_result.completion_event or "",
|
|
414
419
|
)
|
|
415
420
|
return
|
|
421
|
+
|
|
422
|
+
if ended:
|
|
423
|
+
if self._classifier.is_substantive_assistant_reply(assistant_content):
|
|
424
|
+
await self._complete_turn(
|
|
425
|
+
session_id,
|
|
426
|
+
loop_id,
|
|
427
|
+
assistant_content.strip(),
|
|
428
|
+
started_at,
|
|
429
|
+
end_reason,
|
|
430
|
+
)
|
|
431
|
+
return
|
|
432
|
+
empty = RuntimeError(f"turn ended ({end_reason}) with no assistant content")
|
|
433
|
+
await self._fail_turn(session_id, loop_id, empty)
|
|
434
|
+
raise empty
|
|
416
435
|
finally:
|
|
417
436
|
query_timer.cancel()
|
|
418
437
|
with contextlib.suppress(asyncio.CancelledError):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/attachments.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/broadcaster.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/chunk_filter.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/daemon_session.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/events.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/managed_client.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/observability.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/query_gate.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/session_store.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/appkit/thinking_step.py
RENAMED
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/command_client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/protocol_params.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/stream_terminal.py
RENAMED
|
File without changes
|
{soothe_client_python-1.0.1 → soothe_client_python-1.0.2}/src/soothe_client/turn_boundary.py
RENAMED
|
File without changes
|
|
File without changes
|