Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8a9b400
feat: implement initial configurations for aws bedrock support
bsanchez-aplyca Jun 25, 2025
9b3b0ff
fix: typo on setup docs
bsanchez-aplyca Jun 25, 2025
19ed85d
fix(aws_bedrock): updated variable names and implementation to suppor…
bsanchez-aplyca Jun 25, 2025
32fac4c
feat(aws_bedrock): implemented aws_bedrock provider for llm
bsanchez-aplyca Jun 25, 2025
858a685
feat: implemented AWS bedrock embeddings support
bsanchez-aplyca Jun 25, 2025
f95f2a0
fix: prevent out of range error for responses wihtout braces
bsanchez-aplyca Jun 26, 2025
aea6607
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Jul 10, 2025
c5bd220
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Jul 15, 2025
93b579c
fix: updated files to new project directory schema
bsanchez-aplyca Jul 15, 2025
4e2128d
Merge branch 'main' of https://github.com/microsoft/NLWeb into implem…
chelseacarter29 Jul 17, 2025
ce4abfd
adding logic to install bedrock requirements from embedding model if …
chelseacarter29 Jul 17, 2025
c4ce76a
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Jul 22, 2025
e92c6a2
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Jul 24, 2025
cdd3bcf
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Jul 30, 2025
cd49302
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Aug 4, 2025
f4e502b
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Aug 8, 2025
54d8b75
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Aug 12, 2025
97907d0
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Aug 20, 2025
961c58a
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Sep 3, 2025
834332b
Merge branch 'main' of github.com:Aplyca/NLWeb into implement-aws-bed…
bsanchez-aplyca Sep 11, 2025
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
21 changes: 20 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ QDRANT_API_KEY="<OPTIONAL>"
POSTGRES_CONNECTION_STRING="postgresql://<HOST>:<PORT>/<DATABASE>?user=<USERNAME>&sslmode=require"
POSTGRES_PASSWORD="<PASSWORD>"

# Local Directory for file writes
#NLWEB_OUTPUT_DIR=/home/sites/data/nlweb

# NLWeb Logging profile (production, development, testing)
# This is used to set the logging level and other configurations in config/config_logging.py
NLWEB_LOGGING_PROFILE=production

# Hugging Face Inference Providers env variables
HF_TOKEN="<TODO>"

# AWS Bedrock env variables
AWS_BEDROCK_API_KEY="<TODO>"
AWS_BEDROCK_REGION="us-east-1"

# Cloudflare AutoRAG env variables
CLOUDFLARE_API_TOKEN="<TODO>"
CLOUDFLARE_RAG_ID_ENV="<TODO>"
CLOUDFLARE_ACCOUNT_ID="<TODO>"

# SNOWFLAKE ENV VARIABLES
SNOWFLAKE_ACCOUNT_URL="<TODO>"
SNOWFLAKE_PAT="<TODO>"
Expand All @@ -75,4 +94,4 @@ SNOWFLAKE_EMBEDDING_MODEL=snowflake-arctic-embed-l-v2.0
# Fully qualified name of the cortex search service in your snowflake account
# For example TEMP.NLWEB.NLWEB_SAMPLE
# if you used snowflake.sql with --database TEMP --schema NLWEB
SNOWFLAKE_CORTEX_SEARCH_SERVICE=TODO
SNOWFLAKE_CORTEX_SEARCH_SERVICE=TODO
2 changes: 2 additions & 0 deletions code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ code/
| └── whoHandler.py #
├── embedding/
| ├── anthropic_embedding.py #
| ├── aws_bedrock_embedding.py #
| ├── azure_oai_embedding.py #
| ├── embedding.py #
| ├── gemini_embedding.py #
Expand All @@ -37,6 +38,7 @@ code/
| ├── snowflake_embedding.py #
├── llm/
| ├── anthropic.py #
| ├── aws_bedrock.py #
| ├── azure_deepseek.py #
| ├── azure_llama.py #
| ├── azure_oai.py #
Expand Down
83 changes: 80 additions & 3 deletions code/python/core/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from typing import Optional, List
import asyncio
import threading
import sys
import subprocess

from core.config import CONFIG
from misc.logger.logging_config_helper import get_configured_logger, LogLevel
Expand All @@ -23,9 +25,64 @@
"gemini": threading.Lock(),
"azure_openai": threading.Lock(),
"snowflake": threading.Lock(),
"elasticsearch": threading.Lock()
"elasticsearch": threading.Lock(),
"aws_bedrock": threading.Lock()
}

