Compare commits

...
4 changed files with 255 additions and 2 deletions
+55
View File
@@ -28,6 +28,7 @@ The `semantica.llms` module provides a unified interface for connecting to Large
## When To Use / When Not To Use
**Use LLM integrations for:**
- Text generation, summarization, and question-answering tasks
- Complex reasoning that requires natural language understanding
- Structured data extraction from unstructured text
@@ -35,6 +36,7 @@ The `semantica.llms` module provides a unified interface for connecting to Large
- Tasks where context, ambiguity, or domain knowledge matter
**Deterministic tools may be better for:**
- Pattern matching that regular expressions can handle
- Simple rule-based classification with clear criteria
- Mathematical calculations or statistical analysis
@@ -42,6 +44,7 @@ The `semantica.llms` module provides a unified interface for connecting to Large
- Data transformations with known logic
**A full LLM may be unnecessary for:**
- Simple keyword search or exact string matching
- Deterministic workflows with predefined decision trees
- High-frequency, low-latency operations where inference overhead matters
@@ -143,6 +146,58 @@ risk_data = oai.generate_structured(
The default model `gpt-3.5-turbo` is fine for classification and light extraction. Switch to `gpt-4o` for complex multi-step regulatory reasoning or document understanding.
## Anthropic — Complex Reasoning and Structured Extraction
**Anthropic** provides the Claude model family, built with an emphasis on careful, instruction-following behavior and strong performance on multi-step reasoning, long-document analysis, and code-related tasks. Claude models tend to be more cautious about ambiguous instructions than other providers. That matters when the cost of a confidently wrong answer is high.
The `Anthropic` provider wraps the Claude API. Reach for it when the task involves reasoning through several dependent steps (not just single-turn extraction), when you're processing long source documents that need to stay in context, or when you need schema-validated structured output rather than best-effort JSON.
Install with `pip install "semantica[llm-anthropic]"` (or just `pip install anthropic`) before using this provider.
```python
from semantica.llms import Anthropic
claude = Anthropic(model="claude-3-sonnet-20240229", api_key="YOUR_ANTHROPIC_KEY")
# api_key falls back to the ANTHROPIC_API_KEY environment variable
# is_available() only confirms a client was constructed from some key.
# It does not validate the key or check network reachability - an
# invalid or expired key still passes this check and fails at generate().
if not claude.is_available():
raise RuntimeError("Anthropic provider not configured - set ANTHROPIC_API_KEY")
# Plain generation - multi-step reasoning over a contract clause
verdict = claude.generate(
"A vendor contract has a 30-day termination-for-convenience clause "
"but a 90-day data-return obligation that survives termination. "
"If the customer terminates on day 1, when must vendor-held data "
"be returned? Answer with the date basis only.",
temperature=0.1,
)
print(verdict)
# "Day 120 from termination notice. The 90-day return period runs from
# the termination date (day 30), not from the notice date."
# Structured, schema-validated output
from pydantic import BaseModel
class ContractRisk(BaseModel):
clause: str
risk_level: str
days_to_deadline: int
risk = claude.generate_typed(
"Extract the termination clause risk from: vendor contract, "
"30-day termination for convenience, 90-day post-termination "
"data return obligation.",
schema=ContractRisk,
)
print(risk.risk_level, risk.days_to_deadline)
# "medium" 90
```
Model selection follows the same tier structure as the other providers: a Haiku model for high-volume classification where cost matters more than depth, a Sonnet model as the default for most extraction and reasoning tasks, an Opus model when a task genuinely needs the deepest reasoning available and latency/cost are secondary. Check Anthropic's docs for the current model identifiers, since they're versioned and change over time.
## LiteLLM — One Interface, 100+ Providers
**LiteLLM** is a universal adapter that provides a single interface to over 100 different LLM providers, including Anthropic Claude, Azure OpenAI, AWS Bedrock, Google Vertex AI, and local Ollama instances. It acts as a translation layer, converting your unified API calls into provider-specific requests, enabling easy switching between providers without code changes.
+8 -2
View File
@@ -10,9 +10,10 @@ Supported Providers:
- OpenAI: OpenAI API (GPT-3.5, GPT-4, etc.)
- HuggingFaceLLM: HuggingFace Transformers for local LLM inference
- LiteLLM: Unified interface to 100+ LLM providers (OpenAI, Anthropic, Groq, Azure, Bedrock, Vertex AI, etc.)
- Anthropic: Anthropic Claude API (Claude sonnet, Opus, Haiku, etc.)
Example Usage:
>>> from semantica.llms import Groq, OpenAI, HuggingFaceLLM, LiteLLM
>>> from semantica.llms import Groq, OpenAI, HuggingFaceLLM, LiteLLM, Anthropic
>>>
>>> # Groq provider
>>> groq = Groq(model="llama-3.1-8b-instant", api_key="your-key")
@@ -32,6 +33,10 @@ Example Usage:
>>> # Or use other providers via LiteLLM
>>> llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
>>> response = llm.generate("Hello, world!")
>>>
>>> # Anthropic provider
>>> claude = Anthropic(model="claude-3-sonnet-20240229", api_key="the-key")
>>> response = claude.generate("Hello, world!")
Author: Semantica Contributors
License: MIT
@@ -41,6 +46,7 @@ from .groq import Groq
from .openai import OpenAI
from .huggingface import HuggingFaceLLM
from .litellm import LiteLLM
from .anthropic import Anthropic
__all__ = ["Groq", "OpenAI", "HuggingFaceLLM", "LiteLLM"]
__all__ = ["Groq", "OpenAI", "HuggingFaceLLM", "LiteLLM", "Anthropic"]
+116
View File
@@ -0,0 +1,116 @@
"""
Anthropic LLM Provider
Wrapper for Anthropic Claude API provider with clean interface
"""
from typing import Any, Dict, List, Optional, Union
from ..semantic_extract.providers import AnthropicProvider
from ..utils.exceptions import ProcessingError
from ..utils.logging import get_logger
logger = get_logger("llms.anthropic")
class Anthropic:
"""
Anthropic Claude LLM provider wrapper.
Provides clean interface to Anthropic's Claude API.
Example:
>>> from semantica.llms import Anthropic
>>> claude = Anthropic(model="claude-3-sonnet-20240229", api_key="the-key")
>>> response = claude.generate("What is API key?")
"""
def __init__(
self,
model: str = "claude-3-sonnet-20240229",
api_key: Optional[str] = None,
**kwargs
):
"""
Init Anthropic provider.
Args:
model: Model name (default: claude-3-sonnet-20240229)
api_key: Anthropic API key (default: from ANTHROPIC_API_KEY env var)
**kwargs: Addition provider options
"""
self.provider = AnthropicProvider(api_key=api_key, model=model, **kwargs)
self.model = model
self.api_key = api_key
def is_available(self) -> bool:
""" Check if Anthropic provider is available"""
return self.provider.is_available()
def generate(self, prompt: str, **kwargs) -> str:
"""
Generate text from prompt.
Args:
prompt: Input prompt text
**kwargs: Generation options (temperature, max_tokens, etc.)
Returns:
Generated text response
Raises:
ProcessingError: If provider is not available or generation fails
"""
if not self.is_available():
raise ProcessingError(
"Anthropic provider not available. set ANTHROPIC_API_KEY or pass api_key."
)
return self.provider.generate(prompt, **kwargs)
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
"""
Generates structured JSON output.
Args:
prompt: Input prompt text
**kwargs: Generation options
Returns:
Parsed JSON response. A dict for a top-level JSON object, or a
list if the model returns a top-level JSON array.
Raises:
ProcessingError: If provider is not available or generation fails
"""
if not self.is_available():
raise ProcessingError(
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
)
return self.provider.generate_structured(prompt, **kwargs)
def generate_typed(self, prompt: str, schema: Any, max_retries: int =3, **kwargs) -> Any:
"""
Generate output validated against a Pydantic schema.
Args:
prompt: Input prompt text
schema: Pydantic model class to validate the output against
max_retries: Number of retries if validation fails (default: 3)
**kwargs: Generation options
Returns:
An instance of `schema`, populated from model's reponse
Raises:
ProcessingError: If provider is not available or generation fails
"""
if not self.is_available():
raise ProcessingError(
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
)
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
+76
View File
@@ -0,0 +1,76 @@
"""Tests for the Anthropic LLM provider wrapper (semantica.llms.Anthropic)."""
from unittest.mock import MagicMock
import pytest
from semantica.llms import Anthropic
from semantica.utils.exceptions import ProcessingError
def test_construction_stores_model_and_api_key():
"""Anthropic(...) should not crash and should remember what it was given."""
claude = Anthropic(model="claude-3-sonnet-20240229", api_key="fake-key")
assert claude.model == "claude-3-sonnet-20240229"
assert claude.api_key == "fake-key"
def test_is_available_false_with_no_key(monkeypatch):
"""Without a real key, is_available() must be a real False, not truthy junk.
api_key=None alone isn't enough to prove this: AnthropicProvider falls
back to the ANTHROPIC_API_KEY environment variable, so this test has to
clear it too or it would pass/fail depending on whoever's machine or CI
runner happens to run it.
"""
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
claude = Anthropic(api_key=None)
assert claude.is_available() is False
def test_generate_raises_clear_error_when_unavailable():
"""generate() must fail loudly."""
claude = Anthropic(api_key=None)
with pytest.raises(ProcessingError, match="Anthropic provider not available"):
claude.generate("hello")
def test_generate_forwards_to_the_real_provider_when_available():
"""When available, generate() must actually call through to the real provider."""
claude = Anthropic(api_key="fake-key")
claude.provider = MagicMock()
claude.provider.is_available.return_value = True
claude.provider.generate.return_value = "a fake response"
result = claude.generate("hello", temperature=0.5)
assert result == "a fake response"
claude.provider.generate.assert_called_once_with("hello", temperature=0.5)
def test_generate_structured_forwards_to_the_real_provider():
claude = Anthropic(api_key="fake-key")
claude.provider = MagicMock()
claude.provider.is_available.return_value = True
claude.provider.generate_structured.return_value = {"key": "value"}
result = claude.generate_structured("hello")
assert result == {"key": "value"}
claude.provider.generate_structured.assert_called_once_with("hello")
def test_generate_typed_forwards_schema_and_max_retries():
claude = Anthropic(api_key="fake-key")
claude.provider = MagicMock()
claude.provider.is_available.return_value = True
fake_schema = object()
claude.provider.generate_typed.return_value = "typed result"
result = claude.generate_typed("hello", fake_schema, max_retries=5)
assert result == "typed result"
claude.provider.generate_typed.assert_called_once_with(
"hello", fake_schema, max_retries=5
)