Coverage for src / lexigram / ai / relay / mappers / base.py: 100%

26 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-08 23:08 +0800

1"""Typed per-format mapper interfaces for the relay conversion engine. 

2 

3Each wire format implements one :class:`FormatMapper` with the six 

4canonical operations: request/response in both directions plus 

5stream conversion in both directions. Mappers may reject a feature by 

6recording a :class:`RelayLoss` and a warning, but must return 

7``Err(RelayError)`` for malformed or impossible payloads. 

8""" 

9 

10from __future__ import annotations 

11 

12from collections.abc import Sequence 

13from typing import Any, Protocol, runtime_checkable 

14from uuid import uuid4 

15 

16from lexigram.ai.relay.context import ConversionContext 

17from lexigram.contracts.ai.exceptions import RelayError 

18from lexigram.contracts.ai.relay.ir import ( 

19 RelayRequest, 

20 RelayResponse, 

21 StreamDelta, 

22 StreamState, 

23) 

24from lexigram.contracts.ai.relay.types import RelayFormat, RelayLoss 

25from lexigram.contracts.core.result import Result 

26 

27__all__ = ["FormatMapper", "record_loss", "warning_messages"] 

28 

29 

30@runtime_checkable 

31class FormatMapper(Protocol): 

32 """One wire format's typed bidirectional mapping to the canonical IR. 

33 

34 Concrete mappers implement the same operations the container-facing 

35 ``RelayMapperProtocol`` declares; the registry in the relay engine 

36 holds :class:`FormatMapper` implementations. 

37 """ 

38 

39 @property 

40 def format(self) -> RelayFormat: 

41 """The wire format this mapper serves; concrete mappers define it.""" 

42 ... 

43 

44 def request_to_ir( 

45 self, payload: Any, *, context: ConversionContext 

46 ) -> Result[RelayRequest, RelayError]: 

47 """Convert a source request DTO into canonical ``RelayRequest``.""" 

48 ... 

49 

50 def ir_to_request( 

51 self, request: RelayRequest, *, context: ConversionContext 

52 ) -> Result[Any, RelayError]: 

53 """Convert a canonical ``RelayRequest`` into the target request DTO.""" 

54 ... 

55 

56 def response_to_ir( 

57 self, payload: Any, *, context: ConversionContext 

58 ) -> Result[RelayResponse, RelayError]: 

59 """Convert a source response DTO into canonical ``RelayResponse``.""" 

60 ... 

61 

62 def ir_to_response( 

63 self, response: RelayResponse, *, context: ConversionContext 

64 ) -> Result[Any, RelayError]: 

65 """Convert a canonical ``RelayResponse`` into the target response DTO.""" 

66 ... 

67 

68 def stream_to_delta( 

69 self, event: Any, *, state: StreamState 

70 ) -> Result[tuple[StreamDelta, ...], RelayError]: 

71 """Convert one source stream event into canonical ``StreamDelta``s.""" 

72 ... 

73 

74 def delta_to_stream( 

75 self, delta: StreamDelta, *, state: StreamState 

76 ) -> Result[tuple[Any, ...], RelayError]: 

77 """Convert one canonical ``StreamDelta`` into target stream events.""" 

78 ... 

79 

80 

81def record_loss( 

82 context: ConversionContext, 

83 *, 

84 field: str, 

85 target: RelayFormat, 

86 reason: str, 

87 severity: str = "warning", 

88) -> None: 

89 """Record a semantic loss on the conversion context. 

90 

91 The engine copies the accumulated losses into the 

92 ``RelayConvertResult`` and surfaces ``warning``/``error`` losses as 

93 warnings. 

94 

95 Args: 

96 context: The per-conversion context holding the loss sink. 

97 field: Source wire field (or feature) that was dropped or adapted. 

98 target: Target format the loss applies to. 

99 reason: Machine-readable reason (e.g. ``json_mode_not_supported``). 

100 severity: ``error``, ``warning``, or ``info``. 

101 """ 

102 context.losses.append( 

103 RelayLoss(field=field, target=target, reason=reason, severity=severity) 

104 ) 

105 

106 

107def warning_messages(losses: Sequence[RelayLoss]) -> tuple[str, ...]: 

108 """Render losses into stable warning strings. 

109 

110 Args: 

111 losses: Loss records accumulated during conversion. 

112 

113 Returns: 

114 One ``"field: reason (target, severity)"`` string per loss. 

115 """ 

116 return tuple( 

117 f"{loss.field}: {loss.reason} ({loss.target.value}, {loss.severity})" 

118 for loss in losses 

119 ) 

120 

121 

122def new_uuid() -> str: 

123 """Return a fresh 32-hex identifier (relaykit ``GetUUID`` shape).""" 

124 return uuid4().hex