applied-cli 0.1.0__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.
- applied_cli/__init__.py +2 -0
- applied_cli/auth_store.py +263 -0
- applied_cli/commands/__init__.py +2 -0
- applied_cli/commands/_hints.py +11 -0
- applied_cli/commands/_normalize.py +79 -0
- applied_cli/commands/_parsers.py +58 -0
- applied_cli/commands/_ui.py +33 -0
- applied_cli/commands/agent.py +1231 -0
- applied_cli/commands/auth.py +739 -0
- applied_cli/commands/chat.py +379 -0
- applied_cli/commands/coverage.py +348 -0
- applied_cli/commands/discover.py +1006 -0
- applied_cli/commands/fix.py +1204 -0
- applied_cli/commands/insights.py +614 -0
- applied_cli/commands/intents.py +447 -0
- applied_cli/commands/rate.py +508 -0
- applied_cli/commands/responses.py +604 -0
- applied_cli/commands/shop.py +1757 -0
- applied_cli/commands/simulate.py +330 -0
- applied_cli/commands/spec.py +238 -0
- applied_cli/config.py +50 -0
- applied_cli/error_reporting.py +38 -0
- applied_cli/http.py +1614 -0
- applied_cli/main.py +90 -0
- applied_cli/mcp_server.py +738 -0
- applied_cli/presets/demo.yaml +170 -0
- applied_cli/runtime.py +53 -0
- applied_cli/shop_spec.py +398 -0
- applied_cli/spec_workflow.py +432 -0
- applied_cli-0.1.0.dist-info/METADATA +176 -0
- applied_cli-0.1.0.dist-info/RECORD +34 -0
- applied_cli-0.1.0.dist-info/WHEEL +5 -0
- applied_cli-0.1.0.dist-info/entry_points.txt +3 -0
- applied_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import uuid
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
from applied_cli.commands._normalize import (
|
|
8
|
+
AGENT_TYPE_ALIASES,
|
|
9
|
+
MODALITY_ALIASES,
|
|
10
|
+
RESPONSE_TYPE_ALIASES,
|
|
11
|
+
)
|
|
12
|
+
from applied_cli.http import (
|
|
13
|
+
APIError,
|
|
14
|
+
create_agent,
|
|
15
|
+
create_response,
|
|
16
|
+
list_responses,
|
|
17
|
+
patch_response,
|
|
18
|
+
update_agent,
|
|
19
|
+
)
|
|
20
|
+
from applied_cli.runtime import resolve_runtime as _resolve_runtime
|
|
21
|
+
|
|
22
|
+
ALLOWED_RESPONSE_TYPES = {"escalation", "exact", "qa", "context"}
|
|
23
|
+
ALLOWED_CHANNELS = {"chat", "email", "sms"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _spec_error(path: str, message: str) -> None:
|
|
27
|
+
raise ValueError(f"spec.{path}: {message}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _validate_string(value: Any, *, path: str, required: bool = False) -> str:
|
|
31
|
+
if value is None:
|
|
32
|
+
if required:
|
|
33
|
+
_spec_error(path, "is required")
|
|
34
|
+
return ""
|
|
35
|
+
if not isinstance(value, str):
|
|
36
|
+
_spec_error(path, "must be a string")
|
|
37
|
+
normalized = value.strip()
|
|
38
|
+
if required and not normalized:
|
|
39
|
+
_spec_error(path, "cannot be empty")
|
|
40
|
+
return normalized
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _validate_string_list(value: Any, *, path: str) -> list[str]:
|
|
44
|
+
if value is None:
|
|
45
|
+
return []
|
|
46
|
+
if not isinstance(value, list):
|
|
47
|
+
_spec_error(path, "must be an array of strings")
|
|
48
|
+
out: list[str] = []
|
|
49
|
+
for idx, item in enumerate(value):
|
|
50
|
+
if not isinstance(item, str):
|
|
51
|
+
_spec_error(f"{path}[{idx}]", "must be a string")
|
|
52
|
+
normalized = item.strip()
|
|
53
|
+
if not normalized:
|
|
54
|
+
_spec_error(f"{path}[{idx}]", "cannot be empty")
|
|
55
|
+
out.append(normalized)
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _validate_bool(value: Any, *, path: str, default: bool) -> bool:
|
|
60
|
+
if value is None:
|
|
61
|
+
return default
|
|
62
|
+
if not isinstance(value, bool):
|
|
63
|
+
_spec_error(path, "must be a boolean")
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _validate_dict(value: Any, *, path: str) -> dict[str, Any]:
|
|
68
|
+
if value is None:
|
|
69
|
+
return {}
|
|
70
|
+
if not isinstance(value, dict):
|
|
71
|
+
_spec_error(path, "must be an object")
|
|
72
|
+
return value
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_question(text: str) -> str:
|
|
76
|
+
return re.sub(r"\s+", " ", text.strip().lower())
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_and_validate_spec(spec_path: str) -> dict[str, Any]:
|
|
80
|
+
try:
|
|
81
|
+
raw_text = Path(spec_path).read_text(encoding="utf-8")
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
raise ValueError(f"spec: unable to read file ({exc})") from exc
|
|
84
|
+
try:
|
|
85
|
+
payload = json.loads(raw_text)
|
|
86
|
+
except json.JSONDecodeError as exc:
|
|
87
|
+
raise ValueError(f"spec: invalid JSON ({exc})") from exc
|
|
88
|
+
if not isinstance(payload, dict):
|
|
89
|
+
_spec_error("", "root must be an object")
|
|
90
|
+
|
|
91
|
+
agent_raw = payload.get("agent")
|
|
92
|
+
if not isinstance(agent_raw, dict):
|
|
93
|
+
_spec_error("agent", "is required and must be an object")
|
|
94
|
+
|
|
95
|
+
agent: dict[str, Any] = {
|
|
96
|
+
"name": _validate_string(agent_raw.get("name"), path="agent.name", required=True),
|
|
97
|
+
"modality": _validate_string(
|
|
98
|
+
agent_raw.get("modality"), path="agent.modality", required=True
|
|
99
|
+
),
|
|
100
|
+
}
|
|
101
|
+
modality_key = agent["modality"].strip().lower()
|
|
102
|
+
if modality_key not in MODALITY_ALIASES:
|
|
103
|
+
_spec_error(
|
|
104
|
+
"agent.modality",
|
|
105
|
+
"must be one of: all, call, sms, email, chat, internal",
|
|
106
|
+
)
|
|
107
|
+
agent["modality"] = MODALITY_ALIASES[modality_key]
|
|
108
|
+
for field in (
|
|
109
|
+
"description",
|
|
110
|
+
"model",
|
|
111
|
+
"type",
|
|
112
|
+
"email",
|
|
113
|
+
"phone",
|
|
114
|
+
"escalation_mode",
|
|
115
|
+
"escalation_wait_time_mode",
|
|
116
|
+
"prompt",
|
|
117
|
+
"guardrail",
|
|
118
|
+
):
|
|
119
|
+
if field in agent_raw and agent_raw.get(field) is not None:
|
|
120
|
+
agent[field] = _validate_string(
|
|
121
|
+
agent_raw.get(field), path=f"agent.{field}", required=False
|
|
122
|
+
)
|
|
123
|
+
if "type" in agent:
|
|
124
|
+
normalized_type_key = agent["type"].strip().lower()
|
|
125
|
+
if normalized_type_key not in AGENT_TYPE_ALIASES:
|
|
126
|
+
_spec_error(
|
|
127
|
+
"agent.type",
|
|
128
|
+
"must be one of: generic, customer_support, conversion, sales, marketing, engineering, product",
|
|
129
|
+
)
|
|
130
|
+
agent["type"] = AGENT_TYPE_ALIASES[normalized_type_key]
|
|
131
|
+
if "metadata" in agent_raw:
|
|
132
|
+
agent["metadata"] = _validate_dict(agent_raw.get("metadata"), path="agent.metadata")
|
|
133
|
+
if "suggestions" in agent_raw:
|
|
134
|
+
agent["suggestions"] = _validate_string_list(
|
|
135
|
+
agent_raw.get("suggestions"), path="agent.suggestions"
|
|
136
|
+
)
|
|
137
|
+
if "personality" in agent_raw:
|
|
138
|
+
agent["personality"] = _validate_string_list(
|
|
139
|
+
agent_raw.get("personality"), path="agent.personality"
|
|
140
|
+
)
|
|
141
|
+
for bool_field in ("auto_reply", "use_guardrails"):
|
|
142
|
+
if bool_field in agent_raw:
|
|
143
|
+
agent[bool_field] = _validate_bool(
|
|
144
|
+
agent_raw.get(bool_field), path=f"agent.{bool_field}", default=False
|
|
145
|
+
)
|
|
146
|
+
if "response_delay_in_seconds" in agent_raw:
|
|
147
|
+
delay = agent_raw.get("response_delay_in_seconds")
|
|
148
|
+
if not isinstance(delay, int) or delay < 0:
|
|
149
|
+
_spec_error("agent.response_delay_in_seconds", "must be a non-negative integer")
|
|
150
|
+
agent["response_delay_in_seconds"] = delay
|
|
151
|
+
|
|
152
|
+
responses_raw = payload.get("responses", [])
|
|
153
|
+
if responses_raw is None:
|
|
154
|
+
responses_raw = []
|
|
155
|
+
if not isinstance(responses_raw, list):
|
|
156
|
+
_spec_error("responses", "must be an array")
|
|
157
|
+
responses: list[dict[str, Any]] = []
|
|
158
|
+
for idx, response in enumerate(responses_raw):
|
|
159
|
+
if not isinstance(response, dict):
|
|
160
|
+
_spec_error(f"responses[{idx}]", "must be an object")
|
|
161
|
+
response_type = _validate_string(
|
|
162
|
+
response.get("type"), path=f"responses[{idx}].type", required=True
|
|
163
|
+
).lower()
|
|
164
|
+
if response_type not in ALLOWED_RESPONSE_TYPES:
|
|
165
|
+
_spec_error(
|
|
166
|
+
f"responses[{idx}].type",
|
|
167
|
+
f"must be one of: {', '.join(sorted(ALLOWED_RESPONSE_TYPES))}",
|
|
168
|
+
)
|
|
169
|
+
question = _validate_string(
|
|
170
|
+
response.get("question"), path=f"responses[{idx}].question", required=True
|
|
171
|
+
)
|
|
172
|
+
answer = _validate_string(
|
|
173
|
+
response.get("answer"), path=f"responses[{idx}].answer", required=True
|
|
174
|
+
)
|
|
175
|
+
entry: dict[str, Any] = {
|
|
176
|
+
"type": RESPONSE_TYPE_ALIASES[response_type],
|
|
177
|
+
"question": question,
|
|
178
|
+
"answer": answer,
|
|
179
|
+
"active": _validate_bool(
|
|
180
|
+
response.get("active"), path=f"responses[{idx}].active", default=True
|
|
181
|
+
),
|
|
182
|
+
}
|
|
183
|
+
if "guardrail" in response:
|
|
184
|
+
entry["guardrail"] = _validate_string(
|
|
185
|
+
response.get("guardrail"), path=f"responses[{idx}].guardrail"
|
|
186
|
+
)
|
|
187
|
+
if "fields_to_extract" in response:
|
|
188
|
+
fields = response.get("fields_to_extract")
|
|
189
|
+
if not isinstance(fields, list):
|
|
190
|
+
_spec_error(f"responses[{idx}].fields_to_extract", "must be an array")
|
|
191
|
+
entry["fields_to_extract"] = fields
|
|
192
|
+
responses.append(entry)
|
|
193
|
+
|
|
194
|
+
tests_raw = payload.get("tests", [])
|
|
195
|
+
if tests_raw is None:
|
|
196
|
+
tests_raw = []
|
|
197
|
+
if not isinstance(tests_raw, list):
|
|
198
|
+
_spec_error("tests", "must be an array")
|
|
199
|
+
tests: list[dict[str, Any]] = []
|
|
200
|
+
for idx, test in enumerate(tests_raw):
|
|
201
|
+
if not isinstance(test, dict):
|
|
202
|
+
_spec_error(f"tests[{idx}]", "must be an object")
|
|
203
|
+
channel = _validate_string(
|
|
204
|
+
test.get("channel"), path=f"tests[{idx}].channel", required=True
|
|
205
|
+
).lower()
|
|
206
|
+
if channel not in ALLOWED_CHANNELS:
|
|
207
|
+
_spec_error(
|
|
208
|
+
f"tests[{idx}].channel",
|
|
209
|
+
f"must be one of: {', '.join(sorted(ALLOWED_CHANNELS))}",
|
|
210
|
+
)
|
|
211
|
+
turns = _validate_string_list(test.get("turns"), path=f"tests[{idx}].turns")
|
|
212
|
+
if not turns:
|
|
213
|
+
_spec_error(f"tests[{idx}].turns", "must include at least one message")
|
|
214
|
+
test_entry: dict[str, Any] = {
|
|
215
|
+
"name": _validate_string(
|
|
216
|
+
test.get("name"), path=f"tests[{idx}].name", required=False
|
|
217
|
+
)
|
|
218
|
+
or f"test-{idx + 1}",
|
|
219
|
+
"channel": channel,
|
|
220
|
+
"turns": turns,
|
|
221
|
+
"include_references": _validate_bool(
|
|
222
|
+
test.get("include_references"),
|
|
223
|
+
path=f"tests[{idx}].include_references",
|
|
224
|
+
default=True,
|
|
225
|
+
),
|
|
226
|
+
"auto_rate": _validate_bool(
|
|
227
|
+
test.get("auto_rate"), path=f"tests[{idx}].auto_rate", default=True
|
|
228
|
+
),
|
|
229
|
+
}
|
|
230
|
+
for text_field in ("pass_status", "feedback", "reference_notes"):
|
|
231
|
+
if text_field in test and test.get(text_field) is not None:
|
|
232
|
+
test_entry[text_field] = _validate_string(
|
|
233
|
+
test.get(text_field), path=f"tests[{idx}].{text_field}"
|
|
234
|
+
)
|
|
235
|
+
for float_field in ("csat_score", "reference_score"):
|
|
236
|
+
if float_field in test and test.get(float_field) is not None:
|
|
237
|
+
value = test.get(float_field)
|
|
238
|
+
if not isinstance(value, (int, float)):
|
|
239
|
+
_spec_error(f"tests[{idx}].{float_field}", "must be a number")
|
|
240
|
+
test_entry[float_field] = float(value)
|
|
241
|
+
tests.append(test_entry)
|
|
242
|
+
|
|
243
|
+
benchmark_name = _validate_string(
|
|
244
|
+
payload.get("benchmark_name"), path="benchmark_name", required=False
|
|
245
|
+
)
|
|
246
|
+
return {
|
|
247
|
+
"agent": agent,
|
|
248
|
+
"responses": responses,
|
|
249
|
+
"tests": tests,
|
|
250
|
+
"benchmark_name": benchmark_name or "CLI Self-Rated Conversations",
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def resolve_runtime(
|
|
255
|
+
*,
|
|
256
|
+
base_url: Optional[str],
|
|
257
|
+
shop_id: Optional[str],
|
|
258
|
+
api_token: Optional[str],
|
|
259
|
+
) -> tuple[str, str, str]:
|
|
260
|
+
# Compatibility wrapper; prefer importing from applied_cli.runtime directly.
|
|
261
|
+
return _resolve_runtime(base_url=base_url, shop_id=shop_id, api_token=api_token)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def build_agent_payload(spec: dict[str, Any]) -> dict[str, Any]:
|
|
265
|
+
agent = dict(spec["agent"])
|
|
266
|
+
payload: dict[str, Any] = {
|
|
267
|
+
"name": agent["name"],
|
|
268
|
+
"modality": agent["modality"],
|
|
269
|
+
}
|
|
270
|
+
for key in (
|
|
271
|
+
"description",
|
|
272
|
+
"model",
|
|
273
|
+
"type",
|
|
274
|
+
"email",
|
|
275
|
+
"phone",
|
|
276
|
+
"metadata",
|
|
277
|
+
"suggestions",
|
|
278
|
+
"personality",
|
|
279
|
+
"auto_reply",
|
|
280
|
+
"use_guardrails",
|
|
281
|
+
"escalation_mode",
|
|
282
|
+
"escalation_wait_time_mode",
|
|
283
|
+
"response_delay_in_seconds",
|
|
284
|
+
"prompt",
|
|
285
|
+
"guardrail",
|
|
286
|
+
):
|
|
287
|
+
if key in agent:
|
|
288
|
+
payload[key] = agent[key]
|
|
289
|
+
return payload
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _agent_attached(response_row: dict[str, Any], *, agent_id: str) -> bool:
|
|
293
|
+
agents = response_row.get("agents")
|
|
294
|
+
if not isinstance(agents, list):
|
|
295
|
+
return False
|
|
296
|
+
for item in agents:
|
|
297
|
+
if isinstance(item, dict) and str(item.get("id")) == agent_id:
|
|
298
|
+
return True
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _response_payload(spec_response: dict[str, Any], *, agent_id: str) -> dict[str, Any]:
|
|
303
|
+
payload: dict[str, Any] = {
|
|
304
|
+
"type": spec_response["type"],
|
|
305
|
+
"question": spec_response["question"],
|
|
306
|
+
"answer": spec_response["answer"],
|
|
307
|
+
"active": bool(spec_response.get("active", True)),
|
|
308
|
+
"agent_ids": [agent_id],
|
|
309
|
+
}
|
|
310
|
+
if spec_response.get("guardrail"):
|
|
311
|
+
payload["guardrail"] = spec_response["guardrail"]
|
|
312
|
+
if "fields_to_extract" in spec_response:
|
|
313
|
+
payload["fields_to_extract"] = spec_response["fields_to_extract"]
|
|
314
|
+
return payload
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def upsert_spec_responses(
|
|
318
|
+
*,
|
|
319
|
+
base_url: str,
|
|
320
|
+
shop_id: str,
|
|
321
|
+
api_token: str,
|
|
322
|
+
agent_id: str,
|
|
323
|
+
responses: list[dict[str, Any]],
|
|
324
|
+
dry_run: bool = False,
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
if dry_run:
|
|
327
|
+
try:
|
|
328
|
+
uuid.UUID(agent_id)
|
|
329
|
+
except ValueError:
|
|
330
|
+
return {
|
|
331
|
+
"created_ids": [],
|
|
332
|
+
"updated_ids": [],
|
|
333
|
+
"created_count": len(responses),
|
|
334
|
+
"updated_count": 0,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
existing = list_responses(
|
|
338
|
+
base_url=base_url,
|
|
339
|
+
shop_id=shop_id,
|
|
340
|
+
api_token=api_token,
|
|
341
|
+
agent_id=agent_id,
|
|
342
|
+
limit=500,
|
|
343
|
+
)
|
|
344
|
+
existing_by_key: dict[tuple[str, str], dict[str, Any]] = {}
|
|
345
|
+
for row in existing:
|
|
346
|
+
if not _agent_attached(row, agent_id=agent_id):
|
|
347
|
+
continue
|
|
348
|
+
response_type = str(row.get("type") or "").strip().lower()
|
|
349
|
+
question = _normalize_question(str(row.get("question") or ""))
|
|
350
|
+
if response_type and question:
|
|
351
|
+
existing_by_key[(response_type, question)] = row
|
|
352
|
+
|
|
353
|
+
created: list[str] = []
|
|
354
|
+
updated: list[str] = []
|
|
355
|
+
planned_create = 0
|
|
356
|
+
planned_update = 0
|
|
357
|
+
for spec_response in responses:
|
|
358
|
+
key = (
|
|
359
|
+
spec_response["type"],
|
|
360
|
+
_normalize_question(spec_response["question"]),
|
|
361
|
+
)
|
|
362
|
+
payload = _response_payload(spec_response, agent_id=agent_id)
|
|
363
|
+
matched = existing_by_key.get(key)
|
|
364
|
+
if matched:
|
|
365
|
+
if dry_run:
|
|
366
|
+
planned_update += 1
|
|
367
|
+
continue
|
|
368
|
+
updated_row = patch_response(
|
|
369
|
+
base_url=base_url,
|
|
370
|
+
shop_id=shop_id,
|
|
371
|
+
api_token=api_token,
|
|
372
|
+
response_id=str(matched.get("id")),
|
|
373
|
+
payload=payload,
|
|
374
|
+
)
|
|
375
|
+
updated.append(str(updated_row.get("id")))
|
|
376
|
+
else:
|
|
377
|
+
if dry_run:
|
|
378
|
+
planned_create += 1
|
|
379
|
+
continue
|
|
380
|
+
created_row = create_response(
|
|
381
|
+
base_url=base_url,
|
|
382
|
+
shop_id=shop_id,
|
|
383
|
+
api_token=api_token,
|
|
384
|
+
payload=payload,
|
|
385
|
+
)
|
|
386
|
+
created.append(str(created_row.get("id")))
|
|
387
|
+
return {
|
|
388
|
+
"created_ids": created,
|
|
389
|
+
"updated_ids": updated,
|
|
390
|
+
"created_count": len(created) if not dry_run else planned_create,
|
|
391
|
+
"updated_count": len(updated) if not dry_run else planned_update,
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def create_agent_from_spec(
|
|
396
|
+
*,
|
|
397
|
+
base_url: str,
|
|
398
|
+
shop_id: str,
|
|
399
|
+
api_token: str,
|
|
400
|
+
spec: dict[str, Any],
|
|
401
|
+
dry_run: bool = False,
|
|
402
|
+
) -> dict[str, Any]:
|
|
403
|
+
payload = build_agent_payload(spec)
|
|
404
|
+
if dry_run:
|
|
405
|
+
return {"id": "dry-run", "payload": payload}
|
|
406
|
+
return create_agent(
|
|
407
|
+
base_url=base_url,
|
|
408
|
+
shop_id=shop_id,
|
|
409
|
+
api_token=api_token,
|
|
410
|
+
payload=payload,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def apply_agent_settings_from_spec(
|
|
415
|
+
*,
|
|
416
|
+
base_url: str,
|
|
417
|
+
shop_id: str,
|
|
418
|
+
api_token: str,
|
|
419
|
+
agent_id: str,
|
|
420
|
+
spec: dict[str, Any],
|
|
421
|
+
dry_run: bool = False,
|
|
422
|
+
) -> dict[str, Any]:
|
|
423
|
+
payload = build_agent_payload(spec)
|
|
424
|
+
if dry_run:
|
|
425
|
+
return {"id": agent_id, "payload": payload}
|
|
426
|
+
return update_agent(
|
|
427
|
+
base_url=base_url,
|
|
428
|
+
shop_id=shop_id,
|
|
429
|
+
api_token=api_token,
|
|
430
|
+
agent_id=agent_id,
|
|
431
|
+
payload=payload,
|
|
432
|
+
)
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: applied-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI and MCP server for Applied Labs AI support agents
|
|
5
|
+
Author: Applied Labs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/appliedlabs/applied-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/appliedlabs/applied-cli
|
|
9
|
+
Keywords: applied-labs,ai-agents,support,mcp,claude
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: httpx>=0.28.1
|
|
17
|
+
Requires-Dist: keyring>=25.6.0
|
|
18
|
+
Requires-Dist: pyyaml>=6.0
|
|
19
|
+
Requires-Dist: typer>=0.16.0
|
|
20
|
+
Provides-Extra: mcp
|
|
21
|
+
Requires-Dist: mcp>=1.2.0; extra == "mcp"
|
|
22
|
+
|
|
23
|
+
# Applied Labs CLI
|
|
24
|
+
|
|
25
|
+
CLI and Claude Code plugin for managing Applied Labs AI support agents.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
### As a Claude Code Plugin
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# From a marketplace (once published)
|
|
33
|
+
/plugin install applied-labs@marketplace-name
|
|
34
|
+
|
|
35
|
+
# Or test locally
|
|
36
|
+
claude --plugin-dir /path/to/applied-cli
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### As a standalone CLI
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install applied-cli
|
|
43
|
+
|
|
44
|
+
# Or with MCP server support
|
|
45
|
+
pip install "applied-cli[mcp]"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Authentication
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
applied-cli auth login # Opens browser for approval
|
|
52
|
+
applied-cli auth status # Check current shop
|
|
53
|
+
applied-cli auth shops # List available shops
|
|
54
|
+
applied-cli auth use-shop NAME # Switch shops
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Quick Start
|
|
58
|
+
|
|
59
|
+
### 1. Set up a new shop
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Generate spec template
|
|
63
|
+
applied-cli shop template > my-shop.yaml
|
|
64
|
+
|
|
65
|
+
# Edit the spec with your configuration...
|
|
66
|
+
|
|
67
|
+
# Run setup
|
|
68
|
+
applied-cli shop setup --spec my-shop.yaml --json
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 2. Test your agent
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
applied-cli chat --agent-id <uuid> --message "Hello"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Fix failing scenarios
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Get context for failures
|
|
81
|
+
applied-cli test fix context --benchmark-id <uuid> --json
|
|
82
|
+
|
|
83
|
+
# Update knowledge base
|
|
84
|
+
applied-cli knowledge upsert --agent-id <uuid> --type qa \
|
|
85
|
+
--question "What is your return policy?" \
|
|
86
|
+
--answer "30 day returns on all items."
|
|
87
|
+
|
|
88
|
+
# Batch test fixes
|
|
89
|
+
applied-cli test fix batch --source <failing-benchmark> --target <validation-benchmark>
|
|
90
|
+
|
|
91
|
+
# Check progress
|
|
92
|
+
applied-cli test fix status --source <source> --target <target>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Command Reference
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
applied-cli
|
|
99
|
+
├── auth # Login, logout, switch shops
|
|
100
|
+
├── shop # Bootstrap new shops from YAML spec
|
|
101
|
+
├── agent # List, create, update agents
|
|
102
|
+
├── chat # Send a message to an agent
|
|
103
|
+
├── conversations # List, show, import conversations
|
|
104
|
+
├── insights # Generate analytics reports
|
|
105
|
+
├── knowledge # Q&A entries, escalation rules
|
|
106
|
+
├── taxonomy # Topic/intent classification
|
|
107
|
+
├── test # Testing workflows
|
|
108
|
+
│ ├── benchmarks # Scenario collections
|
|
109
|
+
│ ├── scenarios # Individual test cases (includes rate)
|
|
110
|
+
│ ├── runs # Execution records
|
|
111
|
+
│ ├── coverage # Coverage summaries
|
|
112
|
+
│ └── fix # Fix failing scenarios
|
|
113
|
+
└── simulate # Generate test conversations
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## MCP Server
|
|
117
|
+
|
|
118
|
+
The CLI includes an MCP server for Claude integrations:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# Run directly (after pip install)
|
|
122
|
+
applied-cli-mcp
|
|
123
|
+
|
|
124
|
+
# Or via uvx (after publishing to PyPI)
|
|
125
|
+
uvx --from "applied-cli[mcp]" applied-cli-mcp
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Claude Desktop Configuration
|
|
129
|
+
|
|
130
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"applied-labs": {
|
|
136
|
+
"command": "applied-cli-mcp"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Plugin Skills
|
|
143
|
+
|
|
144
|
+
When installed as a Claude Code plugin, these skills are available:
|
|
145
|
+
|
|
146
|
+
- `/applied-labs:setup-shop` - Guided shop setup workflow
|
|
147
|
+
- `/applied-labs:fix-scenarios` - Fix failing test scenarios
|
|
148
|
+
|
|
149
|
+
## Environment Variables
|
|
150
|
+
|
|
151
|
+
| Variable | Description |
|
|
152
|
+
|----------|-------------|
|
|
153
|
+
| `APPLIED_ENDPOINT` | `prod`, `dev`, `local`, or full URL |
|
|
154
|
+
| `APPLIED_SHOP_ID` | Pre-select shop UUID |
|
|
155
|
+
| `APPLIED_API_TOKEN` | Skip browser auth |
|
|
156
|
+
| `APPLIED_PROFILE` | Named credential profile |
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Install in development mode
|
|
162
|
+
pip install -e ".[mcp]"
|
|
163
|
+
|
|
164
|
+
# Test CLI
|
|
165
|
+
applied-cli --help
|
|
166
|
+
|
|
167
|
+
# Test MCP server
|
|
168
|
+
applied-cli-mcp
|
|
169
|
+
|
|
170
|
+
# Test as Claude Code plugin
|
|
171
|
+
claude --plugin-dir .
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
applied_cli/__init__.py,sha256=dKRQ2wgw2GHlPoGnG0-iQ0JFj5dFj_6fKIpH3xcAcC4,28
|
|
2
|
+
applied_cli/auth_store.py,sha256=g8lsP_Vw5cERhc8WewQHVOhkNBzCmG_EmDlCLRt5Qbc,7191
|
|
3
|
+
applied_cli/config.py,sha256=OHcJwhujSZe8TecyIoNu9JZnYm_ydVgEiGKRXZPuZo0,1430
|
|
4
|
+
applied_cli/error_reporting.py,sha256=9e9VCjT-HQUiuATPEFiBXwVqT5ze_yOOp33PHbXP6aA,1227
|
|
5
|
+
applied_cli/http.py,sha256=X-uh7IGVHqINpG0sjE_moNm0CLQil7zETOYR_2pVI2A,45369
|
|
6
|
+
applied_cli/main.py,sha256=Mo9rBNWAIMivZrU46_kpi-W2aFLYrZy2qZUZ0ElmJsw,5527
|
|
7
|
+
applied_cli/mcp_server.py,sha256=D09xjnAt0Bg3kn-Ghc8bhtPD_GyDl66SNoZUGKp4U3s,20955
|
|
8
|
+
applied_cli/runtime.py,sha256=lL9q1EO4bOR9XKbGM5WX9esTaaLIHA3pcbmcdvxTw6I,1725
|
|
9
|
+
applied_cli/shop_spec.py,sha256=ROwW1sWpqf3pMWWxa6BQjpwPTxV7esGk9FBXs5u8qh8,15500
|
|
10
|
+
applied_cli/spec_workflow.py,sha256=fBkC6NBjcu-SA0zESQBwjnFlea7q349rPBJi6_NlLaE,14523
|
|
11
|
+
applied_cli/commands/__init__.py,sha256=8UhFLtjsbXglNTf90b5gflyijI2j6b7CtUEcgo753Wo,27
|
|
12
|
+
applied_cli/commands/_hints.py,sha256=FXTeP7aBTFwzr4n6FqccZV9EJ6K9BzhVdJN89xCQOb4,410
|
|
13
|
+
applied_cli/commands/_normalize.py,sha256=SXeLN_EYy0MnawYQ88IPihfYMHKcd77nkGYy1mZ5RmY,2113
|
|
14
|
+
applied_cli/commands/_parsers.py,sha256=98njCaisw82Dfkl-Fi3FJej3gJR48UPtuXH16W7bDAY,1831
|
|
15
|
+
applied_cli/commands/_ui.py,sha256=OROXC4vN0pVn3hcdUEuBJ3Y1Eobou0aEQ0kzZ3c3p78,863
|
|
16
|
+
applied_cli/commands/agent.py,sha256=ky_7m-Denj_XHnP2E0n0zWXpWt_xNHGw24M3K9Xj2TU,47904
|
|
17
|
+
applied_cli/commands/auth.py,sha256=DpnFrz2M6uTUDodpsP96TwcNW_B4DkXC2oIb4j21Quw,29014
|
|
18
|
+
applied_cli/commands/chat.py,sha256=QWPWMBnVY0GdLBYXVzwrcC1uCP1M6xvr_GOamlH2Gac,12865
|
|
19
|
+
applied_cli/commands/coverage.py,sha256=5yjOkF2OevHD6Ti193umSDMD-cCVbFHnp93oHo6xi0Q,12242
|
|
20
|
+
applied_cli/commands/discover.py,sha256=R5ob-GdS9vtYMO1vpFCgDzwNtoGBaKRhjMmmghvU28c,38702
|
|
21
|
+
applied_cli/commands/fix.py,sha256=2UWB8nm3cbAgORzrZnSNUN91rWob4voqre3wEtGpJbs,44032
|
|
22
|
+
applied_cli/commands/insights.py,sha256=09x2_bRLc-58gJ9RjE3qS6mlEjswgLeIsJsro4cPkBg,19943
|
|
23
|
+
applied_cli/commands/intents.py,sha256=OEwGBj-HdbBWdy2IZYV1f9VbnuKHVJ3--g2K3gBd7pc,14781
|
|
24
|
+
applied_cli/commands/rate.py,sha256=N3tlIpUSOi-w7JRTN4pjYt0IWLDx1uNRtYrJYUIZGdo,17543
|
|
25
|
+
applied_cli/commands/responses.py,sha256=a8c4zFpJyBQlW9NLkR8_YSK01AtYx8edwY_HnQ3YCNI,24380
|
|
26
|
+
applied_cli/commands/shop.py,sha256=K7D_xNzKVHf10Yn6B0XnJ6nyuVoMqb5L590pTr96vkA,65276
|
|
27
|
+
applied_cli/commands/simulate.py,sha256=77IEh30im_Ou4KEcI6TBi0q0ve5oGTGd8nouLPfyOMU,12115
|
|
28
|
+
applied_cli/commands/spec.py,sha256=dv1R392wPxIKs6G-8rmblmkcDOxmVtt0EujFttkE4LM,8504
|
|
29
|
+
applied_cli/presets/demo.yaml,sha256=kDCktFFFY2FK2QAaLIf-nc6-HIsqpRs3jx_0K6KQSaU,7313
|
|
30
|
+
applied_cli-0.1.0.dist-info/METADATA,sha256=jOOh6bMuBaGUfCcRt5cA9mIdrTRLll630T3qj-brqpE,4266
|
|
31
|
+
applied_cli-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
32
|
+
applied_cli-0.1.0.dist-info/entry_points.txt,sha256=D7ckvwRElXVNC-C-YDQW-Kt-06RWoJjj0MtcieH9ZpY,99
|
|
33
|
+
applied_cli-0.1.0.dist-info/top_level.txt,sha256=FqgB1tOiax6kgE0hvzkIeXiRZL_R8fPojvdX_Old_zk,12
|
|
34
|
+
applied_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
applied_cli
|