# NeuronLens Python Package

A Python package for accessing NeuronLens interpretability analysis functions.

## Server Deployment (New Machine)

**Setup:** Clone `sae_app_v3`, set `NGROK_AUTH_TOKEN` and `API_KEY` in environment (or `cloud_config.json`), run `./start_frontend_backend.sh && ./api_key_util/start_ngrok.sh`. **Override API URL:** `NEURONLENS_API_URL` env var or `pip/config.json` (1-2 places control everything). See [Developer Guide](#developer-guide).

## Installation

```bash
pip install neuronlens
```

For development (editable install):
```bash
cd sae_app_v3/pip
pip install -e .
```

## Publishing to PyPI

To publish a new version to PyPI:

1. **Update version** in `setup.py` and `__init__.py`

2. **Build package**:
   ```bash
   cd sae_app_v3/pip
   python -m build
   ```

3. **Publish to PyPI** (using token from `.env` file):
   ```bash
   # Load PyPI credentials from .env (not committed to git)
   export $(cat .env | xargs)
   python -m twine upload dist/*
   ```

**Note:** PyPI API token is stored in `sae_app_v3/pip/.env` (excluded from git via `.gitignore`).

## Quick Start

### With API Key (Required)

**Note:** API key is required for all functions. You must provide it either via parameter or environment variable.

```python
import neuronlens

# Initialize with API key
NEURONLENS_API_KEY = "nlive_your_api_key_here"
engine = neuronlens.Engine(api_key=NEURONLENS_API_KEY)

# Optionally specify frontend API URL (defaults to http://localhost:8001)
# engine = neuronlens.Engine(api_key=NEURONLENS_API_KEY, api_url="https://your-ngrok-url.ngrok-free.dev")

# Use functions directly
result = engine.analyze_agent("Get Tesla stock price")
print(f"Alignment: {result['alignment_status']}")
```

### Environment Variables

You can also set the API key via environment variables:

```bash
export NEURONLENS_API_KEY="nlive_your_api_key_here"
export NEURONLENS_API_URL="https://your-ngrok-url.ngrok-free.dev"  # Optional
```

Then initialize:
```python
import neuronlens
engine = neuronlens.Engine()  # Automatically picks up from environment
```

## Architecture

The package works in two modes:

- **Direct mode**: When `sae_app_v3` is available, imports and calls functions directly
- **HTTP mode**: When `sae_app_v3` is not available (pip install), makes HTTP requests to frontend server (port 8001)

**Server Requirements:**
- Backend server must be running on `http://localhost:8000` (GPU operations)
- Frontend server must be running on `http://localhost:8001` (High-level API)
- Frontend server can be exposed via ngrok for remote access

## Function Reference

### High-Level Analysis Functions

**`engine.analyze_agent(query, response=None, model_path=None, sae_path=None, layer=19, api_url=None, **kwargs)`**
Analyze agent tool intent and alignment. Returns alignment_status, tool_called, intent_scores, top_features.

**`engine.analyze_sentiment(text, top_n=10, model_path=None, sae_path=None, layer=10, api_url=None, **kwargs)`**
Analyze sentiment with SAE feature attribution. Returns sentiment (predicted_label, confidence, probabilities) and top_features.
Note: top_n only applies in HTTP mode; direct mode uses hardcoded top_n=10.

**`engine.analyze_trading(text, ticker=None, model_path=None, sae_path=None, layer=19, api_url=None, **kwargs)`**
Extract trading signals from news text. Returns trading signals and direction.

**`engine.analyze_reasoning(prompt, model_path=None, sae_path=None, layer=28, expected_buckets=None, api_url=None, **kwargs)`**
Analyze CoT reasoning faithfulness. Returns reasoning analysis with bucket alignment.

**`engine.analyze_hallucination(prompt, max_tokens=256, temperature=0.7, api_url=None, **kwargs)`**
Token-level hallucination detection. Returns token_details with risk scores per token.

**`engine.search_features(query, k=10, layer=16, api_url=None, **kwargs)`**
Search features using semantic similarity. Returns list of features with feature_id, score, and label.

**`engine.steer_features(prompt, features, model="meta-llama/Llama-2-7b-hf", api_url=None, **kwargs)`**
Apply feature steering. Features: list of dicts with "id" and "magnitude" (-1.0 to +1.0). Returns original_text and steered_text.

### Common Backend Wrapper Functions

**`engine.extract_top_features(text, model_path, sae_path, layer, top_n=10, use_hf_model=False, use_hf_sae=False, sae_config_path=None, api_url=None, **kwargs)`**
Extract top SAE features from text. Returns dictionary with features array and top_features list.

**`engine.unload_model(model_path, device=None, **kwargs)`**
Unload specific model from cache. Returns success status.

**`engine.unload_all_models(**kwargs)`**
Unload all models from cache. Returns success status.

**`engine.model_cache_status(**kwargs)`**
Get model cache status. Returns dictionary with loaded_models list and cache information.

## Complete Examples

### Agent Lens Analysis

```python
import neuronlens

engine = neuronlens.Engine(api_key="nlive_your_key")

result = engine.analyze_agent("What's the latest news about Apple?")

print(f"Query: {result['query']}")
print(f"Alignment Status: {result['alignment_status']}")  # GREEN, AMBER, or RED
print(f"Expected Tools: {result['expected_tools']}")
print(f"Tool Called: {result['tool_called']}")
print(f"Intent Scores: {result['intent_scores']}")

# Top activated features
for feature in result['top_features'][:5]:
    print(f"Feature #{feature['feature_id']}: {feature['activation']:.4f}")
```

### Sentiment Analysis

```python
import neuronlens

engine = neuronlens.Engine(api_key="nlive_your_key")

result = engine.analyze_sentiment(
    text="Strong quarterly earnings exceeded expectations",
    top_n=10
)

sentiment = result['sentiment']
print(f"Predicted Label: {sentiment['predicted_label']}")
print(f"Confidence: {sentiment['confidence']:.3f}")

# Top contributing features
for feature in result['top_features']:
    print(f"Feature #{feature['feature_id']}: {feature['activation']:.4f}")
```

### Trading Lens

```python
import neuronlens

engine = neuronlens.Engine(api_key="nlive_your_key")

# Extract signals
signals = engine.analyze_trading(
    text="Apple earnings beat expectations by 15%",
    ticker="AAPL"
)

print(f"Signals: {signals}")
```

### Reasoning Lens

```python
import neuronlens

engine = neuronlens.Engine(api_key="nlive_your_key")

result = engine.analyze_reasoning(
    prompt="What is 2+2? Show your reasoning step by step.",
    max_new_tokens=2048
)

print(f"Reasoning Analysis: {result}")
```

### Hallucination Detection

```python
import neuronlens

engine = neuronlens.Engine(api_key="nlive_your_key")

result = engine.analyze_hallucination(
    prompt="Write a summary about Apple",
    max_tokens=256,
    temperature=0.7
)

print(f"Token Details: {len(result.get('token_details', []))} tokens analyzed")
```

### Search & Steer

```python
import neuronlens

engine = neuronlens.Engine(api_key="nlive_your_key")

# Search for features
features = engine.search_features(
    query="market volatility and uncertainty",
    k=10,
    layer=16
)

# Steer using found features
steered = engine.steer_features(
    prompt="What is the market outlook for tech stocks?",
    features=[
        {"id": features[0]['feature_id'], "magnitude": 0.7},  # Steer towards
        {"id": features[1]['feature_id'], "magnitude": -0.5}  # Steer away
    ],
    model="meta-llama/Llama-2-7b-hf"
)

print(f"Original: {steered['original_text']}")
print(f"Steered: {steered['steered_text']}")
```

### Common Functions

```python
import neuronlens

engine = neuronlens.Engine(api_key="nlive_your_key")

# Extract features directly
features = engine.extract_top_features(
    text="Strong earnings report",
    model_path="ProsusAI/finbert",
    sae_path="/path/to/sae",
    layer=10,
    top_n=10
)

# Check cache status
status = engine.model_cache_status()
print(f"Models loaded: {status.get('loaded_models', [])}")

# Unload all models
engine.unload_all_models()
```

## How It Works

### Direct Mode (when sae_app_v3 is available)
- Imports functions directly from `sae_app_v3.api_functions`
- Uses `APIClient.patch_requests()` to inject API key into HTTP requests
- Calls backend functions directly for common operations
- No HTTP overhead for function calls

### HTTP Mode (when sae_app_v3 not available, pip install)
- Makes HTTP requests to frontend server on port 8001
- All requests include `X-API-Key` header
- Works standalone without sae_app_v3 codebase

The `Engine` class automatically:
1. Detects if `sae_app_v3` is available
2. Uses direct mode if available, HTTP mode otherwise
3. Injects `X-API-Key` header in all HTTP requests
4. Handles async functions automatically

## Error Handling

```python
import neuronlens

try:
    engine = neuronlens.Engine(api_key="invalid_key")
    result = engine.analyze_agent("Get Tesla price")
except ValueError as e:
    if "API key is required" in str(e):
        print("Error: API key is required")
    else:
        print(f"Configuration error: {e}")
except requests.exceptions.HTTPError as e:
    if e.response.status_code == 401:
        print("Authentication failed: Invalid API key")
    else:
        print(f"API error: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")
```

## Requirements

- Python 3.8+
- requests>=2.25.0
- numpy>=1.20.0

For full functionality, you need:
- Backend server running on port 8000
- Frontend server running on port 8001
- See `FUNCTION_LIST.md` in sae_app_v3 for server setup instructions

## Developer Guide

### Updating API URL When Ngrok Changes

When the backend API URL changes (e.g., new ngrok URL), update it using one of these methods (priority order):

1. **Config File** (`sae_app_v3/pip/config.json`):
   ```json
   {"api_url": "https://new-ngrok-url.ngrok-free.dev", "timeout": 30}
   ```

2. **Environment Variable**:
   ```bash
   export NEURONLENS_API_URL="https://new-ngrok-url.ngrok-free.dev"
   ```

3. **`.ngrok_url` File** (Auto-detected):
   ```bash
   echo "https://new-ngrok-url.ngrok-free.dev" > sae_app_v3/.ngrok_url
   ```

4. **Ngrok API** (Auto-detected from `http://127.0.0.1:4040/api/tunnels`)

5. **Engine Constructor**:
   ```python
   engine = neuronlens.Engine(api_key="key", api_url="https://new-url.ngrok-free.dev")
   ```

### API Key Configuration

**Where to put the API key:**

- **Environment Variable**: `export NEURONLENS_API_KEY="nlive_your_key"` or `export API_KEY="nlive_your_key"`
- **`.env` File** (`sae_app_v3/.env`): `API_KEY=nlive_your_key` (load with `python-dotenv`)
- **Engine Constructor**: `engine = neuronlens.Engine(api_key="nlive_your_key")`

**Note:** API key is **required** for all functions. Engine raises `ValueError` if missing.

### HTTP Mode (Standalone)

When `sae_app_v3` is not available (pip install), package works **completely via HTTP**:
- All calls make HTTP requests to frontend server (port 8001) via ngrok
- No local code dependencies - works from any machine
- API key automatically injected in `X-API-Key` header

```python
engine = neuronlens.Engine(api_key="key", api_url="https://ngrok-url.ngrok-free.dev")
result = engine.analyze_sentiment("text")  # POST to /frontend/analyze_sentiment
```

### Direct Mode (Local Development)

When `sae_app_v3` is available, package uses **direct function calls**:
- High-level: Imports from `sae_app_v3.api_functions`, calls directly, injects API key via patched `requests.post`
- Common: Imports from `sae_app_v3.backend.common` and `sae_app_v3.services.model_loader`, calls directly
- API key still required (validated in constructor)

```python
engine = neuronlens.Engine(api_key="key")
status = engine.model_cache_status()  # Direct call, no HTTP
result = engine.analyze_sentiment("text")  # Direct call, API key injected for internal backend calls
```

### Configuration Files

- **`sae_app_v3/pip/config.json`** - Default API URL (edit this file)
- **`sae_app_v3/.ngrok_url`** - Auto-detected ngrok URL
- **`sae_app_v3/.env`** - API key and URL (requires python-dotenv)
- **`sae_app_v3/pip/.env`** - PyPI API token for publishing (not committed to git)
- **`sae_app_v3/pip/config.py`** - URL detection logic (modify if needed)

## License

MIT License
