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,379 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import uuid
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from applied_cli.commands._hints import suggest_value
|
|
10
|
+
from applied_cli.commands._ui import confirm_or_exit, emit_success, show_target
|
|
11
|
+
from applied_cli.error_reporting import render_api_error
|
|
12
|
+
from applied_cli.http import APIError
|
|
13
|
+
from applied_cli.runtime import resolve_runtime
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Chat with an agent using v1 completion.")
|
|
16
|
+
|
|
17
|
+
MESSAGE_REGEX = re.compile(
|
|
18
|
+
r"(\{(?:(\{(?:(\{(?:(\{(?:(\{(?:(\{(?:(\{(?:(\{(?:(\{[^}{]*\})|[^}{])*\})|[^}{])*\})|[^}{])*\})|[^}{])*\})|[^}{])*\})|[^}{])*\})|[^}{])*\})|[^}{])*\})"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _headers(shop_id: str, api_token: str) -> dict[str, str]:
|
|
23
|
+
return {
|
|
24
|
+
"Authorization": f"Bearer {api_token}",
|
|
25
|
+
"X-Shop-Id": shop_id,
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _verify_agent_scope(
|
|
31
|
+
client: httpx.Client,
|
|
32
|
+
*,
|
|
33
|
+
base_url: str,
|
|
34
|
+
shop_id: str,
|
|
35
|
+
api_token: str,
|
|
36
|
+
agent_id: str,
|
|
37
|
+
) -> dict[str, object]:
|
|
38
|
+
try:
|
|
39
|
+
response = client.get(
|
|
40
|
+
f"{base_url}/v1/agents/{agent_id}/",
|
|
41
|
+
headers=_headers(shop_id, api_token),
|
|
42
|
+
timeout=10.0,
|
|
43
|
+
)
|
|
44
|
+
except httpx.HTTPError as exc:
|
|
45
|
+
raise APIError(
|
|
46
|
+
f"Agent scope check failed to reach server: {exc}",
|
|
47
|
+
code="NETWORK_ERROR",
|
|
48
|
+
hint="Check APPLIED_BASE_URL and confirm the server is reachable.",
|
|
49
|
+
retryable=True,
|
|
50
|
+
method="GET",
|
|
51
|
+
url=f"{base_url}/v1/agents/{agent_id}/",
|
|
52
|
+
suggestions=[
|
|
53
|
+
"Verify `applied-cli auth status` succeeds.",
|
|
54
|
+
"If running locally, confirm server is up on http://localhost:8000.",
|
|
55
|
+
],
|
|
56
|
+
) from exc
|
|
57
|
+
if response.status_code >= 400:
|
|
58
|
+
try:
|
|
59
|
+
detail = response.json()
|
|
60
|
+
except Exception:
|
|
61
|
+
detail = response.text
|
|
62
|
+
code = "AGENT_SCOPE_CHECK_FAILED"
|
|
63
|
+
hint = "Verify --agent-id, --shop-id, and token permissions."
|
|
64
|
+
suggestions = [
|
|
65
|
+
"Run `applied-cli auth status` to verify token validity.",
|
|
66
|
+
"Confirm the agent belongs to the selected shop.",
|
|
67
|
+
]
|
|
68
|
+
if response.status_code == 401:
|
|
69
|
+
code = "AUTH_INVALID_TOKEN"
|
|
70
|
+
hint = "Token rejected while reading agent scope."
|
|
71
|
+
suggestions.insert(0, "Run `applied-cli auth login` with a fresh token.")
|
|
72
|
+
elif response.status_code == 403:
|
|
73
|
+
code = "SHOP_SCOPE_FORBIDDEN"
|
|
74
|
+
hint = "Token does not have permission for this shop or agent."
|
|
75
|
+
elif response.status_code == 404:
|
|
76
|
+
code = "AGENT_NOT_FOUND"
|
|
77
|
+
hint = "Agent was not found in the selected shop scope."
|
|
78
|
+
raise APIError(
|
|
79
|
+
f"Agent/shop scope check failed ({response.status_code}).",
|
|
80
|
+
status_code=response.status_code,
|
|
81
|
+
code=code,
|
|
82
|
+
hint=hint,
|
|
83
|
+
retryable=response.status_code >= 500,
|
|
84
|
+
method="GET",
|
|
85
|
+
url=f"{base_url}/v1/agents/{agent_id}/",
|
|
86
|
+
detail=detail,
|
|
87
|
+
suggestions=suggestions,
|
|
88
|
+
)
|
|
89
|
+
try:
|
|
90
|
+
data = response.json()
|
|
91
|
+
except Exception:
|
|
92
|
+
return {}
|
|
93
|
+
return data if isinstance(data, dict) else {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _create_conversation(
|
|
97
|
+
client: httpx.Client,
|
|
98
|
+
*,
|
|
99
|
+
base_url: str,
|
|
100
|
+
agent_id: str,
|
|
101
|
+
channel: str,
|
|
102
|
+
) -> str:
|
|
103
|
+
payload: dict[str, object] = {
|
|
104
|
+
"agent_id": agent_id,
|
|
105
|
+
"is_test": True,
|
|
106
|
+
"metadata": {
|
|
107
|
+
"isTest": True,
|
|
108
|
+
"source": "applied-cli",
|
|
109
|
+
"context": {"channel": channel},
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
if channel == "email":
|
|
113
|
+
payload["type"] = "email"
|
|
114
|
+
elif channel == "sms":
|
|
115
|
+
payload["type"] = "sms"
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = client.post(
|
|
119
|
+
f"{base_url}/v1/c/",
|
|
120
|
+
json=payload,
|
|
121
|
+
headers={"Content-Type": "application/json"},
|
|
122
|
+
timeout=10.0,
|
|
123
|
+
)
|
|
124
|
+
except httpx.HTTPError as exc:
|
|
125
|
+
raise APIError(
|
|
126
|
+
f"Conversation init failed to reach server: {exc}",
|
|
127
|
+
code="NETWORK_ERROR",
|
|
128
|
+
hint="Check APPLIED_BASE_URL and confirm the server is reachable.",
|
|
129
|
+
retryable=True,
|
|
130
|
+
method="POST",
|
|
131
|
+
url=f"{base_url}/v1/c/",
|
|
132
|
+
suggestions=[
|
|
133
|
+
"Verify local server availability.",
|
|
134
|
+
"Retry after confirming token/shop scope.",
|
|
135
|
+
],
|
|
136
|
+
) from exc
|
|
137
|
+
if response.status_code >= 400:
|
|
138
|
+
try:
|
|
139
|
+
detail = response.json()
|
|
140
|
+
except Exception:
|
|
141
|
+
detail = response.text
|
|
142
|
+
raise APIError(
|
|
143
|
+
f"Conversation init failed ({response.status_code}).",
|
|
144
|
+
status_code=response.status_code,
|
|
145
|
+
code="CONVERSATION_CREATE_FAILED",
|
|
146
|
+
hint="Conversation bootstrap failed before completion call.",
|
|
147
|
+
retryable=response.status_code >= 500,
|
|
148
|
+
method="POST",
|
|
149
|
+
url=f"{base_url}/v1/c/",
|
|
150
|
+
detail=detail,
|
|
151
|
+
suggestions=[
|
|
152
|
+
"Verify agent modality and requested --channel are compatible.",
|
|
153
|
+
"Check token/shop scope can create test conversations.",
|
|
154
|
+
],
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
data = response.json()
|
|
158
|
+
conversation_id = data.get("id")
|
|
159
|
+
if not conversation_id:
|
|
160
|
+
raise APIError("Conversation init succeeded but no conversation id returned.")
|
|
161
|
+
return str(conversation_id)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _stream_v1_completion(
|
|
165
|
+
client: httpx.Client,
|
|
166
|
+
*,
|
|
167
|
+
base_url: str,
|
|
168
|
+
shop_id: str,
|
|
169
|
+
api_token: str,
|
|
170
|
+
agent_id: str,
|
|
171
|
+
payload: dict[str, object],
|
|
172
|
+
) -> str:
|
|
173
|
+
generated_text = ""
|
|
174
|
+
buffer = ""
|
|
175
|
+
content_complete_seen = False
|
|
176
|
+
read_after_complete = False
|
|
177
|
+
with client.stream(
|
|
178
|
+
"POST",
|
|
179
|
+
f"{base_url}/v1/agents/{agent_id}/complete/",
|
|
180
|
+
headers=_headers(shop_id, api_token),
|
|
181
|
+
json=payload,
|
|
182
|
+
timeout=60.0,
|
|
183
|
+
) as response:
|
|
184
|
+
if response.status_code >= 400:
|
|
185
|
+
try:
|
|
186
|
+
detail = response.json()
|
|
187
|
+
except Exception:
|
|
188
|
+
detail = response.text
|
|
189
|
+
raise APIError(
|
|
190
|
+
f"Completion failed ({response.status_code}).",
|
|
191
|
+
status_code=response.status_code,
|
|
192
|
+
code="COMPLETION_REQUEST_FAILED",
|
|
193
|
+
hint="Completion endpoint returned an error for this payload.",
|
|
194
|
+
retryable=response.status_code >= 500,
|
|
195
|
+
method="POST",
|
|
196
|
+
url=f"{base_url}/v1/agents/{agent_id}/complete/",
|
|
197
|
+
detail=detail,
|
|
198
|
+
suggestions=[
|
|
199
|
+
"Verify `context` and transcript payload shape.",
|
|
200
|
+
"Confirm agent is active and has a valid completion configuration.",
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
for chunk in response.iter_text():
|
|
205
|
+
if not chunk:
|
|
206
|
+
continue
|
|
207
|
+
buffer += chunk
|
|
208
|
+
last_consumed = 0
|
|
209
|
+
for match in MESSAGE_REGEX.finditer(buffer):
|
|
210
|
+
raw = match.group(1)
|
|
211
|
+
if not raw:
|
|
212
|
+
continue
|
|
213
|
+
try:
|
|
214
|
+
data = json.loads(raw)
|
|
215
|
+
except json.JSONDecodeError:
|
|
216
|
+
continue
|
|
217
|
+
last_consumed = match.end()
|
|
218
|
+
content = data.get("content")
|
|
219
|
+
if isinstance(content, str) and content:
|
|
220
|
+
generated_text += content
|
|
221
|
+
typer.echo(content, nl=False)
|
|
222
|
+
if bool(data.get("content_complete")):
|
|
223
|
+
content_complete_seen = True
|
|
224
|
+
if last_consumed > 0:
|
|
225
|
+
buffer = buffer[last_consumed:]
|
|
226
|
+
if content_complete_seen:
|
|
227
|
+
if read_after_complete:
|
|
228
|
+
break
|
|
229
|
+
# Read one additional chunk to allow server-side post-stream save hooks.
|
|
230
|
+
read_after_complete = True
|
|
231
|
+
|
|
232
|
+
return generated_text
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@app.command(
|
|
236
|
+
"send",
|
|
237
|
+
help=(
|
|
238
|
+
"Send one message to an agent. Example: applied-cli chat send --agent-id <uuid> "
|
|
239
|
+
"--channel chat --message 'How much does a Ripple cost?'"
|
|
240
|
+
),
|
|
241
|
+
)
|
|
242
|
+
def send(
|
|
243
|
+
agent_id: str = typer.Option(
|
|
244
|
+
..., "--agent-id", "--agent", "--id", help="Target agent UUID."
|
|
245
|
+
),
|
|
246
|
+
message: Optional[str] = typer.Option(
|
|
247
|
+
None, "--message", "-m", help="User message. If omitted, prompt interactively."
|
|
248
|
+
),
|
|
249
|
+
channel: str = typer.Option(
|
|
250
|
+
"chat", help="Channel mode: chat, email, or sms.", case_sensitive=False
|
|
251
|
+
),
|
|
252
|
+
context: str = typer.Option(
|
|
253
|
+
"EVALUATE", help="Completion context (e.g., EVALUATE, LIVE, DEVELOPMENT)."
|
|
254
|
+
),
|
|
255
|
+
conversation_id: Optional[str] = typer.Option(
|
|
256
|
+
None,
|
|
257
|
+
"--conversation-id",
|
|
258
|
+
"--conversation",
|
|
259
|
+
help="Existing conversation UUID. If omitted, create a test conversation.",
|
|
260
|
+
),
|
|
261
|
+
draft: bool = typer.Option(False, help="Do not persist completion when possible."),
|
|
262
|
+
base_url: Optional[str] = typer.Option(None, help="Applied base URL."),
|
|
263
|
+
shop_id: Optional[str] = typer.Option(None, help="Target shop UUID."),
|
|
264
|
+
api_token: Optional[str] = typer.Option(None, help="Applied API token."),
|
|
265
|
+
yes: bool = typer.Option(
|
|
266
|
+
False,
|
|
267
|
+
"--yes",
|
|
268
|
+
"-y",
|
|
269
|
+
help="Skip safety confirmation prompt.",
|
|
270
|
+
),
|
|
271
|
+
) -> None:
|
|
272
|
+
try:
|
|
273
|
+
uuid.UUID(agent_id)
|
|
274
|
+
except ValueError as exc:
|
|
275
|
+
raise typer.BadParameter(
|
|
276
|
+
"agent-id must be a valid UUID (example: 7ef9c71d-6ea8-42a9-9878-b496ed5f830d)"
|
|
277
|
+
) from exc
|
|
278
|
+
|
|
279
|
+
normalized_channel = channel.lower().strip()
|
|
280
|
+
if normalized_channel not in {"chat", "email", "sms"}:
|
|
281
|
+
suggestion = suggest_value(normalized_channel, ["chat", "email", "sms"])
|
|
282
|
+
hint = f" Did you mean '{suggestion}'?" if suggestion else ""
|
|
283
|
+
raise typer.BadParameter(f"channel must be one of: chat, email, sms.{hint}")
|
|
284
|
+
|
|
285
|
+
prompt_message = (message or "").strip()
|
|
286
|
+
if not prompt_message:
|
|
287
|
+
prompt_message = typer.prompt("Message").strip()
|
|
288
|
+
if not prompt_message:
|
|
289
|
+
raise typer.BadParameter("message cannot be empty")
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
resolved_base_url, resolved_shop_id, resolved_token = resolve_runtime(
|
|
293
|
+
base_url=base_url,
|
|
294
|
+
shop_id=shop_id,
|
|
295
|
+
api_token=api_token,
|
|
296
|
+
)
|
|
297
|
+
except APIError as exc:
|
|
298
|
+
typer.echo(render_api_error(exc, action="resolve runtime for chat"), err=True)
|
|
299
|
+
raise typer.Exit(code=1) from exc
|
|
300
|
+
|
|
301
|
+
show_target(
|
|
302
|
+
{
|
|
303
|
+
"base_url": resolved_base_url,
|
|
304
|
+
"shop_id": resolved_shop_id,
|
|
305
|
+
"agent_id": agent_id,
|
|
306
|
+
"channel": normalized_channel,
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
confirm_or_exit(yes=yes)
|
|
310
|
+
|
|
311
|
+
with httpx.Client() as client:
|
|
312
|
+
try:
|
|
313
|
+
agent_payload = _verify_agent_scope(
|
|
314
|
+
client,
|
|
315
|
+
base_url=resolved_base_url,
|
|
316
|
+
shop_id=resolved_shop_id,
|
|
317
|
+
api_token=resolved_token,
|
|
318
|
+
agent_id=agent_id,
|
|
319
|
+
)
|
|
320
|
+
if agent_payload.get("auto_reply") is False:
|
|
321
|
+
typer.echo(
|
|
322
|
+
"Warning: target agent has auto_reply disabled. "
|
|
323
|
+
"Behavior tests may fail until `agent settings reply --auto-reply` is set."
|
|
324
|
+
)
|
|
325
|
+
active_conversation_id = conversation_id or _create_conversation(
|
|
326
|
+
client,
|
|
327
|
+
base_url=resolved_base_url,
|
|
328
|
+
agent_id=agent_id,
|
|
329
|
+
channel=normalized_channel,
|
|
330
|
+
)
|
|
331
|
+
except APIError as exc:
|
|
332
|
+
typer.echo(render_api_error(exc, action="prepare chat conversation"), err=True)
|
|
333
|
+
raise typer.Exit(code=1) from exc
|
|
334
|
+
|
|
335
|
+
transcript = [
|
|
336
|
+
{
|
|
337
|
+
"id": str(uuid.uuid4()),
|
|
338
|
+
"role": "user",
|
|
339
|
+
"content": prompt_message,
|
|
340
|
+
"text": prompt_message,
|
|
341
|
+
"format": "TEXT",
|
|
342
|
+
"entity": {"type": "user"},
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
payload: dict[str, object] = {
|
|
347
|
+
"conversation_id": active_conversation_id,
|
|
348
|
+
"context": context,
|
|
349
|
+
"transcript": transcript,
|
|
350
|
+
"metadata": {
|
|
351
|
+
"source": "applied-cli",
|
|
352
|
+
"isTest": True,
|
|
353
|
+
"channel": normalized_channel,
|
|
354
|
+
},
|
|
355
|
+
"draft": draft,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
typer.echo("\nAssistant:")
|
|
359
|
+
try:
|
|
360
|
+
text = _stream_v1_completion(
|
|
361
|
+
client,
|
|
362
|
+
base_url=resolved_base_url,
|
|
363
|
+
shop_id=resolved_shop_id,
|
|
364
|
+
api_token=resolved_token,
|
|
365
|
+
agent_id=agent_id,
|
|
366
|
+
payload=payload,
|
|
367
|
+
)
|
|
368
|
+
except APIError as exc:
|
|
369
|
+
typer.echo(f"\n{render_api_error(exc, action='chat completion')}", err=True)
|
|
370
|
+
raise typer.Exit(code=1) from exc
|
|
371
|
+
|
|
372
|
+
emit_success(
|
|
373
|
+
output_json=False,
|
|
374
|
+
payload={},
|
|
375
|
+
fields={
|
|
376
|
+
"conversation_id": active_conversation_id,
|
|
377
|
+
"chars": len(text),
|
|
378
|
+
},
|
|
379
|
+
)
|