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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ docker run --rm sethblack/python-seo-analyzer http://example.com/ --sitemap /pat
# Replace "your_api_key_here" with your actual Anthropic API key
docker run --rm -e ANTHROPIC_API_KEY="your_api_key_here" sethblack/python-seo-analyzer http://example.com/ --run-llm-analysis

# Run with AI analysis through OrcaRouter (requires ORCAROUTER_API_KEY)
# Replace "your_api_key_here" with your actual OrcaRouter API key
docker run --rm -e ORCAROUTER_API_KEY="your_api_key_here" sethblack/python-seo-analyzer http://example.com/ --run-llm-analysis

# Save HTML output to your local machine
# This mounts the current directory (.) into /app/output inside the container.
# The output file 'results.html' will be saved in your current directory.
Expand Down Expand Up @@ -84,6 +88,9 @@ docker run --rm my-seo-analyzer http://example.com/
# Run with AI analysis using the locally built image
docker run --rm -e ANTHROPIC_API_KEY="your_api_key_here" my-seo-analyzer http://example.com/ --run-llm-analysis

# Run with AI analysis through OrcaRouter using the locally built image
docker run --rm -e ORCAROUTER_API_KEY="your_api_key_here" my-seo-analyzer http://example.com/ --run-llm-analysis

# Run with HTML output saved locally using the built image
docker run --rm -v "$(pwd):/app/output" my-seo-analyzer /bin/sh -c "python-seo-analyzer http://example.com/ --output-format html > /app/output/results.html"
# Adjust Windows commands as needed (see volume mounting example above)
Expand Down Expand Up @@ -153,6 +160,8 @@ AI Optimization

The first pass of AI optimization features use Anthropic's `claude-3-sonnet-20240229` model to evaluate the content of the site. You will need to have an API key from [Anthropic](https://www.anthropic.com/) to use this feature. The API key needs to be set as the environment variable `ANTHROPIC_API_KEY`. I recommend using a `.env` file to set this variable. Once the API key is set, the AI optimization features can be enabled with the `--run-llm-analysis` flag.

You can also run the AI optimization pass through [OrcaRouter](https://www.orcarouter.ai), an OpenAI- and Anthropic-compatible AI gateway. Set the environment variable `ORCAROUTER_API_KEY` and OrcaRouter is used instead of Anthropic, routing the same analysis through its Anthropic-compatible endpoint. OrcaRouter exposes many models under a `vendor/model` namespace (e.g. `anthropic/claude-sonnet-4.6`, `openai/gpt-4o`); the default model is `anthropic/claude-sonnet-4.6`, and you can pick another one with the `ORCAROUTER_MODEL` environment variable.

Notes
-----

Expand Down
36 changes: 29 additions & 7 deletions pyseoanalyzer/llm_analyst.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,37 @@ class SEORecommendations(BaseModel):
)


# OrcaRouter exposes an Anthropic-compatible endpoint (https://api.orcarouter.ai)
# in front of many model providers using a "vendor/model" namespace. Setting
# ORCAROUTER_API_KEY routes the AI optimization pass through it, otherwise the
# original Anthropic path is used.
ORCAROUTER_API_URL = "https://api.orcarouter.ai"
ORCAROUTER_DEFAULT_MODEL = "anthropic/claude-sonnet-4.6"


class LLMSEOEnhancer:
def __init__(self):
self.llm = ChatAnthropic(
model="claude-3-sonnet-20240229",
anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY"),
temperature=0,
timeout=30,
max_retries=3,
)
orca_router_api_key = os.environ.get("ORCAROUTER_API_KEY")

if orca_router_api_key:
self.llm = ChatAnthropic(
model=os.environ.get(
"ORCAROUTER_MODEL", ORCAROUTER_DEFAULT_MODEL
),
anthropic_api_key=orca_router_api_key,
anthropic_api_url=ORCAROUTER_API_URL,
temperature=0,
timeout=30,
max_retries=3,
)
else:
self.llm = ChatAnthropic(
model="claude-3-sonnet-20240229",
anthropic_api_key=os.environ.get("ANTHROPIC_API_KEY"),
temperature=0,
timeout=30,
max_retries=3,
)
self._setup_chains()

def _setup_chains(self):
Expand Down
40 changes: 34 additions & 6 deletions tests/test_llm_analyst.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import pytest
from pyseoanalyzer.llm_analyst import LLMSEOEnhancer
from langchain_anthropic import ChatAnthropic
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
import json
from pyseoanalyzer.llm_analyst import (
LLMSEOEnhancer,
ORCAROUTER_API_URL,
ORCAROUTER_DEFAULT_MODEL,
)


@pytest.fixture
Expand All @@ -16,15 +17,42 @@ def seo_data():
}


def test_init():
@pytest.fixture
def anthropic_key(monkeypatch):
monkeypatch.delenv("ORCAROUTER_API_KEY", raising=False)
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")


@pytest.fixture
def orcarouter_key(monkeypatch):
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
monkeypatch.setenv("ORCAROUTER_API_KEY", "sk-orca-test")


def test_init_uses_anthropic(anthropic_key):
enhancer = LLMSEOEnhancer()
assert isinstance(enhancer.llm, ChatAnthropic)
assert enhancer.llm.model == "claude-3-sonnet-20240229"
assert enhancer.llm.temperature == 0


def test_init_uses_orcarouter_when_configured(orcarouter_key):
enhancer = LLMSEOEnhancer()
assert isinstance(enhancer.llm, ChatAnthropic)
assert enhancer.llm.model == ORCAROUTER_DEFAULT_MODEL
assert enhancer.llm.anthropic_api_url == ORCAROUTER_API_URL
assert enhancer.llm.temperature == 0


def test_init_orcarouter_honors_model_override(orcarouter_key, monkeypatch):
monkeypatch.setenv("ORCAROUTER_MODEL", "openai/gpt-4o")
enhancer = LLMSEOEnhancer()
assert enhancer.llm.model == "openai/gpt-4o"
assert enhancer.llm.anthropic_api_url == ORCAROUTER_API_URL


@pytest.mark.asyncio
async def test_enhance_seo_analysis(seo_data):
async def test_enhance_seo_analysis(anthropic_key, seo_data):
enhancer = LLMSEOEnhancer()
result = await enhancer.enhance_seo_analysis(seo_data)

Expand Down