|
1
|
"""Interactive setup wizard for PlanOpticon.""" |
|
2
|
|
|
3
|
import os |
|
4
|
import shutil |
|
5
|
from pathlib import Path |
|
6
|
from typing import Dict, Optional, Tuple |
|
7
|
|
|
8
|
import click |
|
9
|
|
|
10
|
PROVIDERS = [ |
|
11
|
("openai", "OpenAI", "OPENAI_API_KEY"), |
|
12
|
("anthropic", "Anthropic", "ANTHROPIC_API_KEY"), |
|
13
|
("gemini", "Google Gemini", "GEMINI_API_KEY"), |
|
14
|
("ollama", "Ollama (local)", None), |
|
15
|
("azure", "Azure OpenAI", "AZURE_OPENAI_API_KEY"), |
|
16
|
("together", "Together AI", "TOGETHER_API_KEY"), |
|
17
|
("fireworks", "Fireworks AI", "FIREWORKS_API_KEY"), |
|
18
|
("cerebras", "Cerebras", "CEREBRAS_API_KEY"), |
|
19
|
("xai", "xAI", "XAI_API_KEY"), |
|
20
|
] |
|
21
|
|
|
22
|
|
|
23
|
def _check_ffmpeg() -> bool: |
|
24
|
return shutil.which("ffmpeg") is not None |
|
25
|
|
|
26
|
|
|
27
|
def _test_provider(provider_id: str, api_key: Optional[str] = None) -> Tuple[bool, str]: |
|
28
|
"""Test that a provider connection works.""" |
|
29
|
if provider_id == "ollama": |
|
30
|
try: |
|
31
|
import subprocess |
|
32
|
|
|
33
|
result = subprocess.run( |
|
34
|
["ollama", "list"], |
|
35
|
capture_output=True, |
|
36
|
text=True, |
|
37
|
timeout=5, |
|
38
|
) |
|
39
|
if result.returncode == 0: |
|
40
|
return True, "Ollama is running" |
|
41
|
return False, "Ollama is installed but not running. Start with: ollama serve" |
|
42
|
except FileNotFoundError: |
|
43
|
return False, "Ollama not found. Install from: https://ollama.ai" |
|
44
|
except Exception as e: |
|
45
|
return False, f"Could not reach Ollama: {e}" |
|
46
|
|
|
47
|
if not api_key: |
|
48
|
return False, "No API key provided" |
|
49
|
|
|
50
|
# For API-based providers, just check the key looks valid |
|
51
|
if len(api_key) < 8: |
|
52
|
return False, "API key looks too short" |
|
53
|
return True, "API key configured" |
|
54
|
|
|
55
|
|
|
56
|
def run_wizard() -> None: |
|
57
|
"""Run the interactive setup wizard.""" |
|
58
|
click.echo() |
|
59
|
click.echo(" PlanOpticon Setup Wizard") |
|
60
|
click.echo(" " + "-" * 30) |
|
61
|
click.echo() |
|
62
|
|
|
63
|
# Step 1: Check prerequisites |
|
64
|
click.echo("Checking prerequisites...") |
|
65
|
click.echo() |
|
66
|
|
|
67
|
if _check_ffmpeg(): |
|
68
|
click.echo(" [ok] FFmpeg found") |
|
69
|
else: |
|
70
|
click.echo(" [!!] FFmpeg not found") |
|
71
|
click.echo(" Install: brew install ffmpeg (macOS)") |
|
72
|
click.echo(" apt install ffmpeg (Ubuntu)") |
|
73
|
click.echo(" winget install ffmpeg (Windows)") |
|
74
|
click.echo() |
|
75
|
|
|
76
|
# Step 2: Choose provider |
|
77
|
click.echo() |
|
78
|
click.echo("Choose your AI provider:") |
|
79
|
click.echo() |
|
80
|
for i, (pid, name, _) in enumerate(PROVIDERS, 1): |
|
81
|
# Check if already configured |
|
82
|
env_key = PROVIDERS[i - 1][2] |
|
83
|
status = "" |
|
84
|
if pid == "ollama": |
|
85
|
if shutil.which("ollama"): |
|
86
|
status = " (installed)" |
|
87
|
elif env_key and os.environ.get(env_key): |
|
88
|
status = " (configured)" |
|
89
|
click.echo(f" {i}. {name}{status}") |
|
90
|
click.echo() |
|
91
|
|
|
92
|
choice = click.prompt( |
|
93
|
"Select provider", |
|
94
|
type=click.IntRange(1, len(PROVIDERS)), |
|
95
|
default=1, |
|
96
|
) |
|
97
|
provider_id, provider_name, env_var = PROVIDERS[choice - 1] |
|
98
|
|
|
99
|
# Step 3: Configure API key |
|
100
|
env_vars: Dict[str, str] = {} |
|
101
|
|
|
102
|
if provider_id == "ollama": |
|
103
|
click.echo() |
|
104
|
ok, msg = _test_provider("ollama") |
|
105
|
if ok: |
|
106
|
click.echo(f" [ok] {msg}") |
|
107
|
else: |
|
108
|
click.echo(f" [!!] {msg}") |
|
109
|
elif env_var: |
|
110
|
existing = os.environ.get(env_var, "") |
|
111
|
if existing: |
|
112
|
click.echo(f"\n {env_var} is already set.") |
|
113
|
if not click.confirm(" Update it?", default=False): |
|
114
|
env_vars[env_var] = existing |
|
115
|
else: |
|
116
|
key = click.prompt(f" Enter {env_var}", hide_input=True) |
|
117
|
env_vars[env_var] = key |
|
118
|
else: |
|
119
|
click.echo(f"\n {provider_name} requires {env_var}.") |
|
120
|
key = click.prompt(f" Enter {env_var}", hide_input=True) |
|
121
|
env_vars[env_var] = key |
|
122
|
|
|
123
|
if env_var in env_vars: |
|
124
|
ok, msg = _test_provider(provider_id, env_vars[env_var]) |
|
125
|
if ok: |
|
126
|
click.echo(f" [ok] {msg}") |
|
127
|
else: |
|
128
|
click.echo(f" [!!] {msg}") |
|
129
|
|
|
130
|
# Step 4: Additional providers? |
|
131
|
click.echo() |
|
132
|
if click.confirm("Configure additional providers?", default=False): |
|
133
|
for pid, pname, evar in PROVIDERS: |
|
134
|
if pid == provider_id or not evar: |
|
135
|
continue |
|
136
|
if os.environ.get(evar): |
|
137
|
continue |
|
138
|
if click.confirm(f" Set up {pname}?", default=False): |
|
139
|
key = click.prompt(f" Enter {evar}", hide_input=True) |
|
140
|
env_vars[evar] = key |
|
141
|
|
|
142
|
# Step 5: Write .env file |
|
143
|
env_path = Path.cwd() / ".env" |
|
144
|
if env_vars: |
|
145
|
click.echo() |
|
146
|
|
|
147
|
if env_path.exists(): |
|
148
|
click.echo(f" .env already exists at {env_path}") |
|
149
|
if not click.confirm(" Append new keys?", default=True): |
|
150
|
click.echo(" Skipping .env update.") |
|
151
|
_print_summary(provider_name, env_vars) |
|
152
|
return |
|
153
|
|
|
154
|
# Read existing content |
|
155
|
existing_content = env_path.read_text() if env_path.exists() else "" |
|
156
|
existing_keys = set() |
|
157
|
for line in existing_content.split("\n"): |
|
158
|
if "=" in line and not line.strip().startswith("#"): |
|
159
|
existing_keys.add(line.split("=", 1)[0].strip()) |
|
160
|
|
|
161
|
new_lines = [] |
|
162
|
for key, val in env_vars.items(): |
|
163
|
if key not in existing_keys: |
|
164
|
new_lines.append(f"{key}={val}") |
|
165
|
|
|
166
|
if new_lines: |
|
167
|
with open(env_path, "a") as f: |
|
168
|
if existing_content and not existing_content.endswith("\n"): |
|
169
|
f.write("\n") |
|
170
|
f.write("\n".join(new_lines) + "\n") |
|
171
|
click.echo(f" Updated {env_path} with {len(new_lines)} key(s)") |
|
172
|
else: |
|
173
|
click.echo(" All keys already in .env") |
|
174
|
|
|
175
|
# Remind about .gitignore |
|
176
|
gitignore = Path.cwd() / ".gitignore" |
|
177
|
if gitignore.exists(): |
|
178
|
content = gitignore.read_text() |
|
179
|
if ".env" not in content: |
|
180
|
click.echo(" [!!] .env is not in .gitignore — consider adding it") |
|
181
|
else: |
|
182
|
click.echo(" [!!] No .gitignore found — make sure .env is not committed") |
|
183
|
|
|
184
|
_print_summary(provider_name, env_vars) |
|
185
|
|
|
186
|
|
|
187
|
def _print_summary(provider_name: str, env_vars: Dict[str, str]) -> None: |
|
188
|
"""Print setup summary.""" |
|
189
|
click.echo() |
|
190
|
click.echo(" Setup complete!") |
|
191
|
click.echo() |
|
192
|
click.echo(f" Provider: {provider_name}") |
|
193
|
if env_vars: |
|
194
|
click.echo(f" Keys configured: {len(env_vars)}") |
|
195
|
click.echo() |
|
196
|
click.echo(" Next steps:") |
|
197
|
click.echo(" planopticon doctor Check setup health") |
|
198
|
click.echo(" planopticon analyze -i VIDEO -o OUTPUT") |
|
199
|
click.echo(" planopticon -I Interactive mode") |
|
200
|
click.echo() |
|
201
|
|