Coverage for haystack/components/generators/chat/llm.py: 98%

40 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 

5from typing import Any, Literal 

6 

7from haystack import component, logging 

8from haystack.components.agents.agent import Agent 

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

10from haystack.core.serialization import component_to_dict, default_from_dict, default_to_dict 

11from haystack.dataclasses import ChatMessage, StreamingCallbackT 

12from haystack.utils.callable_serialization import deserialize_callable, serialize_callable 

13from haystack.utils.deserialization import deserialize_component_inplace 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18@component 

19class LLM(Agent): 

20 """ 

21 A text generation component powered by a large language model. 

22 

23 The LLM component is a simplified version of the Agent that focuses solely on text generation 

24 without tool usage. It processes messages and returns a single response from the language model. 

25 

26 ### Usage examples 

27 ```python 

28 from haystack.components.generators.chat import LLM 

29 from haystack.components.generators.chat import OpenAIChatGenerator 

30 

31 llm = LLM( 

32 chat_generator=OpenAIChatGenerator(), 

33 system_prompt="You are a helpful translation assistant.", 

34 user_prompt="Summarize the following document: {{ document }}", 

35 required_variables=["document"], 

36 ) 

37 

38 result = llm.run(document="The weather is lovely today and the sun is shining. ") 

39 print(result["last_message"].text) 

40 ``` 

41 """ 

42 

43 def __init__( 

44 self, 

45 *, 

46 chat_generator: ChatGenerator, 

47 system_prompt: str | None = None, 

48 user_prompt: str | None = None, 

49 required_variables: list[str] | Literal["*"] = "*", 

50 streaming_callback: StreamingCallbackT | None = None, 

51 ) -> None: 

52 """ 

53 Initialize the LLM component. 

54 

55 :param chat_generator: An instance of the chat generator that the LLM should use. 

56 :param system_prompt: System prompt for the LLM. Can be a plain string template or a Jinja2 message template. 

57 :param user_prompt: User prompt for the LLM. This prompt is appended to the messages provided at 

58 runtime. Can be a plain string template or a Jinja2 message template. If it contains template variables 

59 (e.g., `{{ variable_name }}`), they become inputs to the component. If omitted or if there are no 

60 template variables, `messages` must be provided at runtime instead. 

61 :param required_variables: 

62 Variables that must be provided as input to `user_prompt` or `system_prompt`. 

63 If a variable listed as required is not provided, an exception is raised. 

64 If set to `"*"`, all variables found in the prompt are required. Defaults to `"*"`. 

65 Only relevant when `user_prompt` or `system_prompt` contains template variables. 

66 :param streaming_callback: A callback that will be invoked when a response is streamed from the LLM. 

67 :raises ValueError: If user_prompt contains template variables but required_variables is an empty list. 

68 """ 

69 super(LLM, self).__init__( # noqa: UP008 

70 chat_generator=chat_generator, 

71 system_prompt=system_prompt, 

72 user_prompt=user_prompt, 

73 required_variables=required_variables, 

74 streaming_callback=streaming_callback, 

75 ) 

76 if self._user_chat_prompt_builder is None or len(self._user_chat_prompt_builder.variables) == 0: 

77 # This means user_prompt is empty or has no template variables. 

78 # To ensure properly scheduling we then require messages to be passed at runtime. 

79 component.set_input_type(self, "messages", list[ChatMessage]) 

80 else: 

81 # user prompt was provided with variables 

82 if isinstance(required_variables, list) and len(required_variables) == 0: 

83 raise ValueError( 

84 "required_variables must not be empty. Set it to '*' to require all variables, " 

85 "or provide a non-empty list of variable names." 

86 ) 

87 component.set_input_type(self, "messages", list[ChatMessage], None) 

88 

89 # The Agent base class declares `step_count` and `tool_call_counts` as outputs, but an LLM never has tools 

90 # and always runs exactly one step — those values are uninformative, so drop them from the public surface. 

91 # `token_usage` is still meaningful and stays exposed. 

92 component.set_output_types( 

93 self, messages=list[ChatMessage], last_message=ChatMessage, token_usage=dict[str, Any] 

94 ) 

95 

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

97 """ 

98 Serialize the LLM component to a dictionary. 

99 

100 :return: Dictionary with serialized data. 

101 """ 

102 return default_to_dict( 

103 self, 

104 chat_generator=component_to_dict(obj=self.chat_generator, name="chat_generator"), 

105 system_prompt=self.system_prompt, 

106 user_prompt=self.user_prompt, 

107 required_variables=self.required_variables, 

108 streaming_callback=serialize_callable(self.streaming_callback) if self.streaming_callback else None, 

109 ) 

110 

111 @classmethod 

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

