ailogger-client-dev 0.1.51__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.
@@ -0,0 +1,332 @@
1
+ """
2
+ Typed methods for appending messages to dialogue sessions.
3
+
4
+ This module carries the single mixin class whose typed method lets a caller
5
+ append a message to an existing dialogue session. The method names its
6
+ arguments for what the corresponding command's parameter schema declares,
7
+ so a caller appends a message in the service's own terms and never assembles
8
+ a request envelope, never names a command as a free string and never guesses
9
+ a parameter name.
10
+
11
+ A parameter the command declares as required is a required argument of the
12
+ method; a parameter the command declares as optional is optional on the method
13
+ and is simply not carried when the caller does not supply it, so the service's
14
+ own resolution of an unsupplied optional parameter is never pre-empted by a
15
+ value invented in this client.
16
+
17
+ The method carries its arguments to the invocation seam of G-012/T-002/A-001
18
+ and returns what the seam returns. The method performs no validation of a
19
+ parameter value beyond what the language's own typing expresses: the command's
20
+ declared schema is validated at the service.
21
+
22
+ NETWORK LAW. This module imports nothing from the adapter framework's
23
+ implementation and adds no transport of its own. All network interaction goes
24
+ through the invocation seam and thence through the privately held framework
25
+ client instance, and through nothing else.
26
+
27
+ BOUNDARIES. This module declares no generic call, no completion default, no
28
+ non-blocking path or any job-observing call (all owned by earlier steps).
29
+ Every declaration in this file is fully annotated for the pipeline's named
30
+ strict type check, and the module satisfies its named lint check.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ from typing import Any
36
+
37
+ __all__ = ["MessageMixin"]
38
+
39
+
40
+ class MessageMixin:
41
+ """Mixin providing a typed method for appending messages.
42
+
43
+ Declares the single typed method by which a caller appends a message to
44
+ an existing dialogue session.
45
+
46
+ This mixin expects to be composed with InvocationMixin, which provides
47
+ the _invoke method.
48
+ """
49
+
50
+ async def _invoke(
51
+ self,
52
+ command: str,
53
+ params: dict[str, Any] | None = None,
54
+ *,
55
+ wait_for_result: bool = True,
56
+ ) -> Any:
57
+ """Private invocation entry point.
58
+
59
+ This method is provided by InvocationMixin when this mixin is composed
60
+ into a class. This stub is declared here for type checking purposes
61
+ only.
62
+
63
+ Args:
64
+ command: The name of the command to execute.
65
+ params: An optional mapping of command parameters.
66
+ wait_for_result: When True (default), wait for queued commands to
67
+ completion and return the finished result.
68
+
69
+ Returns:
70
+ The command result.
71
+ """
72
+ raise NotImplementedError # Provided by InvocationMixin
73
+
74
+ async def _submit_queued(self, command: str, params: dict[str, Any]) -> str:
75
+ """Provided by QueuedMixin when this mixin is composed."""
76
+ raise NotImplementedError # Provided by QueuedMixin
77
+
78
+ async def _await_queued_result(
79
+ self,
80
+ job_id: str,
81
+ *,
82
+ command: str,
83
+ timeout: float | None = None,
84
+ ) -> list[Any]:
85
+ """Provided by QueuedMixin when this mixin is composed."""
86
+ raise NotImplementedError # Provided by QueuedMixin
87
+
88
+ async def append_message( # noqa: PLR0913, PLR0917
89
+ self,
90
+ session_id: str,
91
+ text: str,
92
+ sender_kind: str,
93
+ sender_identity: str,
94
+ receiver_kind: str,
95
+ receiver_identity: str,
96
+ priority: int | None = None,
97
+ message_id: str | None = None,
98
+ relations: list[dict[str, Any]] | None = None,
99
+ corrects_message_id: str | None = None,
100
+ attributes: dict[str, Any] | None = None,
101
+ occurred_at: str | None = None,
102
+ force_overwrite: bool | None = None,
103
+ ) -> Any:
104
+ """Append one message to a dialogue session.
105
+
106
+ Appends one message to a dialogue session this service already holds.
107
+ Six values are required of every append without exception: the session
108
+ the message goes into, the text, and each of the two participants as a
109
+ kind together with an identity - the sender and the receiver.
110
+
111
+ Seven more are optional: the priority the message is recorded under
112
+ (recorded; it does not affect ordering or scheduling, and message_search
113
+ accepts it as an equality filter), the identifier it is accepted under
114
+ (the service generates one when none is supplied), the links it
115
+ declares to earlier messages of the same session, the message it
116
+ corrects, free-form attributes recorded alongside its text, the
117
+ moment the message happened
118
+ (``occurred_at``), and ``force_overwrite``.
119
+
120
+ When no identifier is supplied, the ordinary case, the service
121
+ generates a version 4 UUID and the answer carries it back. Sending
122
+ that identifier back with the same content is a retry and is answered
123
+ exactly as the first submission was, with no second message created.
124
+ Sending it back with ANY different content - text, participants,
125
+ priority, correction target, attributes, relations or stated moment -
126
+ is refused as a conflict, unless ``force_overwrite`` is true.
127
+
128
+ Each declared relation names the earlier message it points at and the
129
+ kind of the link. Relations the relation model does not admit are
130
+ refused and nothing is stored. The correction target is stated
131
+ separately and is never one entry of the relations collection. The
132
+ free-form attributes are recorded verbatim and never interpreted - no
133
+ key inside them alters routing, ordering or acceptance.
134
+
135
+ Args:
136
+ session_id: Reference to the dialogue session this message is
137
+ appended to, as the identifier that session was opened under.
138
+ Required of every append: this service appends to no default
139
+ session.
140
+ text: The text of the message being appended, recorded exactly as
141
+ submitted. Required of every append.
142
+ sender_kind: The capacity the sender of this message stands in, as
143
+ one of the four declared participant kinds (human, model, tool,
144
+ system). Required of every append, including one that records
145
+ plain application logging.
146
+ sender_identity: The concrete participant this message is
147
+ attributed to. Required of every append: two senders of the
148
+ same kind stay two participants whenever their identity differs,
149
+ and are never collapsed into one.
150
+ receiver_kind: The capacity the receiver of this message stands in,
151
+ as one of the four declared participant kinds. Required of
152
+ every append, including one that records plain application
153
+ logging.
154
+ receiver_identity: The concrete participant this message is
155
+ addressed to. Required of every append: two receivers of the
156
+ same kind stay two participants whenever their identity differs,
157
+ and are never collapsed into one.
158
+ priority: The priority this message is recorded under, as a
159
+ non-negative whole number. Zero marks the ordinary linear flow
160
+ of the dialogue and a higher value says the message was
161
+ inserted from outside that flow. Optional: omit it to record
162
+ under the priority the service resolves.
163
+ message_id: The identifier this message is to be accepted under, as
164
+ a well-formed version 4 UUID. Optional and legitimately absent:
165
+ the ordinary append supplies none, the service generates one
166
+ and the answer carries it back, and such a submission is a
167
+ first acceptance by construction. Supply the identifier a
168
+ previous answer returned to have a retry recognised as the same
169
+ message.
170
+ relations: The links this message declares to earlier messages of
171
+ the same session, in the order declared: each entry is a
172
+ mapping naming the earlier message (as to_message_id) and the
173
+ kind of the link (as kind). Optional: omit it or declare the
174
+ empty collection when this message declares none.
175
+ corrects_message_id: The identifier of the message this one
176
+ corrects, as a well-formed version 4 UUID, present only when
177
+ this message is issued as a correction of an earlier one.
178
+ Optional: omit it when this message corrects nothing.
179
+ attributes: Free-form attributes recorded alongside the message
180
+ text, stored verbatim and never interpreted. Optional and
181
+ legitimately absent or empty.
182
+ occurred_at: The moment the message happened, as an RFC 3339
183
+ timestamp WITH an explicit UTC offset (``Z`` or ``+hh:mm``;
184
+ fractional seconds allowed). It becomes the message's
185
+ effective event time, stored and returned normalised to UTC:
186
+ reads order by it, the period filters and the working-set
187
+ recency use it. Optional: omit it and the service's receive
188
+ time stands. Passed through unchanged; the service validates
189
+ it.
190
+ force_overwrite: When true, a ``message_id`` that is already
191
+ stored is REPLACED by this payload without any comparison,
192
+ keeping its identifier and sequence number and being analysed
193
+ afresh. Optional, default false (the identifier is recognised:
194
+ an identical repeat returns the original, a differing one is
195
+ a conflict). Passed through unchanged; the service validates
196
+ it.
197
+
198
+ Returns:
199
+ Exactly five facts about the accepted message: the identity it
200
+ was accepted under, as the identifier supplied when one was
201
+ supplied and the one the service generated when none was
202
+ (``message_id``); the position assigned to it within its session
203
+ by the arrival-order model (``sequence_number``); the audience it
204
+ carries (``audience``); and ``text_head`` and ``text_tail``, the
205
+ first and the last five words of the STORED text (words as
206
+ ``str.split()`` yields, joined by one space; they overlap when the
207
+ text has fewer than ten words, each holds all the words when it
208
+ has fewer than five, and both are ``""`` for a text without
209
+ words). A recognised retry answers with the identity, position,
210
+ audience and excerpts already accepted for that identifier; a
211
+ force overwrite answers with the excerpts of the text now stored.
212
+ """
213
+ params: dict[str, Any] = {
214
+ "session_id": session_id,
215
+ "text": text,
216
+ "sender_kind": sender_kind,
217
+ "sender_identity": sender_identity,
218
+ "receiver_kind": receiver_kind,
219
+ "receiver_identity": receiver_identity,
220
+ }
221
+ if priority is not None:
222
+ params["priority"] = priority
223
+ if message_id is not None:
224
+ params["message_id"] = message_id
225
+ if relations is not None:
226
+ params["relations"] = relations
227
+ if corrects_message_id is not None:
228
+ params["corrects_message_id"] = corrects_message_id
229
+ if attributes is not None:
230
+ params["attributes"] = attributes
231
+ if occurred_at is not None:
232
+ params["occurred_at"] = occurred_at
233
+ if force_overwrite is not None:
234
+ params["force_overwrite"] = force_overwrite
235
+ return await self._invoke("append_message", params)
236
+
237
+ async def messages_import(
238
+ self,
239
+ messages: list[dict[str, Any]],
240
+ *,
241
+ timeout: float | None = None,
242
+ ) -> list[dict[str, Any]]:
243
+ """Import a batch of messages and return the per-item outcomes.
244
+
245
+ ``messages_import`` is ALWAYS queued by the service. This method
246
+ submits the job, waits for it to finish through the client's own job
247
+ tracking and returns the job's result: a list with exactly one entry
248
+ per input item, in input order, each ``{"message_id": <id or None>,
249
+ "success": <bool>}`` and, when ``success`` is true, also
250
+ ``"text_head"`` and ``"text_tail"`` (the first and the last five
251
+ words of the item's stored text, as for ``append_message``).
252
+ ``success`` is true when the message is stored
253
+ (newly created, an identical repeat, or overwritten) and false when it
254
+ was not stored (the item failed validation or conflicts with a stored
255
+ message). Items are independent: a false item does not roll back the
256
+ others. Running the same import again answers true for every item and
257
+ adds no row.
258
+
259
+ Args:
260
+ messages: The items to import - exactly the ``append_message``
261
+ parameter set per item, with ``message_id`` and
262
+ ``occurred_at`` required. Passed through unchanged; the
263
+ service validates them.
264
+ timeout: The longest to wait for the job, in seconds. Optional:
265
+ the client's own bound applies when omitted.
266
+
267
+ Returns:
268
+ The per-item outcomes, in input order.
269
+
270
+ Raises:
271
+ ServiceRefusalError: When the whole call is refused (messages
272
+ missing, not an array, empty, over the configured maximum, an
273
+ element that is not an object, an unnamed parameter or a
274
+ repeated message_id) - nothing is written and no job exists.
275
+ CompletionTimeoutError: When the job does not finish in time.
276
+ CompletionFailedError: When the job fails.
277
+ """
278
+ job_id = await self._submit_queued("messages_import", {"messages": messages})
279
+ return [
280
+ dict(entry)
281
+ for entry in await self._await_queued_result(
282
+ job_id, command="messages_import", timeout=timeout
283
+ )
284
+ ]
285
+
286
+ async def mark_messages_for_deletion(
287
+ self,
288
+ message_ids: list[str],
289
+ marked_for_deletion: bool,
290
+ ) -> Any:
291
+ """Atomically set or clear the soft-deletion marker for messages.
292
+
293
+ Returns the command's boolean outcome. A true value means every UUID
294
+ reached the requested marker state. A false value means the batch was
295
+ empty, malformed, duplicate, unknown, or could not be changed, and no
296
+ marker changed. Repeating a valid set or clear is successful.
297
+ """
298
+ return await self._invoke(
299
+ "messages_mark_for_deletion",
300
+ {
301
+ "message_ids": message_ids,
302
+ "marked_for_deletion": marked_for_deletion,
303
+ },
304
+ )
305
+
306
+ async def message_get(self, message_id: str) -> Any:
307
+ """Read one message by its identifier with its vectorization state.
308
+
309
+ Returns the messages_read entry of the message plus
310
+ ``vectorization_state`` (``"pending"``, ``"vectorized"`` or the terminal
311
+ ``"not_vectorizable"`` for whitespace-only text, which is never
312
+ embedded). A
313
+ soft-deleted message is returned with ``soft_deleted`` true.
314
+
315
+ Raises:
316
+ ServiceRefusalError: MESSAGE_UNKNOWN for an unknown id, or the
317
+ REQUEST_PARAMETER_* code for a refused parameter.
318
+ """
319
+ return await self._invoke("message_get", {"message_id": message_id})
320
+
321
+ async def message_delete(self, message_id: str, mode: str) -> Any:
322
+ """Delete one message in the service's soft or hard mode.
323
+
324
+ ``mode="soft"`` marks the message deleted so ordinary reads hide it.
325
+ ``mode="hard"`` physically removes an unreferenced message and its
326
+ outbound derived rows. The service validates the identifier and mode,
327
+ and returns the deletion report it publishes.
328
+ """
329
+ return await self._invoke(
330
+ "message_delete",
331
+ {"message_id": message_id, "mode": mode},
332
+ )
@@ -0,0 +1,195 @@
1
+ """
2
+ Public observation methods of the client: the negative-case probe, the typed
3
+ help, the typed health route and the answer-mode observation.
4
+
5
+ These are not typed methods of any one command of the service. They are the
6
+ caller-facing surface a consumer that has to look at the service from the
7
+ outside - a verification suite above all - needs, so that it never reaches for
8
+ a private attribute of the client, the framework's own client, a raw JSON-RPC
9
+ call, a queue poll or an envelope:
10
+
11
+ - ``probe_command`` sends EXACTLY the parameters it is given to a named
12
+ command - missing, unknown, null and wrongly typed values included, nothing
13
+ added, dropped or validated here - and returns or raises exactly as a typed
14
+ method does. An always-queued command is waited for like its typed method.
15
+ - ``help`` is the service's own ``help`` command, typed.
16
+ - ``health_route`` is the one declared exception to "everything is a command":
17
+ the ``GET /health`` route, answered as the bare report it is.
18
+ - ``command_answer_mode`` reports HOW the service answered a command - inline
19
+ (``direct``) or with a job identity (``queued``) - without the caller looking
20
+ at an envelope and without waiting for a job.
21
+
22
+ This module is deliberately outside the set of modules whose public coroutine
23
+ methods the client-surface agreement counts as typed commands: none of these
24
+ methods is a command of the service.
25
+
26
+ NETWORK LAW. This module imports nothing from the adapter framework and adds
27
+ no transport of its own; everything goes through the invocation seam and the
28
+ privately held framework client that ``ailogger_client.invocation`` guards.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ from collections.abc import Mapping
34
+ from typing import Any
35
+
36
+ from .errors import translated_failures
37
+ from .queued_methods import find_job_id
38
+
39
+ __all__ = ["ObservationMixin"]
40
+
41
+
42
+ class ObservationMixin:
43
+ """
44
+ Mixin providing the probe, help, health-route and answer-mode methods.
45
+
46
+ This mixin expects to be composed with InvocationMixin (``_invoke``,
47
+ ``_invoke_unconverted`` and the private framework client) and QueuedMixin
48
+ (``_await_queued_result``).
49
+ """
50
+
51
+ _client: Any
52
+
53
+ async def _invoke(
54
+ self,
55
+ command: str,
56
+ params: dict[str, Any] | None = None,
57
+ *,
58
+ wait_for_result: bool = True,
59
+ ) -> Any:
60
+ """Private invocation entry point, provided by InvocationMixin."""
61
+ raise NotImplementedError # Provided by InvocationMixin
62
+
63
+ async def _invoke_unconverted(
64
+ self,
65
+ command: str,
66
+ params: dict[str, Any] | None,
67
+ wait_for_result: bool,
68
+ ) -> Any:
69
+ """Unconverted invocation, provided by InvocationMixin."""
70
+ raise NotImplementedError # Provided by InvocationMixin
71
+
72
+ async def _await_queued_result(
73
+ self,
74
+ job_id: str,
75
+ *,
76
+ command: str,
77
+ timeout: float | None = None,
78
+ ) -> list[Any]:
79
+ """Queued completion, provided by QueuedMixin."""
80
+ raise NotImplementedError # Provided by QueuedMixin
81
+
82
+ async def probe_command(
83
+ self, command: str, params: Mapping[str, Any] | None
84
+ ) -> Any:
85
+ """Send EXACTLY these parameters to a command and return its answer.
86
+
87
+ The negative-case probe. The parameter map is sent as given: a missing
88
+ required parameter, an unknown one, an explicit ``None`` and a value of
89
+ the wrong type all go through unchanged - nothing is added, removed,
90
+ defaulted or validated by the client, so the answer is the service's
91
+ own verdict on exactly this request. ``None`` sends no parameters.
92
+
93
+ The outcome is what a typed method's would be: the command's answer in
94
+ the service's ``{success, data}`` form, or a typed error of this
95
+ client (``ServiceRefusalError`` carrying the service's stable code for
96
+ a refusal, ``ServiceUnreachableError``, ...). A command the service
97
+ always queues is waited for as its typed method waits: the answer is
98
+ then ``{"success": True, "data": <the job's result array>}``.
99
+
100
+ Args:
101
+ command: The name of the command to send.
102
+ params: The parameter map to send exactly, or ``None`` for none.
103
+
104
+ Returns:
105
+ The command's answer.
106
+
107
+ Raises:
108
+ ServiceRefusalError: When the service refuses the request.
109
+ AiLoggerClientError: For every other failure, translated.
110
+ """
111
+ sent = None if params is None else dict(params)
112
+ answer = await self._invoke(command, sent, wait_for_result=False)
113
+ job_id = find_job_id(answer)
114
+ if job_id is None:
115
+ return answer
116
+ result = await self._await_queued_result(job_id, command=command)
117
+ return {"success": True, "data": result}
118
+
119
+ async def help(
120
+ self,
121
+ cmdname: str | None = None,
122
+ limit: int | None = None,
123
+ offset: int | None = None,
124
+ ) -> Any:
125
+ """The service's ``help`` command: the catalogue, or one command.
126
+
127
+ Only the arguments that are not ``None`` are sent. The service
128
+ validates them (its help command names ``cmdname``); this method adds
129
+ no check of its own.
130
+
131
+ Args:
132
+ cmdname: A command to describe in detail; omitted lists them all.
133
+ limit: How many entries to return, when the service pages them.
134
+ offset: Where the page starts, when the service pages them.
135
+
136
+ Returns:
137
+ The answer of the ``help`` command.
138
+ """
139
+ params: dict[str, Any] = {}
140
+ if cmdname is not None:
141
+ params["cmdname"] = cmdname
142
+ if limit is not None:
143
+ params["limit"] = limit
144
+ if offset is not None:
145
+ params["offset"] = offset
146
+ return await self._invoke("help", params)
147
+
148
+ async def health_route(self) -> dict[str, Any]:
149
+ """The service's ``GET /health`` route, answered as its bare report.
150
+
151
+ The one declared exception to "everything is a command": the health
152
+ route is not a command of the service, and its answer is the report
153
+ itself - not a command envelope.
154
+
155
+ Returns:
156
+ The health report mapping, as the route answers it.
157
+
158
+ Raises:
159
+ ServiceUnreachableError: When the route cannot be reached.
160
+ AiLoggerClientError: For every other failure, translated.
161
+ """
162
+ with translated_failures():
163
+ report: dict[str, Any] = await self._client.health()
164
+ return report
165
+
166
+ async def command_answer_mode(
167
+ self, command: str, params: Mapping[str, Any] | None = None
168
+ ) -> dict[str, Any]:
169
+ """Report how the service answers a command: inline or queued.
170
+
171
+ Sends the command once with the given parameters and classifies the
172
+ answer: a job identity means ``queued``, anything else means
173
+ ``direct``. A queued command is never waited for - its job is left to
174
+ run. A direct command is run, and an inline refusal of it still answers
175
+ ``direct``: the service answered on the spot, which is what is being
176
+ observed. A request the service declined before queueing, and every
177
+ transport failure, arrives as a typed error.
178
+
179
+ Args:
180
+ command: The command to observe.
181
+ params: The parameters to send, or ``None`` for none.
182
+
183
+ Returns:
184
+ Exactly ``{"answer_mode": "direct" | "queued", "job_id": str | None}``.
185
+
186
+ Raises:
187
+ AiLoggerClientError: When the request fails before the service
188
+ answers it.
189
+ """
190
+ sent = None if params is None else dict(params)
191
+ answer = await self._invoke_unconverted(command, sent, False)
192
+ job_id = find_job_id(answer)
193
+ if job_id is None:
194
+ return {"answer_mode": "direct", "job_id": None}
195
+ return {"answer_mode": "queued", "job_id": job_id}
File without changes