Coverage for haystack/components/routers/llm_messages_router.py: 99%

87 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 13:53 +0000

1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5import re 

6from typing import Any 

7 

8from haystack import component, default_from_dict, default_to_dict 

9from haystack.components.generators.chat.types import ChatGenerator 

10from haystack.components.generators.utils import _trace_chat_generator_run 

11from haystack.core.serialization import component_to_dict 

12from haystack.dataclasses import ChatMessage, ChatRole 

13from haystack.utils import deserialize_chatgenerator_inplace 

14from haystack.utils.async_utils import _execute_component_async 

15 

16 

17@component 

18class LLMMessagesRouter: 

19 """ 

20 Routes Chat Messages to different connections using a generative Language Model to perform classification. 

21 

22 This component can be used with general-purpose LLMs and with specialized LLMs for moderation like Llama Guard. 

23 

24 ### Usage example 

25 

26 ```python 

27 from haystack_integrations.components.generators.huggingface_api import HuggingFaceAPIChatGenerator 

28 from haystack.components.routers.llm_messages_router import LLMMessagesRouter 

29 from haystack.dataclasses import ChatMessage 

30 

31 # initialize a Chat Generator with a generative model for moderation 

32 chat_generator = HuggingFaceAPIChatGenerator( 

33 api_type="serverless_inference_api", 

34 api_params={"model": "openai/gpt-oss-safeguard-20b", "provider": "groq"}, 

35 ) 

36 

37 router = LLMMessagesRouter(chat_generator=chat_generator, 

38 output_names=["unsafe", "safe"], 

39 output_patterns=["unsafe", "safe"]) 

40 

41 

42 print(router.run([ChatMessage.from_user("How to rob a bank?")])) 

43 

44 # { 

45 # 'chat_generator_text': 'unsafe\\nS2', 

46 # 'unsafe': [ 

47 # ChatMessage( 

48 # _role=<ChatRole.USER: 'user'>, 

49 # _content=[TextContent(text='How to rob a bank?')], 

50 # _name=None, 

51 # _meta={} 

52 # ) 

53 # ] 

54 # } 

55 ``` 

56 """ 

57 

58 def __init__( 

59 self, 

60 chat_generator: ChatGenerator, 

61 output_names: list[str], 

62 output_patterns: list[str], 

63 system_prompt: str | None = None, 

64 ) -> None: 

65 """ 

66 Initialize the LLMMessagesRouter component. 

67 

68 :param chat_generator: A ChatGenerator instance which represents the LLM. 

69 :param output_names: A list of output connection names. These can be used to connect the router to other 

70 components. 

71 :param output_patterns: A list of regular expressions to be matched against the output of the LLM. Each pattern 

72 corresponds to an output name. Patterns are evaluated in order. 

73 When using moderation models, refer to the model card to understand the expected outputs. 

74 :param system_prompt: An optional system prompt to customize the behavior of the LLM. 

75 For moderation models, refer to the model card for supported customization options. 

76 

77 :raises ValueError: If output_names and output_patterns are not non-empty lists of the same length. 

78 """ 

79 if not output_names or not output_patterns or len(output_names) != len(output_patterns): 

80 raise ValueError("`output_names` and `output_patterns` must be non-empty lists of the same length") 

81 

82 self._chat_generator = chat_generator 

83 self._system_prompt = system_prompt 

84 self._output_names = output_names 

85 self._output_patterns = output_patterns 

86 

87 self._compiled_patterns = [re.compile(pattern) for pattern in output_patterns] 

88 

89 component.set_output_types( 

90 self, **{"chat_generator_text": str, **dict.fromkeys(output_names + ["unmatched"], list[ChatMessage])} 

91 ) 

92 

93 def warm_up(self) -> None: 

94 """Warm up the underlying chat generator.""" 

95 if hasattr(self._chat_generator, "warm_up"): 

96 self._chat_generator.warm_up() 

97 

98 async def warm_up_async(self) -> None: 

99 """Warm up the underlying chat generator on the serving event loop.""" 

100 if hasattr(self._chat_generator, "warm_up_async"): 

101 await self._chat_generator.warm_up_async() 

102 elif hasattr(self._chat_generator, "warm_up"): 

103 self._chat_generator.warm_up() 

104 

105 def close(self) -> None: 

106 """Release the underlying chat generator's resources.""" 

107 if hasattr(self._chat_generator, "close"): 

108 self._chat_generator.close() 

109 

110 async def close_async(self) -> None: 

111 """Release the underlying chat generator's async resources.""" 

112 if hasattr(self._chat_generator, "close_async"): 

113 await self._chat_generator.close_async() 

114 elif hasattr(self._chat_generator, "close"): 

115 self._chat_generator.close() 

116 

117 def run(self, messages: list[ChatMessage]) -> dict[str, str | list[ChatMessage]]: 

