chattermate-cli 0.2.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.
- chattermate_cli-0.2.0/LICENSE +18 -0
- chattermate_cli-0.2.0/PKG-INFO +113 -0
- chattermate_cli-0.2.0/README.md +95 -0
- chattermate_cli-0.2.0/chattermate_cli/__init__.py +19 -0
- chattermate_cli-0.2.0/chattermate_cli/client.py +275 -0
- chattermate_cli-0.2.0/chattermate_cli/commands/__init__.py +17 -0
- chattermate_cli-0.2.0/chattermate_cli/commands/agent.py +122 -0
- chattermate_cli-0.2.0/chattermate_cli/commands/auth.py +202 -0
- chattermate_cli-0.2.0/chattermate_cli/commands/knowledge.py +104 -0
- chattermate_cli-0.2.0/chattermate_cli/commands/workflow.py +129 -0
- chattermate_cli-0.2.0/chattermate_cli/config.py +101 -0
- chattermate_cli-0.2.0/chattermate_cli/context.py +78 -0
- chattermate_cli-0.2.0/chattermate_cli/main.py +74 -0
- chattermate_cli-0.2.0/chattermate_cli/mcp_server.py +253 -0
- chattermate_cli-0.2.0/chattermate_cli.egg-info/PKG-INFO +113 -0
- chattermate_cli-0.2.0/chattermate_cli.egg-info/SOURCES.txt +22 -0
- chattermate_cli-0.2.0/chattermate_cli.egg-info/dependency_links.txt +1 -0
- chattermate_cli-0.2.0/chattermate_cli.egg-info/entry_points.txt +4 -0
- chattermate_cli-0.2.0/chattermate_cli.egg-info/requires.txt +8 -0
- chattermate_cli-0.2.0/chattermate_cli.egg-info/top_level.txt +1 -0
- chattermate_cli-0.2.0/pyproject.toml +30 -0
- chattermate_cli-0.2.0/setup.cfg +4 -0
- chattermate_cli-0.2.0/tests/test_client.py +134 -0
- chattermate_cli-0.2.0/tests/test_mcp.py +64 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ChatterMate CLI (chattermate-cli)
|
|
2
|
+
Copyright (C) 2024 ChatterMate
|
|
3
|
+
|
|
4
|
+
This program is free software: you can redistribute it and/or modify
|
|
5
|
+
it under the terms of the GNU Affero General Public License as
|
|
6
|
+
published by the Free Software Foundation, either version 3 of the
|
|
7
|
+
License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
This program is distributed in the hope that it will be useful,
|
|
10
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
GNU Affero General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
The full text of the GNU Affero General Public License v3.0 is available at:
|
|
18
|
+
https://www.gnu.org/licenses/agpl-3.0.txt
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chattermate-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: ChatterMate CLI and MCP server — sign up, authenticate, and configure agents, workflows and knowledge from the terminal or an AI agent.
|
|
5
|
+
Author: ChatterMate
|
|
6
|
+
License: AGPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: typer>=0.12
|
|
11
|
+
Requires-Dist: httpx>=0.27
|
|
12
|
+
Requires-Dist: rich>=13.0
|
|
13
|
+
Requires-Dist: mcp>=1.2
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
16
|
+
Requires-Dist: respx>=0.21; extra == "test"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# ChatterMate CLI & MCP server
|
|
20
|
+
|
|
21
|
+
`chattermate-cli` provides a command-line client and an MCP (Model Context Protocol)
|
|
22
|
+
server for ChatterMate. A human can sign up, log in and configure agents, workflows and
|
|
23
|
+
knowledge from the terminal; an AI agent can do the same through the MCP server.
|
|
24
|
+
|
|
25
|
+
This is an **enterprise** feature and depends on the enterprise backend's Personal Access
|
|
26
|
+
Tokens (PATs).
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e . # from this directory, for development
|
|
32
|
+
# or, once published:
|
|
33
|
+
pipx install chattermate-cli
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Console scripts installed: `chattermate` (alias `cmate`) and `chattermate-mcp`.
|
|
37
|
+
|
|
38
|
+
## Configure the API endpoint
|
|
39
|
+
|
|
40
|
+
By default the CLI talks to the hosted API at `https://api.chattermate.chat`. To target a
|
|
41
|
+
local or self-hosted backend, set `CHATTERMATE_API_URL=http://localhost:8000` (or pass
|
|
42
|
+
`--api-url`). Resolution order: `--api-url` flag → `CHATTERMATE_API_URL` env → stored config →
|
|
43
|
+
`https://api.chattermate.chat`.
|
|
44
|
+
|
|
45
|
+
## Quick start (human)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Create an organization + admin (community signup), or log in to an existing one
|
|
49
|
+
chattermate signup
|
|
50
|
+
chattermate login --email you@acme.com
|
|
51
|
+
|
|
52
|
+
chattermate whoami
|
|
53
|
+
|
|
54
|
+
# Mint a long-lived Personal Access Token for CI / AI agents (shown once!)
|
|
55
|
+
chattermate token create laptop-cli
|
|
56
|
+
chattermate token list
|
|
57
|
+
chattermate token revoke <token-id>
|
|
58
|
+
|
|
59
|
+
# Configure resources
|
|
60
|
+
chattermate agent list
|
|
61
|
+
chattermate agent create --name "Support" --type CUSTOMER_SUPPORT -i "Be concise"
|
|
62
|
+
chattermate workflow get <agent-id>
|
|
63
|
+
chattermate workflow create --agent-id <agent-id> --name "Onboarding"
|
|
64
|
+
chattermate knowledge add-url --website https://docs.acme.com --agent-id <agent-id>
|
|
65
|
+
chattermate knowledge status <queue-id>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Every command accepts `--json` for machine-readable output.
|
|
69
|
+
|
|
70
|
+
## Authentication
|
|
71
|
+
|
|
72
|
+
Two credentials are supported:
|
|
73
|
+
|
|
74
|
+
- **Personal Access Token** (`cmat_...`) — preferred for non-interactive use. Pass it via
|
|
75
|
+
the `CHATTERMATE_TOKEN` environment variable. Long-lived and revocable.
|
|
76
|
+
- **JWT login** — `chattermate login` stores access + refresh tokens in
|
|
77
|
+
`~/.chattermate/config.json` (mode `600`) and refreshes automatically.
|
|
78
|
+
|
|
79
|
+
## MCP server (AI agents)
|
|
80
|
+
|
|
81
|
+
`chattermate-mcp` is a stdio MCP server. Point your MCP client at it with a PAT:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"chattermate": {
|
|
87
|
+
"command": "uvx",
|
|
88
|
+
"args": ["--from", "chattermate-cli", "chattermate-mcp"],
|
|
89
|
+
"env": {
|
|
90
|
+
"CHATTERMATE_TOKEN": "cmat_...",
|
|
91
|
+
"CHATTERMATE_API_URL": "https://your-chattermate-host"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`chattermate token create` prints a ready-to-paste version of this snippet.
|
|
99
|
+
|
|
100
|
+
### Tools
|
|
101
|
+
|
|
102
|
+
Read-only: `whoami`, `list_agents`, `get_agent`, `get_workflow`, `get_workflow_nodes`,
|
|
103
|
+
`list_knowledge`, `get_ingestion_status`.
|
|
104
|
+
|
|
105
|
+
Mutating: `create_agent`, `update_agent`, `create_workflow`, `update_workflow`,
|
|
106
|
+
`update_workflow_nodes`, `add_knowledge_url`, `link_knowledge`, `unlink_knowledge`.
|
|
107
|
+
|
|
108
|
+
## Tests
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install -e ".[test]"
|
|
112
|
+
pytest tests/
|
|
113
|
+
```
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ChatterMate CLI & MCP server
|
|
2
|
+
|
|
3
|
+
`chattermate-cli` provides a command-line client and an MCP (Model Context Protocol)
|
|
4
|
+
server for ChatterMate. A human can sign up, log in and configure agents, workflows and
|
|
5
|
+
knowledge from the terminal; an AI agent can do the same through the MCP server.
|
|
6
|
+
|
|
7
|
+
This is an **enterprise** feature and depends on the enterprise backend's Personal Access
|
|
8
|
+
Tokens (PATs).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install -e . # from this directory, for development
|
|
14
|
+
# or, once published:
|
|
15
|
+
pipx install chattermate-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Console scripts installed: `chattermate` (alias `cmate`) and `chattermate-mcp`.
|
|
19
|
+
|
|
20
|
+
## Configure the API endpoint
|
|
21
|
+
|
|
22
|
+
By default the CLI talks to the hosted API at `https://api.chattermate.chat`. To target a
|
|
23
|
+
local or self-hosted backend, set `CHATTERMATE_API_URL=http://localhost:8000` (or pass
|
|
24
|
+
`--api-url`). Resolution order: `--api-url` flag → `CHATTERMATE_API_URL` env → stored config →
|
|
25
|
+
`https://api.chattermate.chat`.
|
|
26
|
+
|
|
27
|
+
## Quick start (human)
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Create an organization + admin (community signup), or log in to an existing one
|
|
31
|
+
chattermate signup
|
|
32
|
+
chattermate login --email you@acme.com
|
|
33
|
+
|
|
34
|
+
chattermate whoami
|
|
35
|
+
|
|
36
|
+
# Mint a long-lived Personal Access Token for CI / AI agents (shown once!)
|
|
37
|
+
chattermate token create laptop-cli
|
|
38
|
+
chattermate token list
|
|
39
|
+
chattermate token revoke <token-id>
|
|
40
|
+
|
|
41
|
+
# Configure resources
|
|
42
|
+
chattermate agent list
|
|
43
|
+
chattermate agent create --name "Support" --type CUSTOMER_SUPPORT -i "Be concise"
|
|
44
|
+
chattermate workflow get <agent-id>
|
|
45
|
+
chattermate workflow create --agent-id <agent-id> --name "Onboarding"
|
|
46
|
+
chattermate knowledge add-url --website https://docs.acme.com --agent-id <agent-id>
|
|
47
|
+
chattermate knowledge status <queue-id>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every command accepts `--json` for machine-readable output.
|
|
51
|
+
|
|
52
|
+
## Authentication
|
|
53
|
+
|
|
54
|
+
Two credentials are supported:
|
|
55
|
+
|
|
56
|
+
- **Personal Access Token** (`cmat_...`) — preferred for non-interactive use. Pass it via
|
|
57
|
+
the `CHATTERMATE_TOKEN` environment variable. Long-lived and revocable.
|
|
58
|
+
- **JWT login** — `chattermate login` stores access + refresh tokens in
|
|
59
|
+
`~/.chattermate/config.json` (mode `600`) and refreshes automatically.
|
|
60
|
+
|
|
61
|
+
## MCP server (AI agents)
|
|
62
|
+
|
|
63
|
+
`chattermate-mcp` is a stdio MCP server. Point your MCP client at it with a PAT:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"mcpServers": {
|
|
68
|
+
"chattermate": {
|
|
69
|
+
"command": "uvx",
|
|
70
|
+
"args": ["--from", "chattermate-cli", "chattermate-mcp"],
|
|
71
|
+
"env": {
|
|
72
|
+
"CHATTERMATE_TOKEN": "cmat_...",
|
|
73
|
+
"CHATTERMATE_API_URL": "https://your-chattermate-host"
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`chattermate token create` prints a ready-to-paste version of this snippet.
|
|
81
|
+
|
|
82
|
+
### Tools
|
|
83
|
+
|
|
84
|
+
Read-only: `whoami`, `list_agents`, `get_agent`, `get_workflow`, `get_workflow_nodes`,
|
|
85
|
+
`list_knowledge`, `get_ingestion_status`.
|
|
86
|
+
|
|
87
|
+
Mutating: `create_agent`, `update_agent`, `create_workflow`, `update_workflow`,
|
|
88
|
+
`update_workflow_nodes`, `add_knowledge_url`, `link_knowledge`, `unlink_knowledge`.
|
|
89
|
+
|
|
90
|
+
## Tests
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -e ".[test]"
|
|
94
|
+
pytest tests/
|
|
95
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatterMate - CLI & MCP SDK
|
|
3
|
+
Copyright (C) 2024 ChatterMate
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as
|
|
7
|
+
published by the Free Software Foundation, either version 3 of the
|
|
8
|
+
License, or (at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatterMate - API Client
|
|
3
|
+
Copyright (C) 2024 ChatterMate
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as
|
|
7
|
+
published by the Free Software Foundation, either version 3 of the
|
|
8
|
+
License, or (at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
|
|
23
|
+
API_PREFIX = "/api/v1"
|
|
24
|
+
PAT_PREFIX = "cmat_"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChatterMateError(Exception):
|
|
28
|
+
"""Raised when the API returns an error or is unreachable."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, status_code: Optional[int] = None):
|
|
31
|
+
super().__init__(message)
|
|
32
|
+
self.status_code = status_code
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Client:
|
|
36
|
+
"""
|
|
37
|
+
Thin HTTP wrapper around the ChatterMate API.
|
|
38
|
+
|
|
39
|
+
Supports two credentials:
|
|
40
|
+
* a Personal Access Token (``cmat_...``) — preferred for non-interactive use, and
|
|
41
|
+
* a JWT access token from ``login`` (auto-refreshed once on 401 using the refresh token).
|
|
42
|
+
|
|
43
|
+
Used by both the CLI commands and the MCP server.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
api_url: str,
|
|
49
|
+
token: Optional[str] = None,
|
|
50
|
+
refresh_token: Optional[str] = None,
|
|
51
|
+
on_tokens: Optional[Callable[[str, Optional[str]], None]] = None,
|
|
52
|
+
timeout: float = 30.0,
|
|
53
|
+
):
|
|
54
|
+
self.api_url = api_url.rstrip("/")
|
|
55
|
+
self.token = token
|
|
56
|
+
self.refresh_token = refresh_token
|
|
57
|
+
self.on_tokens = on_tokens
|
|
58
|
+
self._http = httpx.Client(timeout=timeout)
|
|
59
|
+
|
|
60
|
+
# -- internals ---------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def _is_pat(self) -> bool:
|
|
63
|
+
return bool(self.token and self.token.startswith(PAT_PREFIX))
|
|
64
|
+
|
|
65
|
+
def _auth_headers(self) -> Dict[str, str]:
|
|
66
|
+
return {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
|
67
|
+
|
|
68
|
+
def _refresh(self) -> bool:
|
|
69
|
+
if not self.refresh_token or self._is_pat():
|
|
70
|
+
return False
|
|
71
|
+
try:
|
|
72
|
+
resp = self._http.post(
|
|
73
|
+
f"{self.api_url}{API_PREFIX}/users/refresh",
|
|
74
|
+
cookies={"refresh_token": self.refresh_token},
|
|
75
|
+
)
|
|
76
|
+
except httpx.HTTPError:
|
|
77
|
+
return False
|
|
78
|
+
if resp.status_code != 200:
|
|
79
|
+
return False
|
|
80
|
+
data = resp.json()
|
|
81
|
+
self.token = data.get("access_token", self.token)
|
|
82
|
+
if data.get("refresh_token"):
|
|
83
|
+
self.refresh_token = data["refresh_token"]
|
|
84
|
+
if self.on_tokens:
|
|
85
|
+
self.on_tokens(self.token, self.refresh_token)
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def _format_error(resp: httpx.Response) -> str:
|
|
90
|
+
try:
|
|
91
|
+
detail = resp.json().get("detail")
|
|
92
|
+
except Exception:
|
|
93
|
+
return resp.text or f"HTTP {resp.status_code}"
|
|
94
|
+
if isinstance(detail, list):
|
|
95
|
+
# FastAPI validation errors: list of {loc, msg, ...} — but tolerate plain strings.
|
|
96
|
+
parts = []
|
|
97
|
+
for item in detail:
|
|
98
|
+
if isinstance(item, dict):
|
|
99
|
+
loc = ".".join(str(p) for p in item.get("loc", []) if p not in ("body",))
|
|
100
|
+
msg = item.get("msg", "")
|
|
101
|
+
parts.append(f"{loc}: {msg}".strip(": "))
|
|
102
|
+
else:
|
|
103
|
+
parts.append(str(item))
|
|
104
|
+
return "; ".join(p for p in parts if p) or f"HTTP {resp.status_code}"
|
|
105
|
+
return str(detail) if detail else f"HTTP {resp.status_code}"
|
|
106
|
+
|
|
107
|
+
def request(
|
|
108
|
+
self,
|
|
109
|
+
method: str,
|
|
110
|
+
path: str,
|
|
111
|
+
*,
|
|
112
|
+
json: Any = None,
|
|
113
|
+
params: Optional[Dict[str, Any]] = None,
|
|
114
|
+
data: Optional[Dict[str, Any]] = None,
|
|
115
|
+
auth: bool = True,
|
|
116
|
+
_retry: bool = True,
|
|
117
|
+
) -> Any:
|
|
118
|
+
url = f"{self.api_url}{API_PREFIX}{path}"
|
|
119
|
+
headers = self._auth_headers() if auth else {}
|
|
120
|
+
try:
|
|
121
|
+
resp = self._http.request(
|
|
122
|
+
method, url, json=json, params=params, data=data, headers=headers
|
|
123
|
+
)
|
|
124
|
+
except httpx.HTTPError as e:
|
|
125
|
+
raise ChatterMateError(f"Could not reach {self.api_url}: {e}") from e
|
|
126
|
+
|
|
127
|
+
if resp.status_code == 401 and auth and _retry and self._refresh():
|
|
128
|
+
return self.request(
|
|
129
|
+
method, path, json=json, params=params, data=data, auth=auth, _retry=False
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if resp.status_code >= 400:
|
|
133
|
+
raise ChatterMateError(self._format_error(resp), resp.status_code)
|
|
134
|
+
|
|
135
|
+
if resp.status_code == 204 or not resp.content:
|
|
136
|
+
return None
|
|
137
|
+
ctype = resp.headers.get("content-type", "")
|
|
138
|
+
return resp.json() if "application/json" in ctype else resp.text
|
|
139
|
+
|
|
140
|
+
# -- auth --------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def login(self, email: str, password: str) -> Dict[str, Any]:
|
|
143
|
+
data = self.request(
|
|
144
|
+
"POST", "/users/login",
|
|
145
|
+
data={"username": email, "password": password}, auth=False,
|
|
146
|
+
)
|
|
147
|
+
self.token = data.get("access_token")
|
|
148
|
+
self.refresh_token = data.get("refresh_token")
|
|
149
|
+
return data
|
|
150
|
+
|
|
151
|
+
def signup(
|
|
152
|
+
self, name: str, domain: str, admin_email: str, admin_name: str,
|
|
153
|
+
admin_password: str, timezone: str = "UTC",
|
|
154
|
+
) -> Dict[str, Any]:
|
|
155
|
+
body = {
|
|
156
|
+
"name": name, "domain": domain, "timezone": timezone,
|
|
157
|
+
"admin_email": admin_email, "admin_name": admin_name,
|
|
158
|
+
"admin_password": admin_password,
|
|
159
|
+
}
|
|
160
|
+
data = self.request("POST", "/organizations", json=body, auth=False)
|
|
161
|
+
if isinstance(data, dict) and data.get("access_token"):
|
|
162
|
+
self.token = data.get("access_token")
|
|
163
|
+
self.refresh_token = data.get("refresh_token")
|
|
164
|
+
return data
|
|
165
|
+
|
|
166
|
+
def signup_request_otp(self, email: str) -> Dict[str, Any]:
|
|
167
|
+
"""Enterprise signup step 1: request an email OTP (multi-org instances)."""
|
|
168
|
+
return self.request(
|
|
169
|
+
"POST", "/enterprise/signup/verify-email", json={"email": email}, auth=False
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def signup_verify_otp(
|
|
173
|
+
self, email: str, otp: str, admin_name: str, admin_password: str,
|
|
174
|
+
organization_name: str, domain: str, colorcode: Optional[str] = None,
|
|
175
|
+
) -> Dict[str, Any]:
|
|
176
|
+
"""Enterprise signup step 2: verify OTP and create the org + admin."""
|
|
177
|
+
body = {
|
|
178
|
+
"email": email, "otp": otp, "name": admin_name, "password": admin_password,
|
|
179
|
+
"organization_name": organization_name, "domain": domain,
|
|
180
|
+
}
|
|
181
|
+
if colorcode:
|
|
182
|
+
body["colorcode"] = colorcode
|
|
183
|
+
data = self.request("POST", "/enterprise/signup/verify-otp", json=body, auth=False)
|
|
184
|
+
if isinstance(data, dict) and data.get("access_token"):
|
|
185
|
+
self.token = data.get("access_token")
|
|
186
|
+
self.refresh_token = data.get("refresh_token")
|
|
187
|
+
return data
|
|
188
|
+
|
|
189
|
+
def whoami(self) -> Dict[str, Any]:
|
|
190
|
+
# No dedicated GET /me endpoint exists; an empty PATCH is a safe no-op that
|
|
191
|
+
# returns the authenticated user.
|
|
192
|
+
return self.request("PATCH", "/users/me", json={})
|
|
193
|
+
|
|
194
|
+
# -- personal access tokens -------------------------------------------
|
|
195
|
+
|
|
196
|
+
def create_pat(
|
|
197
|
+
self, name: str, scopes: Optional[List[str]] = None,
|
|
198
|
+
expires_in_days: Optional[int] = None,
|
|
199
|
+
) -> Dict[str, Any]:
|
|
200
|
+
body: Dict[str, Any] = {"name": name}
|
|
201
|
+
if scopes:
|
|
202
|
+
body["scopes"] = scopes
|
|
203
|
+
if expires_in_days is not None:
|
|
204
|
+
body["expires_in_days"] = expires_in_days
|
|
205
|
+
return self.request("POST", "/enterprise/tokens", json=body)
|
|
206
|
+
|
|
207
|
+
def list_pats(self) -> List[Dict[str, Any]]:
|
|
208
|
+
return self.request("GET", "/enterprise/tokens")
|
|
209
|
+
|
|
210
|
+
def revoke_pat(self, token_id: str) -> None:
|
|
211
|
+
return self.request("DELETE", f"/enterprise/tokens/{token_id}")
|
|
212
|
+
|
|
213
|
+
# -- agents ------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
def list_agents(self) -> Any:
|
|
216
|
+
return self.request("GET", "/agent/list")
|
|
217
|
+
|
|
218
|
+
def get_agent(self, agent_id: str) -> Any:
|
|
219
|
+
return self.request("GET", f"/agent/{agent_id}")
|
|
220
|
+
|
|
221
|
+
def create_agent(self, payload: Dict[str, Any]) -> Any:
|
|
222
|
+
return self.request("POST", "/agent", json=payload)
|
|
223
|
+
|
|
224
|
+
def update_agent(self, agent_id: str, payload: Dict[str, Any]) -> Any:
|
|
225
|
+
return self.request("PUT", f"/agent/{agent_id}", json=payload)
|
|
226
|
+
|
|
227
|
+
# -- workflows ---------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def get_workflow_for_agent(self, agent_id: str) -> Any:
|
|
230
|
+
return self.request("GET", f"/workflow/agent/{agent_id}")
|
|
231
|
+
|
|
232
|
+
def create_workflow(self, payload: Dict[str, Any]) -> Any:
|
|
233
|
+
return self.request("POST", "/workflow", json=payload)
|
|
234
|
+
|
|
235
|
+
def update_workflow(self, workflow_id: str, payload: Dict[str, Any]) -> Any:
|
|
236
|
+
return self.request("PUT", f"/workflow/{workflow_id}", json=payload)
|
|
237
|
+
|
|
238
|
+
def get_workflow_nodes(self, workflow_id: str) -> Any:
|
|
239
|
+
return self.request("GET", f"/workflow/{workflow_id}/nodes")
|
|
240
|
+
|
|
241
|
+
def update_workflow_nodes(self, workflow_id: str, payload: Dict[str, Any]) -> Any:
|
|
242
|
+
return self.request("PUT", f"/workflow/{workflow_id}/nodes", json=payload)
|
|
243
|
+
|
|
244
|
+
# -- knowledge ---------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
def add_knowledge(
|
|
247
|
+
self, org_id: str, pdf_urls: Optional[List[str]] = None,
|
|
248
|
+
websites: Optional[List[str]] = None, agent_id: Optional[str] = None,
|
|
249
|
+
) -> Any:
|
|
250
|
+
body: Dict[str, Any] = {
|
|
251
|
+
"org_id": org_id,
|
|
252
|
+
"pdf_urls": pdf_urls or [],
|
|
253
|
+
"websites": websites or [],
|
|
254
|
+
}
|
|
255
|
+
if agent_id:
|
|
256
|
+
body["agent_id"] = agent_id
|
|
257
|
+
return self.request("POST", "/knowledge/add/urls", json=body)
|
|
258
|
+
|
|
259
|
+
def list_knowledge_for_agent(self, agent_id: str) -> Any:
|
|
260
|
+
return self.request("GET", f"/knowledge/agent/{agent_id}")
|
|
261
|
+
|
|
262
|
+
def link_knowledge(self, knowledge_id: int, agent_id: str) -> Any:
|
|
263
|
+
return self.request(
|
|
264
|
+
"POST", "/knowledge/link",
|
|
265
|
+
params={"knowledge_id": knowledge_id, "agent_id": agent_id},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def unlink_knowledge(self, knowledge_id: int, agent_id: str) -> Any:
|
|
269
|
+
return self.request(
|
|
270
|
+
"DELETE", "/knowledge/unlink",
|
|
271
|
+
params={"knowledge_id": knowledge_id, "agent_id": agent_id},
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
def knowledge_queue_status(self, queue_id: int) -> Any:
|
|
275
|
+
return self.request("GET", f"/knowledge/queue/{queue_id}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatterMate - CLI Commands
|
|
3
|
+
Copyright (C) 2024 ChatterMate
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as
|
|
7
|
+
published by the Free Software Foundation, either version 3 of the
|
|
8
|
+
License, or (at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
17
|
+
"""
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChatterMate - CLI Agent Commands
|
|
3
|
+
Copyright (C) 2024 ChatterMate
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as
|
|
7
|
+
published by the Free Software Foundation, either version 3 of the
|
|
8
|
+
License, or (at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <https://www.gnu.org/licenses/>
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from typing import List, Optional
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
from .. import config
|
|
25
|
+
from ..context import console, get_client, output, print_error, run
|
|
26
|
+
|
|
27
|
+
agent_app = typer.Typer(no_args_is_help=True, help="Create and manage AI agents.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@agent_app.command("list")
|
|
31
|
+
def list_agents(as_json: bool = typer.Option(False, "--json", help="Output raw JSON.")):
|
|
32
|
+
"""List the organization's agents."""
|
|
33
|
+
client = get_client()
|
|
34
|
+
data = run(client.list_agents)
|
|
35
|
+
|
|
36
|
+
def render(rows):
|
|
37
|
+
table = Table(title="Agents")
|
|
38
|
+
for col in ("id", "name", "type", "active"):
|
|
39
|
+
table.add_column(col)
|
|
40
|
+
for r in rows or []:
|
|
41
|
+
table.add_row(
|
|
42
|
+
str(r.get("id")), r.get("display_name") or r.get("name", ""),
|
|
43
|
+
str(r.get("agent_type", "")), "yes" if r.get("is_active") else "no",
|
|
44
|
+
)
|
|
45
|
+
console.print(table)
|
|
46
|
+
|
|
47
|
+
output(data, as_json, render)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@agent_app.command("get")
|
|
51
|
+
def get_agent(
|
|
52
|
+
agent_id: str = typer.Argument(..., help="Agent id."),
|
|
53
|
+
as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
|
|
54
|
+
):
|
|
55
|
+
"""Show a single agent."""
|
|
56
|
+
client = get_client()
|
|
57
|
+
output(run(lambda: client.get_agent(agent_id)), as_json)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@agent_app.command("create")
|
|
61
|
+
def create_agent(
|
|
62
|
+
name: str = typer.Option(..., "--name", help="Agent name."),
|
|
63
|
+
agent_type: str = typer.Option(
|
|
64
|
+
"custom", "--type",
|
|
65
|
+
help="Agent type: customer_support, sales, tech_support, general, custom (case-insensitive).",
|
|
66
|
+
),
|
|
67
|
+
instruction: List[str] = typer.Option(
|
|
68
|
+
[], "--instruction", "-i", help="An instruction line (repeatable)."
|
|
69
|
+
),
|
|
70
|
+
description: Optional[str] = typer.Option(None, "--description"),
|
|
71
|
+
display_name: Optional[str] = typer.Option(None, "--display-name"),
|
|
72
|
+
as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
|
|
73
|
+
):
|
|
74
|
+
"""Create an agent. Organization is taken from your stored session."""
|
|
75
|
+
org_id = config.load_config().get("organization_id")
|
|
76
|
+
if not org_id:
|
|
77
|
+
print_error("No organization in session. Run 'chattermate login' or 'whoami' first.")
|
|
78
|
+
raise typer.Exit(code=1)
|
|
79
|
+
payload = {
|
|
80
|
+
"name": name,
|
|
81
|
+
# API enum values are lowercase (customer_support, sales, ...); normalize for convenience.
|
|
82
|
+
"agent_type": agent_type.lower(),
|
|
83
|
+
"instructions": list(instruction) or [f"You are {name}, a helpful assistant."],
|
|
84
|
+
"organization_id": org_id,
|
|
85
|
+
}
|
|
86
|
+
if description is not None:
|
|
87
|
+
payload["description"] = description
|
|
88
|
+
if display_name is not None:
|
|
89
|
+
payload["display_name"] = display_name
|
|
90
|
+
client = get_client()
|
|
91
|
+
data = run(lambda: client.create_agent(payload))
|
|
92
|
+
if not as_json:
|
|
93
|
+
console.print(f"[green]Created agent[/green] {data.get('id')} ({name})")
|
|
94
|
+
output(data, as_json)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@agent_app.command("update")
|
|
98
|
+
def update_agent(
|
|
99
|
+
agent_id: str = typer.Argument(..., help="Agent id."),
|
|
100
|
+
instruction: List[str] = typer.Option(
|
|
101
|
+
[], "--instruction", "-i", help="Replace instructions (repeatable)."
|
|
102
|
+
),
|
|
103
|
+
display_name: Optional[str] = typer.Option(None, "--display-name"),
|
|
104
|
+
active: Optional[bool] = typer.Option(None, "--active/--inactive"),
|
|
105
|
+
as_json: bool = typer.Option(False, "--json", help="Output raw JSON."),
|
|
106
|
+
):
|
|
107
|
+
"""Update an agent's instructions, display name, or active state."""
|
|
108
|
+
payload: dict = {}
|
|
109
|
+
if instruction:
|
|
110
|
+
payload["instructions"] = list(instruction)
|
|
111
|
+
if display_name is not None:
|
|
112
|
+
payload["display_name"] = display_name
|
|
113
|
+
if active is not None:
|
|
114
|
+
payload["is_active"] = active
|
|
115
|
+
if not payload:
|
|
116
|
+
print_error("Nothing to update. Pass --instruction, --display-name or --active/--inactive.")
|
|
117
|
+
raise typer.Exit(code=1)
|
|
118
|
+
client = get_client()
|
|
119
|
+
data = run(lambda: client.update_agent(agent_id, payload))
|
|
120
|
+
if not as_json:
|
|
121
|
+
console.print(f"[green]Updated agent[/green] {agent_id}")
|
|
122
|
+
output(data, as_json)
|