rasa-pro 3.13.1a14__py3-none-any.whl → 3.13.1a16__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.
- rasa/builder/config.py +11 -1
- rasa/builder/exceptions.py +6 -0
- rasa/builder/inkeep-rag-response-schema.json +64 -0
- rasa/builder/inkeep_document_retrieval.py +212 -0
- rasa/builder/llm_service.py +22 -32
- rasa/builder/main.py +95 -9
- rasa/builder/models.py +61 -10
- rasa/builder/project_generator.py +7 -6
- rasa/builder/scrape_rasa_docs.py +4 -4
- rasa/builder/service.py +626 -436
- rasa/builder/training_service.py +3 -3
- rasa/cli/inspect.py +7 -0
- rasa/cli/project_templates/telco/actions/actions_billing.py +6 -5
- rasa/cli/project_templates/telco/actions/actions_get_data_from_db.py +3 -2
- rasa/cli/shell.py +6 -1
- rasa/cli/train.py +4 -0
- rasa/core/tracker_stores/dynamo_tracker_store.py +30 -2
- rasa/model_manager/model_api.py +1 -2
- rasa/shared/core/trackers.py +17 -0
- rasa/shared/importers/utils.py +77 -1
- rasa/studio/upload.py +11 -45
- rasa/utils/json_utils.py +6 -1
- rasa/utils/openapi.py +144 -0
- rasa/utils/plotting.py +1 -1
- rasa/version.py +1 -1
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a16.dist-info}/METADATA +10 -9
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a16.dist-info}/RECORD +30 -27
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a16.dist-info}/NOTICE +0 -0
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a16.dist-info}/WHEEL +0 -0
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a16.dist-info}/entry_points.txt +0 -0
rasa/builder/config.py
CHANGED
|
@@ -43,13 +43,18 @@ def get_default_config(assistant_id: str) -> Dict[str, Any]:
|
|
|
43
43
|
)
|
|
44
44
|
)
|
|
45
45
|
)
|
|
46
|
+
|
|
47
|
+
if not isinstance(base_config, dict):
|
|
48
|
+
raise ValueError("Base config is not a dictionary")
|
|
49
|
+
|
|
46
50
|
base_config["assistant_id"] = assistant_id
|
|
51
|
+
|
|
47
52
|
return base_config
|
|
48
53
|
|
|
49
54
|
|
|
50
55
|
def get_default_endpoints() -> Dict[str, Any]:
|
|
51
56
|
"""Get default endpoints configuration."""
|
|
52
|
-
|
|
57
|
+
endpoints_config = read_yaml_file(
|
|
53
58
|
str(
|
|
54
59
|
importlib_resources.files(PACKAGE_NAME).joinpath(
|
|
55
60
|
"cli/project_templates/default/endpoints.yml"
|
|
@@ -57,6 +62,11 @@ def get_default_endpoints() -> Dict[str, Any]:
|
|
|
57
62
|
)
|
|
58
63
|
)
|
|
59
64
|
|
|
65
|
+
if not isinstance(endpoints_config, dict):
|
|
66
|
+
raise ValueError("Endpoints config is not a dictionary")
|
|
67
|
+
|
|
68
|
+
return endpoints_config
|
|
69
|
+
|
|
60
70
|
|
|
61
71
|
def get_default_credentials() -> Dict[str, Any]:
|
|
62
72
|
"""Get default credentials configuration."""
|
rasa/builder/exceptions.py
CHANGED
|
@@ -29,6 +29,12 @@ class LLMGenerationError(PromptToBotError):
|
|
|
29
29
|
pass
|
|
30
30
|
|
|
31
31
|
|
|
32
|
+
class DocumentRetrievalError(PromptToBotError):
|
|
33
|
+
"""Raised when document retrieval fails."""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
class SchemaValidationError(PromptToBotError):
|
|
33
39
|
"""Raised when schema validation fails."""
|
|
34
40
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"content": {
|
|
6
|
+
"type": "array",
|
|
7
|
+
"items": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"type": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"source": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"properties": {
|
|
16
|
+
"content": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"items": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"type": {
|
|
22
|
+
"type": "string"
|
|
23
|
+
},
|
|
24
|
+
"media_type": {
|
|
25
|
+
"type": "string"
|
|
26
|
+
},
|
|
27
|
+
"text": {
|
|
28
|
+
"type": "string"
|
|
29
|
+
},
|
|
30
|
+
"data": {
|
|
31
|
+
"type": "string"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"type": {
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
"media_type": {
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
"data": {
|
|
43
|
+
"type": "string"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"title": {
|
|
48
|
+
"type": "string"
|
|
49
|
+
},
|
|
50
|
+
"context": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"record_type": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"url": {
|
|
57
|
+
"type": "string"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"required": ["content"]
|
|
64
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
import importlib_resources
|
|
8
|
+
import openai
|
|
9
|
+
import structlog
|
|
10
|
+
from openai.types.chat import ChatCompletion
|
|
11
|
+
|
|
12
|
+
from rasa.builder.exceptions import DocumentRetrievalError
|
|
13
|
+
from rasa.builder.models import Document
|
|
14
|
+
from rasa.constants import PACKAGE_NAME
|
|
15
|
+
from rasa.shared.utils.io import read_json_file
|
|
16
|
+
|
|
17
|
+
INKEEP_API_KEY_ENV_VAR = "INKEEP_API_KEY"
|
|
18
|
+
INKEEP_RAG_RESPONSE_SCHEMA_PATH = str(
|
|
19
|
+
importlib_resources.files(PACKAGE_NAME).joinpath(
|
|
20
|
+
"builder/inkeep-rag-response-schema.json"
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
INKEEP_DOCUMENT_RETRIEVAL_MODEL = "inkeep-rag"
|
|
25
|
+
INKEEP_BASE_URL = "https://api.inkeep.com/v1/"
|
|
26
|
+
|
|
27
|
+
structlogger = structlog.get_logger()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class InKeepDocumentRetrieval:
|
|
31
|
+
"""Handles the document retrieval from InKeep AI."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
api_key: Optional[str] = None,
|
|
36
|
+
):
|
|
37
|
+
self._client: Optional[openai.AsyncOpenAI] = None
|
|
38
|
+
self._rag_schema = read_json_file(INKEEP_RAG_RESPONSE_SCHEMA_PATH)
|
|
39
|
+
self._api_key = api_key or os.getenv(INKEEP_API_KEY_ENV_VAR)
|
|
40
|
+
|
|
41
|
+
async def retrieve_documents(
|
|
42
|
+
self, query: str, temperature: float = 0.0, timeout: float = 30.0
|
|
43
|
+
) -> List[Document]:
|
|
44
|
+
"""Retrieve relevant documents using InKeep AI based on the given query.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
query: The search query
|
|
48
|
+
temperature: Controls randomness in generation (0.0 for deterministic)
|
|
49
|
+
timeout: Timeout for the API call
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
List of Document objects containing retrieved content
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
LLMGenerationError: If the API call fails or returns invalid response
|
|
56
|
+
"""
|
|
57
|
+
response = await self._call_inkeep_rag_api(
|
|
58
|
+
query=query,
|
|
59
|
+
temperature=temperature,
|
|
60
|
+
timeout=timeout,
|
|
61
|
+
)
|
|
62
|
+
documents = self._parse_documents_from_response(response)
|
|
63
|
+
return documents
|
|
64
|
+
|
|
65
|
+
async def _call_inkeep_rag_api(
|
|
66
|
+
self, query: str, temperature: float, timeout: float
|
|
67
|
+
) -> ChatCompletion:
|
|
68
|
+
"""Call InKeep AI RAG's API endpoint and return the response content.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
query: The search query to send to InKeep
|
|
72
|
+
temperature: Controls randomness in generation (0.0 for deterministic)
|
|
73
|
+
timeout: Timeout for the API call
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
The response content from InKeep AI. The response is made of the retrieved
|
|
77
|
+
documents.
|
|
78
|
+
|
|
79
|
+
Raises:
|
|
80
|
+
LLMGenerationError: If the API call fails or returns invalid response
|
|
81
|
+
"""
|
|
82
|
+
request_params = {
|
|
83
|
+
"model": INKEEP_DOCUMENT_RETRIEVAL_MODEL,
|
|
84
|
+
"messages": [{"role": "user", "content": query}],
|
|
85
|
+
"temperature": temperature,
|
|
86
|
+
"timeout": timeout,
|
|
87
|
+
"response_format": {
|
|
88
|
+
"type": "json_schema",
|
|
89
|
+
"json_schema": self._rag_schema,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
try:
|
|
93
|
+
async with self._get_client() as client:
|
|
94
|
+
response = await client.chat.completions.create(**request_params)
|
|
95
|
+
|
|
96
|
+
if not response.choices[0].message.content:
|
|
97
|
+
structlogger.warning(
|
|
98
|
+
"inkeep_document_retrieval.empty_response",
|
|
99
|
+
event_info="InKeep AI returned an empty response. ",
|
|
100
|
+
request_params=request_params,
|
|
101
|
+
response_content=response.choices[0].message.content,
|
|
102
|
+
)
|
|
103
|
+
raise DocumentRetrievalError(
|
|
104
|
+
"InKeep Document Retrieval: Empty response"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return response
|
|
108
|
+
|
|
109
|
+
except openai.OpenAIError as e:
|
|
110
|
+
structlogger.error(
|
|
111
|
+
"inkeep_document_retrieval.api_error",
|
|
112
|
+
event_info="InKeep Document Retrieval: API error",
|
|
113
|
+
request_params=request_params,
|
|
114
|
+
error=e,
|
|
115
|
+
)
|
|
116
|
+
raise DocumentRetrievalError(f"InKeep Document Retrieval: API error: {e}")
|
|
117
|
+
except asyncio.TimeoutError as e:
|
|
118
|
+
structlogger.error(
|
|
119
|
+
"inkeep_document_retrieval.timeout_error",
|
|
120
|
+
event_info="InKeep Document Retrieval: Timeout error",
|
|
121
|
+
request_params=request_params,
|
|
122
|
+
error=e,
|
|
123
|
+
)
|
|
124
|
+
raise DocumentRetrievalError(f"InKeep AI request timed out: {e}")
|
|
125
|
+
except Exception as e:
|
|
126
|
+
structlogger.error(
|
|
127
|
+
"inkeep_document_retrieval.error",
|
|
128
|
+
event_info="InKeep Document Retrieval: Error",
|
|
129
|
+
request_params=request_params,
|
|
130
|
+
error=e,
|
|
131
|
+
)
|
|
132
|
+
raise DocumentRetrievalError(
|
|
133
|
+
f"InKeep Document Retrieval: Unexpected error: {e}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@asynccontextmanager
|
|
137
|
+
async def _get_client(self):
|
|
138
|
+
"""Get or create client that handles the API calls to InKeep AI."""
|
|
139
|
+
if self._client is None:
|
|
140
|
+
self._client = openai.AsyncOpenAI(
|
|
141
|
+
api_key=self._api_key,
|
|
142
|
+
base_url=INKEEP_BASE_URL,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
yield self._client
|
|
147
|
+
except Exception as e:
|
|
148
|
+
structlogger.error(
|
|
149
|
+
"inkeep_document_retrieval.client_error",
|
|
150
|
+
event_info="InKeep Document Retrieval: Client error",
|
|
151
|
+
error=str(e),
|
|
152
|
+
)
|
|
153
|
+
raise
|
|
154
|
+
|
|
155
|
+
def _parse_documents_from_response(
|
|
156
|
+
self, response: ChatCompletion
|
|
157
|
+
) -> List[Document]:
|
|
158
|
+
"""Parse the InKeep AI response into Document objects.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
response: ChatCompletion response from InKeep AI's RAG model.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of Document objects
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
content = response.choices[0].message.content
|
|
168
|
+
if not content:
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
response_data = json.loads(content)
|
|
172
|
+
documents = []
|
|
173
|
+
|
|
174
|
+
for item in response_data.get("content", []):
|
|
175
|
+
try:
|
|
176
|
+
document = Document.from_inkeep_rag_response(item)
|
|
177
|
+
documents.append(document)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
structlogger.warning(
|
|
180
|
+
"inkeep_document_retrieval.invalid_document_skipped",
|
|
181
|
+
event_info=(
|
|
182
|
+
"InKeep Document Retrieval: Invalid document structure "
|
|
183
|
+
"skipped. Returning empty list for this item."
|
|
184
|
+
),
|
|
185
|
+
error=str(e),
|
|
186
|
+
item=item,
|
|
187
|
+
)
|
|
188
|
+
# Continue processing other items, skip this invalid one
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
return documents
|
|
192
|
+
|
|
193
|
+
except json.JSONDecodeError as e:
|
|
194
|
+
structlogger.warning(
|
|
195
|
+
"inkeep_document_retrieval.parse_response_failed",
|
|
196
|
+
event_info=(
|
|
197
|
+
"InKeep Document Retrieval: Parse response failed. "
|
|
198
|
+
"Returning empty list.",
|
|
199
|
+
),
|
|
200
|
+
error=str(e),
|
|
201
|
+
)
|
|
202
|
+
return []
|
|
203
|
+
except Exception as e:
|
|
204
|
+
structlogger.error(
|
|
205
|
+
"inkeep_document_retrieval.parse_response_error",
|
|
206
|
+
event_info=(
|
|
207
|
+
"InKeep Document Retrieval: Parse response error. "
|
|
208
|
+
"Returning empty list.",
|
|
209
|
+
),
|
|
210
|
+
error=str(e),
|
|
211
|
+
)
|
|
212
|
+
return []
|
rasa/builder/llm_service.py
CHANGED
|
@@ -5,17 +5,19 @@ import importlib
|
|
|
5
5
|
import json
|
|
6
6
|
from contextlib import asynccontextmanager
|
|
7
7
|
from copy import deepcopy
|
|
8
|
-
from typing import Any, Dict, List, Optional
|
|
8
|
+
from typing import Any, AsyncGenerator, Dict, List, Optional
|
|
9
9
|
|
|
10
10
|
import importlib_resources
|
|
11
11
|
import openai
|
|
12
12
|
import structlog
|
|
13
13
|
from jinja2 import Template
|
|
14
|
+
from pydantic import ValidationError
|
|
14
15
|
|
|
15
16
|
from rasa.builder import config
|
|
16
17
|
from rasa.builder.exceptions import LLMGenerationError
|
|
18
|
+
from rasa.builder.inkeep_document_retrieval import InKeepDocumentRetrieval
|
|
17
19
|
from rasa.builder.llm_context import tracker_as_llm_context
|
|
18
|
-
from rasa.builder.models import LLMBuilderContext
|
|
20
|
+
from rasa.builder.models import Document, LLMBuilderContext, LLMHelperResponse
|
|
19
21
|
from rasa.constants import PACKAGE_NAME
|
|
20
22
|
from rasa.shared.constants import DOMAIN_SCHEMA_FILE, RESPONSES_SCHEMA_FILE
|
|
21
23
|
from rasa.shared.core.flows.yaml_flows_io import FLOWS_SCHEMA_FILE
|
|
@@ -28,14 +30,14 @@ structlogger = structlog.get_logger()
|
|
|
28
30
|
class LLMService:
|
|
29
31
|
"""Handles OpenAI LLM interactions with caching for efficiency."""
|
|
30
32
|
|
|
31
|
-
def __init__(self):
|
|
33
|
+
def __init__(self) -> None:
|
|
32
34
|
self._client: Optional[openai.AsyncOpenAI] = None
|
|
33
35
|
self._domain_schema: Optional[Dict[str, Any]] = None
|
|
34
36
|
self._flows_schema: Optional[Dict[str, Any]] = None
|
|
35
37
|
self._helper_schema: Optional[Dict[str, Any]] = None
|
|
36
38
|
|
|
37
39
|
@asynccontextmanager
|
|
38
|
-
async def _get_client(self):
|
|
40
|
+
async def _get_client(self) -> AsyncGenerator[openai.AsyncOpenAI, None]:
|
|
39
41
|
"""Get or create OpenAI client with proper resource management."""
|
|
40
42
|
if self._client is None:
|
|
41
43
|
self._client = openai.AsyncOpenAI(timeout=config.OPENAI_TIMEOUT)
|
|
@@ -46,7 +48,7 @@ class LLMService:
|
|
|
46
48
|
structlogger.error("llm.client_error", error=str(e))
|
|
47
49
|
raise
|
|
48
50
|
|
|
49
|
-
def _prepare_schemas(self):
|
|
51
|
+
def _prepare_schemas(self) -> None:
|
|
50
52
|
"""Prepare and cache schemas for LLM generation."""
|
|
51
53
|
if self._domain_schema is None:
|
|
52
54
|
self._domain_schema = _prepare_domain_schema()
|
|
@@ -135,7 +137,7 @@ class LLMService:
|
|
|
135
137
|
|
|
136
138
|
async def generate_helper_response(
|
|
137
139
|
self, messages: List[Dict[str, Any]]
|
|
138
|
-
) ->
|
|
140
|
+
) -> LLMHelperResponse:
|
|
139
141
|
"""Generate helper response using OpenAI."""
|
|
140
142
|
self._prepare_schemas()
|
|
141
143
|
|
|
@@ -158,9 +160,11 @@ class LLMService:
|
|
|
158
160
|
raise LLMGenerationError("Empty response from LLM helper")
|
|
159
161
|
|
|
160
162
|
try:
|
|
161
|
-
return json.loads(content)
|
|
163
|
+
return LLMHelperResponse.model_validate_json(json.loads(content))
|
|
162
164
|
except json.JSONDecodeError as e:
|
|
163
165
|
raise LLMGenerationError(f"Invalid JSON from LLM helper: {e}")
|
|
166
|
+
except ValidationError as e:
|
|
167
|
+
raise LLMGenerationError(f"Invalid JSON from LLM helper: {e}")
|
|
164
168
|
|
|
165
169
|
except openai.OpenAIError as e:
|
|
166
170
|
raise LLMGenerationError(f"OpenAI API error in helper: {e}")
|
|
@@ -169,30 +173,14 @@ class LLMService:
|
|
|
169
173
|
|
|
170
174
|
async def search_documentation(
|
|
171
175
|
self, query: str, max_results: Optional[int] = None
|
|
172
|
-
) -> List[
|
|
176
|
+
) -> List[Document]:
|
|
173
177
|
"""Search documentation using OpenAI vector store."""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
async with self._get_client() as client:
|
|
179
|
-
results = await client.vector_stores.search(
|
|
180
|
-
vector_store_id=config.OPENAI_VECTOR_STORE_ID,
|
|
181
|
-
query=query,
|
|
182
|
-
max_num_results=max_results,
|
|
183
|
-
rewrite_query=True,
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
return results.data
|
|
187
|
-
|
|
188
|
-
except openai.OpenAIError as e:
|
|
189
|
-
structlogger.warning(
|
|
190
|
-
"llm.documentation_search_failed", error=str(e), query=query
|
|
191
|
-
)
|
|
192
|
-
return []
|
|
178
|
+
inkeep_document_retrieval = InKeepDocumentRetrieval()
|
|
179
|
+
documents = await inkeep_document_retrieval.retrieve_documents(query)
|
|
180
|
+
return documents
|
|
193
181
|
|
|
194
182
|
@staticmethod
|
|
195
|
-
def _format_chat_dump(messages) -> str:
|
|
183
|
+
def _format_chat_dump(messages: List[Dict[str, Any]]) -> str:
|
|
196
184
|
"""Format chat messages for documentation search."""
|
|
197
185
|
result = ""
|
|
198
186
|
for message in messages:
|
|
@@ -213,16 +201,15 @@ class LLMService:
|
|
|
213
201
|
return result
|
|
214
202
|
|
|
215
203
|
@staticmethod
|
|
216
|
-
def _format_documentation_results(results) -> str:
|
|
204
|
+
def _format_documentation_results(results: List[Document]) -> str:
|
|
217
205
|
"""Format documentation search results."""
|
|
218
206
|
if not results:
|
|
219
207
|
return "<sources>No relevant documentation found.</sources>"
|
|
220
208
|
|
|
221
209
|
formatted_results = ""
|
|
222
210
|
for result in results:
|
|
223
|
-
formatted_result = f"<result url='{result.
|
|
224
|
-
|
|
225
|
-
formatted_result += f"<content>{part.text}</content>"
|
|
211
|
+
formatted_result = f"<result url='{result.url}'>"
|
|
212
|
+
formatted_result += f"<content>{result.content}</content>"
|
|
226
213
|
formatted_results += formatted_result + "</result>"
|
|
227
214
|
|
|
228
215
|
return f"<sources>{formatted_results}</sources>"
|
|
@@ -233,6 +220,9 @@ def _prepare_domain_schema() -> Dict[str, Any]:
|
|
|
233
220
|
"""Prepare domain schema by removing unnecessary parts."""
|
|
234
221
|
domain_schema = deepcopy(read_schema_file(DOMAIN_SCHEMA_FILE, PACKAGE_NAME, False))
|
|
235
222
|
|
|
223
|
+
if not isinstance(domain_schema, dict):
|
|
224
|
+
raise ValueError("Domain schema is not a dictionary")
|
|
225
|
+
|
|
236
226
|
# Remove parts not needed for CALM bots
|
|
237
227
|
unnecessary_keys = ["intents", "entities", "forms", "config", "session_config"]
|
|
238
228
|
|
rasa/builder/main.py
CHANGED
|
@@ -5,13 +5,23 @@ import logging
|
|
|
5
5
|
import sys
|
|
6
6
|
from typing import Optional
|
|
7
7
|
|
|
8
|
+
import structlog
|
|
9
|
+
from sanic import HTTPResponse, Sanic
|
|
10
|
+
from sanic.request import Request
|
|
11
|
+
from sanic_openapi import openapi3_blueprint
|
|
12
|
+
|
|
8
13
|
import rasa.core.utils
|
|
14
|
+
from rasa.builder import config
|
|
9
15
|
from rasa.builder.logging_utils import collecting_logs_processor
|
|
10
|
-
from rasa.builder.service import
|
|
16
|
+
from rasa.builder.service import bp, setup_project_generator
|
|
17
|
+
from rasa.core.channels.studio_chat import StudioChatInput
|
|
18
|
+
from rasa.server import configure_cors
|
|
11
19
|
from rasa.utils.common import configure_logging_and_warnings
|
|
12
20
|
from rasa.utils.log_utils import configure_structlog
|
|
13
21
|
from rasa.utils.sanic_error_handler import register_custom_sanic_error_handler
|
|
14
22
|
|
|
23
|
+
structlogger = structlog.get_logger()
|
|
24
|
+
|
|
15
25
|
|
|
16
26
|
def setup_logging() -> None:
|
|
17
27
|
"""Setup logging configuration."""
|
|
@@ -31,22 +41,98 @@ def setup_logging() -> None:
|
|
|
31
41
|
)
|
|
32
42
|
|
|
33
43
|
|
|
44
|
+
def setup_input_channel() -> StudioChatInput:
|
|
45
|
+
"""Setup the input channel for chat interactions."""
|
|
46
|
+
studio_chat_credentials = config.get_default_credentials().get(
|
|
47
|
+
StudioChatInput.name()
|
|
48
|
+
)
|
|
49
|
+
return StudioChatInput.from_credentials(credentials=studio_chat_credentials)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def setup_middleware(app: Sanic) -> None:
|
|
53
|
+
"""Setup middleware for request/response processing."""
|
|
54
|
+
|
|
55
|
+
@app.middleware("request")
|
|
56
|
+
async def log_request(request: Request) -> None:
|
|
57
|
+
structlogger.info(
|
|
58
|
+
"request.received",
|
|
59
|
+
method=request.method,
|
|
60
|
+
path=request.path,
|
|
61
|
+
remote_addr=request.remote_addr or "unknown",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@app.middleware("response")
|
|
65
|
+
async def log_response(request: Request, response: HTTPResponse) -> None:
|
|
66
|
+
structlogger.info(
|
|
67
|
+
"request.completed",
|
|
68
|
+
method=request.method,
|
|
69
|
+
path=request.path,
|
|
70
|
+
status=response.status,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_app(project_folder: Optional[str] = None) -> Sanic:
|
|
75
|
+
"""Create and configure the Sanic app."""
|
|
76
|
+
app = Sanic("BotBuilderService")
|
|
77
|
+
|
|
78
|
+
# Basic app configuration
|
|
79
|
+
app.config.REQUEST_TIMEOUT = 60 # 1 minute timeout
|
|
80
|
+
app.ctx.agent = None
|
|
81
|
+
|
|
82
|
+
# Set up project generator and store in app context
|
|
83
|
+
app.ctx.project_generator = setup_project_generator(project_folder)
|
|
84
|
+
|
|
85
|
+
# Set up input channel and store in app context
|
|
86
|
+
app.ctx.input_channel = setup_input_channel()
|
|
87
|
+
|
|
88
|
+
# Register the blueprint
|
|
89
|
+
app.blueprint(bp)
|
|
90
|
+
|
|
91
|
+
# OpenAPI docs
|
|
92
|
+
app.blueprint(openapi3_blueprint)
|
|
93
|
+
app.config.API_TITLE = "Bot Builder API"
|
|
94
|
+
app.config.API_VERSION = rasa.__version__
|
|
95
|
+
app.config.API_DESCRIPTION = (
|
|
96
|
+
"API for building conversational AI bots from prompts and templates. "
|
|
97
|
+
"The API allows to change the assistant and retrain it with new data."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Setup middleware
|
|
101
|
+
setup_middleware(app)
|
|
102
|
+
|
|
103
|
+
configure_cors(app, cors_origins=config.CORS_ORIGINS)
|
|
104
|
+
|
|
105
|
+
# Register input channel webhooks
|
|
106
|
+
from rasa.core import channels
|
|
107
|
+
|
|
108
|
+
channels.channel.register([app.ctx.input_channel], app, route="/webhooks/")
|
|
109
|
+
|
|
110
|
+
return app
|
|
111
|
+
|
|
112
|
+
|
|
34
113
|
def main(project_folder: Optional[str] = None) -> None:
|
|
35
114
|
"""Main entry point."""
|
|
36
115
|
try:
|
|
37
116
|
# Setup logging
|
|
38
117
|
setup_logging()
|
|
39
118
|
|
|
40
|
-
# Create and configure
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
register_custom_sanic_error_handler(service.app)
|
|
44
|
-
|
|
45
|
-
# Log available routes
|
|
46
|
-
rasa.core.utils.list_routes(service.app)
|
|
119
|
+
# Create and configure app
|
|
120
|
+
app = create_app(project_folder)
|
|
121
|
+
register_custom_sanic_error_handler(app)
|
|
47
122
|
|
|
48
123
|
# Run the service
|
|
49
|
-
|
|
124
|
+
structlogger.info(
|
|
125
|
+
"service.starting",
|
|
126
|
+
host=config.BUILDER_SERVER_HOST,
|
|
127
|
+
port=config.BUILDER_SERVER_PORT,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
app.run(
|
|
131
|
+
host=config.BUILDER_SERVER_HOST,
|
|
132
|
+
port=config.BUILDER_SERVER_PORT,
|
|
133
|
+
legacy=True,
|
|
134
|
+
motd=False,
|
|
135
|
+
)
|
|
50
136
|
|
|
51
137
|
except KeyboardInterrupt:
|
|
52
138
|
print("\nService stopped by user")
|
rasa/builder/models.py
CHANGED
|
@@ -19,7 +19,7 @@ class PromptRequest(BaseModel):
|
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
@validator("prompt")
|
|
22
|
-
def validate_prompt(cls, v):
|
|
22
|
+
def validate_prompt(cls, v: str) -> str:
|
|
23
23
|
if not v.strip():
|
|
24
24
|
raise ValueError("Prompt cannot be empty or whitespace only")
|
|
25
25
|
return v.strip()
|
|
@@ -39,7 +39,7 @@ class TemplateRequest(BaseModel):
|
|
|
39
39
|
)
|
|
40
40
|
|
|
41
41
|
@validator("template_name")
|
|
42
|
-
def validate_template_name(cls, v):
|
|
42
|
+
def validate_template_name(cls, v: Any) -> Any:
|
|
43
43
|
if v not in ProjectTemplateName:
|
|
44
44
|
raise ValueError(
|
|
45
45
|
f"Template name must be one of {ProjectTemplateName.supported_values()}"
|
|
@@ -129,14 +129,6 @@ class LLMHelperResponse(BaseModel):
|
|
|
129
129
|
content_blocks: List[Union[TextBlock, CodeBlock, FileBlock, LinkBlock]] = Field(...)
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
class ApiResponse(BaseModel):
|
|
133
|
-
"""Standard API response model."""
|
|
134
|
-
|
|
135
|
-
status: str = Field(...)
|
|
136
|
-
message: Optional[str] = Field(None)
|
|
137
|
-
data: Optional[Dict[str, Any]] = Field(None)
|
|
138
|
-
|
|
139
|
-
|
|
140
132
|
class ApiErrorResponse(BaseModel):
|
|
141
133
|
"""API error response model."""
|
|
142
134
|
|
|
@@ -172,3 +164,62 @@ class TrainingResult(BaseModel):
|
|
|
172
164
|
success: bool = Field(...)
|
|
173
165
|
model_path: Optional[str] = Field(None)
|
|
174
166
|
error: Optional[str] = Field(None)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
BotFiles = Dict[str, Optional[str]]
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Document(BaseModel):
|
|
173
|
+
"""Model for document retrieval results."""
|
|
174
|
+
|
|
175
|
+
content: str = Field(...)
|
|
176
|
+
url: Optional[str] = Field(None)
|
|
177
|
+
title: Optional[str] = Field(None)
|
|
178
|
+
metadata: Optional[Dict[str, Any]] = Field(None)
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def from_inkeep_rag_response(cls, rag_item: Dict[str, Any]) -> "Document":
|
|
182
|
+
"""Create a Document object from a single InKeep RAG response item.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
rag_item: Single item from InKeep RAG response
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Document object with extracted content and metadata
|
|
189
|
+
"""
|
|
190
|
+
source = rag_item.get("source", {})
|
|
191
|
+
text_content = cls._extract_text_from_source(source)
|
|
192
|
+
|
|
193
|
+
return cls(
|
|
194
|
+
content=text_content.strip() if text_content else "",
|
|
195
|
+
url=rag_item.get("url"),
|
|
196
|
+
title=rag_item.get("title"),
|
|
197
|
+
metadata={
|
|
198
|
+
"type": rag_item.get("type"),
|
|
199
|
+
"record_type": rag_item.get("record_type"),
|
|
200
|
+
"context": rag_item.get("context"),
|
|
201
|
+
"media_type": source.get("media_type"),
|
|
202
|
+
},
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _extract_text_from_source(source: Dict[str, Any]) -> str:
|
|
207
|
+
"""Extract text content from InKeep source object.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
source: Source object from InKeep RAG response
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Extracted text content
|
|
214
|
+
"""
|
|
215
|
+
# Try to extract from content array first
|
|
216
|
+
if "content" in source:
|
|
217
|
+
text_parts = []
|
|
218
|
+
for content_item in source["content"]:
|
|
219
|
+
if content_item.get("type") == "text" and content_item.get("text"):
|
|
220
|
+
text_parts.append(content_item["text"])
|
|
221
|
+
if text_parts:
|
|
222
|
+
return "\n".join(text_parts)
|
|
223
|
+
|
|
224
|
+
# Fallback to source data
|
|
225
|
+
return source.get("data", "")
|