118 """ 

119 Classify the messages based on LLM output and route them to the appropriate output connection. 

120 

121 :param messages: A list of ChatMessages to be routed. Only user and assistant messages are supported. 

122 

123 :returns: A dictionary with the following keys: 

124 - "chat_generator_text": The text output of the LLM, useful for debugging. 

125 - "output_names": Each contains the list of messages that matched the corresponding pattern. 

126 - "unmatched": The messages that did not match any of the output patterns. 

127 

128 :raises ValueError: If messages is an empty list or contains messages with unsupported roles. 

129 """ 

130 if not messages: 

131 raise ValueError("`messages` must be a non-empty list.") 

132 if not all(message.is_from(ChatRole.USER) or message.is_from(ChatRole.ASSISTANT) for message in messages): 

133 msg = ( 

134 "`messages` must contain only user and assistant messages. To customize the behavior of the " 

135 "`chat_generator`, you can use the `system_prompt` parameter." 

136 ) 

137 raise ValueError(msg) 

138 

139 self.warm_up() 

140 

141 messages_for_inference = [] 

142 if self._system_prompt: 

143 messages_for_inference.append(ChatMessage.from_system(self._system_prompt)) 

144 messages_for_inference.extend(messages) 

145 

146 with _trace_chat_generator_run(self._chat_generator, {"messages": messages_for_inference}) as span: 

147 generator_result = self._chat_generator.run(messages=messages_for_inference) 

148 span.set_content_tag("haystack.component.output", generator_result) 

149 chat_generator_text = generator_result["replies"][0].text 

150 

151 output = {"chat_generator_text": chat_generator_text} 

152 

153 for output_name, pattern in zip(self._output_names, self._compiled_patterns, strict=True): 

154 if pattern.search(chat_generator_text): 

155 output[output_name] = messages 

156 break 

157 else: 

158 output["unmatched"] = messages 

159 

160 return output 

161 

162 async def run_async(self, messages: list[ChatMessage]) -> dict[str, str | list[ChatMessage]]: 

163 """ 

164 Asynchronously classify the messages based on LLM output and route them to the appropriate output connection. 

165 

166 This is the asynchronous version of the `run` method. It has the same parameters and return values 

167 but can be used with `await` in an async code. If the chat generator only implements a synchronous 

168 `run` method, it is executed in a thread to avoid blocking the event loop. 

169 

170 :param messages: A list of ChatMessages to be routed. Only user and assistant messages are supported. 

171 

172 :returns: A dictionary with the following keys: 

173 - "chat_generator_text": The text output of the LLM, useful for debugging. 

174 - "output_names": Each contains the list of messages that matched the corresponding pattern. 

175 - "unmatched": The messages that did not match any of the output patterns. 

176 

177 :raises ValueError: If messages is an empty list or contains messages with unsupported roles. 

178 """ 

179 if not messages: 

180 raise ValueError("`messages` must be a non-empty list.") 

181 if not all(message.is_from(ChatRole.USER) or message.is_from(ChatRole.ASSISTANT) for message in messages): 

182 msg = ( 

183 "`messages` must contain only user and assistant messages. To customize the behavior of the " 

184 "`chat_generator`, you can use the `system_prompt` parameter." 

185 ) 

186 raise ValueError(msg) 

187 

188 await self.warm_up_async() 

189 

190 messages_for_inference = [] 

191 if self._system_prompt: 

192 messages_for_inference.append(ChatMessage.from_system(self._system_prompt)) 

193 messages_for_inference.extend(messages) 

194 

195 with _trace_chat_generator_run(self._chat_generator, {"messages": messages_for_inference}) as span: 

196 generator_result = await _execute_component_async(self._chat_generator, messages=messages_for_inference) 

197 span.set_content_tag("haystack.component.output", generator_result) 

198 chat_generator_text = generator_result["replies"][0].text 

199 

200 output = {"chat_generator_text": chat_generator_text} 

201 

202 for output_name, pattern in zip(self._output_names, self._compiled_patterns, strict=True): 

203 if pattern.search(chat_generator_text): 

204 output[output_name] = messages 

205 break 

206 else: 

207 output["unmatched"] = messages 

208 

209 return output 

210 

211 def to_dict(self) -> dict[str, Any]: 

212 """ 

213 Serialize this component to a dictionary. 

214 

215 :returns: 

216 The serialized component as a dictionary. 

217 """ 

218 return default_to_dict( 

219 self, 

220 chat_generator=component_to_dict(obj=self._chat_generator, name="chat_generator"), 

221 output_names=self._output_names, 

222 output_patterns=self._output_patterns, 

223 system_prompt=self._system_prompt, 

224 ) 

225 

226 @classmethod 

227 def from_dict(cls, data: dict[str, Any]) -> "LLMMessagesRouter": 

228 """ 

229 Deserialize this component from a dictionary. 

230 

231 :param data: 

232 The dictionary representation of this component. 

233 :returns: 

234 The deserialized component instance. 

235 """ 

236 if data["init_parameters"].get("chat_generator"): 

237 deserialize_chatgenerator_inplace(data["init_parameters"], key="chat_generator") 

238 

239 return default_from_dict(cls, data)