Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 82 additions & 1 deletion cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from cortex.env_manager import EnvironmentManager, get_env_manager
from cortex.installation_history import InstallationHistory, InstallationStatus, InstallationType
from cortex.llm.interpreter import CommandInterpreter
from cortex.llm_router import LLMRouter, TaskType
from cortex.network_config import NetworkConfig
from cortex.notification_manager import NotificationManager
from cortex.stack_manager import StackManager
Expand Down Expand Up @@ -677,7 +678,6 @@ def install(
interpreter = CommandInterpreter(api_key=api_key, provider=provider)

self._print_status("📦", "Planning installation...")

for _ in range(10):
self._animate_spinner("Analyzing system requirements...")
self._clear_line()
Expand Down Expand Up @@ -2126,6 +2126,28 @@ def main():

# Wizard command
wizard_parser = subparsers.add_parser("wizard", help="Configure API key interactively")
diagnose_parser = subparsers.add_parser(
"diagnose",
help="Diagnose installation or system errors",
)

diagnose_parser.add_argument(
"--image",
type=str,
help="Path to error screenshot (PNG, JPG, WebP)",
)

diagnose_parser.add_argument(
"--clipboard",
action="store_true",
help="Read error screenshot from clipboard",
)

diagnose_parser.add_argument(
"--text",
type=str,
help="Raw error text (fallback)",
)

# Status command (includes comprehensive health checks)
subparsers.add_parser("status", help="Show comprehensive system status and health checks")
Expand Down Expand Up @@ -2511,6 +2533,65 @@ def main():
dry_run=args.dry_run,
parallel=args.parallel,
)
elif args.command == "diagnose":
import io

from PIL import Image, ImageGrab
Comment on lines +2537 to +2539
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Wrap PIL imports in try-except to provide better error handling.

The PIL imports on Line 1938 occur outside any try-except block. If Pillow is not installed, the ImportError will be raised before reaching the try-except blocks that handle image loading (Lines 1943-1947, 1950-1958), resulting in a less user-friendly error message.

Consider wrapping the imports:

🔎 Proposed fix
         elif args.command == "diagnose":
-            import io
-
-            from PIL import Image, ImageGrab
+            try:
+                import io
+                from PIL import Image, ImageGrab
+            except ImportError:
+                cli._print_error("Pillow is required for the diagnose command")
+                cx_print("Install with: pip install pillow", "info")
+                return 1

             image = None
🤖 Prompt for AI Agents
In cortex/cli.py around lines 1936 to 1938, the PIL imports (from PIL import
Image, ImageGrab) are executed outside any try-except so an ImportError will
surface before the existing image-loading error handling; wrap these imports in
a try-except ImportError block (or move them inside the existing try blocks) and
on ImportError raise or log a clear user-friendly message indicating Pillow is
required and how to install it (e.g., suggest pip install Pillow), so the script
fails with a helpful error instead of a raw ImportError.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pavanimanchala53 Address this one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!


image = None

if args.image:
try:
image = Image.open(args.image)
except Exception as e:
cli._print_error(f"Failed to load image: {e}")
return 1

elif args.clipboard:
try:

image = ImageGrab.grabclipboard()
if image is None:
cli._print_error("No image found in clipboard")
return 1
except Exception as e:
cli._print_error(f"Failed to read clipboard: {e}")
return 1

# Get API key for LLM router (diagnose_image needs Claude Vision)
api_key = cli._get_api_key()
if not api_key:
return 1

router = LLMRouter(claude_api_key=api_key)
if image:
cx_print("🔍 Analyzing error screenshot...")
diagnosis = router.diagnose_image(image)
cx_print(diagnosis)
return 0

if args.text:
cx_print("🔍 Analyzing error message...", "info")

response = router.complete(
messages=[
{
"role": "system",
"content": "You are a Linux system debugging expert.",
},
{
"role": "user",
"content": f"Diagnose this error and suggest fixes:\n{args.text}",
},
],
task_type=TaskType.ERROR_DEBUGGING,
)

cx_print(response.content)
return 0
cli._print_error("Provide an image, clipboard, or error message")
return 1

