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