113 """ 

114 Deserialize the LLM from a dictionary. 

115 

116 :param data: Dictionary to deserialize from. 

117 :return: Deserialized LLM instance. 

118 """ 

119 init_params = data.get("init_parameters", {}) 

120 

121 deserialize_component_inplace(init_params, key="chat_generator") 

122 

123 if init_params.get("streaming_callback") is not None: 

124 init_params["streaming_callback"] = deserialize_callable(init_params["streaming_callback"]) 

125 

126 return default_from_dict(cls, data) 

127 

128 def run( # type: ignore[override] # `messages` is in **kwargs to allow dynamic required/optional status 

129 self, 

130 *, 

131 streaming_callback: StreamingCallbackT | None = None, 

132 generation_kwargs: dict[str, Any] | None = None, 

133 **kwargs: Any, 

134 ) -> dict[str, Any]: 

135 """ 

136 Process messages and generate a response from the language model. 

137 

138 :param messages: Optional list of ChatMessage objects to prepend to the conversation. Whether this is 

139 required or optional depends on the `user_prompt` configuration: if `user_prompt` has no template 

140 variables, `messages` must be provided. Passed via `**kwargs`. 

141 :param streaming_callback: A callback that will be invoked when a response is streamed from the LLM. 

142 :param generation_kwargs: Additional keyword arguments for the chat generator. These are merged per key 

143 with the `generation_kwargs` passed at the chat generator's initialization: keys provided here take 

144 precedence, keys set only at initialization are kept. 

145 :param kwargs: Additional keyword arguments. These are used to fill template variables in `user_prompt` or 

146 `system_prompt` (the keys must match template variable names). 

147 :returns: 

148 A dictionary with the following keys: 

149 - "messages": List of all messages exchanged during the LLM's run. 

150 - "last_message": The last message exchanged during the LLM's run. 

151 - "token_usage": Token usage from the LLM call (e.g. prompt_tokens, completion_tokens). Empty if the 

152 chat generator did not return usage data. 

153 """ 

154 # `messages` is intentionally omitted from the signature so the framework can treat it as required 

155 # or optional depending on init configuration. See __init__ for details. 

156 messages = kwargs.pop("messages", None) 

157 result = super(LLM, self).run( # noqa: UP008 

158 messages=messages or [], 

159 streaming_callback=streaming_callback, 

160 generation_kwargs=generation_kwargs, 

161 **kwargs, 

162 ) 

163 # Inherited Agent-internal bookkeeping that isn't useful at the LLM surface. 

164 result.pop("step_count", None) 

165 result.pop("tool_call_counts", None) 

166 return result 

167 

168 async def run_async( # type: ignore[override] # `messages` is in **kwargs to allow dynamic required/optional status 

169 self, 

170 *, 

171 streaming_callback: StreamingCallbackT | None = None, 

172 generation_kwargs: dict[str, Any] | None = None, 

173 **kwargs: Any, 

174 ) -> dict[str, Any]: 

175 """ 

176 Asynchronously process messages and generate a response from the language model. 

177 

178 :param messages: Optional list of ChatMessage objects to prepend to the conversation. Whether this is 

179 required or optional depends on the `user_prompt` configuration: if `user_prompt` has no template 

180 variables, `messages` must be provided. Passed via `**kwargs`. 

181 :param streaming_callback: An asynchronous callback that will be invoked when a response is streamed 

182 from the LLM. 

183 :param generation_kwargs: Additional keyword arguments for the chat generator. These are merged per key 

184 with the `generation_kwargs` passed at the chat generator's initialization: keys provided here take 

185 precedence, keys set only at initialization are kept. 

186 :param kwargs: Additional keyword arguments. These are used to fill template variables in `user_prompt` or 

187 `system_prompt` (the keys must match template variable names). 

188 :returns: 

189 A dictionary with the following keys: 

190 - "messages": List of all messages exchanged during the LLM's run. 

191 - "last_message": The last message exchanged during the LLM's run. 

192 - "token_usage": Token usage from the LLM call (e.g. prompt_tokens, completion_tokens). Empty if the 

193 chat generator did not return usage data. 

194 """ 

195 # `messages` is intentionally omitted from the signature so the framework can treat it as required 

196 # or optional depending on init configuration. See __init__ for details. 

197 messages = kwargs.pop("messages", None) 

198 result = await super(LLM, self).run_async( # noqa: UP008 

199 messages=messages or [], 

200 streaming_callback=streaming_callback, 

201 generation_kwargs=generation_kwargs, 

202 **kwargs, 

203 ) 

204 # Inherited Agent-internal bookkeeping that isn't useful at the LLM surface. 

205 result.pop("step_count", None) 

206 result.pop("tool_call_counts", None) 

207 return result