PlanOpticon

planopticon / video_processor / cli / doctor.py
Blame History Raw 195 lines
1
"""Diagnostic checks for PlanOpticon setup."""
2
3
import logging
4
import os
5
import shutil
6
import sys
7
from pathlib import Path
8
from typing import List, Tuple
9
10
logger = logging.getLogger(__name__)
11
12
# (check_name, status, detail)
13
CheckResult = Tuple[str, str, str]
14
15
16
def check_python_version() -> CheckResult:
17
"""Check Python version meets minimum."""
18
v = sys.version_info
19
version = f"{v.major}.{v.minor}.{v.micro}"
20
if v >= (3, 10):
21
return ("Python", "ok", version)
22
return ("Python", "warn", f"{version} (3.10+ recommended)")
23
24
25
def check_ffmpeg() -> CheckResult:
26
"""Check if ffmpeg is installed and accessible."""
27
path = shutil.which("ffmpeg")
28
if path:
29
return ("FFmpeg", "ok", path)
30
return ("FFmpeg", "missing", "Install via: brew install ffmpeg / apt install ffmpeg")
31
32
33
def check_api_keys() -> List[CheckResult]:
34
"""Check for configured API keys."""
35
keys = {
36
"OpenAI": "OPENAI_API_KEY",
37
"Anthropic": "ANTHROPIC_API_KEY",
38
"Google Gemini": "GEMINI_API_KEY",
39
"Azure OpenAI": "AZURE_OPENAI_API_KEY",
40
"Together": "TOGETHER_API_KEY",
41
"Fireworks": "FIREWORKS_API_KEY",
42
"Cerebras": "CEREBRAS_API_KEY",
43
"xAI": "XAI_API_KEY",
44
"Mistral": "MISTRAL_API_KEY",
45
"Cohere": "COHERE_API_KEY",
46
"HuggingFace": "HUGGINGFACE_API_KEY",
47
}
48
results = []
49
for name, env in keys.items():
50
val = os.environ.get(env, "")
51
if val:
52
masked = val[:4] + "..." + val[-4:] if len(val) > 8 else "***"
53
results.append((f" {name}", "ok", f"{env}={masked}"))
54
else:
55
results.append((f" {name}", "not set", env))
56
return results
57
58
59
def check_ollama() -> CheckResult:
60
"""Check if Ollama is running locally."""
61
path = shutil.which("ollama")
62
if not path:
63
return ("Ollama", "not installed", "Optional: https://ollama.ai")
64
try:
65
import subprocess
66
67
result = subprocess.run(
68
["ollama", "list"],
69
capture_output=True,
70
text=True,
71
timeout=5,
72
)
73
if result.returncode == 0:
74
models = [
75
line.split()[0] for line in result.stdout.strip().split("\n")[1:] if line.strip()
76
]
77
if models:
78
return ("Ollama", "ok", f"{len(models)} models: {', '.join(models[:3])}")
79
return ("Ollama", "ok", "Running but no models pulled")
80
return ("Ollama", "warn", "Installed but not running")
81
except Exception:
82
return ("Ollama", "warn", "Installed but not reachable")
83
84
85
def check_optional_deps() -> List[CheckResult]:
86
"""Check optional Python dependencies."""
87
deps = [
88
("reportlab", "PDF export"),
89
("pptx", "PPTX export"),
90
("markdown", "HTML reports"),
91
("torch", "GPU acceleration"),
92
("yt_dlp", "YouTube download"),
93
("feedparser", "RSS sources"),
94
("bs4", "Web scraping"),
95
]
96
results = []
97
for module, purpose in deps:
98
try:
99
__import__(module)
100
results.append((f" {module}", "ok", purpose))
101
except ImportError:
102
results.append((f" {module}", "not installed", purpose))
103
return results
104
105
106
def check_dotenv() -> CheckResult:
107
"""Check if .env file exists in current directory."""
108
env_path = Path.cwd() / ".env"
109
if env_path.exists():
110
return (".env file", "ok", str(env_path))
111
return (".env file", "not found", "Run `planopticon init` to create one")
112
113
114
def check_knowledge_graph() -> CheckResult:
115
"""Check for knowledge graph files in common locations."""
116
from video_processor.integrators.graph_discovery import find_nearest_graph
117
118
path = find_nearest_graph()
119
if path:
120
return ("Knowledge graph", "ok", str(path))
121
return ("Knowledge graph", "not found", "Run `planopticon analyze` to create one")
122
123
124
def run_all_checks() -> List[CheckResult]:
125
"""Run all diagnostic checks and return results."""
126
results = []
127
128
results.append(check_python_version())
129
results.append(check_ffmpeg())
130
results.append(check_dotenv())
131
132
results.append(("API Keys", "section", ""))
133
results.extend(check_api_keys())
134
135
results.append(check_ollama())
136
137
results.append(("Optional Dependencies", "section", ""))
138
results.extend(check_optional_deps())
139
140
results.append(check_knowledge_graph())
141
142
return results
143
144
145
def format_results(results: List[CheckResult]) -> str:
146
"""Format check results for terminal display."""
147
lines = ["", "PlanOpticon Doctor", ""]
148
status_icons = {
149
"ok": "[ok]",
150
"warn": "[!!]",
151
"missing": "[XX]",
152
"not set": "[--]",
153
"not found": "[--]",
154
"not installed": "[--]",
155
"section": "---",
156
}
157
158
any_issues = False
159
for name, status, detail in results:
160
icon = status_icons.get(status, "[??]")
161
if status == "section":
162
lines.append(f"\n{name}:")
163
continue
164
if status in ("missing", "warn"):
165
any_issues = True
166
detail_str = f" {detail}" if detail else ""
167
lines.append(f" {icon} {name}{detail_str}")
168
169
lines.append("")
170
if any_issues:
171
lines.append("Some issues found. Run `planopticon init` for guided setup.")
172
else:
173
has_key = any(
174
s == "ok"
175
for n, s, _ in results
176
if n.strip()
177
in (
178
"OpenAI",
179
"Anthropic",
180
"Google Gemini",
181
"Azure OpenAI",
182
"Together",
183
"Fireworks",
184
"Cerebras",
185
"xAI",
186
)
187
)
188
if has_key:
189
lines.append("Setup looks good!")
190
else:
191
lines.append("No API keys configured. Run `planopticon init` to set up a provider.")
192
lines.append("")
193
194
return "\n".join(lines)
195

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button