Skip to content
Open
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
216 changes: 207 additions & 9 deletions cortex/ask.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
"""Natural language query interface for Cortex.

Handles user questions about installed packages, configurations,
and system state using LLM with semantic caching.
and system state using LLM with semantic caching. Also provides
educational content and tracks learning progress.
"""

import json
import os
import platform
import re
import shutil
import sqlite3
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Any


Expand Down Expand Up @@ -132,6 +136,132 @@ def gather_context(self) -> dict[str, Any]:
}


class LearningTracker:
"""Tracks educational topics the user has explored."""

PROGRESS_FILE = Path.home() / ".cortex" / "learning_history.json"

# Patterns that indicate educational questions
EDUCATIONAL_PATTERNS = [
r"^explain\b",
r"^teach\s+me\b",
r"^what\s+is\b",
r"^what\s+are\b",
r"^how\s+does\b",
r"^how\s+do\b",
r"^how\s+to\b",
r"\bbest\s+practices?\b",
r"^tutorial\b",
r"^guide\s+to\b",
r"^learn\s+about\b",
r"^introduction\s+to\b",
r"^basics\s+of\b",
]

def __init__(self):
"""Initialize the learning tracker."""
self._compiled_patterns = [re.compile(p, re.IGNORECASE) for p in self.EDUCATIONAL_PATTERNS]

def is_educational_query(self, question: str) -> bool:
"""Determine if a question is educational in nature."""
for pattern in self._compiled_patterns:
if pattern.search(question):
return True
return False

def extract_topic(self, question: str) -> str:
"""Extract the main topic from an educational question."""
# Remove common prefixes
topic = question.lower()
prefixes_to_remove = [
r"^explain\s+",
r"^teach\s+me\s+about\s+",
r"^teach\s+me\s+",
r"^what\s+is\s+",
r"^what\s+are\s+",
r"^how\s+does\s+",
r"^how\s+do\s+",
r"^how\s+to\s+",
r"^tutorial\s+on\s+",
r"^guide\s+to\s+",
r"^learn\s+about\s+",
r"^introduction\s+to\s+",
r"^basics\s+of\s+",
r"^best\s+practices\s+for\s+",
]
for prefix in prefixes_to_remove:
topic = re.sub(prefix, "", topic, flags=re.IGNORECASE)

# Clean up and truncate
topic = topic.strip("? ").strip()
# Take first 50 chars as topic identifier
if len(topic) > 50:
topic = topic[:50].rsplit(" ", 1)[0]
return topic

def record_topic(self, question: str) -> None:
"""Record that the user explored an educational topic."""
if not self.is_educational_query(question):
return

topic = self.extract_topic(question)
if not topic:
return

history = self._load_history()

# Update or add topic
if topic in history["topics"]:
history["topics"][topic]["count"] += 1
history["topics"][topic]["last_accessed"] = datetime.now().isoformat()
else:
history["topics"][topic] = {
"count": 1,
"first_accessed": datetime.now().isoformat(),
"last_accessed": datetime.now().isoformat(),
}

history["total_queries"] = history.get("total_queries", 0) + 1
self._save_history(history)

def get_history(self) -> dict[str, Any]:
"""Get the learning history."""
return self._load_history()

def get_recent_topics(self, limit: int = 5) -> list[str]:
"""Get recently explored topics."""
history = self._load_history()
topics = history.get("topics", {})

# Sort by last_accessed
sorted_topics = sorted(
topics.items(),
key=lambda x: x[1].get("last_accessed", ""),
reverse=True,
)
return [t[0] for t in sorted_topics[:limit]]

def _load_history(self) -> dict[str, Any]:
"""Load learning history from file."""
if not self.PROGRESS_FILE.exists():
return {"topics": {}, "total_queries": 0}

try:
with open(self.PROGRESS_FILE) as f:
return json.load(f)
except (json.JSONDecodeError, OSError):
return {"topics": {}, "total_queries": 0}

def _save_history(self, history: dict[str, Any]) -> None:
"""Save learning history to file."""
try:
self.PROGRESS_FILE.parent.mkdir(parents=True, exist_ok=True)
with open(self.PROGRESS_FILE, "w") as f:
json.dump(history, f, indent=2)
except OSError:
pass # Silently fail if we can't write


