ontora-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.
- ontora_cli-0.1.0/PKG-INFO +118 -0
- ontora_cli-0.1.0/README.md +93 -0
- ontora_cli-0.1.0/ontora_cli/__init__.py +3 -0
- ontora_cli-0.1.0/ontora_cli/client.py +72 -0
- ontora_cli-0.1.0/ontora_cli/commands/__init__.py +0 -0
- ontora_cli-0.1.0/ontora_cli/commands/auth.py +40 -0
- ontora_cli-0.1.0/ontora_cli/commands/campaigns.py +161 -0
- ontora_cli-0.1.0/ontora_cli/commands/contacts.py +64 -0
- ontora_cli-0.1.0/ontora_cli/commands/query.py +41 -0
- ontora_cli-0.1.0/ontora_cli/commands/reports.py +39 -0
- ontora_cli-0.1.0/ontora_cli/commands/transcripts.py +65 -0
- ontora_cli-0.1.0/ontora_cli/commands/webhooks.py +90 -0
- ontora_cli-0.1.0/ontora_cli/config.py +72 -0
- ontora_cli-0.1.0/ontora_cli/formatters.py +84 -0
- ontora_cli-0.1.0/ontora_cli/main.py +70 -0
- ontora_cli-0.1.0/ontora_cli/schemas.py +46 -0
- ontora_cli-0.1.0/ontora_cli.egg-info/PKG-INFO +118 -0
- ontora_cli-0.1.0/ontora_cli.egg-info/SOURCES.txt +22 -0
- ontora_cli-0.1.0/ontora_cli.egg-info/dependency_links.txt +1 -0
- ontora_cli-0.1.0/ontora_cli.egg-info/entry_points.txt +2 -0
- ontora_cli-0.1.0/ontora_cli.egg-info/requires.txt +5 -0
- ontora_cli-0.1.0/ontora_cli.egg-info/top_level.txt +1 -0
- ontora_cli-0.1.0/pyproject.toml +41 -0
- ontora_cli-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ontora-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ontora CLI — manage interview campaigns, transcripts, reports, and webhooks from the terminal.
|
|
5
|
+
Author: Ontora
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Documentation, https://docs.ontora.com/cli/install
|
|
8
|
+
Project-URL: API, https://docs.ontora.com/api-reference/introduction
|
|
9
|
+
Project-URL: Homepage, https://docs.ontora.com
|
|
10
|
+
Keywords: ontora,interviews,research,synthesis,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: typer>=0.12.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: rich>=13.7.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: pydantic>=2.6.0
|
|
25
|
+
|
|
26
|
+
# Ontora CLI
|
|
27
|
+
|
|
28
|
+
Manage Ontora interview campaigns, transcripts, reports, and webhooks from the terminal.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ontora-cli
|
|
34
|
+
# or, from this repo:
|
|
35
|
+
cd cli && pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ontora auth login # Paste your API key (or set ONTORA_API_KEY)
|
|
42
|
+
ontora auth status # Verify connection
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Get an API key from the Ontora web app (Settings → API Keys) or via
|
|
46
|
+
`POST /v1/api-keys`. You'll also need to know your workspace ID.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export ONTORA_WORKSPACE_ID=<uuid>
|
|
50
|
+
# or set per-command with --workspace
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Campaigns
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ontora campaigns list
|
|
57
|
+
ontora campaigns create --config campaign.yaml
|
|
58
|
+
ontora campaigns show <id>
|
|
59
|
+
ontora campaigns start <id>
|
|
60
|
+
ontora campaigns pause <id>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Config-as-code (YAML)
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
name: "Q2 Procurement Discovery"
|
|
67
|
+
goal: "Map end-to-end procurement workflow to identify automation opportunities"
|
|
68
|
+
channel: in_app_chat
|
|
69
|
+
duration_minutes: 30
|
|
70
|
+
interview_context_for_employee: >
|
|
71
|
+
We're mapping how procurement works across the organization.
|
|
72
|
+
success_criteria: >
|
|
73
|
+
Complete process map with step-level pain points from every department.
|
|
74
|
+
|
|
75
|
+
topics:
|
|
76
|
+
- name: "Daily Workflow"
|
|
77
|
+
goal: "Understand the step-by-step procurement process"
|
|
78
|
+
priority: required
|
|
79
|
+
success_threshold: "Can describe process end-to-end with tools used"
|
|
80
|
+
sub_topics: ["intake", "approval", "vendor selection", "PO creation"]
|
|
81
|
+
questions:
|
|
82
|
+
- text: "Walk me through a typical purchase request"
|
|
83
|
+
required: true
|
|
84
|
+
- text: "What tools do you use at each step?"
|
|
85
|
+
|
|
86
|
+
contacts:
|
|
87
|
+
- name: "Jane Smith"
|
|
88
|
+
email: "jane@company.com"
|
|
89
|
+
department: "Procurement"
|
|
90
|
+
job_title: "Senior Buyer"
|
|
91
|
+
seniority_level: 4
|
|
92
|
+
# Or: { file: contacts.csv }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Transcripts and reports
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
ontora transcripts list <campaign-id>
|
|
99
|
+
ontora transcripts get <campaign-id> <conversation-id>
|
|
100
|
+
ontora transcripts export <campaign-id> [--output ./out] # ZIP of .md files
|
|
101
|
+
ontora reports show <campaign-id>
|
|
102
|
+
ontora reports export <campaign-id> [--output report.md]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Graph RAG query
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ontora query <campaign-id> "What are the top pain points across procurement?"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Webhooks
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ontora webhooks list
|
|
115
|
+
ontora webhooks add https://your-app.example/hook --events interview.completed,campaign.completed
|
|
116
|
+
ontora webhooks test <id>
|
|
117
|
+
ontora webhooks remove <id>
|
|
118
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Ontora CLI
|
|
2
|
+
|
|
3
|
+
Manage Ontora interview campaigns, transcripts, reports, and webhooks from the terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install ontora-cli
|
|
9
|
+
# or, from this repo:
|
|
10
|
+
cd cli && pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ontora auth login # Paste your API key (or set ONTORA_API_KEY)
|
|
17
|
+
ontora auth status # Verify connection
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Get an API key from the Ontora web app (Settings → API Keys) or via
|
|
21
|
+
`POST /v1/api-keys`. You'll also need to know your workspace ID.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export ONTORA_WORKSPACE_ID=<uuid>
|
|
25
|
+
# or set per-command with --workspace
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Campaigns
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
ontora campaigns list
|
|
32
|
+
ontora campaigns create --config campaign.yaml
|
|
33
|
+
ontora campaigns show <id>
|
|
34
|
+
ontora campaigns start <id>
|
|
35
|
+
ontora campaigns pause <id>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Config-as-code (YAML)
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
name: "Q2 Procurement Discovery"
|
|
42
|
+
goal: "Map end-to-end procurement workflow to identify automation opportunities"
|
|
43
|
+
channel: in_app_chat
|
|
44
|
+
duration_minutes: 30
|
|
45
|
+
interview_context_for_employee: >
|
|
46
|
+
We're mapping how procurement works across the organization.
|
|
47
|
+
success_criteria: >
|
|
48
|
+
Complete process map with step-level pain points from every department.
|
|
49
|
+
|
|
50
|
+
topics:
|
|
51
|
+
- name: "Daily Workflow"
|
|
52
|
+
goal: "Understand the step-by-step procurement process"
|
|
53
|
+
priority: required
|
|
54
|
+
success_threshold: "Can describe process end-to-end with tools used"
|
|
55
|
+
sub_topics: ["intake", "approval", "vendor selection", "PO creation"]
|
|
56
|
+
questions:
|
|
57
|
+
- text: "Walk me through a typical purchase request"
|
|
58
|
+
required: true
|
|
59
|
+
- text: "What tools do you use at each step?"
|
|
60
|
+
|
|
61
|
+
contacts:
|
|
62
|
+
- name: "Jane Smith"
|
|
63
|
+
email: "jane@company.com"
|
|
64
|
+
department: "Procurement"
|
|
65
|
+
job_title: "Senior Buyer"
|
|
66
|
+
seniority_level: 4
|
|
67
|
+
# Or: { file: contacts.csv }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Transcripts and reports
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
ontora transcripts list <campaign-id>
|
|
74
|
+
ontora transcripts get <campaign-id> <conversation-id>
|
|
75
|
+
ontora transcripts export <campaign-id> [--output ./out] # ZIP of .md files
|
|
76
|
+
ontora reports show <campaign-id>
|
|
77
|
+
ontora reports export <campaign-id> [--output report.md]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Graph RAG query
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
ontora query <campaign-id> "What are the top pain points across procurement?"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Webhooks
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
ontora webhooks list
|
|
90
|
+
ontora webhooks add https://your-app.example/hook --events interview.completed,campaign.completed
|
|
91
|
+
ontora webhooks test <id>
|
|
92
|
+
ontora webhooks remove <id>
|
|
93
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Thin httpx wrapper around the Ontora REST API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ontora_cli.config import CliConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiError(Exception):
|
|
14
|
+
def __init__(self, status: int, detail: Any):
|
|
15
|
+
self.status = status
|
|
16
|
+
self.detail = detail
|
|
17
|
+
super().__init__(f"HTTP {status}: {detail}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OntoraClient:
|
|
21
|
+
def __init__(self, cfg: CliConfig, api_key: str):
|
|
22
|
+
self.cfg = cfg
|
|
23
|
+
self._client = httpx.Client(
|
|
24
|
+
base_url=cfg.base_url,
|
|
25
|
+
headers={"X-API-Key": api_key, "User-Agent": "ontora-cli/0.1.0"},
|
|
26
|
+
timeout=60.0,
|
|
27
|
+
follow_redirects=True,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
def close(self) -> None:
|
|
31
|
+
self._client.close()
|
|
32
|
+
|
|
33
|
+
def __enter__(self) -> "OntoraClient":
|
|
34
|
+
return self
|
|
35
|
+
|
|
36
|
+
def __exit__(self, *_: Any) -> None:
|
|
37
|
+
self.close()
|
|
38
|
+
|
|
39
|
+
def _raise(self, r: httpx.Response) -> None:
|
|
40
|
+
if r.is_success:
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
detail = r.json()
|
|
44
|
+
except json.JSONDecodeError:
|
|
45
|
+
detail = r.text
|
|
46
|
+
raise ApiError(r.status_code, detail)
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------------ HTTP
|
|
49
|
+
def get(self, path: str, params: dict | None = None) -> Any:
|
|
50
|
+
r = self._client.get(path, params=params)
|
|
51
|
+
self._raise(r)
|
|
52
|
+
return r.json() if r.content else None
|
|
53
|
+
|
|
54
|
+
def post(self, path: str, json_body: dict | None = None, params: dict | None = None) -> Any:
|
|
55
|
+
r = self._client.post(path, json=json_body, params=params)
|
|
56
|
+
self._raise(r)
|
|
57
|
+
return r.json() if r.content else None
|
|
58
|
+
|
|
59
|
+
def patch(self, path: str, json_body: dict) -> Any:
|
|
60
|
+
r = self._client.patch(path, json=json_body)
|
|
61
|
+
self._raise(r)
|
|
62
|
+
return r.json() if r.content else None
|
|
63
|
+
|
|
64
|
+
def delete(self, path: str) -> None:
|
|
65
|
+
r = self._client.delete(path)
|
|
66
|
+
self._raise(r)
|
|
67
|
+
|
|
68
|
+
def get_raw(self, path: str, params: dict | None = None) -> httpx.Response:
|
|
69
|
+
"""Return the raw response (for downloading binary/markdown)."""
|
|
70
|
+
r = self._client.get(path, params=params)
|
|
71
|
+
self._raise(r)
|
|
72
|
+
return r
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Auth commands: login, status."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
|
|
8
|
+
from ontora_cli.client import ApiError, OntoraClient
|
|
9
|
+
from ontora_cli.config import load_config, save_config
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="API key and connection management.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def login(
|
|
16
|
+
api_key: str = typer.Option(..., "--api-key", "-k", prompt=True, hide_input=True, help="Your Ontora API key (ont_...)"),
|
|
17
|
+
base_url: str | None = typer.Option(None, "--base-url", help="Override API base URL."),
|
|
18
|
+
workspace: str | None = typer.Option(None, "--workspace", help="Default workspace ID."),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Save an API key + optional defaults to ~/.ontora/config.yaml."""
|
|
21
|
+
save_config(api_key=api_key, base_url=base_url, workspace_id=workspace)
|
|
22
|
+
rprint("[green]Saved credentials to ~/.ontora/config.yaml[/green]")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command()
|
|
26
|
+
def status() -> None:
|
|
27
|
+
"""Verify the API key works by calling the health endpoint."""
|
|
28
|
+
cfg = load_config()
|
|
29
|
+
if not cfg.api_key:
|
|
30
|
+
rprint("[red]No API key configured.[/red] Run `ontora auth login`.")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
try:
|
|
33
|
+
with OntoraClient(cfg, cfg.api_key) as c:
|
|
34
|
+
c.get("/v1/health")
|
|
35
|
+
rprint(f"[green]Connected.[/green] base_url={cfg.base_url}")
|
|
36
|
+
if cfg.workspace_id:
|
|
37
|
+
rprint(f"Default workspace: {cfg.workspace_id}")
|
|
38
|
+
except ApiError as e:
|
|
39
|
+
rprint(f"[red]API error ({e.status}):[/red] {e.detail}")
|
|
40
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Campaign commands: list, create (from YAML), show, start, pause."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
import yaml
|
|
10
|
+
from rich import print as rprint
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
|
|
14
|
+
from ontora_cli.client import ApiError, OntoraClient
|
|
15
|
+
from ontora_cli.config import load_config, require_api_key, require_workspace
|
|
16
|
+
from ontora_cli.formatters import print_campaigns
|
|
17
|
+
from ontora_cli.schemas import CampaignYaml
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(help="Manage interview campaigns.")
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command("list")
|
|
24
|
+
def list_cmd(
|
|
25
|
+
workspace: str | None = typer.Option(None, "--workspace", "-w"),
|
|
26
|
+
status: str | None = typer.Option(None, "--status", help="Filter by draft/active/paused/completed"),
|
|
27
|
+
) -> None:
|
|
28
|
+
cfg = load_config()
|
|
29
|
+
key = require_api_key(cfg)
|
|
30
|
+
ws = require_workspace(cfg, workspace)
|
|
31
|
+
params = {"workspace_id": ws}
|
|
32
|
+
if status:
|
|
33
|
+
params["status"] = status
|
|
34
|
+
with OntoraClient(cfg, key) as c:
|
|
35
|
+
data = c.get("/v1/interviews", params=params)
|
|
36
|
+
print_campaigns(data or [])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command("show")
|
|
40
|
+
def show_cmd(interview_id: str) -> None:
|
|
41
|
+
cfg = load_config()
|
|
42
|
+
key = require_api_key(cfg)
|
|
43
|
+
with OntoraClient(cfg, key) as c:
|
|
44
|
+
data = c.get(f"/v1/interviews/{interview_id}/detail")
|
|
45
|
+
progress = None
|
|
46
|
+
try:
|
|
47
|
+
progress = c.get(f"/v1/interviews/{interview_id}/progress")
|
|
48
|
+
except ApiError:
|
|
49
|
+
pass
|
|
50
|
+
rprint(Panel.fit(f"[bold]{data['name']}[/bold]\n{data.get('goal') or ''}", title="Campaign"))
|
|
51
|
+
rprint(f"Status: [cyan]{data.get('status')}[/cyan] Channel: {data.get('channel')}")
|
|
52
|
+
if progress:
|
|
53
|
+
rprint(
|
|
54
|
+
f"Progress: {progress.get('completed', 0)}/{progress.get('total', 0)} "
|
|
55
|
+
f"({progress.get('completion_percent', 0):.1f}%)"
|
|
56
|
+
)
|
|
57
|
+
rprint(f"Topics: {len(data.get('question_sets') or [])} Contacts: {len(data.get('contacts') or [])}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@app.command("create")
|
|
61
|
+
def create_cmd(
|
|
62
|
+
config_file: Path = typer.Option(..., "--config", "-c", exists=True, readable=True, help="Campaign YAML file"),
|
|
63
|
+
workspace: str | None = typer.Option(None, "--workspace", "-w"),
|
|
64
|
+
start: bool = typer.Option(False, "--start", help="Start the campaign immediately after creation"),
|
|
65
|
+
) -> None:
|
|
66
|
+
cfg = load_config()
|
|
67
|
+
key = require_api_key(cfg)
|
|
68
|
+
ws = require_workspace(cfg, workspace)
|
|
69
|
+
|
|
70
|
+
with config_file.open() as f:
|
|
71
|
+
raw = yaml.safe_load(f)
|
|
72
|
+
spec = CampaignYaml.model_validate(raw)
|
|
73
|
+
|
|
74
|
+
with OntoraClient(cfg, key) as c:
|
|
75
|
+
# 1. Create interview shell
|
|
76
|
+
create_body = {
|
|
77
|
+
"workspace_id": ws,
|
|
78
|
+
"name": spec.name,
|
|
79
|
+
"goal": spec.goal,
|
|
80
|
+
"description": spec.description,
|
|
81
|
+
"channel": spec.channel,
|
|
82
|
+
"interview_duration_minutes": spec.duration_minutes,
|
|
83
|
+
"completion_threshold": spec.completion_threshold,
|
|
84
|
+
"interview_context_for_employee": spec.interview_context_for_employee,
|
|
85
|
+
"success_criteria": spec.success_criteria,
|
|
86
|
+
"agent_behavior": spec.agent_behavior,
|
|
87
|
+
"welcome_screen": spec.welcome_screen,
|
|
88
|
+
"closing_message": spec.closing_message,
|
|
89
|
+
"timezone": spec.timezone,
|
|
90
|
+
"interview_order": "seniority_high_first",
|
|
91
|
+
}
|
|
92
|
+
create_body = {k: v for k, v in create_body.items() if v is not None}
|
|
93
|
+
interview = c.post("/v1/interviews", json_body=create_body)
|
|
94
|
+
interview_id = interview["id"]
|
|
95
|
+
rprint(f"[green]Created campaign[/green] {interview_id}")
|
|
96
|
+
|
|
97
|
+
# 2. Question sets
|
|
98
|
+
for idx, topic in enumerate(spec.topics):
|
|
99
|
+
qset_body = {
|
|
100
|
+
"name": topic.name,
|
|
101
|
+
"description": topic.goal,
|
|
102
|
+
"sort_order": idx,
|
|
103
|
+
"topic_metadata": {
|
|
104
|
+
"priority": topic.priority,
|
|
105
|
+
"success_threshold": topic.success_threshold,
|
|
106
|
+
"sub_topics": topic.sub_topics,
|
|
107
|
+
},
|
|
108
|
+
"questions": [
|
|
109
|
+
{"question_text": q.text, "is_required": q.required, "sort_order": qi}
|
|
110
|
+
for qi, q in enumerate(topic.questions)
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
c.post(f"/v1/interviews/{interview_id}/question-sets", json_body=qset_body)
|
|
114
|
+
if spec.topics:
|
|
115
|
+
rprint(f"[green]Added {len(spec.topics)} topic(s)[/green]")
|
|
116
|
+
|
|
117
|
+
# 3. Contacts — inline + CSV references
|
|
118
|
+
inline_contacts = []
|
|
119
|
+
csv_contacts = []
|
|
120
|
+
for contact in spec.contacts:
|
|
121
|
+
if contact.file:
|
|
122
|
+
csv_path = (config_file.parent / contact.file).resolve()
|
|
123
|
+
with open(csv_path) as f:
|
|
124
|
+
for row in csv.DictReader(f):
|
|
125
|
+
csv_contacts.append({k.strip(): v for k, v in row.items() if v})
|
|
126
|
+
else:
|
|
127
|
+
d = contact.model_dump(exclude_none=True, exclude={"file"})
|
|
128
|
+
if d:
|
|
129
|
+
inline_contacts.append(d)
|
|
130
|
+
|
|
131
|
+
all_contacts = inline_contacts + csv_contacts
|
|
132
|
+
if all_contacts:
|
|
133
|
+
c.post(
|
|
134
|
+
f"/v1/interviews/{interview_id}/contacts/bulk",
|
|
135
|
+
json_body={"contacts": all_contacts},
|
|
136
|
+
)
|
|
137
|
+
rprint(f"[green]Added {len(all_contacts)} contact(s)[/green]")
|
|
138
|
+
|
|
139
|
+
if start:
|
|
140
|
+
resp = c.post(f"/v1/interviews/{interview_id}/start")
|
|
141
|
+
rprint(f"[bold green]Started![/bold green] {resp.get('message', '')}")
|
|
142
|
+
|
|
143
|
+
rprint(f"\nView: {cfg.base_url.replace('api.', 'app.')}/interviews/{interview_id}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command("start")
|
|
147
|
+
def start_cmd(interview_id: str) -> None:
|
|
148
|
+
cfg = load_config()
|
|
149
|
+
key = require_api_key(cfg)
|
|
150
|
+
with OntoraClient(cfg, key) as c:
|
|
151
|
+
resp = c.post(f"/v1/interviews/{interview_id}/start")
|
|
152
|
+
rprint(f"[green]{resp.get('message', 'Started.')}[/green]")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command("pause")
|
|
156
|
+
def pause_cmd(interview_id: str) -> None:
|
|
157
|
+
cfg = load_config()
|
|
158
|
+
key = require_api_key(cfg)
|
|
159
|
+
with OntoraClient(cfg, key) as c:
|
|
160
|
+
c.post(f"/v1/interviews/{interview_id}/pause")
|
|
161
|
+
rprint("[yellow]Paused.[/yellow]")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Contact commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich import print as rprint
|
|
10
|
+
|
|
11
|
+
from ontora_cli.client import OntoraClient
|
|
12
|
+
from ontora_cli.config import load_config, require_api_key
|
|
13
|
+
from ontora_cli.formatters import print_contacts
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Contacts.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def list_cmd(interview_id: str, status: str | None = typer.Option(None, "--status")) -> None:
|
|
20
|
+
cfg = load_config()
|
|
21
|
+
key = require_api_key(cfg)
|
|
22
|
+
params = {"status": status} if status else None
|
|
23
|
+
with OntoraClient(cfg, key) as c:
|
|
24
|
+
data = c.get(f"/v1/interviews/{interview_id}/contacts", params=params)
|
|
25
|
+
print_contacts(data or [])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command("add")
|
|
29
|
+
def add_cmd(
|
|
30
|
+
interview_id: str,
|
|
31
|
+
name: str = typer.Option(..., "--name"),
|
|
32
|
+
email: str | None = typer.Option(None, "--email"),
|
|
33
|
+
phone: str | None = typer.Option(None, "--phone"),
|
|
34
|
+
job_title: str | None = typer.Option(None, "--job-title"),
|
|
35
|
+
department: str | None = typer.Option(None, "--department"),
|
|
36
|
+
seniority_level: int | None = typer.Option(None, "--seniority"),
|
|
37
|
+
) -> None:
|
|
38
|
+
cfg = load_config()
|
|
39
|
+
key = require_api_key(cfg)
|
|
40
|
+
body = {
|
|
41
|
+
"name": name,
|
|
42
|
+
"email": email,
|
|
43
|
+
"phone": phone,
|
|
44
|
+
"job_title": job_title,
|
|
45
|
+
"department": department,
|
|
46
|
+
"seniority_level": seniority_level,
|
|
47
|
+
}
|
|
48
|
+
body = {k: v for k, v in body.items() if v is not None}
|
|
49
|
+
with OntoraClient(cfg, key) as c:
|
|
50
|
+
resp = c.post(f"/v1/interviews/{interview_id}/contacts", json_body=body)
|
|
51
|
+
rprint(f"[green]Added {resp.get('name')} ({resp.get('id')})[/green]")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command("import")
|
|
55
|
+
def import_cmd(interview_id: str, csv_path: Path = typer.Argument(..., exists=True, readable=True)) -> None:
|
|
56
|
+
cfg = load_config()
|
|
57
|
+
key = require_api_key(cfg)
|
|
58
|
+
rows = []
|
|
59
|
+
with csv_path.open() as f:
|
|
60
|
+
for r in csv.DictReader(f):
|
|
61
|
+
rows.append({k.strip(): v for k, v in r.items() if v})
|
|
62
|
+
with OntoraClient(cfg, key) as c:
|
|
63
|
+
resp = c.post(f"/v1/interviews/{interview_id}/contacts/bulk", json_body={"contacts": rows})
|
|
64
|
+
rprint(f"[green]Imported {resp.get('created', len(rows))} contacts[/green]")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Graph RAG query command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
from rich.markdown import Markdown
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
|
|
10
|
+
from ontora_cli.client import OntoraClient
|
|
11
|
+
from ontora_cli.config import load_config, require_api_key
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.callback(invoke_without_command=True)
|
|
17
|
+
def query(
|
|
18
|
+
interview_id: str = typer.Argument(...),
|
|
19
|
+
question: str = typer.Argument(...),
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Ask a natural-language question about a campaign's data."""
|
|
22
|
+
cfg = load_config()
|
|
23
|
+
key = require_api_key(cfg)
|
|
24
|
+
with OntoraClient(cfg, key) as c:
|
|
25
|
+
resp = c.post(
|
|
26
|
+
f"/v1/interviews/{interview_id}/graph/query",
|
|
27
|
+
json_body={"question": question},
|
|
28
|
+
)
|
|
29
|
+
answer = resp.get("answer") or ""
|
|
30
|
+
rprint(Panel(Markdown(answer), title="Answer", border_style="cyan"))
|
|
31
|
+
|
|
32
|
+
patterns = resp.get("patterns") or []
|
|
33
|
+
if patterns:
|
|
34
|
+
rprint("\n[bold]Patterns[/bold]")
|
|
35
|
+
for p in patterns:
|
|
36
|
+
rprint(f"- {p.get('text', p)}")
|
|
37
|
+
divergences = resp.get("divergences") or []
|
|
38
|
+
if divergences:
|
|
39
|
+
rprint("\n[bold]Divergences[/bold]")
|
|
40
|
+
for d in divergences:
|
|
41
|
+
rprint(f"- {d.get('text', d)}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Report / synthesis commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich import print as rprint
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
|
|
11
|
+
from ontora_cli.client import OntoraClient
|
|
12
|
+
from ontora_cli.config import load_config, require_api_key
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(help="Synthesis reports.")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("show")
|
|
18
|
+
def show_cmd(interview_id: str) -> None:
|
|
19
|
+
cfg = load_config()
|
|
20
|
+
key = require_api_key(cfg)
|
|
21
|
+
with OntoraClient(cfg, key) as c:
|
|
22
|
+
resp = c.get_raw(f"/v1/interviews/{interview_id}/export/summary")
|
|
23
|
+
rprint(Markdown(resp.text))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("export")
|
|
27
|
+
def export_cmd(
|
|
28
|
+
interview_id: str,
|
|
29
|
+
kind: str = typer.Option("report", "--kind", "-k", help="report | summary"),
|
|
30
|
+
output: Path | None = typer.Option(None, "--output", "-o"),
|
|
31
|
+
) -> None:
|
|
32
|
+
cfg = load_config()
|
|
33
|
+
key = require_api_key(cfg)
|
|
34
|
+
endpoint = f"/v1/interviews/{interview_id}/export/{kind}"
|
|
35
|
+
with OntoraClient(cfg, key) as c:
|
|
36
|
+
resp = c.get_raw(endpoint)
|
|
37
|
+
out = output or Path(f"{interview_id[:8]}_{kind}.md")
|
|
38
|
+
out.write_text(resp.text)
|
|
39
|
+
rprint(f"[green]Wrote {out}[/green]")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Transcript and report commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich import print as rprint
|
|
9
|
+
from rich.markdown import Markdown
|
|
10
|
+
|
|
11
|
+
from ontora_cli.client import OntoraClient
|
|
12
|
+
from ontora_cli.config import load_config, require_api_key
|
|
13
|
+
from ontora_cli.formatters import print_transcripts
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Transcripts.")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def list_cmd(interview_id: str) -> None:
|
|
20
|
+
cfg = load_config()
|
|
21
|
+
key = require_api_key(cfg)
|
|
22
|
+
with OntoraClient(cfg, key) as c:
|
|
23
|
+
data = c.get(f"/v1/interviews/{interview_id}/conversations")
|
|
24
|
+
# The endpoint may return {"conversations": [...]} or a list; handle both.
|
|
25
|
+
rows = data.get("conversations") if isinstance(data, dict) else data
|
|
26
|
+
print_transcripts(rows or [])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("get")
|
|
30
|
+
def get_cmd(
|
|
31
|
+
interview_id: str,
|
|
32
|
+
conversation_id: str,
|
|
33
|
+
format: str = typer.Option("md", "--format", "-f", help="md | json"),
|
|
34
|
+
output: Path | None = typer.Option(None, "--output", "-o", help="Write to file instead of stdout"),
|
|
35
|
+
) -> None:
|
|
36
|
+
cfg = load_config()
|
|
37
|
+
key = require_api_key(cfg)
|
|
38
|
+
with OntoraClient(cfg, key) as c:
|
|
39
|
+
if format == "json":
|
|
40
|
+
data = c.get(f"/v1/interviews/{interview_id}/conversations/{conversation_id}")
|
|
41
|
+
text = __import__("json").dumps(data, indent=2, default=str)
|
|
42
|
+
else:
|
|
43
|
+
resp = c.get_raw(f"/v1/interviews/{interview_id}/export/transcript/{conversation_id}")
|
|
44
|
+
text = resp.text
|
|
45
|
+
if output:
|
|
46
|
+
output.write_text(text)
|
|
47
|
+
rprint(f"[green]Wrote {output}[/green]")
|
|
48
|
+
else:
|
|
49
|
+
if format == "md":
|
|
50
|
+
rprint(Markdown(text))
|
|
51
|
+
else:
|
|
52
|
+
typer.echo(text)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@app.command("export")
|
|
56
|
+
def export_cmd(
|
|
57
|
+
interview_id: str,
|
|
58
|
+
output: Path = typer.Option(Path("./transcripts.zip"), "--output", "-o"),
|
|
59
|
+
) -> None:
|
|
60
|
+
cfg = load_config()
|
|
61
|
+
key = require_api_key(cfg)
|
|
62
|
+
with OntoraClient(cfg, key) as c:
|
|
63
|
+
resp = c.get_raw(f"/v1/interviews/{interview_id}/export/transcripts")
|
|
64
|
+
output.write_bytes(resp.content)
|
|
65
|
+
rprint(f"[green]Saved ZIP to {output}[/green]")
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Webhook commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import print as rprint
|
|
7
|
+
|
|
8
|
+
from ontora_cli.client import OntoraClient
|
|
9
|
+
from ontora_cli.config import load_config, require_api_key, require_workspace
|
|
10
|
+
from ontora_cli.formatters import print_webhooks
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Outbound webhook endpoints.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("list")
|
|
16
|
+
def list_cmd(workspace: str | None = typer.Option(None, "--workspace", "-w")) -> None:
|
|
17
|
+
cfg = load_config()
|
|
18
|
+
key = require_api_key(cfg)
|
|
19
|
+
ws = require_workspace(cfg, workspace)
|
|
20
|
+
with OntoraClient(cfg, key) as c:
|
|
21
|
+
data = c.get("/v1/webhook-endpoints", params={"workspace_id": ws})
|
|
22
|
+
print_webhooks(data or [])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("add")
|
|
26
|
+
def add_cmd(
|
|
27
|
+
url: str = typer.Argument(...),
|
|
28
|
+
events: str = typer.Option("", "--events", help="Comma-separated event types. Empty = all."),
|
|
29
|
+
description: str | None = typer.Option(None, "--description"),
|
|
30
|
+
workspace: str | None = typer.Option(None, "--workspace", "-w"),
|
|
31
|
+
) -> None:
|
|
32
|
+
cfg = load_config()
|
|
33
|
+
key = require_api_key(cfg)
|
|
34
|
+
ws = require_workspace(cfg, workspace)
|
|
35
|
+
event_list = [e.strip() for e in events.split(",") if e.strip()]
|
|
36
|
+
with OntoraClient(cfg, key) as c:
|
|
37
|
+
resp = c.post(
|
|
38
|
+
"/v1/webhook-endpoints",
|
|
39
|
+
json_body={
|
|
40
|
+
"workspace_id": ws,
|
|
41
|
+
"url": url,
|
|
42
|
+
"events": event_list,
|
|
43
|
+
"description": description,
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
rprint(f"[green]Created endpoint[/green] {resp['id']}")
|
|
47
|
+
rprint(f"[bold yellow]Signing secret (save now, not shown again):[/bold yellow] {resp['secret']}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command("test")
|
|
51
|
+
def test_cmd(endpoint_id: str) -> None:
|
|
52
|
+
cfg = load_config()
|
|
53
|
+
key = require_api_key(cfg)
|
|
54
|
+
with OntoraClient(cfg, key) as c:
|
|
55
|
+
resp = c.post(f"/v1/webhook-endpoints/{endpoint_id}/test")
|
|
56
|
+
rprint(f"[green]Enqueued {resp.get('deliveries_enqueued', 0)} test delivery(ies)[/green]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("remove")
|
|
60
|
+
def remove_cmd(endpoint_id: str) -> None:
|
|
61
|
+
cfg = load_config()
|
|
62
|
+
key = require_api_key(cfg)
|
|
63
|
+
with OntoraClient(cfg, key) as c:
|
|
64
|
+
c.delete(f"/v1/webhook-endpoints/{endpoint_id}")
|
|
65
|
+
rprint("[yellow]Removed.[/yellow]")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@app.command("deliveries")
|
|
69
|
+
def deliveries_cmd(endpoint_id: str, limit: int = typer.Option(25, "--limit", "-n")) -> None:
|
|
70
|
+
cfg = load_config()
|
|
71
|
+
key = require_api_key(cfg)
|
|
72
|
+
with OntoraClient(cfg, key) as c:
|
|
73
|
+
data = c.get(f"/v1/webhook-endpoints/{endpoint_id}/deliveries", params={"limit": limit})
|
|
74
|
+
from rich.table import Table
|
|
75
|
+
from rich.console import Console
|
|
76
|
+
table = Table(title="Deliveries")
|
|
77
|
+
table.add_column("Event")
|
|
78
|
+
table.add_column("Status")
|
|
79
|
+
table.add_column("Attempts")
|
|
80
|
+
table.add_column("Response")
|
|
81
|
+
table.add_column("Created")
|
|
82
|
+
for d in data or []:
|
|
83
|
+
table.add_row(
|
|
84
|
+
d.get("event_type", ""),
|
|
85
|
+
str(d.get("status", "")),
|
|
86
|
+
str(d.get("attempts", 0)),
|
|
87
|
+
str(d.get("response_status") or "—"),
|
|
88
|
+
str(d.get("created_at", ""))[:19],
|
|
89
|
+
)
|
|
90
|
+
Console().print(table)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""CLI config: API key + base URL + workspace, loaded from env vars or `~/.ontora/config.yaml`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.ontora.com"
|
|
12
|
+
CONFIG_PATH = Path.home() / ".ontora" / "config.yaml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class CliConfig:
|
|
17
|
+
api_key: str | None
|
|
18
|
+
base_url: str
|
|
19
|
+
workspace_id: str | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_file() -> dict:
|
|
23
|
+
if not CONFIG_PATH.exists():
|
|
24
|
+
return {}
|
|
25
|
+
try:
|
|
26
|
+
with CONFIG_PATH.open("r") as f:
|
|
27
|
+
data = yaml.safe_load(f) or {}
|
|
28
|
+
return data if isinstance(data, dict) else {}
|
|
29
|
+
except (OSError, yaml.YAMLError):
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_config() -> CliConfig:
|
|
34
|
+
file_cfg = _load_file()
|
|
35
|
+
return CliConfig(
|
|
36
|
+
api_key=os.environ.get("ONTORA_API_KEY") or file_cfg.get("api_key"),
|
|
37
|
+
base_url=os.environ.get("ONTORA_BASE_URL") or file_cfg.get("base_url") or DEFAULT_BASE_URL,
|
|
38
|
+
workspace_id=os.environ.get("ONTORA_WORKSPACE_ID") or file_cfg.get("workspace_id"),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def save_config(*, api_key: str | None = None, base_url: str | None = None, workspace_id: str | None = None) -> None:
|
|
43
|
+
current = _load_file()
|
|
44
|
+
if api_key is not None:
|
|
45
|
+
current["api_key"] = api_key
|
|
46
|
+
if base_url is not None:
|
|
47
|
+
current["base_url"] = base_url
|
|
48
|
+
if workspace_id is not None:
|
|
49
|
+
current["workspace_id"] = workspace_id
|
|
50
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
with CONFIG_PATH.open("w") as f:
|
|
52
|
+
yaml.safe_dump(current, f)
|
|
53
|
+
# Restrict to owner-read (best-effort)
|
|
54
|
+
try:
|
|
55
|
+
CONFIG_PATH.chmod(0o600)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def require_api_key(cfg: CliConfig) -> str:
|
|
61
|
+
if not cfg.api_key:
|
|
62
|
+
raise RuntimeError("No API key configured. Run `ontora auth login` or set ONTORA_API_KEY.")
|
|
63
|
+
return cfg.api_key
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def require_workspace(cfg: CliConfig, cli_override: str | None) -> str:
|
|
67
|
+
ws = cli_override or cfg.workspace_id
|
|
68
|
+
if not ws:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
"No workspace ID configured. Set ONTORA_WORKSPACE_ID or pass --workspace."
|
|
71
|
+
)
|
|
72
|
+
return ws
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Rendering helpers for rich output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def print_campaigns(campaigns: list[dict[str, Any]]) -> None:
|
|
14
|
+
table = Table(title="Campaigns")
|
|
15
|
+
table.add_column("ID", style="dim")
|
|
16
|
+
table.add_column("Name")
|
|
17
|
+
table.add_column("Status")
|
|
18
|
+
table.add_column("Progress")
|
|
19
|
+
table.add_column("Updated")
|
|
20
|
+
for c in campaigns:
|
|
21
|
+
completed = c.get("completed_count", 0) or 0
|
|
22
|
+
total = c.get("contact_count", 0) or 0
|
|
23
|
+
prog = f"{completed}/{total}"
|
|
24
|
+
table.add_row(
|
|
25
|
+
str(c.get("id", ""))[:8],
|
|
26
|
+
c.get("name", ""),
|
|
27
|
+
str(c.get("status", "")),
|
|
28
|
+
prog,
|
|
29
|
+
str(c.get("updated_at", ""))[:16],
|
|
30
|
+
)
|
|
31
|
+
console.print(table)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def print_contacts(contacts: list[dict[str, Any]]) -> None:
|
|
35
|
+
table = Table(title="Contacts")
|
|
36
|
+
table.add_column("ID", style="dim")
|
|
37
|
+
table.add_column("Name")
|
|
38
|
+
table.add_column("Email")
|
|
39
|
+
table.add_column("Dept")
|
|
40
|
+
table.add_column("Status")
|
|
41
|
+
for c in contacts:
|
|
42
|
+
table.add_row(
|
|
43
|
+
str(c.get("id", ""))[:8],
|
|
44
|
+
c.get("name", ""),
|
|
45
|
+
c.get("email", "") or "",
|
|
46
|
+
c.get("department", "") or "",
|
|
47
|
+
str(c.get("status", "")),
|
|
48
|
+
)
|
|
49
|
+
console.print(table)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def print_transcripts(convs: list[dict[str, Any]]) -> None:
|
|
53
|
+
table = Table(title="Transcripts")
|
|
54
|
+
table.add_column("ID", style="dim")
|
|
55
|
+
table.add_column("Contact")
|
|
56
|
+
table.add_column("Status")
|
|
57
|
+
table.add_column("Duration")
|
|
58
|
+
table.add_column("Summary")
|
|
59
|
+
for c in convs:
|
|
60
|
+
table.add_row(
|
|
61
|
+
str(c.get("id", ""))[:8],
|
|
62
|
+
c.get("contact_name", "") or "",
|
|
63
|
+
str(c.get("status", "")),
|
|
64
|
+
f"{c.get('duration_minutes') or '—'} min",
|
|
65
|
+
(c.get("summary") or "")[:60],
|
|
66
|
+
)
|
|
67
|
+
console.print(table)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def print_webhooks(hooks: list[dict[str, Any]]) -> None:
|
|
71
|
+
table = Table(title="Webhooks")
|
|
72
|
+
table.add_column("ID", style="dim")
|
|
73
|
+
table.add_column("URL")
|
|
74
|
+
table.add_column("Events")
|
|
75
|
+
table.add_column("Active")
|
|
76
|
+
for h in hooks:
|
|
77
|
+
events = ", ".join(h.get("events") or []) or "*all*"
|
|
78
|
+
table.add_row(
|
|
79
|
+
str(h.get("id", ""))[:8],
|
|
80
|
+
h.get("url", ""),
|
|
81
|
+
events,
|
|
82
|
+
"yes" if h.get("active") else "no",
|
|
83
|
+
)
|
|
84
|
+
console.print(table)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Ontora CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import typer
|
|
9
|
+
from rich import print as rprint
|
|
10
|
+
|
|
11
|
+
from ontora_cli import __version__
|
|
12
|
+
from ontora_cli.client import ApiError
|
|
13
|
+
from ontora_cli.commands import auth, campaigns, contacts, query, reports, transcripts, webhooks
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
name="ontora",
|
|
17
|
+
help="Ontora CLI — manage interview campaigns, transcripts, and reports.",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
app.add_typer(auth.app, name="auth")
|
|
22
|
+
app.add_typer(campaigns.app, name="campaigns")
|
|
23
|
+
app.add_typer(contacts.app, name="contacts")
|
|
24
|
+
app.add_typer(transcripts.app, name="transcripts")
|
|
25
|
+
app.add_typer(reports.app, name="reports")
|
|
26
|
+
app.add_typer(webhooks.app, name="webhooks")
|
|
27
|
+
app.add_typer(query.app, name="query")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _version_callback(value: bool) -> None:
|
|
31
|
+
if value:
|
|
32
|
+
rprint(f"ontora-cli {__version__}")
|
|
33
|
+
raise typer.Exit()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.callback()
|
|
37
|
+
def root(
|
|
38
|
+
version: bool = typer.Option(
|
|
39
|
+
False,
|
|
40
|
+
"--version",
|
|
41
|
+
callback=_version_callback,
|
|
42
|
+
is_eager=True,
|
|
43
|
+
help="Print the CLI version and exit.",
|
|
44
|
+
),
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Manage interview campaigns, transcripts, and reports."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command()
|
|
50
|
+
def version() -> None:
|
|
51
|
+
"""Print the CLI version."""
|
|
52
|
+
rprint(f"ontora-cli {__version__}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _main() -> None:
|
|
56
|
+
try:
|
|
57
|
+
app()
|
|
58
|
+
except ApiError as e:
|
|
59
|
+
rprint(f"[red]API error ({e.status}):[/red] {e.detail}")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
except httpx.RequestError as e:
|
|
62
|
+
rprint(f"[red]Network error:[/red] {e}")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
except RuntimeError as e:
|
|
65
|
+
rprint(f"[red]Error:[/red] {e}")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
_main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""YAML config schemas for campaign-as-code."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class QuestionYaml(BaseModel):
|
|
9
|
+
text: str
|
|
10
|
+
required: bool = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TopicYaml(BaseModel):
|
|
14
|
+
name: str
|
|
15
|
+
goal: str | None = None
|
|
16
|
+
priority: str = "required" # required | important | optional
|
|
17
|
+
success_threshold: str | None = None
|
|
18
|
+
sub_topics: list[str] = Field(default_factory=list)
|
|
19
|
+
questions: list[QuestionYaml] = Field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ContactYaml(BaseModel):
|
|
23
|
+
name: str | None = None
|
|
24
|
+
email: str | None = None
|
|
25
|
+
phone: str | None = None
|
|
26
|
+
job_title: str | None = None
|
|
27
|
+
department: str | None = None
|
|
28
|
+
seniority_level: int | None = None
|
|
29
|
+
file: str | None = None # Path to CSV for bulk contacts
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CampaignYaml(BaseModel):
|
|
33
|
+
name: str
|
|
34
|
+
goal: str
|
|
35
|
+
description: str | None = None
|
|
36
|
+
channel: str = "in_app_chat"
|
|
37
|
+
duration_minutes: int = 30
|
|
38
|
+
completion_threshold: float = 80.0
|
|
39
|
+
interview_context_for_employee: str | None = None
|
|
40
|
+
success_criteria: str | None = None
|
|
41
|
+
agent_behavior: str | None = None
|
|
42
|
+
welcome_screen: str | None = None
|
|
43
|
+
closing_message: str | None = None
|
|
44
|
+
timezone: str = "UTC"
|
|
45
|
+
topics: list[TopicYaml] = Field(default_factory=list)
|
|
46
|
+
contacts: list[ContactYaml] = Field(default_factory=list)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ontora-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ontora CLI — manage interview campaigns, transcripts, reports, and webhooks from the terminal.
|
|
5
|
+
Author: Ontora
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Documentation, https://docs.ontora.com/cli/install
|
|
8
|
+
Project-URL: API, https://docs.ontora.com/api-reference/introduction
|
|
9
|
+
Project-URL: Homepage, https://docs.ontora.com
|
|
10
|
+
Keywords: ontora,interviews,research,synthesis,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: typer>=0.12.0
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Requires-Dist: rich>=13.7.0
|
|
23
|
+
Requires-Dist: pyyaml>=6.0
|
|
24
|
+
Requires-Dist: pydantic>=2.6.0
|
|
25
|
+
|
|
26
|
+
# Ontora CLI
|
|
27
|
+
|
|
28
|
+
Manage Ontora interview campaigns, transcripts, reports, and webhooks from the terminal.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ontora-cli
|
|
34
|
+
# or, from this repo:
|
|
35
|
+
cd cli && pip install -e .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Setup
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
ontora auth login # Paste your API key (or set ONTORA_API_KEY)
|
|
42
|
+
ontora auth status # Verify connection
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Get an API key from the Ontora web app (Settings → API Keys) or via
|
|
46
|
+
`POST /v1/api-keys`. You'll also need to know your workspace ID.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
export ONTORA_WORKSPACE_ID=<uuid>
|
|
50
|
+
# or set per-command with --workspace
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Campaigns
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ontora campaigns list
|
|
57
|
+
ontora campaigns create --config campaign.yaml
|
|
58
|
+
ontora campaigns show <id>
|
|
59
|
+
ontora campaigns start <id>
|
|
60
|
+
ontora campaigns pause <id>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Config-as-code (YAML)
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
name: "Q2 Procurement Discovery"
|
|
67
|
+
goal: "Map end-to-end procurement workflow to identify automation opportunities"
|
|
68
|
+
channel: in_app_chat
|
|
69
|
+
duration_minutes: 30
|
|
70
|
+
interview_context_for_employee: >
|
|
71
|
+
We're mapping how procurement works across the organization.
|
|
72
|
+
success_criteria: >
|
|
73
|
+
Complete process map with step-level pain points from every department.
|
|
74
|
+
|
|
75
|
+
topics:
|
|
76
|
+
- name: "Daily Workflow"
|
|
77
|
+
goal: "Understand the step-by-step procurement process"
|
|
78
|
+
priority: required
|
|
79
|
+
success_threshold: "Can describe process end-to-end with tools used"
|
|
80
|
+
sub_topics: ["intake", "approval", "vendor selection", "PO creation"]
|
|
81
|
+
questions:
|
|
82
|
+
- text: "Walk me through a typical purchase request"
|
|
83
|
+
required: true
|
|
84
|
+
- text: "What tools do you use at each step?"
|
|
85
|
+
|
|
86
|
+
contacts:
|
|
87
|
+
- name: "Jane Smith"
|
|
88
|
+
email: "jane@company.com"
|
|
89
|
+
department: "Procurement"
|
|
90
|
+
job_title: "Senior Buyer"
|
|
91
|
+
seniority_level: 4
|
|
92
|
+
# Or: { file: contacts.csv }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Transcripts and reports
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
ontora transcripts list <campaign-id>
|
|
99
|
+
ontora transcripts get <campaign-id> <conversation-id>
|
|
100
|
+
ontora transcripts export <campaign-id> [--output ./out] # ZIP of .md files
|
|
101
|
+
ontora reports show <campaign-id>
|
|
102
|
+
ontora reports export <campaign-id> [--output report.md]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Graph RAG query
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ontora query <campaign-id> "What are the top pain points across procurement?"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Webhooks
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ontora webhooks list
|
|
115
|
+
ontora webhooks add https://your-app.example/hook --events interview.completed,campaign.completed
|
|
116
|
+
ontora webhooks test <id>
|
|
117
|
+
ontora webhooks remove <id>
|
|
118
|
+
```
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
ontora_cli/__init__.py
|
|
4
|
+
ontora_cli/client.py
|
|
5
|
+
ontora_cli/config.py
|
|
6
|
+
ontora_cli/formatters.py
|
|
7
|
+
ontora_cli/main.py
|
|
8
|
+
ontora_cli/schemas.py
|
|
9
|
+
ontora_cli.egg-info/PKG-INFO
|
|
10
|
+
ontora_cli.egg-info/SOURCES.txt
|
|
11
|
+
ontora_cli.egg-info/dependency_links.txt
|
|
12
|
+
ontora_cli.egg-info/entry_points.txt
|
|
13
|
+
ontora_cli.egg-info/requires.txt
|
|
14
|
+
ontora_cli.egg-info/top_level.txt
|
|
15
|
+
ontora_cli/commands/__init__.py
|
|
16
|
+
ontora_cli/commands/auth.py
|
|
17
|
+
ontora_cli/commands/campaigns.py
|
|
18
|
+
ontora_cli/commands/contacts.py
|
|
19
|
+
ontora_cli/commands/query.py
|
|
20
|
+
ontora_cli/commands/reports.py
|
|
21
|
+
ontora_cli/commands/transcripts.py
|
|
22
|
+
ontora_cli/commands/webhooks.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ontora_cli
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ontora-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Ontora CLI — manage interview campaigns, transcripts, reports, and webhooks from the terminal."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [{ name = "Ontora" }]
|
|
12
|
+
license = "LicenseRef-Proprietary"
|
|
13
|
+
keywords = ["ontora", "interviews", "research", "synthesis", "cli"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"typer>=0.12.0",
|
|
25
|
+
"httpx>=0.27.0",
|
|
26
|
+
"rich>=13.7.0",
|
|
27
|
+
"pyyaml>=6.0",
|
|
28
|
+
"pydantic>=2.6.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
ontora = "ontora_cli.main:_main"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Documentation = "https://docs.ontora.com/cli/install"
|
|
36
|
+
API = "https://docs.ontora.com/api-reference/introduction"
|
|
37
|
+
Homepage = "https://docs.ontora.com"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["."]
|
|
41
|
+
include = ["ontora_cli*"]
|