rasa-pro 3.13.1a14__py3-none-any.whl → 3.13.1a15__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/llm_service.py +9 -6
- rasa/builder/main.py +95 -9
- rasa/builder/models.py +2 -2
- rasa/builder/project_generator.py +5 -5
- rasa/builder/scrape_rasa_docs.py +4 -4
- rasa/builder/service.py +621 -435
- 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.1a15.dist-info}/METADATA +10 -9
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a15.dist-info}/RECORD +27 -26
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a15.dist-info}/NOTICE +0 -0
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a15.dist-info}/WHEEL +0 -0
- {rasa_pro-3.13.1a14.dist-info → rasa_pro-3.13.1a15.dist-info}/entry_points.txt +0 -0
rasa/builder/service.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Main service for the prompt-to-bot functionality."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
from typing import Optional
|
|
4
|
+
from typing import Any, Optional
|
|
5
5
|
|
|
6
6
|
import structlog
|
|
7
|
-
from sanic import
|
|
7
|
+
from sanic import Blueprint, HTTPResponse, response
|
|
8
8
|
from sanic.request import Request
|
|
9
|
+
from sanic_openapi import openapi
|
|
9
10
|
|
|
10
|
-
from rasa.builder import config
|
|
11
11
|
from rasa.builder.exceptions import (
|
|
12
12
|
LLMGenerationError,
|
|
13
13
|
ProjectGenerationError,
|
|
@@ -30,523 +30,709 @@ from rasa.builder.training_service import train_and_load_agent
|
|
|
30
30
|
from rasa.builder.validation_service import validate_project
|
|
31
31
|
from rasa.cli.scaffold import ProjectTemplateName
|
|
32
32
|
from rasa.core.channels.studio_chat import StudioChatInput
|
|
33
|
-
from rasa.server import configure_cors
|
|
34
33
|
from rasa.shared.core.trackers import DialogueStateTracker
|
|
34
|
+
from rasa.studio.upload import CALMUserData, extract_calm_import_parts_from_importer
|
|
35
|
+
from rasa.utils.openapi import model_to_schema
|
|
35
36
|
|
|
36
37
|
structlogger = structlog.get_logger()
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
# Create the blueprint
|
|
40
|
+
bp = Blueprint("bot_builder", url_prefix="/api")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def setup_project_generator(project_folder: Optional[str] = None) -> ProjectGenerator:
|
|
44
|
+
"""Initialize and return a ProjectGenerator instance."""
|
|
45
|
+
if project_folder is None:
|
|
46
|
+
import tempfile
|
|
47
|
+
|
|
48
|
+
project_folder = tempfile.mkdtemp(prefix="rasa_builder_")
|
|
49
|
+
|
|
50
|
+
# working directory needs to be the project folder, e.g.
|
|
51
|
+
# for relative paths (./docs) in a projects config to work
|
|
52
|
+
os.chdir(project_folder)
|
|
53
|
+
|
|
54
|
+
structlogger.info(
|
|
55
|
+
"bot_builder_service.service_initialized", project_folder=project_folder
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return ProjectGenerator(project_folder)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_project_generator(request: Request) -> ProjectGenerator:
|
|
62
|
+
"""Get the project generator from app context."""
|
|
63
|
+
return request.app.ctx.project_generator
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_input_channel(request: Request) -> StudioChatInput:
|
|
67
|
+
"""Get the input channel from app context."""
|
|
68
|
+
return request.app.ctx.input_channel
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def extract_calm_import_parts_from_project_generator(
|
|
72
|
+
project_generator: ProjectGenerator,
|
|
73
|
+
) -> CALMUserData:
|
|
74
|
+
"""Extract CALMUserData from a ProjectGenerator.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
project_generator: The project generator to extract data from
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
CALMUserData containing flows, domain, config, endpoints, and nlu data
|
|
81
|
+
"""
|
|
82
|
+
# Get the training data importer
|
|
83
|
+
importer = project_generator._create_importer()
|
|
84
|
+
|
|
85
|
+
# Extract endpoints (if exists)
|
|
86
|
+
endpoints_path = project_generator.project_folder / "endpoints.yml"
|
|
87
|
+
if endpoints_path.exists():
|
|
88
|
+
from rasa.shared.utils.yaml import read_yaml_file
|
|
89
|
+
|
|
90
|
+
endpoints = read_yaml_file(endpoints_path, expand_env_vars=False)
|
|
91
|
+
else:
|
|
92
|
+
endpoints = {}
|
|
93
|
+
|
|
94
|
+
# Use the shared function with the importer and project data paths
|
|
95
|
+
return extract_calm_import_parts_from_importer(
|
|
96
|
+
importer=importer,
|
|
97
|
+
config=None, # Let the shared function get config from importer
|
|
98
|
+
endpoints=endpoints,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# Health check endpoint
|
|
103
|
+
@bp.route("/", methods=["GET"])
|
|
104
|
+
@openapi.summary("Health check endpoint")
|
|
105
|
+
@openapi.description("Returns the health status of the Bot Builder service")
|
|
106
|
+
@openapi.tag("health")
|
|
107
|
+
@openapi.response(200, {"application/json": {"status": str, "service": str}})
|
|
108
|
+
async def health(request: Request) -> HTTPResponse:
|
|
109
|
+
"""Health check endpoint."""
|
|
110
|
+
return response.json({"status": "ok", "service": "bot-builder"})
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@bp.route("/prompt-to-bot", methods=["POST"])
|
|
114
|
+
@openapi.summary("Generate bot from natural language prompt")
|
|
115
|
+
@openapi.description(
|
|
116
|
+
"Creates a complete conversational AI bot from a natural language prompt "
|
|
117
|
+
"using LLM generation. Returns server-sent events (SSE) for real-time "
|
|
118
|
+
"progress tracking through the entire bot creation process.\n\n"
|
|
119
|
+
"**SSE Event Flow:**\n"
|
|
120
|
+
"1. `received` - Request received by server\n"
|
|
121
|
+
"2. `generating` - Generating bot project files\n"
|
|
122
|
+
"3. `generation_success` - Bot generation completed successfully\n"
|
|
123
|
+
"4. `training` - Training the bot model\n"
|
|
124
|
+
"5. `train_success` - Model training completed\n"
|
|
125
|
+
"6. `done` - Bot creation completed\n\n"
|
|
126
|
+
"**Error Events (can occur at any time):**\n"
|
|
127
|
+
"- `generation_error` - Failed to generate bot from prompt\n"
|
|
128
|
+
"- `train_error` - Bot generated but training failed\n"
|
|
129
|
+
"- `validation_error` - Generated bot configuration is invalid\n"
|
|
130
|
+
"- `error` - Unexpected error occurred\n\n"
|
|
131
|
+
"**Usage:** Send POST request with Content-Type: application/json and "
|
|
132
|
+
"Accept: text/event-stream"
|
|
133
|
+
)
|
|
134
|
+
@openapi.tag("bot-generation")
|
|
135
|
+
@openapi.body(
|
|
136
|
+
{"application/json": model_to_schema(PromptRequest)},
|
|
137
|
+
description="Prompt request with natural language description and client ID "
|
|
138
|
+
"for tracking",
|
|
139
|
+
required=True,
|
|
140
|
+
)
|
|
141
|
+
@openapi.response(
|
|
142
|
+
200,
|
|
143
|
+
{"text/event-stream": str},
|
|
144
|
+
description="Server-sent events stream with real-time progress updates",
|
|
145
|
+
)
|
|
146
|
+
@openapi.response(
|
|
147
|
+
400,
|
|
148
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
149
|
+
description="Validation error in request payload",
|
|
150
|
+
)
|
|
151
|
+
@openapi.response(
|
|
152
|
+
500,
|
|
153
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
154
|
+
description="Internal server error",
|
|
155
|
+
)
|
|
156
|
+
async def handle_prompt_to_bot(request: Request) -> None:
|
|
157
|
+
"""Handle prompt-to-bot generation requests."""
|
|
158
|
+
sse_response = await request.respond(content_type="text/event-stream")
|
|
159
|
+
project_generator = get_project_generator(request)
|
|
160
|
+
input_channel = get_input_channel(request)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
# 1. Received
|
|
164
|
+
await _send_sse_event(
|
|
165
|
+
sse_response,
|
|
166
|
+
ServerSentEvent(event="received", data={"status": "received"}),
|
|
60
167
|
)
|
|
61
168
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
self.app.config.REQUEST_TIMEOUT = 60 # 1 minute timeout
|
|
65
|
-
self.app.ctx.agent = None
|
|
66
|
-
self.input_channel = self.setup_input_channel()
|
|
67
|
-
self.setup_routes()
|
|
68
|
-
self.setup_middleware()
|
|
69
|
-
|
|
70
|
-
configure_cors(self.app, cors_origins=config.CORS_ORIGINS)
|
|
71
|
-
|
|
72
|
-
def setup_input_channel(self) -> StudioChatInput:
|
|
73
|
-
"""Setup the input channel for chat interactions."""
|
|
74
|
-
studio_chat_credentials = config.get_default_credentials().get(
|
|
75
|
-
StudioChatInput.name()
|
|
76
|
-
)
|
|
77
|
-
return StudioChatInput.from_credentials(credentials=studio_chat_credentials)
|
|
169
|
+
# Validate request
|
|
170
|
+
prompt_data = PromptRequest(**request.json)
|
|
78
171
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
self.handle_prompt_to_bot, "/api/prompt-to-bot", methods=["POST"]
|
|
172
|
+
# 2. Generating
|
|
173
|
+
await _send_sse_event(
|
|
174
|
+
sse_response,
|
|
175
|
+
ServerSentEvent(event="generating", data={"status": "generating"}),
|
|
84
176
|
)
|
|
85
|
-
self.app.add_route(
|
|
86
|
-
self.handle_template_to_bot, "/api/template-to-bot", methods=["POST"]
|
|
87
|
-
)
|
|
88
|
-
self.app.add_route(self.get_bot_data, "/api/bot-data", methods=["GET"])
|
|
89
|
-
self.app.add_route(self.update_bot_data, "/api/bot-data", methods=["PUT"])
|
|
90
|
-
self.app.add_route(self.llm_builder, "/api/llm-builder", methods=["POST"])
|
|
91
|
-
|
|
92
|
-
# Health check
|
|
93
|
-
self.app.add_route(self.health, "/", methods=["GET"])
|
|
94
|
-
|
|
95
|
-
# Register input channel webhooks
|
|
96
|
-
from rasa.core import channels
|
|
97
|
-
|
|
98
|
-
channels.channel.register([self.input_channel], self.app, route="/webhooks/")
|
|
99
|
-
|
|
100
|
-
def setup_middleware(self) -> None:
|
|
101
|
-
"""Setup middleware for request/response processing."""
|
|
102
|
-
|
|
103
|
-
@self.app.middleware("request") # type: ignore[no-untyped-call]
|
|
104
|
-
async def log_request(request: Request) -> None:
|
|
105
|
-
structlogger.info(
|
|
106
|
-
"request.received",
|
|
107
|
-
method=request.method,
|
|
108
|
-
path=request.path,
|
|
109
|
-
remote_addr=request.remote_addr or "unknown",
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
@self.app.middleware("response") # type: ignore[no-untyped-call]
|
|
113
|
-
async def log_response(request: Request, response: HTTPResponse) -> None:
|
|
114
|
-
structlogger.info(
|
|
115
|
-
"request.completed",
|
|
116
|
-
method=request.method,
|
|
117
|
-
path=request.path,
|
|
118
|
-
status=response.status,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
async def health(self, request: Request) -> HTTPResponse:
|
|
122
|
-
"""Health check endpoint."""
|
|
123
|
-
return response.json({"status": "ok", "service": "bot-builder"})
|
|
124
|
-
|
|
125
|
-
async def handle_prompt_to_bot(self, request: Request) -> None:
|
|
126
|
-
"""Handle prompt-to-bot generation requests."""
|
|
127
|
-
sse_response = await request.respond(content_type="text/event-stream")
|
|
128
177
|
|
|
129
178
|
try:
|
|
130
|
-
#
|
|
131
|
-
await
|
|
132
|
-
|
|
133
|
-
|
|
179
|
+
# Generate project with retries
|
|
180
|
+
bot_files = await project_generator.generate_project_with_retries(
|
|
181
|
+
prompt_data.prompt,
|
|
182
|
+
template=ProjectTemplateName.PLAIN,
|
|
134
183
|
)
|
|
135
184
|
|
|
136
|
-
|
|
137
|
-
prompt_data = PromptRequest(**request.json)
|
|
138
|
-
|
|
139
|
-
# 2. Generating
|
|
140
|
-
await self._send_sse_event(
|
|
185
|
+
await _send_sse_event(
|
|
141
186
|
sse_response,
|
|
142
|
-
ServerSentEvent(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
# Generate project with retries
|
|
147
|
-
bot_files = await self.project_generator.generate_project_with_retries(
|
|
148
|
-
prompt_data.prompt,
|
|
149
|
-
template=ProjectTemplateName.PLAIN,
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
await self._send_sse_event(
|
|
153
|
-
sse_response,
|
|
154
|
-
ServerSentEvent(
|
|
155
|
-
event="generation_success",
|
|
156
|
-
data={"status": "generation_success"},
|
|
157
|
-
),
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
except (ProjectGenerationError, LLMGenerationError) as e:
|
|
161
|
-
await self._send_sse_event(
|
|
162
|
-
sse_response,
|
|
163
|
-
ServerSentEvent(
|
|
164
|
-
event="generation_error",
|
|
165
|
-
data={"status": "generation_error", "error": str(e)},
|
|
166
|
-
),
|
|
167
|
-
)
|
|
168
|
-
await sse_response.eof()
|
|
169
|
-
return
|
|
170
|
-
|
|
171
|
-
# 3. Training
|
|
172
|
-
await self._send_sse_event(
|
|
173
|
-
sse_response,
|
|
174
|
-
ServerSentEvent(event="training", data={"status": "training"}),
|
|
187
|
+
ServerSentEvent(
|
|
188
|
+
event="generation_success",
|
|
189
|
+
data={"status": "generation_success"},
|
|
190
|
+
),
|
|
175
191
|
)
|
|
176
192
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
importer = self.project_generator._create_importer()
|
|
180
|
-
self.app.ctx.agent = await train_and_load_agent(importer)
|
|
181
|
-
|
|
182
|
-
# Update input channel with new agent
|
|
183
|
-
self.input_channel.agent = self.app.ctx.agent
|
|
184
|
-
|
|
185
|
-
await self._send_sse_event(
|
|
186
|
-
sse_response,
|
|
187
|
-
ServerSentEvent(
|
|
188
|
-
event="train_success", data={"status": "train_success"}
|
|
189
|
-
),
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
except TrainingError as e:
|
|
193
|
-
await self._send_sse_event(
|
|
194
|
-
sse_response,
|
|
195
|
-
ServerSentEvent(
|
|
196
|
-
event="train_error",
|
|
197
|
-
data={"status": "train_error", "error": str(e)},
|
|
198
|
-
),
|
|
199
|
-
)
|
|
200
|
-
await sse_response.eof()
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
# 4. Done
|
|
204
|
-
await self._send_sse_event(
|
|
193
|
+
except (ProjectGenerationError, LLMGenerationError) as e:
|
|
194
|
+
await _send_sse_event(
|
|
205
195
|
sse_response,
|
|
206
196
|
ServerSentEvent(
|
|
207
|
-
event="
|
|
208
|
-
data={
|
|
209
|
-
"status": "done",
|
|
210
|
-
},
|
|
197
|
+
event="generation_error",
|
|
198
|
+
data={"status": "generation_error", "error": str(e)},
|
|
211
199
|
),
|
|
212
200
|
)
|
|
201
|
+
await sse_response.eof()
|
|
202
|
+
return
|
|
213
203
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
204
|
+
# 3. Training
|
|
205
|
+
await _send_sse_event(
|
|
206
|
+
sse_response,
|
|
207
|
+
ServerSentEvent(event="training", data={"status": "training"}),
|
|
208
|
+
)
|
|
219
209
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
|
|
210
|
+
try:
|
|
211
|
+
# Train and load agent
|
|
212
|
+
importer = project_generator._create_importer()
|
|
213
|
+
request.app.ctx.agent = await train_and_load_agent(importer)
|
|
214
|
+
|
|
215
|
+
# Update input channel with new agent
|
|
216
|
+
input_channel.agent = request.app.ctx.agent
|
|
217
|
+
|
|
218
|
+
await _send_sse_event(
|
|
225
219
|
sse_response,
|
|
226
220
|
ServerSentEvent(
|
|
227
|
-
event="
|
|
228
|
-
data={"status": "validation_error", "error": str(e)},
|
|
221
|
+
event="train_success", data={"status": "train_success"}
|
|
229
222
|
),
|
|
230
223
|
)
|
|
231
224
|
|
|
232
|
-
except
|
|
233
|
-
|
|
234
|
-
"bot_builder_service.prompt_to_bot.unexpected_error", error=str(e)
|
|
235
|
-
)
|
|
236
|
-
await self._send_sse_event(
|
|
225
|
+
except TrainingError as e:
|
|
226
|
+
await _send_sse_event(
|
|
237
227
|
sse_response,
|
|
238
228
|
ServerSentEvent(
|
|
239
|
-
event="
|
|
229
|
+
event="train_error",
|
|
230
|
+
data={"status": "train_error", "error": str(e)},
|
|
240
231
|
),
|
|
241
232
|
)
|
|
242
|
-
finally:
|
|
243
233
|
await sse_response.eof()
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
# 4. Done
|
|
237
|
+
await _send_sse_event(
|
|
238
|
+
sse_response,
|
|
239
|
+
ServerSentEvent(
|
|
240
|
+
event="done",
|
|
241
|
+
data={
|
|
242
|
+
"status": "done",
|
|
243
|
+
},
|
|
244
|
+
),
|
|
245
|
+
)
|
|
244
246
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
247
|
+
structlogger.info(
|
|
248
|
+
"bot_builder_service.prompt_to_bot.success",
|
|
249
|
+
client_id=prompt_data.client_id,
|
|
250
|
+
files_generated=list(bot_files.keys()),
|
|
251
|
+
)
|
|
248
252
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
except ValidationError as e:
|
|
254
|
+
structlogger.error(
|
|
255
|
+
"bot_builder_service.prompt_to_bot.validation_error", error=str(e)
|
|
256
|
+
)
|
|
257
|
+
await _send_sse_event(
|
|
258
|
+
sse_response,
|
|
259
|
+
ServerSentEvent(
|
|
260
|
+
event="validation_error",
|
|
261
|
+
data={"status": "validation_error", "error": str(e)},
|
|
262
|
+
),
|
|
263
|
+
)
|
|
255
264
|
|
|
256
|
-
|
|
257
|
-
|
|
265
|
+
except Exception as e:
|
|
266
|
+
structlogger.error(
|
|
267
|
+
"bot_builder_service.prompt_to_bot.unexpected_error", error=str(e)
|
|
268
|
+
)
|
|
269
|
+
await _send_sse_event(
|
|
270
|
+
sse_response,
|
|
271
|
+
ServerSentEvent(event="error", data={"status": "error", "error": str(e)}),
|
|
272
|
+
)
|
|
273
|
+
finally:
|
|
274
|
+
await sse_response.eof()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@bp.route("/template-to-bot", methods=["POST"])
|
|
278
|
+
@openapi.summary("Generate bot from predefined template")
|
|
279
|
+
@openapi.description(
|
|
280
|
+
"Creates a complete conversational AI bot from a predefined template with "
|
|
281
|
+
"immediate setup. Returns server-sent events (SSE) for real-time progress "
|
|
282
|
+
"tracking through the entire bot creation process.\n\n"
|
|
283
|
+
"**SSE Event Flow:**\n"
|
|
284
|
+
"1. `received` - Request received by server\n"
|
|
285
|
+
"2. `generating` - Initializing bot from template\n"
|
|
286
|
+
"3. `generation_success` - Template initialization completed successfully\n"
|
|
287
|
+
"4. `training` - Training the bot model\n"
|
|
288
|
+
"5. `train_success` - Model training completed\n"
|
|
289
|
+
"6. `done` - Bot creation completed\n\n"
|
|
290
|
+
"**Error Events (can occur at any time):**\n"
|
|
291
|
+
"- `generation_error` - Failed to initialize bot from template\n"
|
|
292
|
+
"- `train_error` - Template loaded but training failed\n"
|
|
293
|
+
"- `validation_error` - Template configuration is invalid\n"
|
|
294
|
+
"- `error` - Unexpected error occurred\n\n"
|
|
295
|
+
"**Usage:** Send POST request with Content-Type: application/json and "
|
|
296
|
+
"Accept: text/event-stream\n"
|
|
297
|
+
"**Templates Available:** Check available templates through the API or "
|
|
298
|
+
"documentation"
|
|
299
|
+
)
|
|
300
|
+
@openapi.tag("bot-generation")
|
|
301
|
+
@openapi.body(
|
|
302
|
+
{"application/json": model_to_schema(TemplateRequest)},
|
|
303
|
+
description="Template request with template name and client ID for " "tracking",
|
|
304
|
+
required=True,
|
|
305
|
+
)
|
|
306
|
+
@openapi.response(
|
|
307
|
+
200,
|
|
308
|
+
{"text/event-stream": model_to_schema(ServerSentEvent)},
|
|
309
|
+
description="Server-sent events stream with real-time progress updates",
|
|
310
|
+
example=ServerSentEvent(
|
|
311
|
+
event="generation_success",
|
|
312
|
+
data={"status": "generation_success"},
|
|
313
|
+
).model_dump(),
|
|
314
|
+
)
|
|
315
|
+
@openapi.response(
|
|
316
|
+
400,
|
|
317
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
318
|
+
description="Validation error in request payload or invalid template name",
|
|
319
|
+
)
|
|
320
|
+
@openapi.response(
|
|
321
|
+
500,
|
|
322
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
323
|
+
description="Internal server error",
|
|
324
|
+
)
|
|
325
|
+
async def handle_template_to_bot(request: Request) -> None:
|
|
326
|
+
"""Handle template-to-bot generation requests."""
|
|
327
|
+
sse_response = await request.respond(content_type="text/event-stream")
|
|
328
|
+
project_generator = get_project_generator(request)
|
|
329
|
+
input_channel = get_input_channel(request)
|
|
330
|
+
|
|
331
|
+
try:
|
|
332
|
+
# 1. Received
|
|
333
|
+
await _send_sse_event(
|
|
334
|
+
sse_response,
|
|
335
|
+
ServerSentEvent(event="received", data={"status": "received"}),
|
|
336
|
+
)
|
|
258
337
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
338
|
+
# Validate request
|
|
339
|
+
template_data = TemplateRequest(**request.json)
|
|
340
|
+
|
|
341
|
+
# 2. Generating
|
|
342
|
+
await _send_sse_event(
|
|
343
|
+
sse_response,
|
|
344
|
+
ServerSentEvent(event="generating", data={"status": "generating"}),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
# Generate project with retries
|
|
349
|
+
project_generator.init_from_template(
|
|
350
|
+
template_data.template_name,
|
|
263
351
|
)
|
|
352
|
+
bot_files = project_generator.get_bot_files()
|
|
264
353
|
|
|
265
|
-
|
|
266
|
-
# Generate project with retries
|
|
267
|
-
self.project_generator.init_from_template(
|
|
268
|
-
template_data.template_name,
|
|
269
|
-
)
|
|
270
|
-
bot_files = self.project_generator.get_bot_files()
|
|
271
|
-
|
|
272
|
-
await self._send_sse_event(
|
|
273
|
-
sse_response,
|
|
274
|
-
ServerSentEvent(
|
|
275
|
-
event="generation_success",
|
|
276
|
-
data={"status": "generation_success"},
|
|
277
|
-
),
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
except ProjectGenerationError as e:
|
|
281
|
-
await self._send_sse_event(
|
|
282
|
-
sse_response,
|
|
283
|
-
ServerSentEvent(
|
|
284
|
-
event="generation_error",
|
|
285
|
-
data={"status": "generation_error", "error": str(e)},
|
|
286
|
-
),
|
|
287
|
-
)
|
|
288
|
-
await sse_response.eof()
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
# 3. Training
|
|
292
|
-
await self._send_sse_event(
|
|
354
|
+
await _send_sse_event(
|
|
293
355
|
sse_response,
|
|
294
|
-
ServerSentEvent(
|
|
356
|
+
ServerSentEvent(
|
|
357
|
+
event="generation_success",
|
|
358
|
+
data={"status": "generation_success"},
|
|
359
|
+
),
|
|
295
360
|
)
|
|
296
361
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
importer = self.project_generator._create_importer()
|
|
300
|
-
self.app.ctx.agent = await train_and_load_agent(importer)
|
|
301
|
-
|
|
302
|
-
# Update input channel with new agent
|
|
303
|
-
self.input_channel.agent = self.app.ctx.agent
|
|
304
|
-
|
|
305
|
-
await self._send_sse_event(
|
|
306
|
-
sse_response,
|
|
307
|
-
ServerSentEvent(
|
|
308
|
-
event="train_success", data={"status": "train_success"}
|
|
309
|
-
),
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
except TrainingError as e:
|
|
313
|
-
await self._send_sse_event(
|
|
314
|
-
sse_response,
|
|
315
|
-
ServerSentEvent(
|
|
316
|
-
event="train_error",
|
|
317
|
-
data={"status": "train_error", "error": str(e)},
|
|
318
|
-
),
|
|
319
|
-
)
|
|
320
|
-
await sse_response.eof()
|
|
321
|
-
return
|
|
322
|
-
|
|
323
|
-
# 4. Done
|
|
324
|
-
await self._send_sse_event(
|
|
362
|
+
except ProjectGenerationError as e:
|
|
363
|
+
await _send_sse_event(
|
|
325
364
|
sse_response,
|
|
326
365
|
ServerSentEvent(
|
|
327
|
-
event="
|
|
328
|
-
data={
|
|
329
|
-
"status": "done",
|
|
330
|
-
},
|
|
366
|
+
event="generation_error",
|
|
367
|
+
data={"status": "generation_error", "error": str(e)},
|
|
331
368
|
),
|
|
332
369
|
)
|
|
370
|
+
await sse_response.eof()
|
|
371
|
+
return
|
|
333
372
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
373
|
+
# 3. Training
|
|
374
|
+
await _send_sse_event(
|
|
375
|
+
sse_response,
|
|
376
|
+
ServerSentEvent(event="training", data={"status": "training"}),
|
|
377
|
+
)
|
|
339
378
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
)
|
|
344
|
-
|
|
379
|
+
try:
|
|
380
|
+
# Train and load agent
|
|
381
|
+
importer = project_generator._create_importer()
|
|
382
|
+
request.app.ctx.agent = await train_and_load_agent(importer)
|
|
383
|
+
|
|
384
|
+
# Update input channel with new agent
|
|
385
|
+
input_channel.agent = request.app.ctx.agent
|
|
386
|
+
|
|
387
|
+
await _send_sse_event(
|
|
345
388
|
sse_response,
|
|
346
389
|
ServerSentEvent(
|
|
347
|
-
event="
|
|
348
|
-
data={"status": "validation_error", "error": str(e)},
|
|
390
|
+
event="train_success", data={"status": "train_success"}
|
|
349
391
|
),
|
|
350
392
|
)
|
|
351
393
|
|
|
352
|
-
except
|
|
353
|
-
|
|
354
|
-
"bot_builder_service.template_to_bot.unexpected_error", error=str(e)
|
|
355
|
-
)
|
|
356
|
-
await self._send_sse_event(
|
|
394
|
+
except TrainingError as e:
|
|
395
|
+
await _send_sse_event(
|
|
357
396
|
sse_response,
|
|
358
397
|
ServerSentEvent(
|
|
359
|
-
event="
|
|
398
|
+
event="train_error",
|
|
399
|
+
data={"status": "train_error", "error": str(e)},
|
|
360
400
|
),
|
|
361
401
|
)
|
|
362
|
-
finally:
|
|
363
402
|
await sse_response.eof()
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
# 4. Done
|
|
406
|
+
await _send_sse_event(
|
|
407
|
+
sse_response,
|
|
408
|
+
ServerSentEvent(
|
|
409
|
+
event="done",
|
|
410
|
+
data={
|
|
411
|
+
"status": "done",
|
|
412
|
+
},
|
|
413
|
+
),
|
|
414
|
+
)
|
|
364
415
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
416
|
+
structlogger.info(
|
|
417
|
+
"bot_builder_service.template_to_bot.success",
|
|
418
|
+
client_id=template_data.client_id,
|
|
419
|
+
files_generated=list(bot_files.keys()),
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
except ValidationError as e:
|
|
423
|
+
structlogger.error(
|
|
424
|
+
"bot_builder_service.template_to_bot.validation_error", error=str(e)
|
|
425
|
+
)
|
|
426
|
+
await _send_sse_event(
|
|
427
|
+
sse_response,
|
|
428
|
+
ServerSentEvent(
|
|
429
|
+
event="validation_error",
|
|
430
|
+
data={"status": "validation_error", "error": str(e)},
|
|
431
|
+
),
|
|
374
432
|
)
|
|
375
433
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
434
|
+
except Exception as e:
|
|
435
|
+
structlogger.error(
|
|
436
|
+
"bot_builder_service.template_to_bot.unexpected_error", error=str(e)
|
|
437
|
+
)
|
|
438
|
+
await _send_sse_event(
|
|
439
|
+
sse_response,
|
|
440
|
+
ServerSentEvent(event="error", data={"status": "error", "error": str(e)}),
|
|
441
|
+
)
|
|
442
|
+
finally:
|
|
443
|
+
await sse_response.eof()
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
@bp.route("/files", methods=["GET"])
|
|
447
|
+
@openapi.summary("Get bot files")
|
|
448
|
+
@openapi.description("Retrieves the current bot configuration files and data")
|
|
449
|
+
@openapi.tag("bot-files")
|
|
450
|
+
@openapi.response(
|
|
451
|
+
200,
|
|
452
|
+
{"application/json": model_to_schema(ApiResponse)},
|
|
453
|
+
description="Bot files retrieved successfully",
|
|
454
|
+
)
|
|
455
|
+
@openapi.response(
|
|
456
|
+
500,
|
|
457
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
458
|
+
description="Internal server error",
|
|
459
|
+
)
|
|
460
|
+
async def get_bot_files(request: Request) -> HTTPResponse:
|
|
461
|
+
"""Get current bot files."""
|
|
462
|
+
project_generator = get_project_generator(request)
|
|
463
|
+
bot_files = project_generator.get_bot_files()
|
|
464
|
+
return response.json(
|
|
465
|
+
ApiResponse(
|
|
466
|
+
status="success",
|
|
467
|
+
message="Bot files fetched successfully",
|
|
468
|
+
data={"files": bot_files},
|
|
469
|
+
).model_dump()
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@bp.route("/files", methods=["PUT"])
|
|
474
|
+
@openapi.summary("Update bot files")
|
|
475
|
+
@openapi.description(
|
|
476
|
+
"Updates the bot configuration files and retrains the model. "
|
|
477
|
+
"Returns server-sent events for real-time progress tracking."
|
|
478
|
+
)
|
|
479
|
+
@openapi.tag("bot-files")
|
|
480
|
+
@openapi.body(
|
|
481
|
+
{"application/json": dict},
|
|
482
|
+
description="Bot files containing updated configuration files",
|
|
483
|
+
required=True,
|
|
484
|
+
)
|
|
485
|
+
@openapi.response(
|
|
486
|
+
200,
|
|
487
|
+
{"text/event-stream": str},
|
|
488
|
+
description="Server-sent events stream with update progress",
|
|
489
|
+
)
|
|
490
|
+
@openapi.response(
|
|
491
|
+
400,
|
|
492
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
493
|
+
description="Validation error in bot files",
|
|
494
|
+
)
|
|
495
|
+
@openapi.response(
|
|
496
|
+
500,
|
|
497
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
498
|
+
description="Internal server error",
|
|
499
|
+
)
|
|
500
|
+
async def update_bot_files(request: Request) -> None:
|
|
501
|
+
"""Update bot files with server-sent events for progress tracking."""
|
|
502
|
+
sse_response = await request.respond(content_type="text/event-stream")
|
|
503
|
+
project_generator = get_project_generator(request)
|
|
504
|
+
input_channel = get_input_channel(request)
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
# 1. Received
|
|
508
|
+
await _send_sse_event(
|
|
509
|
+
sse_response,
|
|
510
|
+
ServerSentEvent(event="received", data={"status": "received"}),
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# Update bot files
|
|
514
|
+
bot_files = request.json
|
|
515
|
+
project_generator.update_bot_files(bot_files)
|
|
516
|
+
|
|
517
|
+
# 2. Validating
|
|
518
|
+
await _send_sse_event(
|
|
519
|
+
sse_response,
|
|
520
|
+
ServerSentEvent(event="validating", data={"status": "validating"}),
|
|
521
|
+
)
|
|
379
522
|
|
|
380
523
|
try:
|
|
381
|
-
|
|
382
|
-
await
|
|
383
|
-
sse_response,
|
|
384
|
-
ServerSentEvent(event="received", data={"status": "received"}),
|
|
385
|
-
)
|
|
524
|
+
importer = project_generator._create_importer()
|
|
525
|
+
validation_error = await validate_project(importer)
|
|
386
526
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
self.project_generator.update_bot_files(bot_data)
|
|
527
|
+
if validation_error:
|
|
528
|
+
raise ValidationError(validation_error)
|
|
390
529
|
|
|
391
|
-
|
|
392
|
-
await self._send_sse_event(
|
|
530
|
+
await _send_sse_event(
|
|
393
531
|
sse_response,
|
|
394
|
-
ServerSentEvent(
|
|
532
|
+
ServerSentEvent(
|
|
533
|
+
event="validation_success",
|
|
534
|
+
data={"status": "validation_success"},
|
|
535
|
+
),
|
|
395
536
|
)
|
|
396
537
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
validation_error = await validate_project(importer)
|
|
400
|
-
|
|
401
|
-
if validation_error:
|
|
402
|
-
raise ValidationError(validation_error)
|
|
403
|
-
|
|
404
|
-
await self._send_sse_event(
|
|
405
|
-
sse_response,
|
|
406
|
-
ServerSentEvent(
|
|
407
|
-
event="validation_success",
|
|
408
|
-
data={"status": "validation_success"},
|
|
409
|
-
),
|
|
410
|
-
)
|
|
411
|
-
|
|
412
|
-
except ValidationError as e:
|
|
413
|
-
await self._send_sse_event(
|
|
414
|
-
sse_response,
|
|
415
|
-
ServerSentEvent(
|
|
416
|
-
event="validation_error",
|
|
417
|
-
data={"status": "validation_error", "error": str(e)},
|
|
418
|
-
),
|
|
419
|
-
)
|
|
420
|
-
await sse_response.eof()
|
|
421
|
-
return
|
|
422
|
-
|
|
423
|
-
# 3. Training
|
|
424
|
-
await self._send_sse_event(
|
|
538
|
+
except ValidationError as e:
|
|
539
|
+
await _send_sse_event(
|
|
425
540
|
sse_response,
|
|
426
|
-
ServerSentEvent(
|
|
541
|
+
ServerSentEvent(
|
|
542
|
+
event="validation_error",
|
|
543
|
+
data={"status": "validation_error", "error": str(e)},
|
|
544
|
+
),
|
|
427
545
|
)
|
|
546
|
+
await sse_response.eof()
|
|
547
|
+
return
|
|
548
|
+
|
|
549
|
+
# 3. Training
|
|
550
|
+
await _send_sse_event(
|
|
551
|
+
sse_response,
|
|
552
|
+
ServerSentEvent(event="training", data={"status": "training"}),
|
|
553
|
+
)
|
|
428
554
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
sse_response,
|
|
435
|
-
ServerSentEvent(
|
|
436
|
-
event="train_success", data={"status": "train_success"}
|
|
437
|
-
),
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
except TrainingError as e:
|
|
441
|
-
await self._send_sse_event(
|
|
442
|
-
sse_response,
|
|
443
|
-
ServerSentEvent(
|
|
444
|
-
event="train_error",
|
|
445
|
-
data={"status": "train_error", "error": str(e)},
|
|
446
|
-
),
|
|
447
|
-
)
|
|
448
|
-
await sse_response.eof()
|
|
449
|
-
return
|
|
450
|
-
|
|
451
|
-
# 4. Done
|
|
452
|
-
await self._send_sse_event(
|
|
555
|
+
try:
|
|
556
|
+
request.app.ctx.agent = await train_and_load_agent(importer)
|
|
557
|
+
input_channel.agent = request.app.ctx.agent
|
|
558
|
+
|
|
559
|
+
await _send_sse_event(
|
|
453
560
|
sse_response,
|
|
454
561
|
ServerSentEvent(
|
|
455
|
-
event="
|
|
456
|
-
data={
|
|
457
|
-
"status": "done",
|
|
458
|
-
"bot_data": self.project_generator.get_bot_files(),
|
|
459
|
-
},
|
|
562
|
+
event="train_success", data={"status": "train_success"}
|
|
460
563
|
),
|
|
461
564
|
)
|
|
462
565
|
|
|
463
|
-
except
|
|
464
|
-
await
|
|
566
|
+
except TrainingError as e:
|
|
567
|
+
await _send_sse_event(
|
|
465
568
|
sse_response,
|
|
466
569
|
ServerSentEvent(
|
|
467
|
-
event="
|
|
570
|
+
event="train_error",
|
|
571
|
+
data={"status": "train_error", "error": str(e)},
|
|
468
572
|
),
|
|
469
573
|
)
|
|
470
|
-
finally:
|
|
471
574
|
await sse_response.eof()
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
# 4. Done
|
|
578
|
+
await _send_sse_event(
|
|
579
|
+
sse_response,
|
|
580
|
+
ServerSentEvent(
|
|
581
|
+
event="done",
|
|
582
|
+
data={
|
|
583
|
+
"status": "done",
|
|
584
|
+
},
|
|
585
|
+
),
|
|
586
|
+
)
|
|
472
587
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
current_tracker = await self.current_tracker_from_input_channel()
|
|
481
|
-
bot_logs = get_recent_logs()
|
|
482
|
-
chat_bot_files = self.project_generator.get_bot_files()
|
|
483
|
-
|
|
484
|
-
# create LLM builder context
|
|
485
|
-
llm_builder_context = LLMBuilderContext(
|
|
486
|
-
tracker=current_tracker,
|
|
487
|
-
bot_logs=bot_logs,
|
|
488
|
-
chat_bot_files=chat_bot_files,
|
|
489
|
-
chat_history=builder_request.messages,
|
|
490
|
-
)
|
|
588
|
+
except Exception as e:
|
|
589
|
+
await _send_sse_event(
|
|
590
|
+
sse_response,
|
|
591
|
+
ServerSentEvent(event="error", data={"status": "error", "error": str(e)}),
|
|
592
|
+
)
|
|
593
|
+
finally:
|
|
594
|
+
await sse_response.eof()
|
|
491
595
|
|
|
492
|
-
# Generate response
|
|
493
|
-
messages = await llm_service.create_helper_messages(llm_builder_context)
|
|
494
|
-
llm_response = await llm_service.generate_helper_response(messages)
|
|
495
596
|
|
|
496
|
-
|
|
597
|
+
@bp.route("/data", methods=["GET"])
|
|
598
|
+
@openapi.summary("Get bot data")
|
|
599
|
+
@openapi.description(
|
|
600
|
+
"Retrieves the current bot data in CALM import format with flows, domain, "
|
|
601
|
+
"config, endpoints, and NLU data"
|
|
602
|
+
)
|
|
603
|
+
@openapi.tag("bot-data")
|
|
604
|
+
@openapi.response(
|
|
605
|
+
200,
|
|
606
|
+
{"application/json": model_to_schema(ApiResponse)},
|
|
607
|
+
description="Bot data retrieved successfully",
|
|
608
|
+
)
|
|
609
|
+
@openapi.response(
|
|
610
|
+
500,
|
|
611
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
612
|
+
description="Internal server error",
|
|
613
|
+
)
|
|
614
|
+
async def get_bot_data(request: Request) -> HTTPResponse:
|
|
615
|
+
"""Get current bot data in CALM import format."""
|
|
616
|
+
try:
|
|
617
|
+
project_generator = get_project_generator(request)
|
|
618
|
+
calm_parts = extract_calm_import_parts_from_project_generator(project_generator)
|
|
497
619
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
"
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
620
|
+
return response.json(
|
|
621
|
+
ApiResponse(
|
|
622
|
+
status="success",
|
|
623
|
+
message="Bot data fetched successfully",
|
|
624
|
+
data=calm_parts.model_dump(),
|
|
625
|
+
).model_dump()
|
|
626
|
+
)
|
|
627
|
+
except Exception as e:
|
|
628
|
+
structlogger.error("bot_builder_service.get_bot_data.error", error=str(e))
|
|
629
|
+
return response.json(
|
|
630
|
+
ApiErrorResponse(
|
|
631
|
+
error="Failed to retrieve bot data",
|
|
632
|
+
details={"error": str(e)},
|
|
633
|
+
).model_dump(),
|
|
634
|
+
status=500,
|
|
635
|
+
)
|
|
508
636
|
|
|
509
|
-
except Exception as e:
|
|
510
|
-
structlogger.error(
|
|
511
|
-
"bot_builder_service.llm_builder.unexpected_error", error=str(e)
|
|
512
|
-
)
|
|
513
|
-
return response.json(
|
|
514
|
-
ApiErrorResponse(
|
|
515
|
-
error="Unexpected error in LLM builder",
|
|
516
|
-
details=None,
|
|
517
|
-
).model_dump(),
|
|
518
|
-
status=500,
|
|
519
|
-
)
|
|
520
637
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
638
|
+
@bp.route("/llm-builder", methods=["POST"])
|
|
639
|
+
@openapi.summary("LLM assistant for bot building")
|
|
640
|
+
@openapi.description(
|
|
641
|
+
"Provides LLM-powered assistance for bot building tasks, including "
|
|
642
|
+
"debugging, suggestions, and explanations"
|
|
643
|
+
)
|
|
644
|
+
@openapi.tag("llm-assistant")
|
|
645
|
+
@openapi.body(
|
|
646
|
+
{"application/json": model_to_schema(LLMBuilderRequest)},
|
|
647
|
+
description="LLM builder request containing chat messages and context",
|
|
648
|
+
required=True,
|
|
649
|
+
)
|
|
650
|
+
@openapi.response(
|
|
651
|
+
200,
|
|
652
|
+
{"application/json": dict},
|
|
653
|
+
description="LLM response with assistance and suggestions",
|
|
654
|
+
)
|
|
655
|
+
@openapi.response(
|
|
656
|
+
400,
|
|
657
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
658
|
+
description="Validation error in request",
|
|
659
|
+
)
|
|
660
|
+
@openapi.response(
|
|
661
|
+
502,
|
|
662
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
663
|
+
description="LLM generation failed",
|
|
664
|
+
)
|
|
665
|
+
@openapi.response(
|
|
666
|
+
500,
|
|
667
|
+
{"application/json": model_to_schema(ApiErrorResponse)},
|
|
668
|
+
description="Internal server error",
|
|
669
|
+
)
|
|
670
|
+
async def llm_builder(request: Request) -> HTTPResponse:
|
|
671
|
+
"""Handle LLM builder requests."""
|
|
672
|
+
project_generator = get_project_generator(request)
|
|
673
|
+
input_channel = get_input_channel(request)
|
|
674
|
+
|
|
675
|
+
try:
|
|
676
|
+
# Validate request
|
|
677
|
+
builder_request = LLMBuilderRequest(**request.json)
|
|
678
|
+
|
|
679
|
+
# Get current conversation context
|
|
680
|
+
current_tracker = await current_tracker_from_input_channel(
|
|
681
|
+
request.app, input_channel
|
|
545
682
|
)
|
|
683
|
+
bot_logs = get_recent_logs()
|
|
684
|
+
chat_bot_files = project_generator.get_bot_files()
|
|
685
|
+
|
|
686
|
+
# create LLM builder context
|
|
687
|
+
llm_builder_context = LLMBuilderContext(
|
|
688
|
+
tracker=current_tracker,
|
|
689
|
+
bot_logs=bot_logs,
|
|
690
|
+
chat_bot_files=chat_bot_files,
|
|
691
|
+
chat_history=builder_request.messages,
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Generate response
|
|
695
|
+
messages = await llm_service.create_helper_messages(llm_builder_context)
|
|
696
|
+
llm_response = await llm_service.generate_helper_response(messages)
|
|
697
|
+
|
|
698
|
+
return response.json(llm_response)
|
|
546
699
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
700
|
+
except LLMGenerationError as e:
|
|
701
|
+
structlogger.error(
|
|
702
|
+
"bot_builder_service.llm_builder.generation_error", error=str(e)
|
|
703
|
+
)
|
|
704
|
+
return response.json(
|
|
705
|
+
ApiErrorResponse(
|
|
706
|
+
error="LLM helper generation failed", details={"llm_error": str(e)}
|
|
707
|
+
).model_dump(),
|
|
708
|
+
status=502,
|
|
552
709
|
)
|
|
710
|
+
|
|
711
|
+
except Exception as e:
|
|
712
|
+
structlogger.error(
|
|
713
|
+
"bot_builder_service.llm_builder.unexpected_error", error=str(e)
|
|
714
|
+
)
|
|
715
|
+
return response.json(
|
|
716
|
+
ApiErrorResponse(
|
|
717
|
+
error="Unexpected error in LLM builder",
|
|
718
|
+
details=None,
|
|
719
|
+
).model_dump(),
|
|
720
|
+
status=500,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
async def current_tracker_from_input_channel(
|
|
725
|
+
app: Any, input_channel: StudioChatInput
|
|
726
|
+
) -> Optional[DialogueStateTracker]:
|
|
727
|
+
"""Generate chat bot context from current conversation."""
|
|
728
|
+
if app.ctx.agent and input_channel.latest_tracker_session_id:
|
|
729
|
+
return await app.ctx.agent.tracker_store.retrieve(
|
|
730
|
+
input_channel.latest_tracker_session_id
|
|
731
|
+
)
|
|
732
|
+
else:
|
|
733
|
+
return None
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
async def _send_sse_event(sse_response: HTTPResponse, event: ServerSentEvent) -> None:
|
|
737
|
+
"""Send a server-sent event."""
|
|
738
|
+
await sse_response.send(event.format())
|