subactor-shell 0.2.2__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.
- subactor_shell/__init__.py +3 -0
- subactor_shell/__main__.py +4 -0
- subactor_shell/acp_agent.py +363 -0
- subactor_shell/app.py +482 -0
- subactor_shell/artifacts.py +198 -0
- subactor_shell/catalog.py +416 -0
- subactor_shell/chat.py +517 -0
- subactor_shell/compiler.py +191 -0
- subactor_shell/config.py +374 -0
- subactor_shell/connectors.py +503 -0
- subactor_shell/context_builder.py +153 -0
- subactor_shell/control.py +141 -0
- subactor_shell/control_env.py +93 -0
- subactor_shell/intent_ir.py +187 -0
- subactor_shell/models.py +78 -0
- subactor_shell/operations.py +287 -0
- subactor_shell/orchestration.py +324 -0
- subactor_shell/policy.py +38 -0
- subactor_shell/providers/__init__.py +63 -0
- subactor_shell/providers/anthropic.py +101 -0
- subactor_shell/providers/base.py +81 -0
- subactor_shell/providers/mock.py +32 -0
- subactor_shell/providers/openai_compat.py +303 -0
- subactor_shell/providers/subactor_control.py +191 -0
- subactor_shell/redaction.py +62 -0
- subactor_shell/repl.py +480 -0
- subactor_shell/routing.py +334 -0
- subactor_shell/secret_refs.py +82 -0
- subactor_shell/store.py +857 -0
- subactor_shell/terminal.py +79 -0
- subactor_shell/token_budget.py +46 -0
- subactor_shell/vault.py +170 -0
- subactor_shell-0.2.2.dist-info/METADATA +449 -0
- subactor_shell-0.2.2.dist-info/RECORD +38 -0
- subactor_shell-0.2.2.dist-info/WHEEL +5 -0
- subactor_shell-0.2.2.dist-info/entry_points.txt +2 -0
- subactor_shell-0.2.2.dist-info/licenses/LICENSE +13 -0
- subactor_shell-0.2.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import unicodedata
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from difflib import SequenceMatcher
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Iterable
|
|
12
|
+
|
|
13
|
+
from .intent_ir import IntentIR, IntentValidationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_WORD_RE = re.compile(r"[\w.-]+", re.UNICODE)
|
|
17
|
+
_TEMPLATE_FIELD_RE = re.compile(r"\{([A-Za-z][A-Za-z0-9_.-]{0,63})\}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize_text(value: str) -> str:
|
|
21
|
+
value = unicodedata.normalize("NFKC", value).casefold()
|
|
22
|
+
value = re.sub(r"[^\w{}:/.=-]+", " ", value, flags=re.UNICODE)
|
|
23
|
+
return " ".join(value.split())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def text_tokens(value: str) -> list[str]:
|
|
27
|
+
return [token.casefold() for token in _WORD_RE.findall(normalize_text(value)) if token]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(slots=True)
|
|
31
|
+
class IntentDefinition:
|
|
32
|
+
id: str
|
|
33
|
+
description: str
|
|
34
|
+
phrases: list[str]
|
|
35
|
+
required_args: list[str] = field(default_factory=list)
|
|
36
|
+
optional_args: list[str] = field(default_factory=list)
|
|
37
|
+
defaults: dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
execution: dict[str, Any] = field(default_factory=lambda: {"kind": "chat"})
|
|
39
|
+
risk: str = "low"
|
|
40
|
+
constraints: list[str] = field(default_factory=list)
|
|
41
|
+
source: str = "builtin"
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def allowed_args(self) -> set[str]:
|
|
45
|
+
return set(self.required_args) | set(self.optional_args) | set(self.defaults)
|
|
46
|
+
|
|
47
|
+
def to_summary(self) -> dict[str, Any]:
|
|
48
|
+
return {
|
|
49
|
+
"id": self.id,
|
|
50
|
+
"description": self.description,
|
|
51
|
+
"required_args": self.required_args,
|
|
52
|
+
"optional_args": self.optional_args,
|
|
53
|
+
"risk": self.risk,
|
|
54
|
+
"execution_kind": str(self.execution.get("kind", "chat")),
|
|
55
|
+
"phrases": self.phrases[:6],
|
|
56
|
+
"source": self.source,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def validate_ir(self, payload: IntentIR | dict[str, Any]) -> IntentIR:
|
|
60
|
+
ir = payload if isinstance(payload, IntentIR) else IntentIR.from_dict(payload)
|
|
61
|
+
if ir.intent_id != self.id:
|
|
62
|
+
raise IntentValidationError("IntentIR nie odpowiada wybranej definicji")
|
|
63
|
+
unknown = set(ir.args) - self.allowed_args
|
|
64
|
+
if unknown:
|
|
65
|
+
raise IntentValidationError(
|
|
66
|
+
"IntentIR zawiera niedozwolone argumenty: " + ", ".join(sorted(unknown))
|
|
67
|
+
)
|
|
68
|
+
args = {**self.defaults, **ir.args}
|
|
69
|
+
missing = [name for name in self.required_args if name not in args or args[name] in ("", None)]
|
|
70
|
+
unresolved = list(dict.fromkeys([*ir.unresolved, *missing]))
|
|
71
|
+
constraints = list(dict.fromkeys([*self.constraints, *ir.constraints]))
|
|
72
|
+
return IntentIR(
|
|
73
|
+
v=1,
|
|
74
|
+
intent_id=ir.intent_id,
|
|
75
|
+
mode=ir.mode,
|
|
76
|
+
args=args,
|
|
77
|
+
requirements=ir.requirements,
|
|
78
|
+
constraints=constraints,
|
|
79
|
+
unresolved=unresolved,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass(slots=True)
|
|
84
|
+
class CandidateMatch:
|
|
85
|
+
intent: IntentDefinition
|
|
86
|
+
score: float
|
|
87
|
+
matched_phrase: str
|
|
88
|
+
extracted_args: dict[str, str] = field(default_factory=dict)
|
|
89
|
+
exact: bool = False
|
|
90
|
+
|
|
91
|
+
def to_dict(self) -> dict[str, Any]:
|
|
92
|
+
return {
|
|
93
|
+
"intent_id": self.intent.id,
|
|
94
|
+
"score": round(self.score, 6),
|
|
95
|
+
"matched_phrase": self.matched_phrase,
|
|
96
|
+
"extracted_args": self.extracted_args,
|
|
97
|
+
"exact": self.exact,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class IntentCatalog:
|
|
102
|
+
def __init__(self, intents: Iterable[IntentDefinition]):
|
|
103
|
+
self._by_id: dict[str, IntentDefinition] = {}
|
|
104
|
+
for intent in intents:
|
|
105
|
+
if intent.id:
|
|
106
|
+
self._by_id[intent.id] = intent
|
|
107
|
+
serialized = json.dumps(
|
|
108
|
+
[item.to_summary() | {"execution": item.execution} for item in self.list()],
|
|
109
|
+
ensure_ascii=False,
|
|
110
|
+
sort_keys=True,
|
|
111
|
+
separators=(",", ":"),
|
|
112
|
+
)
|
|
113
|
+
self.fingerprint = hashlib.sha256(serialized.encode("utf-8")).hexdigest()
|
|
114
|
+
|
|
115
|
+
def get(self, intent_id: str) -> IntentDefinition | None:
|
|
116
|
+
return self._by_id.get(intent_id)
|
|
117
|
+
|
|
118
|
+
def list(self) -> list[IntentDefinition]:
|
|
119
|
+
return [self._by_id[key] for key in sorted(self._by_id)]
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def load(cls, paths: Iterable[Path] = ()) -> "IntentCatalog":
|
|
123
|
+
intents = {item.id: item for item in builtin_intents()}
|
|
124
|
+
for path in paths:
|
|
125
|
+
for definition in load_intent_definitions(path):
|
|
126
|
+
intents[definition.id] = definition
|
|
127
|
+
return cls(intents.values())
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CandidateRetriever:
|
|
131
|
+
def __init__(self, catalog: IntentCatalog):
|
|
132
|
+
self.catalog = catalog
|
|
133
|
+
self._docs: list[tuple[IntentDefinition, str, list[str]]] = []
|
|
134
|
+
document_frequency: dict[str, int] = {}
|
|
135
|
+
for intent in catalog.list():
|
|
136
|
+
phrases = intent.phrases or [intent.description, intent.id]
|
|
137
|
+
for phrase in phrases:
|
|
138
|
+
tokens = text_tokens(_TEMPLATE_FIELD_RE.sub(" value ", phrase))
|
|
139
|
+
self._docs.append((intent, phrase, tokens))
|
|
140
|
+
for token in set(tokens):
|
|
141
|
+
document_frequency[token] = document_frequency.get(token, 0) + 1
|
|
142
|
+
count = max(1, len(self._docs))
|
|
143
|
+
self._idf = {
|
|
144
|
+
token: math.log((count + 1) / (frequency + 0.5)) + 1.0
|
|
145
|
+
for token, frequency in document_frequency.items()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
def retrieve(self, text: str, *, top_k: int = 5) -> list[CandidateMatch]:
|
|
149
|
+
normalized = normalize_text(text)
|
|
150
|
+
query_tokens = text_tokens(normalized)
|
|
151
|
+
query_set = set(query_tokens)
|
|
152
|
+
best: dict[str, CandidateMatch] = {}
|
|
153
|
+
for intent, phrase, phrase_tokens in self._docs:
|
|
154
|
+
extracted = match_phrase_template(text, phrase)
|
|
155
|
+
normalized_phrase = normalize_text(_TEMPLATE_FIELD_RE.sub(" value ", phrase))
|
|
156
|
+
literal_exact = normalize_text(text) == normalize_text(phrase)
|
|
157
|
+
exact = literal_exact or extracted is not None
|
|
158
|
+
if exact:
|
|
159
|
+
score = 1.0 if literal_exact else 0.985
|
|
160
|
+
else:
|
|
161
|
+
phrase_set = set(phrase_tokens)
|
|
162
|
+
shared = query_set & phrase_set
|
|
163
|
+
query_weight = sum(self._idf.get(token, 1.0) for token in query_set) or 1.0
|
|
164
|
+
overlap = sum(self._idf.get(token, 1.0) for token in shared) / query_weight
|
|
165
|
+
sequence = SequenceMatcher(None, normalized, normalized_phrase).ratio()
|
|
166
|
+
containment = 1.0 if normalized_phrase and normalized_phrase in normalized else 0.0
|
|
167
|
+
coverage = len(shared) / max(1, len(phrase_set))
|
|
168
|
+
score = 0.48 * overlap + 0.24 * sequence + 0.18 * coverage + 0.10 * containment
|
|
169
|
+
candidate = CandidateMatch(
|
|
170
|
+
intent=intent,
|
|
171
|
+
score=max(0.0, min(1.0, score)),
|
|
172
|
+
matched_phrase=phrase,
|
|
173
|
+
extracted_args=extracted or {},
|
|
174
|
+
exact=exact,
|
|
175
|
+
)
|
|
176
|
+
previous = best.get(intent.id)
|
|
177
|
+
if previous is None or candidate.score > previous.score:
|
|
178
|
+
best[intent.id] = candidate
|
|
179
|
+
return sorted(best.values(), key=lambda item: (-item.score, item.intent.id))[: max(1, top_k)]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def match_phrase_template(text: str, phrase: str) -> dict[str, str] | None:
|
|
183
|
+
fields = list(_TEMPLATE_FIELD_RE.finditer(phrase))
|
|
184
|
+
if not fields:
|
|
185
|
+
return {} if normalize_text(text) == normalize_text(phrase) else None
|
|
186
|
+
parts: list[str] = []
|
|
187
|
+
cursor = 0
|
|
188
|
+
for index, match in enumerate(fields):
|
|
189
|
+
literal = phrase[cursor : match.start()]
|
|
190
|
+
escaped = re.escape(literal).replace(r"\ ", r"\s+")
|
|
191
|
+
parts.append(escaped)
|
|
192
|
+
parts.append(fr"(?P<f{index}>.+?)")
|
|
193
|
+
cursor = match.end()
|
|
194
|
+
parts.append(re.escape(phrase[cursor:]).replace(r"\ ", r"\s+"))
|
|
195
|
+
matched = re.match(r"^\s*" + "".join(parts) + r"\s*$", text, flags=re.IGNORECASE | re.UNICODE)
|
|
196
|
+
if not matched:
|
|
197
|
+
return None
|
|
198
|
+
result: dict[str, str] = {}
|
|
199
|
+
for index, field_match in enumerate(fields):
|
|
200
|
+
value = matched.group(f"f{index}").strip().strip("\"'")
|
|
201
|
+
if value:
|
|
202
|
+
result[field_match.group(1)] = value
|
|
203
|
+
return result
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def builtin_intents() -> list[IntentDefinition]:
|
|
207
|
+
return [
|
|
208
|
+
IntentDefinition(
|
|
209
|
+
id="bridge.help",
|
|
210
|
+
description="Pokaż możliwości i bezpieczne komendy Subactor Shell Bridge.",
|
|
211
|
+
phrases=["pomoc", "pokaż pomoc", "co potrafisz", "help"],
|
|
212
|
+
execution={"kind": "builtin", "operation": "bridge.help", "effect": "read"},
|
|
213
|
+
),
|
|
214
|
+
IntentDefinition(
|
|
215
|
+
id="session.list",
|
|
216
|
+
description="Pokaż zapisane sesje rozmów.",
|
|
217
|
+
phrases=["pokaż sesje", "lista sesji", "ostatnie rozmowy", "wymień sesje"],
|
|
218
|
+
optional_args=["limit"],
|
|
219
|
+
defaults={"limit": 20},
|
|
220
|
+
execution={
|
|
221
|
+
"kind": "builtin",
|
|
222
|
+
"operation": "session.list",
|
|
223
|
+
"effect": "read",
|
|
224
|
+
"argument_map": {"limit": "$args.limit"},
|
|
225
|
+
},
|
|
226
|
+
),
|
|
227
|
+
IntentDefinition(
|
|
228
|
+
id="data.list",
|
|
229
|
+
description="Pokaż nazwy jawnych danych i artefaktów bez rozwijania treści.",
|
|
230
|
+
phrases=["pokaż dane", "lista danych", "jakie dane są zapisane"],
|
|
231
|
+
execution={"kind": "builtin", "operation": "data.list", "effect": "read"},
|
|
232
|
+
),
|
|
233
|
+
IntentDefinition(
|
|
234
|
+
id="secret.list",
|
|
235
|
+
description="Pokaż aliasy i referencje sekretów bez odczytywania wartości.",
|
|
236
|
+
phrases=["pokaż bindingi sekretów", "lista sekretów", "jakie sekrety są podpięte"],
|
|
237
|
+
execution={"kind": "builtin", "operation": "secret.list", "effect": "read"},
|
|
238
|
+
constraints=["no_secret_export"],
|
|
239
|
+
),
|
|
240
|
+
IntentDefinition(
|
|
241
|
+
id="usage.summary",
|
|
242
|
+
description="Pokaż telemetryczne zużycie tokenów i udział tras lokalnych.",
|
|
243
|
+
phrases=["pokaż zużycie tokenów", "statystyki tokenów", "metryki llm", "koszt tokenów"],
|
|
244
|
+
execution={"kind": "builtin", "operation": "usage.summary", "effect": "read"},
|
|
245
|
+
),
|
|
246
|
+
IntentDefinition(
|
|
247
|
+
id="control.status",
|
|
248
|
+
description="Sprawdź status istniejącego Subactor Control przez cli.status.",
|
|
249
|
+
phrases=[
|
|
250
|
+
"pokaż status subactora",
|
|
251
|
+
"sprawdź status subactora",
|
|
252
|
+
"status control",
|
|
253
|
+
"jakie zadania są otwarte",
|
|
254
|
+
"jakie zadania sa otwarte",
|
|
255
|
+
"pokaż otwarte zadania",
|
|
256
|
+
"pokaz otwarte zadania",
|
|
257
|
+
"co teraz",
|
|
258
|
+
],
|
|
259
|
+
execution={
|
|
260
|
+
"kind": "connector",
|
|
261
|
+
"connector": "subactor_cli",
|
|
262
|
+
"operation": "cli.status",
|
|
263
|
+
"effect": "read",
|
|
264
|
+
},
|
|
265
|
+
),
|
|
266
|
+
IntentDefinition(
|
|
267
|
+
id="control.plan",
|
|
268
|
+
description="Utwórz plan w istniejącym Subactor Control przez cli.plan.",
|
|
269
|
+
phrases=["zaplanuj {request}", "przygotuj plan {request}", "stwórz plan {request}"],
|
|
270
|
+
required_args=["request"],
|
|
271
|
+
execution={
|
|
272
|
+
"kind": "connector",
|
|
273
|
+
"connector": "subactor_control",
|
|
274
|
+
"operation": "cli.plan",
|
|
275
|
+
"effect": "read",
|
|
276
|
+
"argument_map": {"request": "$args.request"},
|
|
277
|
+
},
|
|
278
|
+
),
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def load_intent_definitions(path: Path) -> list[IntentDefinition]:
|
|
283
|
+
path = path.expanduser()
|
|
284
|
+
if not path.exists():
|
|
285
|
+
return []
|
|
286
|
+
files = [path]
|
|
287
|
+
if path.is_dir():
|
|
288
|
+
files = sorted(item for item in path.rglob("*.json") if "schema" not in item.name.casefold())
|
|
289
|
+
result: list[IntentDefinition] = []
|
|
290
|
+
for file_path in files:
|
|
291
|
+
if not file_path.is_file():
|
|
292
|
+
continue
|
|
293
|
+
try:
|
|
294
|
+
payload = json.loads(file_path.read_text(encoding="utf-8"))
|
|
295
|
+
except (OSError, json.JSONDecodeError):
|
|
296
|
+
continue
|
|
297
|
+
if isinstance(payload, dict) and isinstance(payload.get("intents"), list):
|
|
298
|
+
entries: list[Any] = payload["intents"]
|
|
299
|
+
elif isinstance(payload, list):
|
|
300
|
+
entries = payload
|
|
301
|
+
else:
|
|
302
|
+
entries = [payload]
|
|
303
|
+
for entry in entries:
|
|
304
|
+
definition = _definition_from_payload(entry, source=str(file_path))
|
|
305
|
+
if definition:
|
|
306
|
+
result.append(definition)
|
|
307
|
+
return result
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _nested(payload: dict[str, Any], *paths: str) -> Any:
|
|
311
|
+
for path in paths:
|
|
312
|
+
current: Any = payload
|
|
313
|
+
found = True
|
|
314
|
+
for segment in path.split("."):
|
|
315
|
+
if not isinstance(current, dict) or segment not in current:
|
|
316
|
+
found = False
|
|
317
|
+
break
|
|
318
|
+
current = current[segment]
|
|
319
|
+
if found:
|
|
320
|
+
return current
|
|
321
|
+
return None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _string_list(value: Any) -> list[str]:
|
|
325
|
+
if isinstance(value, str):
|
|
326
|
+
return [value]
|
|
327
|
+
if isinstance(value, list):
|
|
328
|
+
return [str(item) for item in value if isinstance(item, (str, int, float)) and str(item).strip()]
|
|
329
|
+
return []
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _definition_from_payload(payload: Any, *, source: str) -> IntentDefinition | None:
|
|
333
|
+
if not isinstance(payload, dict):
|
|
334
|
+
return None
|
|
335
|
+
intent_id = _nested(
|
|
336
|
+
payload,
|
|
337
|
+
"intent_id",
|
|
338
|
+
"intentId",
|
|
339
|
+
"id",
|
|
340
|
+
"name",
|
|
341
|
+
"nlp_uri",
|
|
342
|
+
"nlpUri",
|
|
343
|
+
"uri",
|
|
344
|
+
"nlp.uri",
|
|
345
|
+
)
|
|
346
|
+
if not isinstance(intent_id, str) or not intent_id.strip():
|
|
347
|
+
return None
|
|
348
|
+
intent_id = intent_id.strip()
|
|
349
|
+
description = str(_nested(payload, "description", "summary", "title", "nlp.description") or intent_id)
|
|
350
|
+
phrases: list[str] = []
|
|
351
|
+
for key in ("phrases", "createPhrases", "examples", "utterances", "nlp.phrases", "nlp.createPhrases"):
|
|
352
|
+
phrases.extend(_string_list(_nested(payload, key)))
|
|
353
|
+
phrases = list(dict.fromkeys(item.strip() for item in phrases if item.strip())) or [description, intent_id]
|
|
354
|
+
|
|
355
|
+
required = _string_list(
|
|
356
|
+
_nested(
|
|
357
|
+
payload,
|
|
358
|
+
"required_args",
|
|
359
|
+
"requiredArgs",
|
|
360
|
+
"required",
|
|
361
|
+
"situation_schema.required",
|
|
362
|
+
"situationSchema.required",
|
|
363
|
+
"input_schema.required",
|
|
364
|
+
"inputSchema.required",
|
|
365
|
+
)
|
|
366
|
+
)
|
|
367
|
+
properties = _nested(
|
|
368
|
+
payload,
|
|
369
|
+
"situation_schema.properties",
|
|
370
|
+
"situationSchema.properties",
|
|
371
|
+
"input_schema.properties",
|
|
372
|
+
"inputSchema.properties",
|
|
373
|
+
"args_schema.properties",
|
|
374
|
+
)
|
|
375
|
+
optional = [str(key) for key in properties if str(key) not in required] if isinstance(properties, dict) else []
|
|
376
|
+
optional.extend(_string_list(_nested(payload, "optional_args", "optionalArgs")))
|
|
377
|
+
optional = list(dict.fromkeys(optional))
|
|
378
|
+
|
|
379
|
+
execution = _nested(payload, "execution", "runtime.execution")
|
|
380
|
+
if not isinstance(execution, dict):
|
|
381
|
+
connector = _nested(payload, "connector", "connector_id", "connectorId")
|
|
382
|
+
operation = _nested(payload, "operation", "operation_id", "operationId")
|
|
383
|
+
recipe = _nested(payload, "recipe", "recipe_uri", "recipeUri", "urirun")
|
|
384
|
+
if connector and operation:
|
|
385
|
+
execution = {
|
|
386
|
+
"kind": "connector",
|
|
387
|
+
"connector": str(connector),
|
|
388
|
+
"operation": str(operation),
|
|
389
|
+
"effect": str(_nested(payload, "effect") or "external_write"),
|
|
390
|
+
}
|
|
391
|
+
elif recipe:
|
|
392
|
+
execution = {
|
|
393
|
+
"kind": "process_pack",
|
|
394
|
+
"connector": str(connector or "process_pack"),
|
|
395
|
+
"operation": str(recipe),
|
|
396
|
+
"effect": str(_nested(payload, "effect") or "external_write"),
|
|
397
|
+
}
|
|
398
|
+
else:
|
|
399
|
+
execution = {"kind": "chat"}
|
|
400
|
+
defaults = _nested(payload, "defaults", "nlp.defaults")
|
|
401
|
+
if not isinstance(defaults, dict):
|
|
402
|
+
defaults = {}
|
|
403
|
+
constraints = _string_list(_nested(payload, "constraints", "policy.constraints"))
|
|
404
|
+
risk = str(_nested(payload, "risk", "risk_class", "riskClass") or "low")
|
|
405
|
+
return IntentDefinition(
|
|
406
|
+
id=intent_id,
|
|
407
|
+
description=description,
|
|
408
|
+
phrases=phrases,
|
|
409
|
+
required_args=required,
|
|
410
|
+
optional_args=optional,
|
|
411
|
+
defaults=defaults,
|
|
412
|
+
execution=dict(execution),
|
|
413
|
+
risk=risk,
|
|
414
|
+
constraints=constraints,
|
|
415
|
+
source=source,
|
|
416
|
+
)
|