class AskHandler:
"""Handles natural language questions about the system."""

Expand All @@ -155,6 +285,7 @@ def __init__(
self.offline = offline
self.model = model or self._default_model()
self.info_gatherer = SystemInfoGatherer()
self.learning_tracker = LearningTracker()

# Initialize cache
try:
Expand Down Expand Up @@ -201,18 +332,63 @@ def _initialize_client(self):
raise ValueError(f"Unsupported provider: {self.provider}")

def _get_system_prompt(self, context: dict[str, Any]) -> str:
return f"""You are a helpful Linux system assistant. Answer questions about the user's system clearly and concisely.
return f"""You are a helpful Linux system assistant and tutor. You help users with both system-specific questions AND educational queries about Linux, packages, and best practices.

System Context:
{json.dumps(context, indent=2)}

Rules:
1. Provide direct, human-readable answers
2. Use the system context to give accurate information
## Query Type Detection

Automatically detect the type of question and respond appropriately:

### Educational Questions (tutorials, explanations, learning)
Triggered by questions like: "explain...", "teach me...", "how does X work", "what is...", "best practices for...", "tutorial on...", "learn about...", "guide to..."

For educational questions:
1. Provide structured, tutorial-style explanations
2. Include practical code examples with proper formatting
3. Highlight best practices and common pitfalls to avoid
4. Break complex topics into digestible sections
5. Use clear section labels and bullet points for readability
6. Mention related topics the user might want to explore next
7. Tailor examples to the user's system when relevant (e.g., use apt for Debian-based systems)

### Diagnostic Questions (system-specific, troubleshooting)
Triggered by questions about: current system state, "why is my...", "what packages...", "check my...", specific errors, system status

For diagnostic questions:
1. Analyze the provided system context
2. Give specific, actionable answers
3. Be concise but informative
4. If you don't have enough information, say so clearly
5. For package compatibility questions, consider the system's Python version and OS
6. Return ONLY the answer text, no JSON or markdown formatting"""

## Output Formatting Rules (CRITICAL - Follow exactly)
1. NEVER use markdown headings (# or ##) - they render poorly in terminals
2. For section titles, use **Bold Text** on its own line instead
3. Use bullet points (-) for lists
4. Use numbered lists (1. 2. 3.) for sequential steps
5. Use triple backticks with language name for code blocks (```bash)
6. Use *italic* sparingly for emphasis
7. Keep lines under 100 characters when possible
8. Add blank lines between sections for readability
9. For tables, use simple text formatting, not markdown tables

Example of good formatting:
**Installation Steps**

1. Update your package list:
```bash
sudo apt update
```

2. Install the package:
```bash
sudo apt install nginx
```

**Key Points**
- Point one here
- Point two here"""

def _call_openai(self, question: str, system_prompt: str) -> str:
response = self.client.chat.completions.create(
Expand All @@ -222,7 +398,7 @@ def _call_openai(self, question: str, system_prompt: str) -> str:
{"role": "user", "content": question},
],
temperature=0.3,
max_tokens=500,
max_tokens=2000,
)
# Defensive: content may be None or choices could be empty in edge cases
try:
Expand All @@ -234,7 +410,7 @@ def _call_openai(self, question: str, system_prompt: str) -> str:
def _call_claude(self, question: str, system_prompt: str) -> str:
response = self.client.messages.create(
model=self.model,
max_tokens=500,
max_tokens=2000,
temperature=0.3,
system=system_prompt,
messages=[{"role": "user", "content": question}],
Expand Down Expand Up @@ -344,4 +520,26 @@ def ask(self, question: str) -> str:
except (OSError, sqlite3.Error):
pass # Silently fail cache writes

# Track educational topics for learning history
self.learning_tracker.record_topic(question)

return answer

def get_learning_history(self) -> dict[str, Any]:
"""Get the user's learning history.

Returns:
Dictionary with topics explored and statistics
"""
return self.learning_tracker.get_history()

def get_recent_topics(self, limit: int = 5) -> list[str]:
"""Get recently explored educational topics.

Args:
limit: Maximum number of topics to return

Returns:
List of topic strings
"""
return self.learning_tracker.get_recent_topics(limit)
5 changes: 4 additions & 1 deletion cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from datetime import datetime
from typing import Any

from rich.markdown import Markdown

from cortex.ask import AskHandler
from cortex.branding import VERSION, console, cx_header, cx_print, show_banner
from cortex.coordinator import InstallationCoordinator, StepStatus
Expand Down Expand Up @@ -297,7 +299,8 @@ def ask(self, question: str) -> int:
offline=self.offline,
)
answer = handler.ask(question)
console.print(answer)
# Render as markdown for proper formatting in terminal
console.print(Markdown(answer))
return 0
except ImportError as e:
# Provide a helpful message if provider SDK is missing
Expand Down
78 changes: 78 additions & 0 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This document provides a comprehensive reference for all commands available in t
|---------|-------------|
| `cortex` | Show help and available commands |
| `cortex install <pkg>` | Install software |
| `cortex ask <question>` | Ask questions about your system or learn about Linux |
| `cortex demo` | See Cortex in action |
| `cortex wizard` | Configure API key |
| `cortex status` | Show comprehensive system status and health checks |
Expand Down Expand Up @@ -71,6 +72,65 @@ cortex install "python3 with pip and virtualenv" --execute

