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.
- README.md +0 -374
- rasa/__init__.py +1 -2
- rasa/__main__.py +5 -0
- rasa/anonymization/anonymization_rule_executor.py +2 -2
- rasa/api.py +27 -23
- rasa/cli/arguments/data.py +27 -2
- rasa/cli/arguments/default_arguments.py +25 -3
- rasa/cli/arguments/run.py +9 -9
- rasa/cli/arguments/train.py +11 -3
- rasa/cli/data.py +70 -8
- rasa/cli/e2e_test.py +104 -431
- rasa/cli/evaluate.py +1 -1
- rasa/cli/interactive.py +1 -0
- rasa/cli/llm_fine_tuning.py +398 -0
- rasa/cli/project_templates/calm/endpoints.yml +1 -1
- rasa/cli/project_templates/tutorial/endpoints.yml +1 -1
- rasa/cli/run.py +15 -14
- rasa/cli/scaffold.py +10 -8
- rasa/cli/studio/studio.py +35 -5
- rasa/cli/train.py +56 -8
- rasa/cli/utils.py +22 -5
- rasa/cli/x.py +1 -1
- rasa/constants.py +7 -1
- rasa/core/actions/action.py +98 -49
- rasa/core/actions/action_run_slot_rejections.py +4 -1
- rasa/core/actions/custom_action_executor.py +9 -6
- rasa/core/actions/direct_custom_actions_executor.py +80 -0
- rasa/core/actions/e2e_stub_custom_action_executor.py +68 -0
- rasa/core/actions/grpc_custom_action_executor.py +2 -2
- rasa/core/actions/http_custom_action_executor.py +6 -5
- rasa/core/agent.py +21 -17
- rasa/core/channels/__init__.py +2 -0
- rasa/core/channels/audiocodes.py +1 -16
- rasa/core/channels/voice_aware/__init__.py +0 -0
- rasa/core/channels/voice_aware/jambonz.py +103 -0
- rasa/core/channels/voice_aware/jambonz_protocol.py +344 -0
- rasa/core/channels/voice_aware/utils.py +20 -0
- rasa/core/channels/voice_native/__init__.py +0 -0
- rasa/core/constants.py +6 -1
- rasa/core/information_retrieval/faiss.py +7 -4
- rasa/core/information_retrieval/information_retrieval.py +8 -0
- rasa/core/information_retrieval/milvus.py +9 -2
- rasa/core/information_retrieval/qdrant.py +1 -1
- rasa/core/nlg/contextual_response_rephraser.py +32 -10
- rasa/core/nlg/summarize.py +4 -3
- rasa/core/policies/enterprise_search_policy.py +113 -45
- rasa/core/policies/flows/flow_executor.py +122 -76
- rasa/core/policies/intentless_policy.py +83 -29
- rasa/core/processor.py +72 -54
- rasa/core/run.py +5 -4
- rasa/core/tracker_store.py +8 -4
- rasa/core/training/interactive.py +1 -1
- rasa/core/utils.py +56 -57
- rasa/dialogue_understanding/coexistence/llm_based_router.py +53 -13
- rasa/dialogue_understanding/commands/__init__.py +6 -0
- rasa/dialogue_understanding/commands/restart_command.py +58 -0
- rasa/dialogue_understanding/commands/session_start_command.py +59 -0
- rasa/dialogue_understanding/commands/utils.py +40 -0
- rasa/dialogue_understanding/generator/constants.py +10 -3
- rasa/dialogue_understanding/generator/flow_retrieval.py +21 -5
- rasa/dialogue_understanding/generator/llm_based_command_generator.py +13 -3
- rasa/dialogue_understanding/generator/multi_step/multi_step_llm_command_generator.py +134 -90
- rasa/dialogue_understanding/generator/nlu_command_adapter.py +47 -7
- rasa/dialogue_understanding/generator/single_step/single_step_llm_command_generator.py +127 -41
- rasa/dialogue_understanding/patterns/restart.py +37 -0
- rasa/dialogue_understanding/patterns/session_start.py +37 -0
- rasa/dialogue_understanding/processor/command_processor.py +16 -3
- rasa/dialogue_understanding/processor/command_processor_component.py +6 -2
- rasa/e2e_test/aggregate_test_stats_calculator.py +134 -0
- rasa/e2e_test/assertions.py +1223 -0
- rasa/e2e_test/assertions_schema.yml +106 -0
- rasa/e2e_test/constants.py +20 -0
- rasa/e2e_test/e2e_config.py +220 -0
- rasa/e2e_test/e2e_config_schema.yml +26 -0
- rasa/e2e_test/e2e_test_case.py +131 -8
- rasa/e2e_test/e2e_test_converter.py +363 -0
- rasa/e2e_test/e2e_test_converter_prompt.jinja2 +70 -0
- rasa/e2e_test/e2e_test_coverage_report.py +364 -0
- rasa/e2e_test/e2e_test_result.py +26 -6
- rasa/e2e_test/e2e_test_runner.py +493 -71
- rasa/e2e_test/e2e_test_schema.yml +96 -0
- rasa/e2e_test/pykwalify_extensions.py +39 -0
- rasa/e2e_test/stub_custom_action.py +70 -0
- rasa/e2e_test/utils/__init__.py +0 -0
- rasa/e2e_test/utils/e2e_yaml_utils.py +55 -0
- rasa/e2e_test/utils/io.py +598 -0
- rasa/e2e_test/utils/validation.py +80 -0
- rasa/engine/graph.py +9 -3
- rasa/engine/recipes/default_components.py +0 -2
- rasa/engine/recipes/default_recipe.py +10 -2
- rasa/engine/storage/local_model_storage.py +40 -12
- rasa/engine/validation.py +78 -1
- rasa/env.py +9 -0
- rasa/graph_components/providers/story_graph_provider.py +59 -6
- rasa/llm_fine_tuning/__init__.py +0 -0
- rasa/llm_fine_tuning/annotation_module.py +241 -0
- rasa/llm_fine_tuning/conversations.py +144 -0
- rasa/llm_fine_tuning/llm_data_preparation_module.py +178 -0
- rasa/llm_fine_tuning/notebooks/unsloth_finetuning.ipynb +407 -0
- rasa/llm_fine_tuning/paraphrasing/__init__.py +0 -0
- rasa/llm_fine_tuning/paraphrasing/conversation_rephraser.py +281 -0
- rasa/llm_fine_tuning/paraphrasing/default_rephrase_prompt_template.jina2 +44 -0
- rasa/llm_fine_tuning/paraphrasing/rephrase_validator.py +121 -0
- rasa/llm_fine_tuning/paraphrasing/rephrased_user_message.py +10 -0
- rasa/llm_fine_tuning/paraphrasing_module.py +128 -0
- rasa/llm_fine_tuning/storage.py +174 -0
- rasa/llm_fine_tuning/train_test_split_module.py +441 -0
- rasa/model_training.py +56 -16
- rasa/nlu/persistor.py +157 -36
- rasa/server.py +45 -10
- rasa/shared/constants.py +76 -16
- rasa/shared/core/domain.py +27 -19
- rasa/shared/core/events.py +28 -2
- rasa/shared/core/flows/flow.py +208 -13
- rasa/shared/core/flows/flow_path.py +84 -0
- rasa/shared/core/flows/flows_list.py +33 -11
- rasa/shared/core/flows/flows_yaml_schema.json +269 -193
- rasa/shared/core/flows/validation.py +112 -25
- rasa/shared/core/flows/yaml_flows_io.py +149 -10
- rasa/shared/core/trackers.py +6 -0
- rasa/shared/core/training_data/structures.py +20 -0
- rasa/shared/core/training_data/visualization.html +2 -2
- rasa/shared/exceptions.py +4 -0
- rasa/shared/importers/importer.py +64 -16
- rasa/shared/nlu/constants.py +2 -0
- rasa/shared/providers/_configs/__init__.py +0 -0
- rasa/shared/providers/_configs/azure_openai_client_config.py +183 -0
- rasa/shared/providers/_configs/client_config.py +57 -0
- rasa/shared/providers/_configs/default_litellm_client_config.py +130 -0
- rasa/shared/providers/_configs/huggingface_local_embedding_client_config.py +234 -0
- rasa/shared/providers/_configs/openai_client_config.py +175 -0
- rasa/shared/providers/_configs/self_hosted_llm_client_config.py +176 -0
- rasa/shared/providers/_configs/utils.py +101 -0
- rasa/shared/providers/_ssl_verification_utils.py +124 -0
- rasa/shared/providers/embedding/__init__.py +0 -0
- rasa/shared/providers/embedding/_base_litellm_embedding_client.py +259 -0
- rasa/shared/providers/embedding/_langchain_embedding_client_adapter.py +74 -0
- rasa/shared/providers/embedding/azure_openai_embedding_client.py +277 -0
- rasa/shared/providers/embedding/default_litellm_embedding_client.py +102 -0
- rasa/shared/providers/embedding/embedding_client.py +90 -0
- rasa/shared/providers/embedding/embedding_response.py +41 -0
- rasa/shared/providers/embedding/huggingface_local_embedding_client.py +191 -0
- rasa/shared/providers/embedding/openai_embedding_client.py +172 -0
- rasa/shared/providers/llm/__init__.py +0 -0
- rasa/shared/providers/llm/_base_litellm_client.py +251 -0
- rasa/shared/providers/llm/azure_openai_llm_client.py +338 -0
- rasa/shared/providers/llm/default_litellm_llm_client.py +84 -0
- rasa/shared/providers/llm/llm_client.py +76 -0
- rasa/shared/providers/llm/llm_response.py +50 -0
- rasa/shared/providers/llm/openai_llm_client.py +155 -0
- rasa/shared/providers/llm/self_hosted_llm_client.py +293 -0
- rasa/shared/providers/mappings.py +75 -0
- rasa/shared/utils/cli.py +30 -0
- rasa/shared/utils/io.py +65 -2
- rasa/shared/utils/llm.py +246 -200
- rasa/shared/utils/yaml.py +121 -15
- rasa/studio/auth.py +6 -4
- rasa/studio/config.py +13 -4
- rasa/studio/constants.py +1 -0
- rasa/studio/data_handler.py +10 -3
- rasa/studio/download.py +19 -13
- rasa/studio/train.py +2 -3
- rasa/studio/upload.py +19 -11
- rasa/telemetry.py +113 -58
- rasa/tracing/instrumentation/attribute_extractors.py +32 -17
- rasa/utils/common.py +18 -19
- rasa/utils/endpoints.py +7 -4
- rasa/utils/json_utils.py +60 -0
- rasa/utils/licensing.py +9 -1
- rasa/utils/ml_utils.py +4 -2
- rasa/validator.py +213 -3
- rasa/version.py +1 -1
- rasa_pro-3.10.16.dist-info/METADATA +196 -0
- {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/RECORD +179 -113
- rasa/nlu/classifiers/llm_intent_classifier.py +0 -519
- rasa/shared/providers/openai/clients.py +0 -43
- rasa/shared/providers/openai/session_handler.py +0 -110
- rasa_pro-3.9.18.dist-info/METADATA +0 -563
- /rasa/{shared/providers/openai → cli/project_templates/tutorial/actions}/__init__.py +0 -0
- /rasa/cli/project_templates/tutorial/{actions.py → actions/actions.py} +0 -0
- {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/NOTICE +0 -0
- {rasa_pro-3.9.18.dist-info → rasa_pro-3.10.16.dist-info}/WHEEL +0 -0
- {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
|