Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/structured/formatter.py: 27%

66 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""Response formatting utilities for LLM outputs.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from typing import TYPE_CHECKING, Any, cast 

7 

8if TYPE_CHECKING: 

9 from lexigram.ai.llm.types import Completion 

10 from lexigram.contracts.core import JSON 

11 

12from lexigram.ai.llm.structured.exceptions import ParseError 

13from lexigram.ai.llm.structured.extractor import JSONExtractor 

14 

15 

16class ResponseFormatter: 

17 """Format and convert LLM responses to various types. 

18 

19 Example: 

20 >>> formatter = ResponseFormatter() 

21 >>> completion = Completion(content="42", ...) 

22 >>> num = formatter.to_int(completion) 

23 >>> print(num) 

24 42 

25 """ 

26 

27 @staticmethod 

28 def to_json(completion: Completion) -> JSON: 

29 """Convert response to JSON. 

30 

31 Args: 

32 completion: LLM completion 

33 

34 Returns: 

35 Parsed JSON 

36 

37 Example: 

38 >>> data = formatter.to_json(completion) 

39 """ 

40 data = JSONExtractor.extract(completion.content) 

41 # If the extractor returned an array, return the first object for callers that 

42 # expect a JSON object (this is a best-effort convenience behavior). 

43 if isinstance(data, list): 

44 if not data: 

45 msg = "Expected JSON object, got empty array" 

46 raise ParseError(msg) 

47 if not isinstance(data[0], dict): 

48 msg = f"Expected JSON object, got array of {type(data[0])}" 

49 raise ParseError(msg) 

50 return cast("dict[str, Any]", data[0]) 

51 return cast("dict[str, Any]", data) 

52 

53 @staticmethod 

54 def to_string(completion: Completion, strip: bool = True) -> str: 

55 """Convert response to string. 

56 

57 Args: 

58 completion: LLM completion 

59 strip: Whether to strip whitespace 

60 

61 Returns: 

62 Response string 

63 

64 Example: 

65 >>> text = formatter.to_string(completion) 

66 """ 

67 content = completion.content 

68 return content.strip() if strip else content 

69 

70 @staticmethod 

71 def to_int(completion: Completion) -> int: 

72 """Convert response to integer. 

73 

74 Args: 

75 completion: LLM completion 

76 

77 Returns: 

78 Parsed integer 

79 

80 Raises: 

81 ParseError: If conversion fails 

82 

83 Example: 

84 >>> num = formatter.to_int(completion) 

85 """ 

86 content = completion.content.strip() 

87 

88 # Try direct conversion 

89 try: 

90 return int(content) 

91 except ValueError: 

92 pass 

93 

94 # Try extracting number from text 

95 numbers = re.findall(r"-?\d+", content) 

96 if numbers: 

97 return int(numbers[0]) 

98 

99 msg = f"Cannot convert to int: {content}" 

100 raise ParseError(msg) 

101 

102 @staticmethod 

103 def to_float(completion: Completion) -> float: 

104 """Convert response to float. 

105 

106 Args: 

107 completion: LLM completion 

108 

109 Returns: 

110 Parsed float 

111 

112 Raises: 

113 ParseError: If conversion fails 

114 

115 Example: 

116 >>> num = formatter.to_float(completion) 

117 """ 

118 content = completion.content.strip() 

119 

120 # Try direct conversion 

121 try: 

122 return float(content) 

123 except ValueError: 

124 pass 

125 

126 # Try extracting number from text 

127 numbers = re.findall(r"-?\d+\.?\d*", content) 

128 if numbers: 

129 return float(numbers[0]) 

130 

131 msg = f"Cannot convert to float: {content}" 

132 raise ParseError(msg) 

133 

134 @staticmethod 

135 def to_bool(completion: Completion) -> bool: 

136 """Convert response to boolean. 

137 

138 Args: 

139 completion: LLM completion 

140 

141 Returns: 

142 Parsed boolean 

143 

144 Example: 

145 >>> result = formatter.to_bool(completion) 

146 """ 

147 content = completion.content.strip().lower() 

148 

149 # Check common boolean representations 

150 if content in ("true", "yes", "1", "y", "correct", "affirmative"): 

151 return True 

152 if content in ("false", "no", "0", "n", "incorrect", "negative"): 

153 return False 

154 

155 msg = f"Cannot convert to bool: {content}" 

156 raise ParseError(msg) 

157 

158 @staticmethod 

159 def to_list(completion: Completion, separator: str = "\n") -> list[str]: 

160 """Convert response to list of strings. 

161 

162 Args: 

163 completion: LLM completion 

164 separator: String separator (default: newline) 

165 

166 Returns: 

167 List of strings 

168 

169 Example: 

170 >>> items = formatter.to_list(completion) 

171 """ 

172 content = completion.content.strip() 

173 

174 # Try parsing as JSON array first 

175 try: 

176 result = JSONExtractor.extract(content) 

177 if isinstance(result, list): 

178 return list(map(str, result)) 

179 except ParseError: 

180 pass 

181 

182 # Split by separator and clean up 

183 items = content.split(separator) 

184 return list(map(str.strip, filter(str.strip, items)))