---

### `cortex ask`

Ask natural language questions about your system or learn about Linux, packages, and best practices. The AI automatically detects whether you're asking a diagnostic question about your system or an educational question to learn something new.

**Usage:**
```bash
cortex ask "<question>"
```

**Question Types:**

**Diagnostic Questions** - Questions about your specific system:
```bash
# System status queries
cortex ask "why is my disk full"
cortex ask "what packages need updating"
cortex ask "is my Python version compatible with TensorFlow"
cortex ask "check my GPU drivers"
```

**Educational Questions** - Learn about Linux, packages, and best practices:
```bash
# Explanations and tutorials
cortex ask "explain how Docker containers work"
cortex ask "what is systemd and how do I use it"
cortex ask "teach me about nginx configuration"
cortex ask "best practices for securing a Linux server"
cortex ask "how to set up a Python virtual environment"
```

**Features:**
- **Automatic Intent Detection**: The AI distinguishes between diagnostic and educational queries
- **System-Aware Responses**: Uses your actual system context (OS, Python version, GPU, etc.)
- **Structured Learning**: Educational responses include examples, best practices, and related topics
- **Learning Progress Tracking**: Educational topics you explore are tracked in `~/.cortex/learning_history.json`
- **Response Caching**: Repeated questions return cached responses for faster performance

**Examples:**
```bash
# Diagnostic: Get specific info about your system
cortex ask "what version of Python do I have"
cortex ask "can I run PyTorch on this system"

# Educational: Learn with structured tutorials
cortex ask "explain how apt package management works"
cortex ask "what are best practices for Docker security"
cortex ask "guide to setting up nginx as a reverse proxy"

# Mix of both
cortex ask "how do I install and configure Redis"
```

**Notes:**
- Educational responses are longer and include code examples with syntax highlighting
- The `--offline` flag can be used to only return cached responses
- Learning history helps track what topics you've explored over time

---

### `cortex demo`

Run an interactive demonstration of Cortex capabilities. Perfect for first-time users or presentations.
Expand Down Expand Up @@ -366,6 +426,24 @@ cortex stack webdev --dry-run
cortex stack webdev
```

### Learning with Cortex Ask
```bash
# 1. Ask diagnostic questions about your system
cortex ask "what version of Python do I have"
cortex ask "is Docker installed"

# 2. Learn about new topics with educational queries
cortex ask "explain how Docker containers work"
cortex ask "best practices for nginx configuration"

# 3. Get step-by-step tutorials
cortex ask "teach me how to set up a Python virtual environment"
cortex ask "guide to configuring SSH keys"

# 4. Your learning topics are automatically tracked
# View at ~/.cortex/learning_history.json
```

---

## Getting Help
Expand Down
Loading
Loading