add AWS Bedrock backend via boto3 Converse API

This commit is contained in:
Safi
2026-05-07 10:51:45 +01:00
parent 669bc32e0d
commit 61e27fb1a5
3 changed files with 70 additions and 15 deletions
+1
View File
@@ -5,6 +5,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## 0.7.9 (unreleased)
- Feat: optional Google Workspace shortcut export for headless extraction -- `graphify extract ./docs --google-workspace` converts `.gdoc`, `.gsheet`, and `.gslides` files into Markdown sidecars with the `gws` CLI before semantic extraction; account email pseudonymized via SHA256 hash; `[google]` extra adds Sheets table rendering support
- Feat: AWS Bedrock backend -- `graphify extract ./docs --backend bedrock`; credentials via standard AWS provider chain (AWS_PROFILE, AWS_REGION, IAM roles, SSO); model via GRAPHIFY_BEDROCK_MODEL (default anthropic.claude-3-5-sonnet-20241022-v2:0); `[bedrock]` extra adds boto3
## 0.7.8 (2026-05-06)
+67 -14
View File
@@ -87,6 +87,15 @@ BACKENDS: dict[str, dict] = {
"pricing": {"input": 0.40, "output": 1.60}, # USD per 1M tokens
"temperature": 0,
},
"bedrock": {
"base_url": "",
# Spec default. Users who want a different model set GRAPHIFY_BEDROCK_MODEL.
"default_model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
"model_env_key": "GRAPHIFY_BEDROCK_MODEL",
"pricing": {"input": 3.0, "output": 15.0}, # USD per 1M tokens
"temperature": 0,
"max_tokens": 16384,
},
}
@@ -155,7 +164,10 @@ def _backend_env_keys(backend: str) -> list[str]:
keys = cfg.get("env_keys")
if keys:
return list(keys)
return [cfg["env_key"]]
env_key = cfg.get("env_key")
if env_key:
return [env_key]
return []
def _get_backend_api_key(backend: str) -> str:
@@ -169,7 +181,8 @@ def _get_backend_api_key(backend: str) -> str:
def _format_backend_env_keys(backend: str) -> str:
"""Return user-facing accepted API-key variable names."""
return " or ".join(_backend_env_keys(backend))
keys = _backend_env_keys(backend)
return " or ".join(keys) if keys else "AWS_PROFILE or AWS_REGION"
def _default_model_for_backend(backend: str) -> str:
@@ -268,6 +281,43 @@ def _call_claude(api_key: str, model: str, user_message: str, max_tokens: int =
return result
def _call_bedrock(model: str, user_message: str, max_tokens: int = 8192) -> dict:
"""Call AWS Bedrock via boto3 Converse API using the standard AWS credential chain."""
try:
import boto3
import botocore.exceptions
except ImportError as exc:
raise ImportError(
"AWS Bedrock extraction requires boto3. Run: pip install graphifyy[bedrock]"
) from exc
region = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1"
profile = os.environ.get("AWS_PROFILE")
session = boto3.Session(profile_name=profile, region_name=region)
client = session.client("bedrock-runtime")
try:
resp = client.converse(
modelId=model,
system=[{"text": _EXTRACTION_SYSTEM}],
messages=[{"role": "user", "content": [{"text": user_message}]}],
inferenceConfig={"maxTokens": max_tokens, "temperature": 0},
)
except botocore.exceptions.ClientError as exc:
code = exc.response["Error"]["Code"]
msg = exc.response["Error"]["Message"]
raise RuntimeError(f"Bedrock API error ({code}): {msg}") from exc
text = resp.get("output", {}).get("message", {}).get("content", [{}])[0].get("text", "{}")
result = _parse_llm_json(text)
usage = resp.get("usage", {})
result["input_tokens"] = usage.get("inputTokens", 0)
result["output_tokens"] = usage.get("outputTokens", 0)
result["model"] = model
result["finish_reason"] = "length" if resp.get("stopReason") == "max_tokens" else "stop"
return result
def extract_files_direct(
files: list[Path],
backend: str = "kimi",
@@ -287,7 +337,7 @@ def extract_files_direct(
key = api_key or _get_backend_api_key(backend)
if not key and backend == "ollama":
key = "ollama" # Ollama ignores auth but openai client requires non-empty
if not key:
if not key and backend != "bedrock":
raise ValueError(
f"No API key for backend '{backend}'. "
f"Set {_format_backend_env_keys(backend)} or pass api_key=."
@@ -298,17 +348,18 @@ def extract_files_direct(
if backend == "claude":
return _call_claude(key, mdl, user_msg, max_tokens=max_out)
else:
return _call_openai_compat(
cfg["base_url"],
key,
mdl,
user_msg,
temperature=cfg.get("temperature", 0),
reasoning_effort=cfg.get("reasoning_effort"),
max_completion_tokens=cfg.get("max_completion_tokens", max_out),
backend=backend,
)
if backend == "bedrock":
return _call_bedrock(mdl, user_msg, max_tokens=max_out)
return _call_openai_compat(
cfg["base_url"],
key,
mdl,
user_msg,
temperature=cfg.get("temperature", 0),
reasoning_effort=cfg.get("reasoning_effort"),
max_completion_tokens=cfg.get("max_completion_tokens", max_out),
backend=backend,
)
def _estimate_file_tokens(path: Path) -> int:
@@ -580,6 +631,8 @@ def detect_backend() -> str | None:
return backend
if os.environ.get("OLLAMA_BASE_URL"):
return "ollama"
if os.environ.get("AWS_PROFILE") or os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION"):
return "bedrock"
for backend in ("claude", "openai"):
if _get_backend_api_key(backend):
return backend
+2 -1
View File
@@ -57,10 +57,11 @@ google = ["openpyxl"]
video = ["faster-whisper", "yt-dlp"]
kimi = ["openai", "tiktoken"]
ollama = ["openai"]
bedrock = ["boto3"]
gemini = ["openai", "tiktoken"]
openai = ["openai", "tiktoken"]
sql = ["tree-sitter-sql"]
all = ["mcp", "neo4j", "pypdf", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper", "yt-dlp", "matplotlib", "openai", "tiktoken", "tree-sitter-sql"]
all = ["mcp", "neo4j", "pypdf", "markdownify", "watchdog", "graspologic; python_version < '3.13'", "python-docx", "openpyxl", "faster-whisper", "yt-dlp", "matplotlib", "openai", "tiktoken", "boto3", "tree-sitter-sql"]
[project.scripts]
graphify = "graphify.__main__:main"