applied-cli 0.1.0__tar.gz
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-0.1.0/PKG-INFO +176 -0
- applied_cli-0.1.0/README.md +154 -0
- applied_cli-0.1.0/applied_cli/__init__.py +2 -0
- applied_cli-0.1.0/applied_cli/auth_store.py +263 -0
- applied_cli-0.1.0/applied_cli/commands/__init__.py +2 -0
- applied_cli-0.1.0/applied_cli/commands/_hints.py +11 -0
- applied_cli-0.1.0/applied_cli/commands/_normalize.py +79 -0
- applied_cli-0.1.0/applied_cli/commands/_parsers.py +58 -0
- applied_cli-0.1.0/applied_cli/commands/_ui.py +33 -0
- applied_cli-0.1.0/applied_cli/commands/agent.py +1231 -0
- applied_cli-0.1.0/applied_cli/commands/auth.py +739 -0
- applied_cli-0.1.0/applied_cli/commands/chat.py +379 -0
- applied_cli-0.1.0/applied_cli/commands/coverage.py +348 -0
- applied_cli-0.1.0/applied_cli/commands/discover.py +1006 -0
- applied_cli-0.1.0/applied_cli/commands/fix.py +1204 -0
- applied_cli-0.1.0/applied_cli/commands/insights.py +614 -0
- applied_cli-0.1.0/applied_cli/commands/intents.py +447 -0
- applied_cli-0.1.0/applied_cli/commands/rate.py +508 -0
- applied_cli-0.1.0/applied_cli/commands/responses.py +604 -0
- applied_cli-0.1.0/applied_cli/commands/shop.py +1757 -0
- applied_cli-0.1.0/applied_cli/commands/simulate.py +330 -0
- applied_cli-0.1.0/applied_cli/commands/spec.py +238 -0
- applied_cli-0.1.0/applied_cli/config.py +50 -0
- applied_cli-0.1.0/applied_cli/error_reporting.py +38 -0
- applied_cli-0.1.0/applied_cli/http.py +1614 -0
- applied_cli-0.1.0/applied_cli/main.py +90 -0
- applied_cli-0.1.0/applied_cli/mcp_server.py +738 -0
- applied_cli-0.1.0/applied_cli/presets/demo.yaml +170 -0
- applied_cli-0.1.0/applied_cli/runtime.py +53 -0
- applied_cli-0.1.0/applied_cli/shop_spec.py +398 -0
- applied_cli-0.1.0/applied_cli/spec_workflow.py +432 -0
- applied_cli-0.1.0/applied_cli.egg-info/PKG-INFO +176 -0
- applied_cli-0.1.0/applied_cli.egg-info/SOURCES.txt +38 -0
- applied_cli-0.1.0/applied_cli.egg-info/dependency_links.txt +1 -0
- applied_cli-0.1.0/applied_cli.egg-info/entry_points.txt +3 -0
- applied_cli-0.1.0/applied_cli.egg-info/requires.txt +7 -0
- applied_cli-0.1.0/applied_cli.egg-info/top_level.txt +1 -0
- applied_cli-0.1.0/pyproject.toml +46 -0
- applied_cli-0.1.0/setup.cfg +4 -0
- applied_cli-0.1.0/tests/test_parsers_and_insights.py +47 -0
|
@@ -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,154 @@
|
|
|
1
|
+
# Applied Labs CLI
|
|
2
|
+
|
|
3
|
+
CLI and Claude Code plugin for managing Applied Labs AI support agents.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### As a Claude Code Plugin
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# From a marketplace (once published)
|
|
11
|
+
/plugin install applied-labs@marketplace-name
|
|
12
|
+
|
|
13
|
+
# Or test locally
|
|
14
|
+
claude --plugin-dir /path/to/applied-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### As a standalone CLI
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install applied-cli
|
|
21
|
+
|
|
22
|
+
# Or with MCP server support
|
|
23
|
+
pip install "applied-cli[mcp]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Authentication
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
applied-cli auth login # Opens browser for approval
|
|
30
|
+
applied-cli auth status # Check current shop
|
|
31
|
+
applied-cli auth shops # List available shops
|
|
32
|
+
applied-cli auth use-shop NAME # Switch shops
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
### 1. Set up a new shop
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Generate spec template
|
|
41
|
+
applied-cli shop template > my-shop.yaml
|
|
42
|
+
|
|
43
|
+
# Edit the spec with your configuration...
|
|
44
|
+
|
|
45
|
+
# Run setup
|
|
46
|
+
applied-cli shop setup --spec my-shop.yaml --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Test your agent
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
applied-cli chat --agent-id <uuid> --message "Hello"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Fix failing scenarios
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Get context for failures
|
|
59
|
+
applied-cli test fix context --benchmark-id <uuid> --json
|
|
60
|
+
|
|
61
|
+
# Update knowledge base
|
|
62
|
+
applied-cli knowledge upsert --agent-id <uuid> --type qa \
|
|
63
|
+
--question "What is your return policy?" \
|
|
64
|
+
--answer "30 day returns on all items."
|
|
65
|
+
|
|
66
|
+
# Batch test fixes
|
|
67
|
+
applied-cli test fix batch --source <failing-benchmark> --target <validation-benchmark>
|
|
68
|
+
|
|
69
|
+
# Check progress
|
|
70
|
+
applied-cli test fix status --source <source> --target <target>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Command Reference
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
applied-cli
|
|
77
|
+
├── auth # Login, logout, switch shops
|
|
78
|
+
├── shop # Bootstrap new shops from YAML spec
|
|
79
|
+
├── agent # List, create, update agents
|
|
80
|
+
├── chat # Send a message to an agent
|
|
81
|
+
├── conversations # List, show, import conversations
|
|
82
|
+
├── insights # Generate analytics reports
|
|
83
|
+
├── knowledge # Q&A entries, escalation rules
|
|
84
|
+
├── taxonomy # Topic/intent classification
|
|
85
|
+
├── test # Testing workflows
|
|
86
|
+
│ ├── benchmarks # Scenario collections
|
|
87
|
+
│ ├── scenarios # Individual test cases (includes rate)
|
|
88
|
+
│ ├── runs # Execution records
|
|
89
|
+
│ ├── coverage # Coverage summaries
|
|
90
|
+
│ └── fix # Fix failing scenarios
|
|
91
|
+
└── simulate # Generate test conversations
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## MCP Server
|
|
95
|
+
|
|
96
|
+
The CLI includes an MCP server for Claude integrations:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Run directly (after pip install)
|
|
100
|
+
applied-cli-mcp
|
|
101
|
+
|
|
102
|
+
# Or via uvx (after publishing to PyPI)
|
|
103
|
+
uvx --from "applied-cli[mcp]" applied-cli-mcp
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Claude Desktop Configuration
|
|
107
|
+
|
|
108
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"applied-labs": {
|
|
114
|
+
"command": "applied-cli-mcp"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Plugin Skills
|
|
121
|
+
|
|
122
|
+
When installed as a Claude Code plugin, these skills are available:
|
|
123
|
+
|
|
124
|
+
- `/applied-labs:setup-shop` - Guided shop setup workflow
|
|
125
|
+
- `/applied-labs:fix-scenarios` - Fix failing test scenarios
|
|
126
|
+
|
|
127
|
+
## Environment Variables
|
|
128
|
+
|
|
129
|
+
| Variable | Description |
|
|
130
|
+
|----------|-------------|
|
|
131
|
+
| `APPLIED_ENDPOINT` | `prod`, `dev`, `local`, or full URL |
|
|
132
|
+
| `APPLIED_SHOP_ID` | Pre-select shop UUID |
|
|
133
|
+
| `APPLIED_API_TOKEN` | Skip browser auth |
|
|
134
|
+
| `APPLIED_PROFILE` | Named credential profile |
|
|
135
|
+
|
|
136
|
+
## Development
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Install in development mode
|
|
140
|
+
pip install -e ".[mcp]"
|
|
141
|
+
|
|
142
|
+
# Test CLI
|
|
143
|
+
applied-cli --help
|
|
144
|
+
|
|
145
|
+
# Test MCP server
|
|
146
|
+
applied-cli-mcp
|
|
147
|
+
|
|
148
|
+
# Test as Claude Code plugin
|
|
149
|
+
claude --plugin-dir .
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from applied_cli.config import Credentials
|
|
6
|
+
|
|
7
|
+
SERVICE_NAME = "applied-cli"
|
|
8
|
+
ACCOUNT_NAME = "default"
|
|
9
|
+
STORE_VERSION = 2
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _fallback_path() -> Path:
|
|
13
|
+
return Path.home() / ".config" / "applied-cli" / "credentials.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _credentials_to_dict(credentials: Credentials) -> dict[str, str]:
|
|
17
|
+
return {
|
|
18
|
+
"base_url": credentials.base_url,
|
|
19
|
+
"shop_id": credentials.shop_id,
|
|
20
|
+
"api_token": credentials.api_token,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _credentials_from_dict(data: dict[str, object]) -> Optional[Credentials]:
|
|
25
|
+
base_url = str(data.get("base_url", "")).strip()
|
|
26
|
+
shop_id = str(data.get("shop_id", "")).strip()
|
|
27
|
+
api_token = str(data.get("api_token", "")).strip()
|
|
28
|
+
if not base_url or not shop_id or not api_token:
|
|
29
|
+
return None
|
|
30
|
+
return Credentials(base_url=base_url, shop_id=shop_id, api_token=api_token)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _normalize_store(raw: object) -> dict[str, object]:
|
|
34
|
+
if not isinstance(raw, dict):
|
|
35
|
+
return {"version": STORE_VERSION, "active_profile": None, "profiles": {}}
|
|
36
|
+
|
|
37
|
+
# Legacy single-profile shape:
|
|
38
|
+
# {"base_url": "...", "shop_id": "...", "api_token": "..."}
|
|
39
|
+
legacy = _credentials_from_dict(raw)
|
|
40
|
+
if legacy:
|
|
41
|
+
return {
|
|
42
|
+
"version": STORE_VERSION,
|
|
43
|
+
"active_profile": "default",
|
|
44
|
+
"profiles": {"default": _credentials_to_dict(legacy)},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
profiles_raw = raw.get("profiles")
|
|
48
|
+
profiles: dict[str, dict[str, str]] = {}
|
|
49
|
+
if isinstance(profiles_raw, dict):
|
|
50
|
+
for profile_name, payload in profiles_raw.items():
|
|
51
|
+
if not isinstance(profile_name, str) or not profile_name.strip():
|
|
52
|
+
continue
|
|
53
|
+
if not isinstance(payload, dict):
|
|
54
|
+
continue
|
|
55
|
+
creds = _credentials_from_dict(payload)
|
|
56
|
+
if creds is None:
|
|
57
|
+
continue
|
|
58
|
+
profiles[profile_name.strip()] = _credentials_to_dict(creds)
|
|
59
|
+
|
|
60
|
+
active_profile_raw = raw.get("active_profile")
|
|
61
|
+
active_profile = (
|
|
62
|
+
active_profile_raw.strip()
|
|
63
|
+
if isinstance(active_profile_raw, str) and active_profile_raw.strip()
|
|
64
|
+
else None
|
|
65
|
+
)
|
|
66
|
+
if active_profile and active_profile not in profiles:
|
|
67
|
+
active_profile = None
|
|
68
|
+
if not active_profile and profiles:
|
|
69
|
+
active_profile = sorted(profiles.keys())[0]
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"version": STORE_VERSION,
|
|
73
|
+
"active_profile": active_profile,
|
|
74
|
+
"profiles": profiles,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _serialize_store(store: dict[str, object]) -> str:
|
|
79
|
+
return json.dumps(
|
|
80
|
+
{
|
|
81
|
+
"version": STORE_VERSION,
|
|
82
|
+
"active_profile": store.get("active_profile"),
|
|
83
|
+
"profiles": store.get("profiles", {}),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _load_store_from_keyring() -> Optional[dict[str, object]]:
|
|
89
|
+
try:
|
|
90
|
+
import keyring
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
raw = keyring.get_password(SERVICE_NAME, ACCOUNT_NAME)
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
if not raw:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
return _normalize_store(json.loads(raw))
|
|
104
|
+
except Exception:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _save_store_to_keyring(store: dict[str, object]) -> bool:
|
|
109
|
+
try:
|
|
110
|
+
import keyring
|
|
111
|
+
except Exception:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
keyring.set_password(
|
|
116
|
+
SERVICE_NAME,
|
|
117
|
+
ACCOUNT_NAME,
|
|
118
|
+
_serialize_store(store),
|
|
119
|
+
)
|
|
120
|
+
return True
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _clear_keyring() -> None:
|
|
126
|
+
try:
|
|
127
|
+
import keyring
|
|
128
|
+
|
|
129
|
+
keyring.delete_password(SERVICE_NAME, ACCOUNT_NAME)
|
|
130
|
+
except Exception:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _load_store_from_file() -> Optional[dict[str, object]]:
|
|
135
|
+
path = _fallback_path()
|
|
136
|
+
if not path.exists():
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
return _normalize_store(json.loads(path.read_text()))
|
|
141
|
+
except Exception:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _save_store_to_file(store: dict[str, object]) -> None:
|
|
146
|
+
path = _fallback_path()
|
|
147
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
path.write_text(_serialize_store(store))
|
|
149
|
+
path.chmod(0o600)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _clear_file() -> None:
|
|
153
|
+
path = _fallback_path()
|
|
154
|
+
if path.exists():
|
|
155
|
+
path.unlink()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _load_store() -> dict[str, object]:
|
|
159
|
+
store = _load_store_from_keyring()
|
|
160
|
+
if store:
|
|
161
|
+
return store
|
|
162
|
+
store = _load_store_from_file()
|
|
163
|
+
if store:
|
|
164
|
+
return store
|
|
165
|
+
return {"version": STORE_VERSION, "active_profile": None, "profiles": {}}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _save_store(store: dict[str, object]) -> str:
|
|
169
|
+
if _save_store_to_keyring(store):
|
|
170
|
+
return "keyring"
|
|
171
|
+
|
|
172
|
+
_save_store_to_file(store)
|
|
173
|
+
return "file"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def save_credentials(
|
|
177
|
+
credentials: Credentials,
|
|
178
|
+
*,
|
|
179
|
+
profile: str = "default",
|
|
180
|
+
set_active: bool = True,
|
|
181
|
+
) -> str:
|
|
182
|
+
profile_name = profile.strip() or "default"
|
|
183
|
+
store = _load_store()
|
|
184
|
+
profiles = store.get("profiles")
|
|
185
|
+
if not isinstance(profiles, dict):
|
|
186
|
+
profiles = {}
|
|
187
|
+
profiles[profile_name] = _credentials_to_dict(credentials)
|
|
188
|
+
store["profiles"] = profiles
|
|
189
|
+
if set_active:
|
|
190
|
+
store["active_profile"] = profile_name
|
|
191
|
+
return _save_store(store)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def load_profiles() -> dict[str, Credentials]:
|
|
195
|
+
store = _load_store()
|
|
196
|
+
profiles = store.get("profiles")
|
|
197
|
+
if not isinstance(profiles, dict):
|
|
198
|
+
return {}
|
|
199
|
+
out: dict[str, Credentials] = {}
|
|
200
|
+
for profile_name, payload in profiles.items():
|
|
201
|
+
if not isinstance(profile_name, str) or not isinstance(payload, dict):
|
|
202
|
+
continue
|
|
203
|
+
creds = _credentials_from_dict(payload)
|
|
204
|
+
if creds is not None:
|
|
205
|
+
out[profile_name] = creds
|
|
206
|
+
return out
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def list_profiles() -> list[str]:
|
|
210
|
+
return sorted(load_profiles().keys())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_active_profile_name() -> Optional[str]:
|
|
214
|
+
store = _load_store()
|
|
215
|
+
active = store.get("active_profile")
|
|
216
|
+
return active if isinstance(active, str) and active.strip() else None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def set_active_profile(profile: str) -> bool:
|
|
220
|
+
profile_name = profile.strip()
|
|
221
|
+
if not profile_name:
|
|
222
|
+
return False
|
|
223
|
+
store = _load_store()
|
|
224
|
+
profiles = store.get("profiles")
|
|
225
|
+
if not isinstance(profiles, dict) or profile_name not in profiles:
|
|
226
|
+
return False
|
|
227
|
+
store["active_profile"] = profile_name
|
|
228
|
+
_save_store(store)
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def delete_profile(profile: str) -> bool:
|
|
233
|
+
profile_name = profile.strip()
|
|
234
|
+
if not profile_name:
|
|
235
|
+
return False
|
|
236
|
+
store = _load_store()
|
|
237
|
+
profiles = store.get("profiles")
|
|
238
|
+
if not isinstance(profiles, dict) or profile_name not in profiles:
|
|
239
|
+
return False
|
|
240
|
+
del profiles[profile_name]
|
|
241
|
+
store["profiles"] = profiles
|
|
242
|
+
active = store.get("active_profile")
|
|
243
|
+
if active == profile_name:
|
|
244
|
+
store["active_profile"] = sorted(profiles.keys())[0] if profiles else None
|
|
245
|
+
_save_store(store)
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def load_credentials(*, profile: Optional[str] = None) -> Optional[Credentials]:
|
|
250
|
+
profiles = load_profiles()
|
|
251
|
+
if not profiles:
|
|
252
|
+
return None
|
|
253
|
+
if profile and profile in profiles:
|
|
254
|
+
return profiles[profile]
|
|
255
|
+
active = get_active_profile_name()
|
|
256
|
+
if active and active in profiles:
|
|
257
|
+
return profiles[active]
|
|
258
|
+
return profiles.get("default")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def clear_credentials() -> None:
|
|
262
|
+
_clear_keyring()
|
|
263
|
+
_clear_file()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from difflib import get_close_matches
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def suggest_value(raw: str, options: Iterable[str]) -> str | None:
|
|
6
|
+
value = raw.strip().lower()
|
|
7
|
+
normalized = [item.strip().lower() for item in options if item.strip()]
|
|
8
|
+
if not value or not normalized:
|
|
9
|
+
return None
|
|
10
|
+
matches = get_close_matches(value, normalized, n=1, cutoff=0.6)
|
|
11
|
+
return matches[0] if matches else None
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from applied_cli.commands._hints import suggest_value
|
|
7
|
+
|
|
8
|
+
MODALITY_ALIASES = {
|
|
9
|
+
"all": "All",
|
|
10
|
+
"call": "Call",
|
|
11
|
+
"sms": "SMS",
|
|
12
|
+
"email": "Email",
|
|
13
|
+
"chat": "Chat",
|
|
14
|
+
"internal": "Internal",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
AGENT_TYPE_ALIASES = {
|
|
18
|
+
"generic": "Generic",
|
|
19
|
+
"customer_support": "Customer Support",
|
|
20
|
+
"customer support": "Customer Support",
|
|
21
|
+
"conversion": "Conversion",
|
|
22
|
+
"sales": "Sales",
|
|
23
|
+
"marketing": "Marketing",
|
|
24
|
+
"engineering": "Engineering",
|
|
25
|
+
"product": "Product",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
RESPONSE_TYPE_ALIASES = {
|
|
29
|
+
"escalation": "escalate",
|
|
30
|
+
"escalate": "escalate",
|
|
31
|
+
"exact": "exact",
|
|
32
|
+
"qa": "qa",
|
|
33
|
+
"context": "context",
|
|
34
|
+
"greeting": "greeting",
|
|
35
|
+
"signature": "signature",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def normalize_question(text: str) -> str:
|
|
40
|
+
return re.sub(r"\s+", " ", text.strip().lower())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normalize_response_type(
|
|
44
|
+
raw: str,
|
|
45
|
+
*,
|
|
46
|
+
field_label: str = "type",
|
|
47
|
+
include_suggestion: bool = False,
|
|
48
|
+
) -> str:
|
|
49
|
+
key = raw.strip().lower()
|
|
50
|
+
if key not in RESPONSE_TYPE_ALIASES:
|
|
51
|
+
suffix = ""
|
|
52
|
+
if include_suggestion:
|
|
53
|
+
suggestion = suggest_value(key, RESPONSE_TYPE_ALIASES.keys())
|
|
54
|
+
if suggestion:
|
|
55
|
+
suffix = f" Did you mean '{suggestion}'?"
|
|
56
|
+
raise typer.BadParameter(
|
|
57
|
+
f"{field_label} must be one of: escalation, exact, qa, context, greeting, signature.{suffix}"
|
|
58
|
+
)
|
|
59
|
+
return RESPONSE_TYPE_ALIASES[key]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize_modality(raw: Optional[str]) -> Optional[str]:
|
|
63
|
+
if raw is None:
|
|
64
|
+
return None
|
|
65
|
+
key = raw.strip().lower()
|
|
66
|
+
if key not in MODALITY_ALIASES:
|
|
67
|
+
raise typer.BadParameter("modality must be one of: all, call, sms, email, chat, internal")
|
|
68
|
+
return MODALITY_ALIASES[key]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def normalize_agent_type(raw: Optional[str]) -> Optional[str]:
|
|
72
|
+
if raw is None:
|
|
73
|
+
return None
|
|
74
|
+
key = raw.strip().lower()
|
|
75
|
+
if key not in AGENT_TYPE_ALIASES:
|
|
76
|
+
raise typer.BadParameter(
|
|
77
|
+
"type must be one of: generic, customer_support, conversion, sales, marketing, engineering, product"
|
|
78
|
+
)
|
|
79
|
+
return AGENT_TYPE_ALIASES[key]
|