|
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
|
|