rasa-pro 3.9.18__py3-none-any.whl → 3.10.16__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.

Potentially problematic release.


This version of rasa-pro might be problematic. Click here for more details.

Files changed (183) hide show
  1. README.md +0 -374
  2. rasa/__init__.py +1 -2
  3. rasa/__main__.py +5 -0
  4. rasa/anonymization/anonymization_rule_executor.py +2 -2
  5. rasa/api.py +27 -23
  6. rasa/cli/arguments/data.py +27 -2
  7. rasa/cli/arguments/default_arguments.py +25 -3
  8. rasa/cli/arguments/run.py +9 -9
  9. rasa/cli/arguments/train.py +11 -3
  10. rasa/cli/data.py +70 -8
  11. rasa/cli/e2e_test.py +104 -431
  12. rasa/cli/evaluate.py +1 -1
  13. rasa/cli/interactive.py +1 -0
  14. rasa/cli/llm_fine_tuning.py +398 -0
  15. rasa/cli/project_templates/calm/endpoints.yml +1 -1
  16. rasa/cli/project_templates/tutorial/endpoints.yml +1 -1
  17. rasa/cli/run.py +15 -14
  18. rasa/cli/scaffold.py +10 -8
  19. rasa/cli/studio/studio.py +35 -5
  20. rasa/cli/train.py +56 -8
  21. rasa/cli/utils.py +22 -5
  22. rasa/cli/x.py +1 -1
  23. rasa/constants.py +7 -1
  24. rasa/core/actions/action.py +98 -49
  25. rasa/core/actions/action_run_slot_rejections.py +4 -1
  26. rasa/core/actions/custom_action_executor.py +9 -6
  27. rasa/core/actions/direct_custom_actions_executor.py +80 -0
  28. rasa/core/actions/e2e_stub_custom_action_executor.py +68 -0
  29. rasa/core/actions/grpc_custom_action_executor.py +2 -2
  30. rasa/core/actions/http_custom_action_executor.py +6 -5
  31. rasa/core/agent.py +21 -17
  32. rasa/core/channels/__init__.py +2 -0
  33. rasa/core/channels/audiocodes.py +1 -16
  34. rasa/core/channels/voice_aware/__init__.py +0 -0
  35. rasa/core/channels/voice_aware/jambonz.py +103 -0
  36. rasa/core/channels/voice_aware/jambonz_protocol.py +344 -0
  37. rasa/core/channels/voice_aware/utils.py +20 -0
  38. rasa/core/channels/voice_native/__init__.py +0 -0
  39. rasa/core/constants.py +6 -1
  40. rasa/core/information_retrieval/faiss.py +7 -4
  41. rasa/core/information_retrieval/information_retrieval.py +8 -0
  42. rasa/core/information_retrieval/milvus.py +9 -2
  43. rasa/core/information_retrieval/qdrant.py +1 -1
  44. rasa/core/nlg/contextual_response_rephraser.py +32 -10
  45. rasa/core/nlg/summarize.py +4 -3
  46. rasa/core/policies/enterprise_search_policy.py +113 -45
  47. rasa/core/policies/flows/flow_executor.py +122 -76
  48. rasa/core/policies/intentless_policy.py +83 -29
  49. rasa/core/processor.py +72 -54
  50. rasa/core/run.py +5 -4
  51. rasa/core/tracker_store.py +8 -4
  52. rasa/core/training/interactive.py +1 -1
  53. rasa/core/utils.py +56 -57
  54. rasa/dialogue_understanding/coexistence/llm_based_router.py +53 -13
  55. rasa/dialogue_understanding/commands/__init__.py +6 -0
  56. rasa/dialogue_understanding/commands/restart_command.py +58 -0
  57. rasa/dialogue_understanding/commands/session_start_command.py +59 -0
  58. rasa/dialogue_understanding/commands/utils.py +40 -0
  59. rasa/dialogue_understanding/generator/constants.py +10 -3
  60. rasa/dialogue_understanding/generator/flow_retrieval.py +21 -5
  61. rasa/dialogue_understanding/generator/llm_based_command_generator.py +13 -3
  62. rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +134 -90
  63. rasa/dialogue_understanding/generator/nlu_command_adapter.py +47 -7
  64. rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +127 -41
  65. rasa/dialogue_understanding/patterns/restart.py +37 -0
  66. rasa/dialogue_understanding/patterns/session_start.py +37 -0
  67. rasa/dialogue_understanding/processor/command_processor.py +16 -3
  68. rasa/dialogue_understanding/processor/command_processor_component.py +6 -2
  69. rasa/e2e_test/aggregate_test_stats_calculator.py +134 -0
  70. rasa/e2e_test/assertions.py +1223 -0
  71. rasa/e2e_test/assertions_schema.yml +106 -0
  72. rasa/e2e_test/constants.py +20 -0
  73. rasa/e2e_test/e2e_config.py +220 -0
  74. rasa/e2e_test/e2e_config_schema.yml +26 -0
  75. rasa/e2e_test/e2e_test_case.py +131 -8
  76. rasa/e2e_test/e2e_test_converter.py +363 -0
  77. rasa/e2e_test/e2e_test_converter_prompt.jinja2 +70 -0
  78. rasa/e2e_test/e2e_test_coverage_report.py +364 -0
  79. rasa/e2e_test/e2e_test_result.py +26 -6
  80. rasa/e2e_test/e2e_test_runner.py +493 -71
  81. rasa/e2e_test/e2e_test_schema.yml +96 -0
  82. rasa/e2e_test/pykwalify_extensions.py +39 -0
  83. rasa/e2e_test/stub_custom_action.py +70 -0
  84. rasa/e2e_test/utils/__init__.py +0 -0
  85. rasa/e2e_test/utils/e2e_yaml_utils.py +55 -0
  86. rasa/e2e_test/utils/io.py +598 -0
  87. rasa/e2e_test/utils/validation.py +80 -0
  88. rasa/engine/graph.py +9 -3
  89. rasa/engine/recipes/default_components.py +0 -2
  90. rasa/engine/recipes/default_recipe.py +10 -2
  91. rasa/engine/storage/local_model_storage.py +40 -12
  92. rasa/engine/validation.py +78 -1
  93. rasa/env.py +9 -0
  94. rasa/graph_components/providers/story_graph_provider.py +59 -6
  95. rasa/llm_fine_tuning/__init__.py +0 -0
  96. rasa/llm_fine_tuning/annotation_module.py +241 -0
  97. rasa/llm_fine_tuning/conversations.py +144 -0
  98. rasa/llm_fine_tuning/llm_data_preparation_module.py +178 -0
  99. rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +407 -0
  100. rasa/llm_fine_tuning/paraphrasing/__init__.py +0 -0
  101. rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +281 -0
  102. rasa/llm_fine_tuning/paraphrasing/default_rephrase_prompt_template.jina2 +44 -0
  103. rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +121 -0
  104. rasa/llm_fine_tuning/paraphrasing/rephrased_user_message.py +10 -0
  105. rasa/llm_fine_tuning/paraphrasing_module.py +128 -0
  106. rasa/llm_fine_tuning/storage.py +174 -0
  107. rasa/llm_fine_tuning/train_test_split_module.py +441 -0
  108. rasa/model_training.py +56 -16
  109. rasa/nlu/persistor.py +157 -36
  110. rasa/server.py +45 -10
  111. rasa/shared/constants.py +76 -16
  112. rasa/shared/core/domain.py +27 -19
  113. rasa/shared/core/events.py +28 -2
  114. rasa/shared/core/flows/flow.py +208 -13
  115. rasa/shared/core/flows/flow_path.py +84 -0
  116. rasa/shared/core/flows/flows_list.py +33 -11
  117. rasa/shared/core/flows/flows_yaml_schema.json +269 -193
  118. rasa/shared/core/flows/validation.py +112 -25
  119. rasa/shared/core/flows/yaml_flows_io.py +149 -10
  120. rasa/shared/core/trackers.py +6 -0
  121. rasa/shared/core/training_data/structures.py +20 -0
  122. rasa/shared/core/training_data/visualization.html +2 -2
  123. rasa/shared/exceptions.py +4 -0
  124. rasa/shared/importers/importer.py +64 -16
  125. rasa/shared/nlu/constants.py +2 -0
  126. rasa/shared/providers/_configs/__init__.py +0 -0
  127. rasa/shared/providers/_configs/azure_openai_client_config.py +183 -0
  128. rasa/shared/providers/_configs/client_config.py +57 -0
  129. rasa/shared/providers/_configs/default_litellm_client_config.py +130 -0
  130. rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +234 -0
  131. rasa/shared/providers/_configs/openai_client_config.py +175 -0
  132. rasa/shared/providers/_configs/self_hosted_llm_client_config.py +176 -0
  133. rasa/shared/providers/_configs/utils.py +101 -0
  134. rasa/shared/providers/_ssl_verification_utils.py +124 -0
  135. rasa/shared/providers/embedding/__init__.py +0 -0
  136. rasa/shared/providers/embedding/_base_litellm_embedding_client.py +259 -0
  137. rasa/shared/providers/embedding/_langchain_embedding_client_adapter.py +74 -0
  138. rasa/shared/providers/embedding/azure_openai_embedding_client.py +277 -0
  139. rasa/shared/providers/embedding/default_litellm_embedding_client.py +102 -0
  140. rasa/shared/providers/embedding/embedding_client.py +90 -0
  141. rasa/shared/providers/embedding/embedding_response.py +41 -0
  142. rasa/shared/providers/embedding/huggingface_local_embedding_client.py +191 -0
  143. rasa/shared/providers/embedding/openai_embedding_client.py +172 -0
  144. rasa/shared/providers/llm/__init__.py +0 -0
  145. rasa/shared/providers/llm/_base_litellm_client.py +251 -0
  146. rasa/shared/providers/llm/azure_openai_llm_client.py +338 -0
  147. rasa/shared/providers/llm/default_litellm_llm_client.py +84 -0
  148. rasa/shared/providers/llm/llm_client.py +76 -0
  149. rasa/shared/providers/llm/llm_response.py +50 -0
  150. rasa/shared/providers/llm/openai_llm_client.py +155 -0
  151. rasa/shared/providers/llm/self_hosted_llm_client.py +293 -0
  152. rasa/shared/providers/mappings.py +75 -0
  153. rasa/shared/utils/cli.py +30 -0
  154. rasa/shared/utils/io.py +65 -2
  155. rasa/shared/utils/llm.py +246 -200
  156. rasa/shared/utils/yaml.py +121 -15
  157. rasa/studio/auth.py +6 -4
  158. rasa/studio/config.py +13 -4
  159. rasa/studio/constants.py +1 -0
  160. rasa/studio/data_handler.py +10 -3
  161. rasa/studio/download.py +19 -13
  162. rasa/studio/train.py +2 -3
  163. rasa/studio/upload.py +19 -11
  164. rasa/telemetry.py +113 -58
  165. rasa/tracing/instrumentation/attribute_extractors.py +32 -17
  166. rasa/utils/common.py +18 -19
  167. rasa/utils/endpoints.py +7 -4
  168. rasa/utils/json_utils.py +60 -0
  169. rasa/utils/licensing.py +9 -1
  170. rasa/utils/ml_utils.py +4 -2
  171. rasa/validator.py +213 -3
  172. rasa/version.py +1 -1
  173. rasa_pro-3.10.16.dist-info/METADATA +196 -0
  174. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/RECORD +179 -113
  175. rasa/nlu/classifiers/llm_intent_classifier.py +0 -519
  176. rasa/shared/providers/openai/clients.py +0 -43
  177. rasa/shared/providers/openai/session_handler.py +0 -110
  178. rasa_pro-3.9.18.dist-info/METADATA +0 -563
  179. /rasa/{shared/providers/openai → cli/project_templates/tutorial/actions}/__init__.py +0 -0
  180. /rasa/cli/project_templates/tutorial/{actions.py → actions/actions.py} +0 -0
  181. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/NOTICE +0 -0
  182. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/WHEEL +0 -0
  183. {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/entry_points.txt +0 -0
@@ -1,519 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any, Dict, List, Optional, Text
4
-
5
- import rasa.shared.utils.io
6
- import structlog
7
- from langchain.chains import LLMChain
8
- from langchain.docstore.document import Document
9
- from langchain.prompts import PromptTemplate
10
- from langchain.vectorstores import FAISS
11
-
12
- from rasa import telemetry
13
- from rasa.engine.graph import ExecutionContext, GraphComponent
14
- from rasa.engine.recipes.default_recipe import DefaultV1Recipe
15
- from rasa.engine.storage.resource import Resource
16
- from rasa.engine.storage.storage import ModelStorage
17
- from rasa.nlu.classifiers.classifier import IntentClassifier
18
- from rasa.shared.constants import INTENT_MESSAGE_PREFIX
19
- from rasa.shared.exceptions import FileIOException
20
- from rasa.shared.nlu.constants import (
21
- INTENT,
22
- INTENT_NAME_KEY,
23
- INTENT_RESPONSE_KEY,
24
- METADATA,
25
- PREDICTED_CONFIDENCE_KEY,
26
- TEXT,
27
- )
28
- from rasa.shared.nlu.training_data.message import Message
29
- from rasa.shared.nlu.training_data.training_data import TrainingData
30
- from rasa.shared.utils.io import deep_container_fingerprint
31
- from rasa.shared.utils.llm import (
32
- DEFAULT_OPENAI_GENERATE_MODEL_NAME,
33
- DEFAULT_OPENAI_MAX_GENERATED_TOKENS,
34
- DEFAULT_OPENAI_TEMPERATURE,
35
- combine_custom_and_default_config,
36
- embedder_factory,
37
- get_prompt_template,
38
- llm_factory,
39
- )
40
-
41
- from rasa.utils.ml_utils import (
42
- load_faiss_vector_store,
43
- persist_faiss_vector_store,
44
- )
45
- from rasa.utils import beta
46
-
47
- structlogger = structlog.get_logger()
48
-
49
- RASA_PRO_BETA_LLM_INTENT = "RASA_PRO_BETA_LLM_INTENT"
50
-
51
- DEFAULT_NUMBER_OF_INTENT_EXAMPLES = 10
52
-
53
- DEFAULT_LLM_CONFIG = {
54
- "_type": "openai",
55
- "request_timeout": 5,
56
- "temperature": DEFAULT_OPENAI_TEMPERATURE,
57
- "model_name": DEFAULT_OPENAI_GENERATE_MODEL_NAME,
58
- "max_tokens": DEFAULT_OPENAI_MAX_GENERATED_TOKENS,
59
- }
60
-
61
- DEFAULT_EMBEDDINGS_CONFIG = {"_type": "openai"}
62
-
63
- EMBEDDINGS_CONFIG_KEY = "embeddings"
64
- LLM_CONFIG_KEY = "llm"
65
-
66
- DEFAULT_INTENT_CLASSIFICATION_PROMPT_TEMPLATE = """Label a users message from a
67
- conversation with an intent. Reply ONLY with the name of the intent.
68
-
69
- The intent should be one of the following:
70
- {% for intent in intents %}- {{intent}}
71
- {% endfor %}
72
- {% for example in examples %}
73
- Message: {{example['text']}}
74
- Intent: {{example['intent']}}
75
- {% endfor %}
76
- Message: {{message}}
77
- Intent:"""
78
-
79
- LLM_INTENT_CLASSIFIER_PROMPT_FILE_NAME = "llm_intent_classifier_prompt.jinja2"
80
-
81
-
82
- @DefaultV1Recipe.register(
83
- DefaultV1Recipe.ComponentType.INTENT_CLASSIFIER, is_trainable=True
84
- )
85
- class LLMIntentClassifier(GraphComponent, IntentClassifier):
86
- """Intent classifier using the LLM to generate the intent classification."""
87
-
88
- @staticmethod
89
- def get_default_config() -> Dict[Text, Any]:
90
- """The component's default config (see parent class for full docstring)."""
91
- return {
92
- "fallback_intent": "out_of_scope",
93
- "prompt": None,
94
- LLM_CONFIG_KEY: None,
95
- EMBEDDINGS_CONFIG_KEY: None,
96
- "number_of_examples": DEFAULT_NUMBER_OF_INTENT_EXAMPLES,
97
- }
98
-
99
- def __init__(
100
- self,
101
- config: Dict[Text, Any],
102
- model_storage: ModelStorage,
103
- resource: Resource,
104
- execution_context: ExecutionContext,
105
- intent_docsearch: Optional[FAISS] = None,
106
- example_docsearch: Optional[FAISS] = None,
107
- available_intents: Optional[List[str]] = None,
108
- prompt_template: Optional[Text] = None,
109
- ) -> None:
110
- """Creates classifier."""
111
- beta.ensure_beta_feature_is_enabled(
112
- "LLM Intent Classifier", env_flag=RASA_PRO_BETA_LLM_INTENT
113
- )
114
-
115
- self.component_config = config
116
- self._model_storage = model_storage
117
- self._resource = resource
118
- self._execution_context = execution_context
119
-
120
- self.fallback_intent = self.component_config.get("fallback_intent")
121
- self.number_of_examples = self.component_config.get(
122
- "number_of_examples", DEFAULT_NUMBER_OF_INTENT_EXAMPLES
123
- )
124
-
125
- self.intent_docsearch = intent_docsearch
126
- self.example_docsearch = example_docsearch
127
- self.available_intents = set(available_intents or [])
128
- self.prompt_template = prompt_template or get_prompt_template(
129
- self.component_config.get("prompt"),
130
- DEFAULT_INTENT_CLASSIFICATION_PROMPT_TEMPLATE,
131
- )
132
-
133
- @classmethod
134
- def create(
135
- cls,
136
- config: Dict[Text, Any],
137
- model_storage: ModelStorage,
138
- resource: Resource,
139
- execution_context: ExecutionContext,
140
- ) -> LLMIntentClassifier:
141
- """Creates a new untrained component (see parent class for full docstring)."""
142
- return cls(config, model_storage, resource, execution_context)
143
-
144
- def embeddings_property(self, prop: str) -> Optional[str]:
145
- """Returns the property of the embeddings config."""
146
- return combine_custom_and_default_config(
147
- self.component_config.get(EMBEDDINGS_CONFIG_KEY), DEFAULT_EMBEDDINGS_CONFIG
148
- ).get(prop)
149
-
150
- def llm_property(self, prop: str) -> Optional[str]:
151
- """Returns the property of the LLM config."""
152
- return combine_custom_and_default_config(
153
- self.component_config.get(LLM_CONFIG_KEY), DEFAULT_LLM_CONFIG
154
- ).get(prop)
155
-
156
- def custom_prompt_template(self) -> Optional[str]:
157
- """Returns the custom prompt template if it is not the default one."""
158
- if self.prompt_template != DEFAULT_INTENT_CLASSIFICATION_PROMPT_TEMPLATE:
159
- return self.prompt_template
160
- else:
161
- return None
162
-
163
- def train(self, training_data: TrainingData) -> Resource:
164
- """Trains the intent classifier on a data set."""
165
- texts = [ex.get(TEXT, "") for ex in training_data.intent_examples]
166
- metadatas = [
167
- {
168
- INTENT: ex.get(INTENT_RESPONSE_KEY) or ex.get(INTENT),
169
- TEXT: ex.get(TEXT),
170
- }
171
- for ex in training_data.intent_examples
172
- ]
173
-
174
- embedder = embedder_factory(
175
- self.component_config.get(EMBEDDINGS_CONFIG_KEY), DEFAULT_EMBEDDINGS_CONFIG
176
- )
177
-
178
- self.example_docsearch = (
179
- FAISS.from_texts(texts, embedder, metadatas) if texts else None
180
- )
181
-
182
- self.available_intents = {e[INTENT].lower() for e in metadatas}
183
-
184
- self.intent_docsearch = (
185
- FAISS.from_texts(
186
- list(self.available_intents),
187
- embedder,
188
- )
189
- if self.available_intents
190
- else None
191
- )
192
-
193
- self.persist()
194
- telemetry.track_llm_intent_train_completed(
195
- embeddings_type=self.embeddings_property("_type"),
196
- embeddings_model=self.embeddings_property("model_name")
197
- or self.embeddings_property("model"),
198
- llm_type=self.llm_property("_type"),
199
- llm_model=self.llm_property("model_name") or self.llm_property("model"),
200
- fallback_intent=self.fallback_intent,
201
- custom_prompt_template=self.custom_prompt_template(),
202
- number_of_examples=self.number_of_examples,
203
- number_of_available_intents=len(self.available_intents),
204
- )
205
- return self._resource
206
-
207
- async def process(self, messages: List[Message]) -> List[Message]:
208
- """Sets the message intent and add it to the output if it exists."""
209
- for message in messages:
210
- if message.get(TEXT, "").startswith(INTENT_MESSAGE_PREFIX):
211
- # llm calls are expensive, so we skip messages
212
- # that start with a slash as they are direct intents
213
- continue
214
- examples = self.select_few_shot_examples(message)
215
- predicted_intent_name = await self.classify_intent_of_message(
216
- message, examples
217
- )
218
-
219
- if not predicted_intent_name:
220
- # or should we predict self.fallback_intent?
221
- continue
222
-
223
- if "/" in predicted_intent_name:
224
- intent_name = predicted_intent_name.split("/")[0]
225
- else:
226
- intent_name = predicted_intent_name
227
-
228
- structlogger.info(
229
- "llmintent.prediction.success",
230
- predicted_intent=intent_name,
231
- llm_prediction=predicted_intent_name,
232
- )
233
-
234
- intent = {
235
- INTENT_NAME_KEY: intent_name,
236
- METADATA: {"llm_intent": predicted_intent_name},
237
- PREDICTED_CONFIDENCE_KEY: 1.0,
238
- }
239
-
240
- message.set(INTENT, intent, add_to_output=True)
241
-
242
- # telemetry.track_llm_intent_predict(
243
- # embeddings_type=self.embeddings_property("_type"),
244
- # embeddings_model=self.embeddings_property("model_name")
245
- # or self.embeddings_property("model"),
246
- # llm_type=self.llm_property("_type"),
247
- # llm_model=self.llm_property("model_name") or self.llm_property("model"),
248
- # )
249
- return messages
250
-
251
- async def _generate_llm_response(
252
- self, message: Message, intents: List[str], few_shot_examples: List[Document]
253
- ) -> Optional[str]:
254
- """Use LLM to generate a response.
255
-
256
- Args:
257
- message: The message.
258
- intents: The intents.
259
- few_shot_examples: The few shot examples.
260
-
261
- Returns:
262
- generated text
263
- """
264
- prompt = self.get_prompt_template_obj()
265
- examples = [
266
- {**{"text": e.page_content}, **e.metadata} for e in few_shot_examples
267
- ]
268
- message_text = message.get(TEXT, "")
269
-
270
- structlogger.debug(
271
- "llmintent.llm.generate",
272
- prompt=prompt.format(
273
- examples=examples,
274
- intents=intents,
275
- message=message_text,
276
- ),
277
- )
278
- chain = LLMChain(
279
- llm=llm_factory(
280
- self.component_config.get(LLM_CONFIG_KEY), DEFAULT_LLM_CONFIG
281
- ),
282
- prompt=self.get_prompt_template_obj(),
283
- )
284
-
285
- try:
286
- return await chain.arun(
287
- examples=examples, intents=intents, message=message_text
288
- )
289
- except Exception as e:
290
- structlogger.error("llmintent.llm.error", error=e)
291
- return None
292
-
293
- def closest_intent_from_training_data(self, generated_intent: str) -> Optional[str]:
294
- """Returns the closest intent from the training data.
295
-
296
- Args:
297
- generated_intent: the intent that was generated by the LLM
298
-
299
- Returns:
300
- the closest intent from the training data.
301
- """
302
- structlogger.debug(
303
- "llmintent.llmresponse.not_in_training_data",
304
- proposed_intent=generated_intent,
305
- )
306
- if not self.intent_docsearch:
307
- return None
308
-
309
- try:
310
- closest_intents = (
311
- self.intent_docsearch.similarity_search_with_relevance_scores(
312
- generated_intent, k=1
313
- )
314
- )
315
- except Exception as e:
316
- structlogger.error("llmintent.llmresponse.intentsearch.error", error=e)
317
- return None
318
-
319
- if not closest_intents:
320
- return None
321
-
322
- closest_intent_name = closest_intents[0][0].page_content
323
-
324
- structlogger.debug(
325
- "llmintent.llmresponse.intentsearch",
326
- intent=closest_intent_name,
327
- score=closest_intents[0][1],
328
- )
329
- return closest_intent_name
330
-
331
- def get_prompt_template_obj(self) -> PromptTemplate:
332
- """Returns the prompt template."""
333
- return PromptTemplate(
334
- input_variables=["examples", "intents", "message"],
335
- template=self.prompt_template,
336
- template_format="jinja2",
337
- )
338
-
339
- async def classify_intent_of_message(
340
- self, message: Message, few_shot_examples: List[Document]
341
- ) -> Optional[Text]:
342
- """Classify the message using an LLM.
343
-
344
- Args:
345
- message: The message to classify.
346
- few_shot_examples: The few shot examples that can be used in the prompt.
347
-
348
-
349
- Returns:
350
- The predicted intent.
351
- """
352
- provided_intents = self.select_intent_examples(message, few_shot_examples)
353
-
354
- generated_intent = await self._generate_llm_response(
355
- message, provided_intents, few_shot_examples
356
- )
357
-
358
- if generated_intent is None:
359
- # something went wrong with the LLM
360
- return None
361
-
362
- generated_intent = generated_intent.strip().lower()
363
-
364
- if generated_intent in self.available_intents:
365
- return generated_intent
366
-
367
- # if the generated intent is not in the training data, we try to
368
- # find the closest intent
369
- return self.closest_intent_from_training_data(generated_intent)
370
-
371
- def persist(self) -> None:
372
- """Persist this model into the passed directory."""
373
- with self._model_storage.write_to(self._resource) as path:
374
- persist_faiss_vector_store(path / "intents_faiss", self.intent_docsearch)
375
- persist_faiss_vector_store(path / "examples_faiss", self.example_docsearch)
376
- rasa.shared.utils.io.dump_obj_as_json_to_file(
377
- path / "intents.json", list(self.available_intents)
378
- )
379
- rasa.shared.utils.io.write_text_file(
380
- self.prompt_template, path / LLM_INTENT_CLASSIFIER_PROMPT_FILE_NAME
381
- )
382
-
383
- def select_intent_examples(
384
- self, message: Message, few_shot_examples: List[Document]
385
- ) -> List[str]:
386
- """Returns the intents that are used in the classification prompt.
387
-
388
- The intents are included in the prompt to help the LLM to generate the
389
- correct intent. The selected intents can be based on the message or on
390
- the few shot examples which are also included in the prompt.
391
-
392
- Including all intents can lead to a very long prompt which will lead
393
- to higher costs and longer response times. In addition, the LLM might
394
- not be able to generate the correct intent if there are too many intents
395
- in the prompt as we can't include an example for every intent. The
396
- classification would in this case just be based on the intent name.
397
-
398
- Args:
399
- message: The message to classify.
400
- few_shot_examples: The few shot examples that can be used in the prompt.
401
-
402
-
403
- Returns:
404
- The intents that are used in the classification prompt.
405
- """
406
- # we sort the list to make sure intents are always in the same order
407
- # independent of the order of the examples
408
- selected_intents = sorted(
409
- list(dict.fromkeys([e.metadata["intent"] for e in few_shot_examples]))
410
- )
411
- if selected_intents:
412
- return selected_intents
413
- else:
414
- return list(self.available_intents)[:5]
415
-
416
- def select_few_shot_examples(self, message: Message) -> List[Document]:
417
- """Selects the few shot examples that should be used for the LLM prompt.
418
-
419
- The examples are included in the classification prompt to help the LLM
420
- to generate the correct intent. Since only a few examples are included
421
- in the prompt, we need to select the most relevant ones.
422
-
423
- Args:
424
- message: the message to find the closest examples for
425
-
426
- Returns:
427
- the closest examples from the embedded training data
428
- """
429
- if not self.example_docsearch:
430
- return []
431
-
432
- # we fetch more examples than we need to make sure that we have enough
433
- # examples that are relevant to the message. the forumla ensures
434
- # that we fetch at least double the number of examples but avoids
435
- # fetching too many additional examples if the number of
436
- # examples is large.
437
-
438
- fetch_k = int(self.number_of_examples * (2 + 10 * 1 / self.number_of_examples))
439
-
440
- try:
441
- return self.example_docsearch.max_marginal_relevance_search(
442
- message.get(TEXT, ""), k=self.number_of_examples, fetch_k=fetch_k
443
- )
444
- except Exception as e:
445
- # this can happen if the message doesn't have a text attribute
446
- structlogger.warning(
447
- "llmintent.embeddong.no_embedding", message=message, error=e
448
- )
449
- return []
450
-
451
- @classmethod
452
- def load(
453
- cls,
454
- config: Dict[Text, Any],
455
- model_storage: ModelStorage,
456
- resource: Resource,
457
- execution_context: ExecutionContext,
458
- **kwargs: Any,
459
- ) -> LLMIntentClassifier:
460
- """Loads trained component (see parent class for full docstring).
461
-
462
- Args:
463
- config: the configuration for this component
464
- model_storage: the model storage
465
- resource: the resource
466
- execution_context: the execution context
467
- **kwargs: additional arguments
468
-
469
-
470
- Returns:
471
- the loaded component
472
- """
473
- example_docsearch = None
474
- intent_docsearch = None
475
- available_intents = None
476
- prompt_template = None
477
-
478
- embedder = embedder_factory(
479
- config.get(EMBEDDINGS_CONFIG_KEY), DEFAULT_EMBEDDINGS_CONFIG
480
- )
481
- try:
482
- with model_storage.read_from(resource) as path:
483
- example_docsearch = load_faiss_vector_store(
484
- path / "examples_faiss", embedder
485
- )
486
- intent_docsearch = load_faiss_vector_store(
487
- path / "intents_faiss", embedder
488
- )
489
- available_intents = rasa.shared.utils.io.read_json_file(
490
- path / "intents.json"
491
- )
492
- prompt_template = rasa.shared.utils.io.read_file(
493
- path / LLM_INTENT_CLASSIFIER_PROMPT_FILE_NAME
494
- )
495
-
496
- except (ValueError, FileNotFoundError, FileIOException) as e:
497
- structlogger.warning(
498
- "llmintent.load.failed", error=e, resource=resource.name
499
- )
500
-
501
- return cls(
502
- config,
503
- model_storage,
504
- resource,
505
- execution_context,
506
- intent_docsearch,
507
- example_docsearch,
508
- available_intents,
509
- prompt_template,
510
- )
511
-
512
- @classmethod
513
- def fingerprint_addon(cls, config: Dict[str, Any]) -> Optional[str]:
514
- """Add a fingerprint of the knowledge base for the graph."""
515
- prompt_template = get_prompt_template(
516
- config.get("prompt"),
517
- DEFAULT_INTENT_CLASSIFICATION_PROMPT_TEMPLATE,
518
- )
519
- return deep_container_fingerprint(prompt_template)
@@ -1,43 +0,0 @@
1
- import os
2
- import ssl
3
- from typing import List, Optional, Sequence, Any
4
-
5
- import certifi
6
- from langchain.chat_models import AzureChatOpenAI
7
- from langchain.embeddings import OpenAIEmbeddings
8
- from langchain.llms.openai import OpenAIChat
9
-
10
- from rasa.shared.constants import (
11
- REQUESTS_CA_BUNDLE_ENV_VAR,
12
- REQUESTS_SSL_CONTEXT_PURPOSE_ENV_VAR,
13
- )
14
- from rasa.shared.providers.openai.session_handler import OpenAISessionHandler
15
-
16
- CERTIFICATE_FILE = os.environ.get(REQUESTS_CA_BUNDLE_ENV_VAR, certifi.where())
17
- SSL_PURPOSE = os.environ.get(
18
- REQUESTS_SSL_CONTEXT_PURPOSE_ENV_VAR, ssl.Purpose.SERVER_AUTH
19
- )
20
-
21
-
22
- class AioHTTPSessionAzureChatOpenAI(AzureChatOpenAI):
23
- async def apredict(
24
- self, text: str, *, stop: Optional[Sequence[str]] = None, **kwargs: Any
25
- ) -> str:
26
- async with OpenAISessionHandler(CERTIFICATE_FILE, SSL_PURPOSE):
27
- return await super().apredict(text, stop=stop, **kwargs)
28
-
29
-
30
- class AioHTTPSessionOpenAIChat(OpenAIChat):
31
- async def apredict(
32
- self, text: str, *, stop: Optional[Sequence[str]] = None, **kwargs: Any
33
- ) -> str:
34
- async with OpenAISessionHandler(CERTIFICATE_FILE, SSL_PURPOSE):
35
- return await super().apredict(text, stop=stop, **kwargs)
36
-
37
-
38
- class AioHTTPSessionOpenAIEmbeddings(OpenAIEmbeddings):
39
- async def aembed_documents(
40
- self, texts: List[str], chunk_size: Optional[int] = 0
41
- ) -> List[List[float]]:
42
- async with OpenAISessionHandler(CERTIFICATE_FILE, SSL_PURPOSE):
43
- return await super().aembed_documents(texts, chunk_size)
@@ -1,110 +0,0 @@
1
- import ssl
2
- from pathlib import Path
3
- from ssl import SSLContext
4
- from typing import Text
5
-
6
- import certifi
7
- import openai
8
- import structlog
9
- from aiohttp import ClientSession, TCPConnector
10
-
11
- structlogger = structlog.get_logger()
12
-
13
-
14
- class OpenAISessionHandler:
15
- """
16
- This context manager is used to manage the aiohttp session for OpenAI. This
17
- session handles calls that use self-signed certificates, the path to which
18
- must be provided.
19
-
20
- Each session is unique to the context in which it is used, ensuring thread
21
- safety across asynchronous tasks.
22
-
23
- Upon entering the context manager, the session is configured with a custom
24
- SSL context. Upon exiting, the session is properly closed and the context
25
- variable is cleared.
26
- """
27
-
28
- def __init__(
29
- self,
30
- certificate_path: Text = certifi.where(),
31
- purpose: ssl.Purpose = ssl.Purpose.SERVER_AUTH,
32
- ):
33
- self._certificate_path = certificate_path
34
- self._purpose = purpose
35
-
36
- async def __aenter__(self) -> ClientSession:
37
- session = openai.aiosession.get()
38
- if session is None:
39
- session = self._create_session()
40
- openai.aiosession.set(session)
41
- structlogger.debug(
42
- "openai_aiohttp_session_handler" ".set_openai_session",
43
- )
44
- return session
45
-
46
- async def __aexit__(self, exc_type, exc_value, traceback) -> None: # type: ignore
47
- session = openai.aiosession.get()
48
- if session:
49
- await session.close()
50
- structlogger.debug(
51
- "openai_aiohttp_session_handler" ".close_openai_session",
52
- )
53
- openai.aiosession.set(None)
54
- structlogger.debug(
55
- "openai_aiohttp_session_handler" ".clear_openai_session",
56
- )
57
-
58
- def _create_session(self) -> ClientSession:
59
- """
60
- Create client session with an SSL context
61
- created from the certificate path
62
- """
63
- ssl_context = self._create_ssl_context()
64
- conn = TCPConnector(ssl=ssl_context)
65
- session = ClientSession(connector=conn)
66
- return session
67
-
68
- def _create_ssl_context(self) -> SSLContext:
69
- """
70
- Create an SSL context using the provided certificate file path.
71
-
72
- Returns:
73
- SSLContext: An SSL context configured with the given certificate.
74
-
75
- Raises:
76
- ValueError: If the certificate path is invalid.
77
- Exception: If there is an error creating the SSL context.
78
- """
79
- if self._certificate_path is None or not Path(self._certificate_path).is_file():
80
- structlogger.error(
81
- "openai_aiohttp_session_handler"
82
- ".create_ssl_context"
83
- ".cannot_load_certificate_from_path",
84
- event_info=(
85
- f"Cannot load certificate file from the "
86
- f"given path: {self._certificate_path}"
87
- ),
88
- )
89
- raise ValueError("Invalid certificate file path.")
90
- try:
91
- ssl_context = ssl.create_default_context(
92
- purpose=self._purpose, cafile=self._certificate_path
93
- )
94
- purpose = (
95
- "SERVER_AUTH"
96
- if self._purpose == ssl.Purpose.SERVER_AUTH
97
- else "CLIENT_AUTH"
98
- )
99
- structlogger.debug(
100
- "openai_aiohttp_session_handler" ".created_ssl_context" ".created",
101
- certificate_file=self._certificate_path,
102
- purpose=f"{purpose} - {self._purpose}",
103
- )
104
- return ssl_context
105
- except Exception as e:
106
- structlogger.error(
107
- "openai_aiohttp_session_handler" ".create_ssl_context" ".error",
108
- error=e,
109
- )
110
- raise