elif args.command == "import":
return cli.import_deps(args)
elif args.command == "history":
Expand Down
84 changes: 83 additions & 1 deletion cortex/llm_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,23 @@
"""

import asyncio
import base64
import io
import json
import logging
import os
import threading
import time
from dataclasses import dataclass
from enum import Enum
from typing import Any
from typing import TYPE_CHECKING, Any

from anthropic import Anthropic, AsyncAnthropic
from openai import AsyncOpenAI, OpenAI

if TYPE_CHECKING:
from PIL import Image

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -326,6 +331,83 @@ def complete(
else:
raise

def diagnose_image(self, image: "Image.Image") -> str:
"""
Diagnose an error screenshot using Claude Vision.
Args:
image: A PIL Image object containing the error screenshot.

Returns:
Diagnosis text from Claude Vision, or a fallback message
if vision support is unavailable.
"""
try:
from PIL import Image
except ImportError:
logger.warning("Pillow not installed, using fallback diagnosis")
return (
"Image diagnosis unavailable (missing pillow).\n"
"Install Pillow to enable image-based error diagnosis:\n"
" pip install pillow\n"
" # or\n"
" cortex install pillow\n"
)

if not self.claude_client:
logger.warning("Claude Vision unavailable, using fallback diagnosis")
return (
"Claude Vision unavailable.\n"
"Configure Claude API key to enable image diagnosis:\n"
" export ANTHROPIC_API_KEY='your-key'\n"
" # or run:\n"
" cortex wizard\n"
)

buf = io.BytesIO()
image.save(buf, format="PNG")
image_bytes = buf.getvalue()

try:
response = self.claude_client.messages.create(
model="claude-opus-4-1-20250805",
max_tokens=500,
messages=[
{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": base64.b64encode(image_bytes).decode(),
},
},
{
"type": "text",
"text": "Diagnose this error screenshot and suggest fixes.",
},
],
}
],
)
if response.content and hasattr(response.content[0], "text"):
return response.content[0].text
return "Claude Vision returned an empty response."

except Exception as e:
logger.warning(
"Claude Vision unavailable, using fallback diagnosis",
exc_info=True,
)
return (
"Claude Vision unavailable.\n"
"Possible reasons:\n"
"- Invalid or missing API key\n"
"- Network or rate limit error\n"
"- Claude service unavailable\n"
)

def _complete_claude(
self,
messages: list[dict[str, str]],
Expand Down
26 changes: 26 additions & 0 deletions docs/image_error_diagnosis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
## Diagnose Errors from Screenshots

Cortex can diagnose system and installation errors directly from screenshots using Vision AI.

This is useful when error messages cannot be easily copied from terminals or GUI dialogs.

### Image-based diagnosis

Diagnose an error from an image file:

cortex diagnose --image /path/to/error.png

Supported formats:
- PNG
- JPG / JPEG

### Clipboard-based diagnosis

Diagnose an error copied to the clipboard:

cortex diagnose --clipboard

### Fallback behavior

If Vision APIs or required dependencies are unavailable, Cortex provides a safe
fallback diagnosis instead of failing.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies = [
"rich>=13.0.0",
"pyyaml>=6.0.0",
"python-dotenv>=1.0.0",
"pillow>=9.0.0",
]

[project.optional-dependencies]
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ black>=24.0.0
ruff>=0.8.0
isort>=5.13.0
pre-commit>=3.0.0
pillow>=9.0.0
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
anthropic>=0.18.0
openai>=1.0.0
requests>=2.32.4
pillow>=9.0.0

# Configuration
PyYAML>=6.0.0
Expand Down
30 changes: 30 additions & 0 deletions tests/test_diagnose_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import pytest

PIL = pytest.importorskip("PIL", reason="Pillow not installed")
from PIL import Image

from cortex.llm_router import LLMRouter


def test_diagnose_image_fallback_without_claude():
"""
If Claude is unavailable, diagnose_image should return
a safe fallback message instead of crashing.
"""
router = LLMRouter(claude_api_key=None)

# Create a dummy image in memory
image = Image.new("RGB", (100, 100), color="red")

result = router.diagnose_image(image)

assert isinstance(result, str)
assert "Claude Vision unavailable" in result


def test_llmrouter_has_diagnose_image_method():
"""
Ensure diagnose_image exists on LLMRouter
"""
router = LLMRouter()
assert hasattr(router, "diagnose_image")
Loading