rasa-pro 3.14.0a1__py3-none-any.whl → 3.14.0a2__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/copilot/copilot.py +1 -23
- rasa/builder/download.py +1 -6
- rasa/builder/main.py +43 -0
- rasa/builder/project_generator.py +17 -128
- rasa/builder/template_cache.py +244 -0
- rasa/cli/project_templates/basic/config.yml +3 -4
- rasa/cli/project_templates/basic/endpoints.yml +2 -12
- rasa/cli/project_templates/default/config.yml +3 -3
- rasa/cli/project_templates/default/endpoints.yml +1 -12
- rasa/cli/project_templates/finance/config.yml +3 -3
- rasa/cli/project_templates/finance/endpoints.yml +2 -12
- rasa/cli/project_templates/telco/config.yml +3 -3
- rasa/cli/project_templates/telco/endpoints.yml +2 -12
- rasa/core/channels/inspector/dist/assets/{arc-18042c22.js → arc-c24d8d79.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{blockDiagram-38ab4fdb-fdd6bcfa.js → blockDiagram-38ab4fdb-1b6b9f26.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{c4Diagram-3d4e48cf-f5ae6786.js → c4Diagram-3d4e48cf-da91d0f9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/channel-d2444dfd.js +1 -0
- rasa/core/channels/inspector/dist/assets/{classDiagram-70f12bd4-81efba3e.js → classDiagram-70f12bd4-6067f302.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{classDiagram-v2-f2320105-3b6b6a92.js → classDiagram-v2-f2320105-705d024a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/clone-281a0990.js +1 -0
- rasa/core/channels/inspector/dist/assets/{createText-2e5e7dd3-31422447.js → createText-2e5e7dd3-3751dffe.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{edges-e0da2a9e-518a90db.js → edges-e0da2a9e-7b25b4af.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{erDiagram-9861fffd-a6d3c25a.js → erDiagram-9861fffd-eb7deea8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDb-956e92f1-e048c2be.js → flowDb-956e92f1-67235ff6.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{flowDiagram-66a62f08-c7474c91.js → flowDiagram-66a62f08-34c3a16a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-aa4cca3b.js +1 -0
- rasa/core/channels/inspector/dist/assets/{flowchart-elk-definition-4a651766-cb4d8723.js → flowchart-elk-definition-4a651766-f1a93631.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{ganttDiagram-c361ad54-346636a2.js → ganttDiagram-c361ad54-a68cbad1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{gitGraphDiagram-72cf32ee-7c508874.js → gitGraphDiagram-72cf32ee-0b1e4a1d.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{graph-14702d8a.js → graph-f3c1d212.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{index-4d4bdf3a.js → index-051c5a6e.js} +131 -131
- rasa/core/channels/inspector/dist/assets/{index-3862675e-f18b534b.js → index-3862675e-34cbca30.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{infoDiagram-f8f76790-64154b83.js → infoDiagram-f8f76790-e69960a1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{journeyDiagram-49397b02-833a5f95.js → journeyDiagram-49397b02-8dd3296a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{layout-5a3b2123.js → layout-e93126bc.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{line-2272a8c7.js → line-15eb1e26.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{linear-35bcf273.js → linear-fec95d33.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{mindmap-definition-fc14e90a-92dcb0e9.js → mindmap-definition-fc14e90a-2557813e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{pieDiagram-8a3498a8-94dbc900.js → pieDiagram-8a3498a8-40d756b1.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{quadrantDiagram-120e2f19-8b7a9c33.js → quadrantDiagram-120e2f19-a48cbdcd.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{requirementDiagram-deff3bca-6f7eab81.js → requirementDiagram-deff3bca-dc778150.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sankeyDiagram-04a897e0-f43e581d.js → sankeyDiagram-04a897e0-10026b94.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{sequenceDiagram-704730f1-0bcbefc3.js → sequenceDiagram-704730f1-3b2ed10a.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-587899a1-b8a74083.js → stateDiagram-587899a1-c5f3b3fb.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{stateDiagram-v2-d93cdb3a-2070218f.js → stateDiagram-v2-d93cdb3a-e503656b.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-6aaf32cf-f1d54e34.js → styles-6aaf32cf-a683ce56.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-9a916d00-980de489.js → styles-9a916d00-02bcdcee.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{styles-c10674c1-3c03abde.js → styles-c10674c1-8e90dbb9.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{svgDrawCommon-08f97a94-46ba068f.js → svgDrawCommon-08f97a94-7c23fc1e.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{timeline-definition-85554ec2-901f5e3d.js → timeline-definition-85554ec2-c42faec8.js} +1 -1
- rasa/core/channels/inspector/dist/assets/{xychartDiagram-e933f94c-acbc628a.js → xychartDiagram-e933f94c-5e3bb0ea.js} +1 -1
- rasa/core/channels/inspector/dist/index.html +1 -1
- rasa/core/channels/inspector/src/App.tsx +5 -24
- rasa/core/channels/inspector/src/components/Chat.tsx +2 -3
- rasa/engine/graph.py +5 -1
- rasa/engine/storage/local_model_storage.py +41 -4
- rasa/version.py +1 -1
- {rasa_pro-3.14.0a1.dist-info → rasa_pro-3.14.0a2.dist-info}/METADATA +4 -4
- {rasa_pro-3.14.0a1.dist-info → rasa_pro-3.14.0a2.dist-info}/RECORD +62 -61
- rasa/core/channels/inspector/dist/assets/channel-b9b536fc.js +0 -1
- rasa/core/channels/inspector/dist/assets/clone-78d2ddcf.js +0 -1
- rasa/core/channels/inspector/dist/assets/flowDiagram-v2-96b9c2cf-8b09c060.js +0 -1
- {rasa_pro-3.14.0a1.dist-info → rasa_pro-3.14.0a2.dist-info}/NOTICE +0 -0
- {rasa_pro-3.14.0a1.dist-info → rasa_pro-3.14.0a2.dist-info}/WHEEL +0 -0
- {rasa_pro-3.14.0a1.dist-info → rasa_pro-3.14.0a2.dist-info}/entry_points.txt +0 -0
rasa/builder/copilot/copilot.py
CHANGED
|
@@ -55,29 +55,7 @@ class Copilot:
|
|
|
55
55
|
@asynccontextmanager
|
|
56
56
|
async def _get_client(self) -> AsyncGenerator[openai.AsyncOpenAI, None]:
|
|
57
57
|
"""Create a fresh OpenAI client, yield it, and always close it."""
|
|
58
|
-
|
|
59
|
-
if config.HELLO_LLM_PROXY_BASE_URL:
|
|
60
|
-
structlogger.debug(
|
|
61
|
-
"copilot.using_llm_proxy", base_url=config.HELLO_LLM_PROXY_BASE_URL
|
|
62
|
-
)
|
|
63
|
-
if not config.RASA_PRO_LICENSE:
|
|
64
|
-
structlogger.error(
|
|
65
|
-
"copilot.proxy_missing_license",
|
|
66
|
-
event_info=(
|
|
67
|
-
"HELLO_LLM_PROXY_BASE_URL is set "
|
|
68
|
-
"but RASA_PRO_LICENSE is missing."
|
|
69
|
-
),
|
|
70
|
-
)
|
|
71
|
-
raise CopilotStreamError(
|
|
72
|
-
"HELLO_LLM_PROXY_BASE_URL is set but RASA_PRO_LICENSE is missing. "
|
|
73
|
-
"Provide a valid license token for proxy authentication."
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
kwargs["base_url"] = config.HELLO_LLM_PROXY_BASE_URL
|
|
77
|
-
kwargs["api_key"] = config.RASA_PRO_LICENSE
|
|
78
|
-
|
|
79
|
-
client = openai.AsyncOpenAI(**kwargs)
|
|
80
|
-
|
|
58
|
+
client = openai.AsyncOpenAI(timeout=config.OPENAI_TIMEOUT)
|
|
81
59
|
try:
|
|
82
60
|
yield client
|
|
83
61
|
except Exception as e:
|
rasa/builder/download.py
CHANGED
|
@@ -20,11 +20,6 @@ def _get_python_version_content() -> str:
|
|
|
20
20
|
return f"{sys.version_info.major}.{sys.version_info.minor}\n"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def _get_rasa_pro_minor_version() -> str:
|
|
24
|
-
"""Get the minor version of Rasa Pro."""
|
|
25
|
-
return ".".join(rasa.__version__.split(".")[:2])
|
|
26
|
-
|
|
27
|
-
|
|
28
23
|
def _get_pyproject_toml_content(project_id: str) -> str:
|
|
29
24
|
"""Generate pyproject.toml file content."""
|
|
30
25
|
return dedent(
|
|
@@ -34,7 +29,7 @@ def _get_pyproject_toml_content(project_id: str) -> str:
|
|
|
34
29
|
version = "0.1.0"
|
|
35
30
|
description = "Add your description for your Rasa bot here"
|
|
36
31
|
readme = "README.md"
|
|
37
|
-
dependencies = ["rasa-pro>={
|
|
32
|
+
dependencies = ["rasa-pro>={rasa.__version__}"]
|
|
38
33
|
requires-python = ">={sys.version_info.major}.{sys.version_info.minor}"
|
|
39
34
|
"""
|
|
40
35
|
)
|
rasa/builder/main.py
CHANGED
|
@@ -22,6 +22,9 @@ from rasa.builder.logging_utils import (
|
|
|
22
22
|
log_request_start,
|
|
23
23
|
)
|
|
24
24
|
from rasa.builder.service import bp, setup_project_generator
|
|
25
|
+
from rasa.builder.template_cache import (
|
|
26
|
+
background_download_template_caches,
|
|
27
|
+
)
|
|
25
28
|
from rasa.builder.training_service import try_load_existing_agent
|
|
26
29
|
from rasa.core.channels.studio_chat import StudioChatInput
|
|
27
30
|
from rasa.server import configure_cors
|
|
@@ -146,9 +149,46 @@ def create_app(project_folder: str) -> Sanic:
|
|
|
146
149
|
except Exception as e:
|
|
147
150
|
structlogger.warning("Failed to load agent on server startup", error=str(e))
|
|
148
151
|
|
|
152
|
+
if config.HELLO_RASA_PROJECT_ID and app.ctx.project_generator.is_empty():
|
|
153
|
+
app.register_listener(background_download_template_caches, "after_server_start")
|
|
154
|
+
else:
|
|
155
|
+
structlogger.debug(
|
|
156
|
+
"builder.main.background_cache_download.disabled",
|
|
157
|
+
event_info=(
|
|
158
|
+
"No hello rasa project id set; skipping background cache download"
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
149
162
|
return app
|
|
150
163
|
|
|
151
164
|
|
|
165
|
+
def _apply_llm_overrides_from_builder_env() -> None:
|
|
166
|
+
# Prefer a dedicated builder key, fall back to license if you proxy with it
|
|
167
|
+
if not config.HELLO_LLM_PROXY_BASE_URL:
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
structlogger.debug(
|
|
171
|
+
"builder.main.using_llm_proxy", base_url=config.HELLO_LLM_PROXY_BASE_URL
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if not config.RASA_PRO_LICENSE:
|
|
175
|
+
structlogger.error(
|
|
176
|
+
"copilot.proxy_missing_license",
|
|
177
|
+
event_info=(
|
|
178
|
+
"HELLO_LLM_PROXY_BASE_URL is set but RASA_PRO_LICENSE is missing."
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
if not os.getenv("OPENAI_API_BASE") and not os.getenv("OPENAI_API_KEY"):
|
|
184
|
+
base_url = config.HELLO_LLM_PROXY_BASE_URL.rstrip("/")
|
|
185
|
+
# needed for litellm client
|
|
186
|
+
os.environ["OPENAI_API_BASE"] = base_url
|
|
187
|
+
# needed for openai async client
|
|
188
|
+
os.environ["OPENAI_BASE_URL"] = base_url
|
|
189
|
+
os.environ["OPENAI_API_KEY"] = config.RASA_PRO_LICENSE
|
|
190
|
+
|
|
191
|
+
|
|
152
192
|
def main(project_folder: Optional[str] = None) -> None:
|
|
153
193
|
"""Main entry point."""
|
|
154
194
|
try:
|
|
@@ -159,6 +199,9 @@ def main(project_folder: Optional[str] = None) -> None:
|
|
|
159
199
|
rasa.telemetry.initialize_telemetry()
|
|
160
200
|
rasa.telemetry.initialize_error_reporting(private_mode=False)
|
|
161
201
|
|
|
202
|
+
# TODO: don't do this when running locally
|
|
203
|
+
_apply_llm_overrides_from_builder_env()
|
|
204
|
+
|
|
162
205
|
# working directory needs to be the project folder, e.g.
|
|
163
206
|
# for relative paths (./docs) in a projects config to work
|
|
164
207
|
if not project_folder:
|
|
@@ -3,23 +3,18 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import shutil
|
|
6
|
-
import tarfile
|
|
7
|
-
import tempfile
|
|
8
6
|
from pathlib import Path
|
|
9
7
|
from textwrap import dedent
|
|
10
|
-
from typing import Any, Dict,
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
11
9
|
|
|
12
|
-
import aiofiles
|
|
13
|
-
import aiohttp
|
|
14
10
|
import structlog
|
|
15
11
|
|
|
16
|
-
import rasa.version
|
|
17
12
|
from rasa.builder import config
|
|
18
13
|
from rasa.builder.exceptions import ProjectGenerationError, ValidationError
|
|
19
14
|
from rasa.builder.llm_service import get_skill_generation_messages, llm_service
|
|
20
|
-
from rasa.builder.logging_utils import capture_exception_with_context
|
|
21
15
|
from rasa.builder.models import BotFiles
|
|
22
16
|
from rasa.builder.project_info import ProjectInfo, ensure_first_used, load_project_info
|
|
17
|
+
from rasa.builder.template_cache import copy_cache_for_template_if_available
|
|
23
18
|
from rasa.builder.training_service import TrainingInput
|
|
24
19
|
from rasa.builder.validation_service import validate_project
|
|
25
20
|
from rasa.cli.scaffold import ProjectTemplateName, create_initial_project
|
|
@@ -48,12 +43,25 @@ class ProjectGenerator:
|
|
|
48
43
|
"""Get the project info."""
|
|
49
44
|
return load_project_info(self.project_folder)
|
|
50
45
|
|
|
46
|
+
def is_empty(self) -> bool:
|
|
47
|
+
"""Check if the project folder is empty.
|
|
48
|
+
|
|
49
|
+
Excluding hidden paths.
|
|
50
|
+
"""
|
|
51
|
+
return not any(
|
|
52
|
+
file.is_file()
|
|
53
|
+
for file in self.project_folder.iterdir()
|
|
54
|
+
if not file.name.startswith(".")
|
|
55
|
+
)
|
|
56
|
+
|
|
51
57
|
async def init_from_template(self, template: ProjectTemplateName) -> None:
|
|
52
58
|
"""Create the initial project files."""
|
|
53
59
|
self.cleanup()
|
|
54
60
|
create_initial_project(self.project_folder.as_posix(), template)
|
|
55
|
-
|
|
56
|
-
#
|
|
61
|
+
# If a local cache for this template exists, copy it into the project.
|
|
62
|
+
# We no longer download here to avoid blocking project creation.
|
|
63
|
+
copy_cache_for_template_if_available(template, self.project_folder)
|
|
64
|
+
# needs to happen after caching, as we download/copy .rasa and that would
|
|
57
65
|
# overwrite the project info file in .rasa
|
|
58
66
|
ensure_first_used(self.project_folder)
|
|
59
67
|
|
|
@@ -341,122 +349,3 @@ class ProjectGenerator:
|
|
|
341
349
|
error=str(e),
|
|
342
350
|
file_path=file_path,
|
|
343
351
|
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
CACHE_BUCKET_URL = "https://trained-templates.s3.us-east-1.amazonaws.com"
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
def _safe_tar_members(
|
|
350
|
-
tar: tarfile.TarFile, destination_directory: Path
|
|
351
|
-
) -> Generator[tarfile.TarInfo, None, None]:
|
|
352
|
-
"""Yield safe members for extraction to prevent path traversal and links.
|
|
353
|
-
|
|
354
|
-
Args:
|
|
355
|
-
tar: Open tar file handle
|
|
356
|
-
destination_directory: Directory to which files will be extracted
|
|
357
|
-
|
|
358
|
-
Yields:
|
|
359
|
-
Members that are safe to extract within destination_directory
|
|
360
|
-
"""
|
|
361
|
-
base_path = destination_directory.resolve()
|
|
362
|
-
|
|
363
|
-
for member in tar.getmembers():
|
|
364
|
-
name = member.name
|
|
365
|
-
# Skip empty names and absolute paths
|
|
366
|
-
if not name or name.startswith("/") or name.startswith("\\"):
|
|
367
|
-
continue
|
|
368
|
-
|
|
369
|
-
# Disallow symlinks and hardlinks
|
|
370
|
-
if member.issym() or member.islnk():
|
|
371
|
-
continue
|
|
372
|
-
|
|
373
|
-
# Compute the final path and ensure it's within base_path
|
|
374
|
-
target_path = (base_path / name).resolve()
|
|
375
|
-
try:
|
|
376
|
-
target_path.relative_to(base_path)
|
|
377
|
-
except ValueError:
|
|
378
|
-
# Member would escape the destination directory
|
|
379
|
-
continue
|
|
380
|
-
|
|
381
|
-
yield member
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
async def download_cache_for_template(
|
|
385
|
-
template: ProjectTemplateName, project_folder: str
|
|
386
|
-
) -> None:
|
|
387
|
-
# get a temp path for the cache file download
|
|
388
|
-
temporary_cache_file = tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False)
|
|
389
|
-
|
|
390
|
-
try:
|
|
391
|
-
url = f"{CACHE_BUCKET_URL}/{rasa.version.__version__}-{template.value}.tar.gz"
|
|
392
|
-
async with aiohttp.ClientSession() as session:
|
|
393
|
-
async with session.get(url) as response:
|
|
394
|
-
response.raise_for_status()
|
|
395
|
-
async with aiofiles.open(temporary_cache_file.name, "wb") as f:
|
|
396
|
-
async for chunk in response.content.iter_chunked(1024 * 1024):
|
|
397
|
-
await f.write(chunk)
|
|
398
|
-
|
|
399
|
-
# extract the cache to the project folder using safe member filtering
|
|
400
|
-
with tarfile.open(temporary_cache_file.name, "r:gz") as tar:
|
|
401
|
-
destination = Path(project_folder)
|
|
402
|
-
destination.mkdir(parents=True, exist_ok=True)
|
|
403
|
-
tar.extractall(
|
|
404
|
-
path=destination,
|
|
405
|
-
members=_safe_tar_members(tar, destination),
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
structlogger.info(
|
|
409
|
-
"project_generator.download_cache_for_template.success",
|
|
410
|
-
template=template,
|
|
411
|
-
event_info=(
|
|
412
|
-
f"Downloaded cache for template, extracted to {project_folder}."
|
|
413
|
-
),
|
|
414
|
-
)
|
|
415
|
-
except aiohttp.ClientResponseError as e:
|
|
416
|
-
if e.status == 403:
|
|
417
|
-
structlogger.debug(
|
|
418
|
-
"project_generator.download_cache_for_template.no_cache_found",
|
|
419
|
-
template=template,
|
|
420
|
-
event_info=("No cache found for template, continuing without it."),
|
|
421
|
-
)
|
|
422
|
-
else:
|
|
423
|
-
structlogger.debug(
|
|
424
|
-
"project_generator.download_cache_for_template.response_error",
|
|
425
|
-
error=str(e),
|
|
426
|
-
status=e.status,
|
|
427
|
-
template=template,
|
|
428
|
-
event_info=(
|
|
429
|
-
"Failed to download cache for template, continuing without it."
|
|
430
|
-
),
|
|
431
|
-
)
|
|
432
|
-
capture_exception_with_context(
|
|
433
|
-
e,
|
|
434
|
-
"project_generator.download_cache_for_template.response_error",
|
|
435
|
-
tags={"template": template.value, "status": str(e.status)},
|
|
436
|
-
)
|
|
437
|
-
except Exception as exc:
|
|
438
|
-
structlogger.debug(
|
|
439
|
-
"project_generator.download_cache_for_template.unexpected_error",
|
|
440
|
-
error=str(exc),
|
|
441
|
-
template=template,
|
|
442
|
-
event_info=(
|
|
443
|
-
"Unexpected error when downloading cache for template, "
|
|
444
|
-
"continuing without it."
|
|
445
|
-
),
|
|
446
|
-
)
|
|
447
|
-
capture_exception_with_context(
|
|
448
|
-
exc,
|
|
449
|
-
"project_generator.download_cache_for_template.unexpected_error",
|
|
450
|
-
tags={"template": template.value},
|
|
451
|
-
)
|
|
452
|
-
finally:
|
|
453
|
-
# Clean up the temporary file
|
|
454
|
-
try:
|
|
455
|
-
Path(temporary_cache_file.name).unlink(missing_ok=True)
|
|
456
|
-
except Exception as exc:
|
|
457
|
-
structlogger.debug(
|
|
458
|
-
"project_generator.download_cache_for_template.cleanup_error",
|
|
459
|
-
error=str(exc),
|
|
460
|
-
template=template,
|
|
461
|
-
event_info=("Failed to cleanup cache for template, ignoring."),
|
|
462
|
-
)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import shutil
|
|
4
|
+
import tarfile
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Generator
|
|
8
|
+
|
|
9
|
+
import aiofiles
|
|
10
|
+
import aiohttp
|
|
11
|
+
import structlog
|
|
12
|
+
from sanic import Sanic
|
|
13
|
+
|
|
14
|
+
import rasa.version
|
|
15
|
+
from rasa.builder.logging_utils import capture_exception_with_context
|
|
16
|
+
from rasa.cli.scaffold import ProjectTemplateName
|
|
17
|
+
|
|
18
|
+
structlogger = structlog.get_logger()
|
|
19
|
+
|
|
20
|
+
CACHE_BUCKET_URL = "https://trained-templates.s3.us-east-1.amazonaws.com"
|
|
21
|
+
|
|
22
|
+
# Root directory for storing downloaded template caches on disk.
|
|
23
|
+
_CACHE_ROOT_DIR = Path(
|
|
24
|
+
os.getenv(
|
|
25
|
+
"RASA_TEMPLATE_CACHE_DIR",
|
|
26
|
+
Path.home().joinpath(".rasa", "template-cache").as_posix(),
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _template_cache_dir(template: ProjectTemplateName) -> Path:
|
|
32
|
+
"""Return the local cache directory for a given template and version."""
|
|
33
|
+
return _CACHE_ROOT_DIR / rasa.version.__version__ / template.value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cache_root_dir() -> Path:
|
|
37
|
+
return Path(
|
|
38
|
+
os.getenv(
|
|
39
|
+
"RASA_TEMPLATE_CACHE_DIR",
|
|
40
|
+
Path.home().joinpath(".rasa", "template-cache").as_posix(),
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _safe_tar_members(
|
|
46
|
+
tar: tarfile.TarFile, destination_directory: Path
|
|
47
|
+
) -> Generator[tarfile.TarInfo, None, None]:
|
|
48
|
+
"""Yield safe members for extraction to prevent path traversal and links.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
tar: Open tar file handle
|
|
52
|
+
destination_directory: Directory to which files will be extracted
|
|
53
|
+
|
|
54
|
+
Yields:
|
|
55
|
+
Members that are safe to extract within destination_directory
|
|
56
|
+
"""
|
|
57
|
+
base_path = destination_directory.resolve()
|
|
58
|
+
|
|
59
|
+
for member in tar.getmembers():
|
|
60
|
+
name = member.name
|
|
61
|
+
# Skip empty names and absolute paths
|
|
62
|
+
if not name or name.startswith("/") or name.startswith("\\"):
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
# Disallow symlinks and hardlinks
|
|
66
|
+
if member.issym() or member.islnk():
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
# Compute the final path and ensure it's within base_path
|
|
70
|
+
target_path = (base_path / name).resolve()
|
|
71
|
+
try:
|
|
72
|
+
target_path.relative_to(base_path)
|
|
73
|
+
except ValueError:
|
|
74
|
+
# Member would escape the destination directory
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
yield member
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _copytree(src: Path, dst: Path) -> None:
|
|
81
|
+
"""Copy directory tree from src to dst, merging into dst.
|
|
82
|
+
|
|
83
|
+
Existing files are overwritten. Hidden files and directories are included, as
|
|
84
|
+
caches can contain `.rasa` metadata that should be applied before calling
|
|
85
|
+
`ensure_first_used`.
|
|
86
|
+
"""
|
|
87
|
+
for root, dirs, files in os.walk(src):
|
|
88
|
+
rel_path = Path(root).relative_to(src)
|
|
89
|
+
target_dir = dst / rel_path
|
|
90
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
for filename in files:
|
|
92
|
+
src_file = Path(root) / filename
|
|
93
|
+
dst_file = target_dir / filename
|
|
94
|
+
shutil.copy2(src_file, dst_file)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def download_cache_for_template(
|
|
98
|
+
template: ProjectTemplateName, target_dir: str
|
|
99
|
+
) -> None:
|
|
100
|
+
# get a temp path for the cache file download
|
|
101
|
+
temporary_cache_file = tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
url = f"{CACHE_BUCKET_URL}/{rasa.version.__version__}-{template.value}.tar.gz"
|
|
105
|
+
async with aiohttp.ClientSession() as session:
|
|
106
|
+
async with session.get(url) as response:
|
|
107
|
+
response.raise_for_status()
|
|
108
|
+
async with aiofiles.open(temporary_cache_file.name, "wb") as f:
|
|
109
|
+
async for chunk in response.content.iter_chunked(1024 * 1024):
|
|
110
|
+
await f.write(chunk)
|
|
111
|
+
|
|
112
|
+
# extract the cache to the project folder using safe member filtering
|
|
113
|
+
with tarfile.open(temporary_cache_file.name, "r:gz") as tar:
|
|
114
|
+
destination = Path(target_dir)
|
|
115
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
tar.extractall(
|
|
117
|
+
path=destination,
|
|
118
|
+
members=_safe_tar_members(tar, destination),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
structlogger.info(
|
|
122
|
+
"project_generator.download_cache_for_template.success",
|
|
123
|
+
template=template,
|
|
124
|
+
event_info=(
|
|
125
|
+
"Downloaded cache for template, extracted to target directory."
|
|
126
|
+
),
|
|
127
|
+
target_dir=target_dir,
|
|
128
|
+
)
|
|
129
|
+
except aiohttp.ClientResponseError as e:
|
|
130
|
+
if e.status == 403:
|
|
131
|
+
structlogger.debug(
|
|
132
|
+
"project_generator.download_cache_for_template.no_cache_found",
|
|
133
|
+
template=template,
|
|
134
|
+
event_info=("No cache found for template, continuing without it."),
|
|
135
|
+
target_dir=target_dir,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
capture_exception_with_context(
|
|
139
|
+
e,
|
|
140
|
+
"project_generator.download_cache_for_template.response_error",
|
|
141
|
+
extra={
|
|
142
|
+
"template": template.value,
|
|
143
|
+
"status": str(e.status),
|
|
144
|
+
"target_dir": target_dir,
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
capture_exception_with_context(
|
|
149
|
+
exc,
|
|
150
|
+
"project_generator.download_cache_for_template.unexpected_error",
|
|
151
|
+
extra={"template": template.value, "target_dir": target_dir},
|
|
152
|
+
)
|
|
153
|
+
finally:
|
|
154
|
+
# Clean up the temporary file
|
|
155
|
+
try:
|
|
156
|
+
Path(temporary_cache_file.name).unlink(missing_ok=True)
|
|
157
|
+
except Exception as exc:
|
|
158
|
+
structlogger.debug(
|
|
159
|
+
"project_generator.download_cache_for_template.cleanup_error",
|
|
160
|
+
error=str(exc),
|
|
161
|
+
template=template,
|
|
162
|
+
event_info=("Failed to cleanup cache for template, ignoring."),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
async def background_download_template_caches(
|
|
167
|
+
app: Sanic, loop: asyncio.AbstractEventLoop
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Kick off background downloads of template caches if enabled."""
|
|
170
|
+
try:
|
|
171
|
+
structlogger.info(
|
|
172
|
+
"builder.main.background_cache_download.start",
|
|
173
|
+
event_info=(
|
|
174
|
+
"Starting background download of template caches for this " "version"
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# Ensure cache root exists
|
|
179
|
+
_cache_root_dir().mkdir(parents=True, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
async def _download(template: ProjectTemplateName) -> None:
|
|
182
|
+
try:
|
|
183
|
+
target_dir = _template_cache_dir(template)
|
|
184
|
+
if target_dir.exists() and any(target_dir.iterdir()):
|
|
185
|
+
structlogger.debug(
|
|
186
|
+
"builder.main.background_cache_download.skipped",
|
|
187
|
+
template=template,
|
|
188
|
+
event_info=(
|
|
189
|
+
"Skipping download of template cache because it "
|
|
190
|
+
"already exists."
|
|
191
|
+
),
|
|
192
|
+
target_dir=target_dir,
|
|
193
|
+
)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
await download_cache_for_template(template, target_dir.as_posix())
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
structlogger.debug(
|
|
200
|
+
"builder.main.background_cache_download.error",
|
|
201
|
+
template=template,
|
|
202
|
+
error=str(exc),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# schedule downloads concurrently without blocking startup
|
|
206
|
+
for template in ProjectTemplateName:
|
|
207
|
+
loop.create_task(_download(template))
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
structlogger.debug(
|
|
210
|
+
"builder.main.background_cache_download.unexpected_error",
|
|
211
|
+
error=str(exc),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def copy_cache_for_template_if_available(
|
|
216
|
+
template: ProjectTemplateName, project_folder: Path
|
|
217
|
+
) -> None:
|
|
218
|
+
"""Copy a previously downloaded cache for `template` into `project_folder`.
|
|
219
|
+
|
|
220
|
+
If the cache does not exist, this function is a no-op.
|
|
221
|
+
"""
|
|
222
|
+
try:
|
|
223
|
+
cache_dir = _template_cache_dir(template)
|
|
224
|
+
if cache_dir.exists() and any(cache_dir.iterdir()):
|
|
225
|
+
_copytree(cache_dir, project_folder)
|
|
226
|
+
structlogger.info(
|
|
227
|
+
"project_generator.copy_cache_for_template.success",
|
|
228
|
+
template=template,
|
|
229
|
+
event_info=(
|
|
230
|
+
"Copied cached template files from cache to project folder."
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
else:
|
|
234
|
+
structlogger.debug(
|
|
235
|
+
"project_generator.copy_cache_for_template.missing",
|
|
236
|
+
template=template,
|
|
237
|
+
event_info=("No local cache found for template; skipping copy."),
|
|
238
|
+
)
|
|
239
|
+
except Exception as exc:
|
|
240
|
+
structlogger.warning(
|
|
241
|
+
"project_generator.copy_cache_for_template.error",
|
|
242
|
+
error=str(exc),
|
|
243
|
+
template=template,
|
|
244
|
+
)
|
|
@@ -11,7 +11,7 @@ assistant_id: placeholder_default
|
|
|
11
11
|
pipeline:
|
|
12
12
|
- name: SearchReadyLLMCommandGenerator
|
|
13
13
|
llm:
|
|
14
|
-
model_group:
|
|
14
|
+
model_group: openai-gpt-4o
|
|
15
15
|
flow_retrieval:
|
|
16
16
|
active: false
|
|
17
17
|
|
|
@@ -23,8 +23,7 @@ policies:
|
|
|
23
23
|
type: "faiss"
|
|
24
24
|
source: "./docs"
|
|
25
25
|
llm:
|
|
26
|
-
model_group:
|
|
26
|
+
model_group: openai-gpt-4o
|
|
27
27
|
embeddings:
|
|
28
|
-
model_group:
|
|
29
|
-
|
|
28
|
+
model_group: openai-embeddings
|
|
30
29
|
check_relevancy: true
|
|
@@ -49,7 +49,7 @@ action_endpoint:
|
|
|
49
49
|
nlg:
|
|
50
50
|
type: rephrase
|
|
51
51
|
llm:
|
|
52
|
-
model_group:
|
|
52
|
+
model_group: openai-gpt-4o
|
|
53
53
|
prompt_template: prompts/rephraser_demo_personality_prompt.jinja2
|
|
54
54
|
rephrase_all: False
|
|
55
55
|
summarize_history: False
|
|
@@ -61,17 +61,7 @@ model_groups:
|
|
|
61
61
|
model: gpt-4o-2024-11-20
|
|
62
62
|
timeout: 7
|
|
63
63
|
max_tokens: 256
|
|
64
|
-
- id:
|
|
65
|
-
models:
|
|
66
|
-
- provider: openai
|
|
67
|
-
api_base: https://hello-llm-proxy.rasa-e2e.workers.dev
|
|
68
|
-
api_key: ${RASA_PRO_LICENSE}
|
|
69
|
-
model: gpt-4o-2024-11-20
|
|
70
|
-
timeout: 7
|
|
71
|
-
max_tokens: 256
|
|
72
|
-
- id: rasa-llm-proxy-openai-embeddings
|
|
64
|
+
- id: openai-embeddings
|
|
73
65
|
models:
|
|
74
66
|
- provider: openai
|
|
75
67
|
model: text-embedding-3-large
|
|
76
|
-
api_base: https://hello-llm-proxy.rasa-e2e.workers.dev/embeddings
|
|
77
|
-
api_key: ${RASA_PRO_LICENSE}
|
|
@@ -9,13 +9,13 @@ language: en
|
|
|
9
9
|
pipeline:
|
|
10
10
|
- name: CompactLLMCommandGenerator
|
|
11
11
|
llm:
|
|
12
|
-
model_group:
|
|
12
|
+
model_group: openai-gpt-4o
|
|
13
13
|
|
|
14
14
|
# Configuration for Rasa Core.
|
|
15
15
|
policies:
|
|
16
16
|
- name: FlowPolicy
|
|
17
17
|
- name: IntentlessPolicy
|
|
18
18
|
llm:
|
|
19
|
-
model_group:
|
|
19
|
+
model_group: openai-gpt-4o
|
|
20
20
|
embeddings:
|
|
21
|
-
model_group:
|
|
21
|
+
model_group: openai-embeddings
|
|
@@ -56,18 +56,7 @@ model_groups:
|
|
|
56
56
|
model: gpt-4o-2024-11-20
|
|
57
57
|
request_timeout: 7
|
|
58
58
|
max_tokens: 256
|
|
59
|
-
- id:
|
|
60
|
-
models:
|
|
61
|
-
- provider: openai
|
|
62
|
-
api_base: https://hello-llm-proxy.rasa-e2e.workers.dev
|
|
63
|
-
api_key: ${RASA_PRO_LICENSE}
|
|
64
|
-
model: gpt-4o-2024-11-20
|
|
65
|
-
timeout: 7
|
|
66
|
-
max_tokens: 256
|
|
67
|
-
- id: rasa-llm-proxy-openai-embeddings
|
|
59
|
+
- id: openai-embeddings
|
|
68
60
|
models:
|
|
69
61
|
- provider: openai
|
|
70
62
|
model: text-embedding-3-large
|
|
71
|
-
api_base: https://hello-llm-proxy.rasa-e2e.workers.dev/embeddings
|
|
72
|
-
api_key: ${RASA_PRO_LICENSE}
|
|
73
|
-
|
|
@@ -5,7 +5,7 @@ assistant_id: placeholder_default
|
|
|
5
5
|
pipeline:
|
|
6
6
|
- name: SearchReadyLLMCommandGenerator
|
|
7
7
|
llm:
|
|
8
|
-
model_group:
|
|
8
|
+
model_group: openai-gpt-4o
|
|
9
9
|
flow_retrieval:
|
|
10
10
|
active: false
|
|
11
11
|
|
|
@@ -17,7 +17,7 @@ policies:
|
|
|
17
17
|
type: "faiss"
|
|
18
18
|
source: "./docs"
|
|
19
19
|
llm:
|
|
20
|
-
model_group:
|
|
20
|
+
model_group: openai-gpt-4o
|
|
21
21
|
embeddings:
|
|
22
|
-
model_group:
|
|
22
|
+
model_group: openai-embeddings
|
|
23
23
|
check_relevancy: true
|
|
@@ -49,7 +49,7 @@ action_endpoint:
|
|
|
49
49
|
nlg:
|
|
50
50
|
type: rephrase
|
|
51
51
|
llm:
|
|
52
|
-
model_group:
|
|
52
|
+
model_group: openai-gpt-4o
|
|
53
53
|
prompt: prompts/rephraser_demo_personality_prompt.jinja2
|
|
54
54
|
rephrase_all: False
|
|
55
55
|
|
|
@@ -60,17 +60,7 @@ model_groups:
|
|
|
60
60
|
model: gpt-4o-2024-11-20
|
|
61
61
|
request_timeout: 7
|
|
62
62
|
max_tokens: 256
|
|
63
|
-
- id:
|
|
64
|
-
models:
|
|
65
|
-
- provider: openai
|
|
66
|
-
api_base: https://hello-llm-proxy.rasa-e2e.workers.dev
|
|
67
|
-
api_key: ${RASA_PRO_LICENSE}
|
|
68
|
-
model: gpt-4o-2024-11-20
|
|
69
|
-
timeout: 7
|
|
70
|
-
max_tokens: 256
|
|
71
|
-
- id: rasa-llm-proxy-openai-embeddings
|
|
63
|
+
- id: openai-embeddings
|
|
72
64
|
models:
|
|
73
65
|
- provider: openai
|
|
74
66
|
model: text-embedding-3-large
|
|
75
|
-
api_base: https://hello-llm-proxy.rasa-e2e.workers.dev/embeddings
|
|
76
|
-
api_key: ${RASA_PRO_LICENSE}
|