# Mapping of embedding provider types to their required pip packages
_embedding_provider_packages = {
"openai": ["openai>=1.12.0"],
"gemini": ["google-cloud-aiplatform>=1.38.0"],
"azure_openai": ["openai>=1.12.0"],
"snowflake": ["httpx>=0.28.1"],
"aws_bedrock": ["boto3>=1.38.15"],
}

# Cache for installed packages - shared with LLM to avoid duplicate installs
try:
from core.llm import _installed_packages
except ImportError:
_installed_packages = set()

def _ensure_package_installed(provider: str):
"""
Ensure the required packages for an embedding provider are installed.

Args:
provider: The name of the embedding provider
"""
if provider not in _embedding_provider_packages:
return

packages = _embedding_provider_packages[provider]
for package in packages:
# Extract package name without version for caching
package_name = package.split(">=")[0].split("==")[0]

if package_name in _installed_packages:
continue

try:
# Try to import the package first
if package_name == "google-cloud-aiplatform":
__import__("vertexai")
else:
__import__(package_name)
_installed_packages.add(package_name)
logger.debug(f"Package {package_name} is already installed")
except ImportError:
# Package not installed, install it
logger.info(f"Installing {package} for {provider} provider...")
try:
subprocess.check_call([
sys.executable, "-m", "pip", "install", package, "--quiet"
])
_installed_packages.add(package_name)
logger.info(f"Successfully installed {package}")
except subprocess.CalledProcessError as e:
logger.error(f"Failed to install {package}: {e}")
raise ValueError(f"Failed to install required package {package} for {provider}")

