feat(llm): add LLM integration module with OpenRouter and Claude Code adapters
Implements markitect/llm/ package with concrete LLMAdapter implementations:
- OpenRouterAdapter: HTTP via urllib with retry/backoff on 429/5xx
- ClaudeCodeAdapter: subprocess-based Claude CLI with stdin piping
- Factory pattern: create_adapter("openrouter") or create_adapter("claude-code")
- API key resolution chain: constructor > env var > project-root key file
- 42 unit tests, 2 integration tests (gated on API key / CLI availability)
Also adds the infospace-with-history example with Wealth of Nations VSM
analysis pipeline, templates, schemas, source chapters, and processed
output for chapters 1-2. process_chapters.py now supports --provider
and --model flags for automatic LLM-driven processing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
58
markitect/llm/factory.py
Normal file
58
markitect/llm/factory.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""
|
||||
Factory for creating LLM adapters by provider name.
|
||||
"""
|
||||
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from markitect.prompts.execution.llm_adapter import LLMAdapter
|
||||
from markitect.llm.exceptions import LLMConfigurationError
|
||||
|
||||
# Lazy imports to avoid pulling in every adapter at module load time.
|
||||
_PROVIDERS: Dict[str, str] = {
|
||||
"openrouter": "markitect.llm.openrouter.OpenRouterAdapter",
|
||||
"claude-code": "markitect.llm.claude_code.ClaudeCodeAdapter",
|
||||
}
|
||||
|
||||
|
||||
def create_adapter(
|
||||
provider: str = "openrouter",
|
||||
model: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> LLMAdapter:
|
||||
"""Instantiate an :class:`LLMAdapter` for the given *provider*.
|
||||
|
||||
Args:
|
||||
provider: ``"openrouter"`` or ``"claude-code"``.
|
||||
model: Model name (passed to the adapter constructor).
|
||||
api_key: Explicit API key (OpenRouter only).
|
||||
system_prompt: Optional system prompt (OpenRouter only).
|
||||
**kwargs: Extra keyword arguments forwarded to the adapter.
|
||||
|
||||
Returns:
|
||||
A ready-to-use :class:`LLMAdapter` instance.
|
||||
|
||||
Raises:
|
||||
LLMConfigurationError: If *provider* is not recognised.
|
||||
"""
|
||||
if provider not in _PROVIDERS:
|
||||
known = ", ".join(sorted(_PROVIDERS))
|
||||
raise LLMConfigurationError(
|
||||
f"Unknown LLM provider {provider!r}. Choose from: {known}",
|
||||
context={"provider": provider},
|
||||
)
|
||||
|
||||
# Lazy import
|
||||
fqn = _PROVIDERS[provider]
|
||||
module_path, class_name = fqn.rsplit(".", 1)
|
||||
import importlib
|
||||
mod = importlib.import_module(module_path)
|
||||
cls = getattr(mod, class_name)
|
||||
|
||||
if provider == "openrouter":
|
||||
return cls(model=model, api_key=api_key, system_prompt=system_prompt, **kwargs)
|
||||
elif provider == "claude-code":
|
||||
return cls(model=model, **kwargs)
|
||||
else:
|
||||
return cls(**kwargs) # pragma: no cover
|
||||
Reference in New Issue
Block a user