async def get_embedding(
text: str,
provider: Optional[str] = None,
Expand Down Expand Up @@ -86,6 +143,9 @@ async def get_embedding(
logger.debug(f"Using embedding model: {model_id}")

try:
# Ensure required packages are installed before importing provider modules
_ensure_package_installed(provider)

# Use a timeout wrapper for all embedding calls
if provider == "openai":
logger.debug("Getting OpenAI embeddings")
Expand Down Expand Up @@ -161,6 +221,14 @@ async def get_embedding(

logger.debug(f"Elasticsearch embeddings received, count: {len(result)}")
return result

if provider == "aws_bedrock":
logger.debug("Getting AWS Bedrock embeddings")
# Import here to avoid potential circular imports
from embedding_providers.aws_bedrock_embedding import get_aws_bedrock_embeddings
result = get_aws_bedrock_embeddings(text, model=model_id, timeout=timeout)
logger.debug(f"AWS Bedrock embeddings received, dimension: {len(result)}")
return result

error_msg = f"No embedding implementation for provider '{provider}'"
logger.error(error_msg)
Expand Down Expand Up @@ -306,14 +374,23 @@ async def batch_get_embeddings(

logger.debug(f"Elasticsearch batch embeddings received, count: {len(result)}")
return result

if provider == "aws_bedrock":
logger.debug("Getting AWS Bedrock batch embeddings")
from embedding_providers.aws_bedrock_embedding import get_aws_bedrock_embeddings
results = []
for text in texts:
result = get_aws_bedrock_embeddings(text, model=model_id, timeout=timeout)
results.append(result)
logger.debug(f"AWS Bedrock batch embeddings received, count: {len(results)}")
return results

# Default implementation if provider doesn't match any above
logger.debug(f"No specific batch implementation for {provider}, processing sequentially")
results = []
for text in texts:
embedding = await get_embedding(text, provider, model)
results.append(embedding)

results.append(embedding)
return results

except asyncio.TimeoutError:
Expand Down
4 changes: 4 additions & 0 deletions code/python/core/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def init():
"snowflake": ["httpx>=0.28.1"],
"huggingface": ["huggingface_hub>=0.31.0"],
"ollama": ["ollama>=0.5.1"],
"aws_bedrock": ["boto3>=1.38.15"],
}

# Cache for installed packages
Expand Down Expand Up @@ -146,6 +147,9 @@ def _get_provider(llm_type: str):
elif llm_type == "ollama":
from llm_providers.ollama import provider as ollama_provider
_loaded_providers[llm_type] = ollama_provider
elif llm_type == "aws_bedrock":
from llm_providers.aws_bedrock import provider as aws_bedrock_provider
_loaded_providers[llm_type] = aws_bedrock_provider
else:
raise ValueError(f"Unknown LLM type: {llm_type}")

Expand Down
160 changes: 160 additions & 0 deletions code/python/embedding_providers/aws_bedrock_embedding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Copyright (c) 2025 Microsoft Corporation.
# Licensed under the MIT License

"""
OpenAI embedding implementation.

WARNING: This code is under development and may undergo changes in future releases.
Backwards compatibility is not guaranteed at this time.
"""

import os
import json
from typing import List, Optional, Any

from botocore.config import Config

import boto3
from core.config import CONFIG
import threading

from misc.logger.logging_config_helper import get_configured_logger, LogLevel

logger = get_configured_logger("aws_bedrock_embedding")

_client_lock = threading.Lock()
aws_bedrock_client = None


def get_aws_bedrock_api_key() -> str:
"""
Retrieve the AWS Bedrock API key from configuration.
"""
# Get the API key from the embedding provider config
provider_config = CONFIG.get_embedding_provider("aws_bedrock")
if provider_config and provider_config.api_key:
api_key = provider_config.api_key
if api_key:
return api_key

# Fallback to environment variable
api_key = os.getenv("AWS_BEDROCK_API_KEY")
if not api_key:
error_msg = "AWS Bedrock API key not found in configuration or environment"
logger.error(error_msg)
raise ValueError(error_msg)

return api_key


def get_aws_bedrock_region() -> str:
"""
Retrieve the AWS Bedrock region from configuration.
"""
# Get the API key from the embedding provider config
provider_config = CONFIG.get_embedding_provider("aws_bedrock")
if provider_config and provider_config.api_version:
aws_region = provider_config.api_version
if aws_region:
return aws_region

# Fallback to environment variable
aws_region = os.getenv("AWS_BEDROCK_REGION")
if not aws_region:
error_msg = "AWS Bedrock region not found in configuration or environment"
logger.error(error_msg)
raise ValueError(error_msg)

return aws_region


def get_runtime_client(timeout: float = 30.0) -> Any:
"""
Configure and return an AWS Bedrock runtime client.
"""
config = Config(connect_timeout=timeout, read_timeout=timeout)

global aws_bedrock_client
with _client_lock:
if aws_bedrock_client is None:
try:
api_key = get_aws_bedrock_api_key()

# Validate API key format
parts = api_key.split(":")
if len(parts) != 2:
error_msg = "AWS Bedrock API key must be in format 'access_key_id:secret_access_key'"
logger.error(error_msg)
raise ValueError(error_msg)

aws_access_key_id = parts[0]
aws_secret_access_key = parts[1]
aws_region = get_aws_bedrock_region()

aws_bedrock_client = boto3.client(
service_name="bedrock-runtime",
region_name=aws_region,
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
config=config,
)
logger.debug("AWS Bedrock client initialized successfully")
except Exception as e:
logger.exception("Failed to initialize AWS Bedrock client")
raise

return aws_bedrock_client

def get_aws_bedrock_embeddings(
text: str, model: Optional[str] = None, timeout: float = 30.0
) -> List[float]:
"""
Generate an embedding for a single text using AWS Bedrock API.

Args:
text: The text to embed
model: Optional model ID to use, defaults to provider's configured model
timeout: Maximum time to wait for the embedding response in seconds

Returns:
List of floats representing the embedding vector
"""
# If model not provided, get it from config
if model is None:
provider_config = CONFIG.get_embedding_provider("aws_bedrock")
if provider_config and provider_config.model:
model = provider_config.model
else:
# Default to a common embedding model
model = "amazon.titan-embed-text-v2:0"

logger.debug(f"Generating AWS Bedrock embedding with model: {model}")
logger.debug(f"Text length: {len(text)} chars")

client = get_runtime_client(timeout)

try:
# Clean input text (replace newlines with spaces)
text = text.replace("\n", " ")

response = client.invoke_model(
modelId=model, body=json.dumps({"inputText": text})
)

model_response = json.loads(response["body"].read())
embedding = model_response["embedding"]
logger.debug(f"AWS Bedrock embedding generated, dimension: {len(embedding)}")
return embedding
except Exception as e:
logger.exception("Error generating AWS Bedrock embedding")
logger.log_with_context(
LogLevel.ERROR,
"AWS Bedrock embedding generation failed",
{
"model": model,
"text_length": len(text),
"error_type": type(e).__name__,
"error_message": str(e),
},
)
raise
Loading