Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 15 min read

Agentic-RAG with Terraform using AWS S3, Bedrock KnowledgeBase, OpenSearch Serverless, Strands-Agent, AgentCore

Published @ AWS Builder Center In this post, we will implement an Agentic Retrieval-Augmented Generation (Agentic RAG) system on AWS using Terraform, Amazon S3, Amazon Bedrock Knowledge Bases (KB), Amazon OpenSearch Se

Agentic-RAG with Terraform using AWS S3, Bedrock KnowledgeBase, OpenSearch Serverless, Strands-Agent, AgentCore

Published @ AWS Builder Center

In this post, we will implement an Agentic Retrieval-Augmented Generation (Agentic RAG) system on AWS using Terraform, Amazon S3, Amazon Bedrock Knowledge Bases (KB), Amazon OpenSearch Serverless, AWS Strands Agents, and Amazon Bedrock AgentCore Runtime.

If you are new to RAG or want to understand how to build the underlying infrastructure, please check out my previous post:

Implementing RAG with Terraform using AWS S3, Bedrock Knowledge Bases, OpenSearch Serverless, and IAM.

I won't cover the RAG implementation details again in this post.

Instead, we will focus on how to use AI agents to retrieve information from Amazon Bedrock Knowledge Bases and how to improve RAG quality by combining Reranker models and Agentic RAG.

We will explore how a reranker improves retrieval precision by selecting the most relevant document chunks, while an AI agent dynamically generates retrieval queries, evaluates the retrieved context with agent-as-tool, and performs additional retrievals when necessary to improve answer completeness.

TLDR: We will build on top of the existing Simple RAG architecture and extend it with a Reranker model and Agentic RAG using Strands Agents and Amazon Bedrock AgentCore Runtime.

What is Agentic RAG?

Agentic Retrieval-Augmented Generation (Agentic RAG) is an AI architecture that combines Retrieval-Augmented Generation (RAG) with autonomous AI agents. Unlike traditional RAG, where the retrieval process follows a predefined sequence of steps, Agentic RAG allows an AI agent to decide when to retrieve information, which tools to use, and how to use the retrieved information to answer a user's question.

An AI agent can interact with external knowledge sources, execute retrieval tools, evaluate the retrieved information, and perform additional searches when the initial results are insufficient. This enables more flexible, iterative, and context-aware information retrieval.

Document Ingestion

- Upload documents β†’ Amazon S3
- Split documents β†’ Chunks
- Generate embeddings β†’ Embedding Model
- Store embeddings + text + metadata β†’ Vector Database

Agentic-RAG Query & Retrieval with Agent/s

- User submits a question
- AI Agent receives and analyzes the question
- Agent decides whether external knowledge is required
- Agent selects and invokes the appropriate retrieval tool
- Retrieve relevant document chunks β†’ Amazon Bedrock Knowledge Bases
- Agent evaluates the retrieved context by calling another agent as tool
- Agent decides whether additional retrieval is needed
- If necessary, performs additional searches or tool calls
- Agent combines the retrieved information and reasoning
- Generate the final answer with relevant citations
  • Traditional RAG: Follows a predefined retrieval pipeline (retrieve β†’ augment β†’ generate).
  • Agentic RAG: The agent dynamically controls the retrieval process (decide β†’ retrieve β†’ evaluate β†’ iterate β†’ generate).

In this project, Amazon S3 acts as the document source, while Amazon Bedrock Knowledge Bases (KB) manages the document ingestion pipeline.

When an ingestion job is triggered, the following steps take place:

  • User uploads a document through the Streamlit UI.
  • The application writes the document directly to the S3 docs bucket.
  • User clicks Sync Knowledge Base in the UI.
  • The application triggers Bedrock's StartIngestionJob API.
  • Bedrock KB reads the documents from S3 and splits their content into smaller pieces called chunks.
  • Bedrock KB generates embeddings for each chunk using the configured embedding model (Titan Text Embeddings v2).
  • Bedrock KB stores the resulting vectors, together with the original text and document metadata, in Amazon OpenSearch Serverless for vector search.

At query/retrieval time, instead of relying on a single retrieval operation, we use an AI agent built with Strands Agents and deployed on Amazon Bedrock AgentCore Runtime.

  • User submits a question through the Streamlit chat input.
  • The application invokes the AgentCore Runtime agent.
  • The agent analyzes the question and generates a focused retrieval query.
  • The agent calls its retrieve_kb(query) tool, which invokes the Bedrock Knowledge Bases Retrieve API.
  • Bedrock KB performs a vector search against OpenSearch Serverless, retrieves 20 candidate chunks, and applies the configured Reranker model (Amazon Rerank 1.0) to select the 5 most relevant chunks.
  • The agent calls its critique_context(question) tool to evaluate whether the retrieved information is sufficient to answer the original question.
  • If the context is insufficient, the agent identifies missing aspects, reformulates the retrieval query, and performs another retrieval.
  • This retrieval and evaluation loop can run up to 3 retrieval calls per question.
  • Once sufficient context is collected, the agent generates the final answer using the retrieved document chunks and includes source citations.

agentAcrh

If you have read this far, we can go deeper 😊

Why Combine Reranking and Agentic RAG?

The Reranker model and Agentic RAG improve different aspects of the retrieval process:

  • Reranker, Improves Precision: Vector search retrieves candidate chunks based on embedding similarity. The reranker evaluates the question and candidate chunks together, re-scores their relevance, and selects the most useful chunks. In our implementation, it reduces 20 candidates to the best 5.
  • Agentic RAG, Improves Recall: Instead of relying on a single retrieval, the agent can generate multiple focused queries, evaluate the retrieved context, identify missing information, and perform additional retrievals when necessary.

By combining both approaches, we aim to retrieve more complete information while ensuring that the most relevant document chunks are passed to the generation model.

The key difference is that Simple RAG performs a single retrieval and reranking operation, while Agentic RAG dynamically controls an iterative retrieval and evaluation process, applying reranking to every retrieval.

Code GitHub Link: Project on GitHub

Whether you're exploring to build your own system, this will give you a clear, practical starting point πŸ˜‰

Table of Contents

  • Terraform: Bedrock AgentCore, ECR, CloudWatch, Bedrock KnowledgeBase, OpenSearch Serverless, S3, IAM
  • Agent Dockerfile, Agent.py
  • Streamlit App: Digesting & Querying
  • All Code & Demo
  • Conclusion
  • References

Terraform: Bedrock AgentCore, ECR, CloudWatch, Bedrock KnowledgeBase, OpenSearch Serverless, S3, IAM

Bedrock AgentCore, ECR-Repository, Agent-Core Runtime, Runtime IAM Policy:

# AWS ECR to store Agent Docker Image
resource "aws_ecr_repository" "agent" {
  name                 = "${var.project_name}-agent"
  image_tag_mutability = "MUTABLE"
  force_delete         = true

  image_scanning_configuration {
    scan_on_push = true
  }
}

# IAM Policy for AgentCore Runtime
data "aws_iam_policy_document" "agent_runtime_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["bedrock-agentcore.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "aws:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }

    condition {
      test     = "ArnLike"
      variable = "aws:SourceArn"
      values   = ["arn:aws:bedrock-agentcore:${var.aws_region}:${data.aws_caller_identity.current.account_id}:*"]
    }
  }
}

resource "aws_iam_role" "agent_runtime_execution" {
  name               = "${var.project_name}-agent-runtime-role"
  assume_role_policy = data.aws_iam_policy_document.agent_runtime_trust.json
}

data "aws_iam_policy_document" "agent_runtime_permissions" {
  statement {
    sid     = "BedrockModelInvoke"
    effect  = "Allow"
    actions = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
    resources = [
      "arn:aws:bedrock:*::foundation-model/*",
      "arn:aws:bedrock:*:${data.aws_caller_identity.current.account_id}:inference-profile/*",
    ]
  }

  statement {
    sid       = "KnowledgeBaseRetrieve"
    effect    = "Allow"
    actions   = ["bedrock:Retrieve"]
    resources = [aws_bedrockagent_knowledge_base.this.arn]
  }

  # Retrieve calls with a rerankingConfiguration also need Rerank on the caller.
  statement {
    sid       = "Rerank"
    effect    = "Allow"
    actions   = ["bedrock:Rerank"]
    resources = ["*"]
  }

  statement {
    sid       = "EcrPull"
    effect    = "Allow"
    actions   = ["ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability"]
    resources = [aws_ecr_repository.agent.arn]
  }

  statement {
    sid       = "EcrAuth"
    effect    = "Allow"
    actions   = ["ecr:GetAuthorizationToken"]
    resources = ["*"]
  }

  statement {
    sid    = "Logs"
    effect = "Allow"
    # PutResourcePolicy lets AgentCore deliver the agent's spans into its own log group.
    actions   = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogStreams", "logs:PutResourcePolicy"]
    resources = ["arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/bedrock-agentcore/*"]
  }

  statement {
    sid       = "LogsDescribe"
    effect    = "Allow"
    actions   = ["logs:DescribeLogGroups"]
    resources = ["arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:*"]
  }

  statement {
    sid       = "Xray"
    effect    = "Allow"
    actions   = ["xray:PutTraceSegments", "xray:PutTelemetryRecords", "xray:GetSamplingRules", "xray:GetSamplingTargets"]
    resources = ["*"]
  }

  # AgentCore Observability metrics (sessions, latency, tokens) from ADOT.
  statement {
    sid       = "Metrics"
    effect    = "Allow"
    actions   = ["cloudwatch:PutMetricData"]
    resources = ["*"]
    condition {
      test     = "StringEquals"
      variable = "cloudwatch:namespace"
      values   = ["bedrock-agentcore"]
    }
  }
}

resource "aws_iam_role_policy" "agent_runtime_permissions" {
  name   = "${var.project_name}-agent-runtime-permissions"
  role   = aws_iam_role.agent_runtime_execution.id
  policy = data.aws_iam_policy_document.agent_runtime_permissions.json
}

# AgentCore Runtime
resource "aws_bedrockagentcore_agent_runtime" "agent" {
  count = var.agent_container_image_uri == "" ? 0 : 1

  agent_runtime_name = replace("${var.project_name}_agent", "-", "_")
  role_arn           = aws_iam_role.agent_runtime_execution.arn

  agent_runtime_artifact {
    container_configuration {
      container_uri = var.agent_container_image_uri
    }
  }

  network_configuration {
    network_mode = "PUBLIC"
  }

  environment_variables = {
    AWS_REGION          = var.aws_region
    KNOWLEDGE_BASE_ID   = aws_bedrockagent_knowledge_base.this.id
    GENERATION_MODEL_ID = var.generation_model_id
    RERANK_MODEL_ARN    = var.enable_reranking ? local.rerank_model_arn : ""
  }
}

# AgentCore Observability, CloudWatch

resource "aws_cloudwatch_log_delivery_source" "agent_traces" {
  count        = length(aws_bedrockagentcore_agent_runtime.agent)
  name         = "${var.project_name}-agent-traces"
  log_type     = "TRACES"
  resource_arn = aws_bedrockagentcore_agent_runtime.agent[0].agent_runtime_arn
}

resource "aws_cloudwatch_log_delivery_destination" "agent_traces" {
  count                     = length(aws_bedrockagentcore_agent_runtime.agent)
  name                      = "${var.project_name}-agent-traces"
  delivery_destination_type = "XRAY"
}

resource "aws_cloudwatch_log_delivery" "agent_traces" {
  count                    = length(aws_bedrockagentcore_agent_runtime.agent)
  delivery_source_name     = aws_cloudwatch_log_delivery_source.agent_traces[0].name
  delivery_destination_arn = aws_cloudwatch_log_delivery_destination.agent_traces[0].arn
}

IAM Policies for Bedrock KnowledgeBase, Bedrock Invoke Models:

data "aws_iam_policy_document" "bedrock_kb_trust" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["bedrock.amazonaws.com"]
    }
    condition {
      test     = "StringEquals"
      variable = "aws:SourceAccount"
      values   = [data.aws_caller_identity.current.account_id]
    }
  }
}

resource "aws_iam_role" "bedrock_kb" {
  name               = "${var.project_name}-kb-role"
  assume_role_policy = data.aws_iam_policy_document.bedrock_kb_trust.json
}

data "aws_iam_policy_document" "bedrock_kb_permissions" {
  statement {
    sid       = "S3Read"
    actions   = ["s3:GetObject", "s3:ListBucket"]
    resources = [aws_s3_bucket.docs.arn, "${aws_s3_bucket.docs.arn}/*"]
  }

  statement {
    sid       = "InvokeEmbeddingModel"
    actions   = ["bedrock:InvokeModel"]
    resources = ["arn:aws:bedrock:${var.aws_region}::foundation-model/${var.embedding_model_id}"]
  }

  # Reranking inside a Retrieve call runs as this service role (not the caller).
  statement {
    sid       = "Rerank"
    actions   = ["bedrock:Rerank"]
    resources = ["*"]
  }

  statement {
    sid       = "InvokeRerankModel"
    actions   = ["bedrock:InvokeModel"]
    resources = [local.rerank_model_arn]
  }

  statement {
    sid       = "OpenSearchServerlessAccess"
    actions   = ["aoss:APIAccessAll"]
    resources = [aws_opensearchserverless_collection.kb.arn]
  }
}

resource "aws_iam_role_policy" "bedrock_kb" {
  name   = "${var.project_name}-kb-policy"
  role   = aws_iam_role.bedrock_kb.id
  policy = data.aws_iam_policy_document.bedrock_kb_permissions.json
}

S3 Bucket, Bucket Policy:

resource "aws_s3_bucket" "docs" {
  bucket = var.docs_bucket_name != null ? var.docs_bucket_name : "${var.project_name}-docs-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_versioning" "docs" {
  bucket = aws_s3_bucket.docs.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "docs" {
  bucket                  = aws_s3_bucket.docs.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Bedrock KnowledgeBase for Vector Database Config (embedding model, embedding dimensions, opensearch serverless collection, chunking), Data Source (S3 Bucket, Vector Ingestion, Chunking Strategy):

locals {
  generation_model_arn = "arn:aws:bedrock:${var.aws_region}:${data.aws_caller_identity.current.account_id}:inference-profile/${var.generation_model_id}"
  embedding_model_arn  = "arn:aws:bedrock:${var.aws_region}::foundation-model/${var.embedding_model_id}"
  rerank_model_arn     = "arn:aws:bedrock:${var.aws_region}::foundation-model/${var.rerank_model_id}"
}

resource "aws_bedrockagent_knowledge_base" "this" {
  name     = "${var.project_name}-kb"
  role_arn = aws_iam_role.bedrock_kb.arn

  knowledge_base_configuration {
    type = "VECTOR"
    vector_knowledge_base_configuration {
      embedding_model_arn = local.embedding_model_arn
      embedding_model_configuration {
        bedrock_embedding_model_configuration {
          dimensions = var.embedding_dimension
        }
      }
    }
  }

  storage_configuration {
    type = "OPENSEARCH_SERVERLESS"
    opensearch_serverless_configuration {
      collection_arn    = aws_opensearchserverless_collection.kb.arn
      vector_index_name = opensearch_index.kb_vector_index.name
      field_mapping {
        vector_field   = "bedrock-knowledge-base-default-vector"
        text_field     = "AMAZON_BEDROCK_TEXT_CHUNK"
        metadata_field = "AMAZON_BEDROCK_METADATA"
      }
    }
  }

  depends_on = [aws_iam_role_policy.bedrock_kb, time_sleep.wait_for_index]
}

resource "aws_bedrockagent_data_source" "docs" {
  knowledge_base_id = aws_bedrockagent_knowledge_base.this.id
  name              = "${var.project_name}-docs-source"

  data_source_configuration {
    type = "S3"
    s3_configuration {
      bucket_arn = aws_s3_bucket.docs.arn
    }
  }

  vector_ingestion_configuration {
    chunking_configuration {
      chunking_strategy = "FIXED_SIZE"
      fixed_size_chunking_configuration {
        max_tokens         = var.chunk_max_tokens
        overlap_percentage = var.chunk_overlap_percentage
      }
    }
  }
}

OpenSearch Serverless Config for security (encryption, network), VectorDB config (embedding dimension, hnsw, faiss, knn_vector):

resource "aws_opensearchserverless_security_policy" "encryption" {
  name = "${var.project_name}-encrypt"
  type = "encryption"
  policy = jsonencode({
    Rules = [{
      ResourceType = "collection"
      Resource     = ["collection/${var.project_name}-kb"]
    }]
    AWSOwnedKey = true
  })
}

resource "aws_opensearchserverless_security_policy" "network" {
  name = "${var.project_name}-network"
  type = "network"
  policy = jsonencode([{
    Rules = [{
      ResourceType = "collection"
      Resource     = ["collection/${var.project_name}-kb"]
    }]
    AllowFromPublic = true
  }])
}

resource "aws_opensearchserverless_collection" "kb" {
  name = "${var.project_name}-kb"
  type = "VECTORSEARCH"

  depends_on = [
    aws_opensearchserverless_security_policy.encryption,
    aws_opensearchserverless_security_policy.network,
  ]
}

resource "aws_opensearchserverless_access_policy" "kb" {
  name = "${var.project_name}-access"
  type = "data"
  policy = jsonencode([{
    Rules = [
      {
        ResourceType = "collection"
        Resource     = ["collection/${var.project_name}-kb"]
        Permission   = ["aoss:*"]
      },
      {
        ResourceType = "index"
        Resource     = ["index/${var.project_name}-kb/*"]
        Permission   = ["aoss:*"]
      }
    ]
    Principal = [
      aws_iam_role.bedrock_kb.arn,
      var.admin_principal_arn != null ? var.admin_principal_arn : data.aws_caller_identity.current.arn,
    ]
  }])
}

resource "time_sleep" "wait_for_access_policy" {
  depends_on      = [aws_opensearchserverless_access_policy.kb]
  create_duration = "60s"
}

provider "opensearch" {
  url               = aws_opensearchserverless_collection.kb.collection_endpoint
  healthcheck       = false
  aws_region        = var.aws_region
  sign_aws_requests = true
}

resource "opensearch_index" "kb_vector_index" {
  name      = "${var.project_name}-index"
  index_knn = true

  mappings = jsonencode({
    properties = {
      "bedrock-knowledge-base-default-vector" = {
        type      = "knn_vector"
        dimension = var.embedding_dimension
        method = {
          name       = "hnsw"
          engine     = "faiss"
          space_type = "l2"
        }
      }
      "AMAZON_BEDROCK_TEXT_CHUNK" = { type = "text" }
      "AMAZON_BEDROCK_METADATA"   = { type = "text", index = false }
    }
  })

  # Bedrock adds its own metadata fields to the mapping during ingestion
  # (x-amz-bedrock-kb-*). Without this, every later apply would see drift and
  # replace the index, wiping all embeddings.
  lifecycle {
    ignore_changes = [mappings]
  }

  depends_on = [time_sleep.wait_for_access_policy]
}

resource "time_sleep" "wait_for_index" {
  depends_on      = [opensearch_index.kb_vector_index]
  create_duration = "30s"
}

Also, app_config.tf, providers.tf, variables.tf, output.tf implemented, you can reach from => Terraform Codes. We can run these TF files with terraform plan, apply, destroy.

Agent Dockerfile, Agent.py

Agent runs on Bedrock AgentCore as container. It has agent tools as functions: retrieve_kb(query: str), critique_context(question: str). It generally retrieves and reranks the chunks of data.

To run on Agentcore, needs to create Docker image with Arm64 based.

FROM --platform=linux/arm64 python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8080
# opentelemetry-instrument (ADOT) sends traces to AgentCore Observability.
CMD ["opentelemetry-instrument", "python3", "agent.py", "--serve"]

Only here the main part, please have a look from here for the details: Agent Code

Agent, System Prompt, Tools:

def run(question: str) -> dict:
    state: dict = {"question": question, "trace": [], "chunks": []}

    agent = Agent(
        model=BedrockModel(
            model_id=_config()["model_id"], temperature=0.2, region_name=_config()["region"]
        ),
        tools=_make_tools(state),
        system_prompt=(
            "You are an agentic RAG assistant. Answer the user's question ONLY "
            "using information retrieved via retrieve_kb.\n\n"
            "Process:\n"
            "1. Call retrieve_kb with a focused query. For compound questions "
            "covering multiple topics, call it once per topic.\n"
            "2. Call critique_context(question) to check if you have enough to "
            "answer fully.\n"
            "3. If not sufficient and you still have retrieval budget, "
            "reformulate the query (or target a missing aspect) and retrieve "
            "again, then critique again.\n"
            "4. Once sufficient, or your retrieval budget is exhausted "
            f"(max {MAX_RETRIEVALS} retrieve_kb calls total), write the final "
            "answer using only retrieved content, citing sources by URI. If "
            "nothing relevant was found, say so plainly instead of guessing."
        ),
    )

    answer = str(agent(question)).strip()
    sources = sorted({c["uri"] for c in state["chunks"] if c["uri"]})
    return {"answer": answer, "citations": sources, "trace": state["trace"]}

Agent Main:

if __name__ == "__main__":
    if "--serve" in sys.argv:
        from bedrock_agentcore.runtime import BedrockAgentCoreApp
        app = BedrockAgentCoreApp()
        @app.entrypoint
        def _invoke(payload: dict, context=None) -> dict:
            question = payload.get("question") or payload.get("prompt") or DEFAULT_QUESTION
            result = run(question)
            result["runtime"] = {
                "session_id": getattr(context, "session_id", None),
                "host": socket.gethostname(),
            }
            return result

        app.run()
    else:
        result = run(" ".join(sys.argv[1:]) or DEFAULT_QUESTION)
        print(json.dumps(result, indent=2))

Streamlit App: Uploading, Digest & Querying

App has 4 main parts as utils: agentcore_ops.py, aws_clients.py, bedrock_ops.py, s3_ops.py.

App Code that includes utils and main: App Code

Start_ingestion_job, get_ingestion_job_status, querying, vector_search with reranker model: bedrock-ops.py:

from utils.aws_clients import get_bedrock_agent_client, get_bedrock_agent_runtime_client, get_config

NUM_CANDIDATES = 20
NUM_RESULTS = 5

def start_ingestion_job() -> str:
    cfg = get_config()
    resp = get_bedrock_agent_client().start_ingestion_job(
        knowledgeBaseId=cfg["knowledge_base_id"],
        dataSourceId=cfg["data_source_id"],
    )
    return resp["ingestionJob"]["ingestionJobId"]


def get_ingestion_job_status(job_id: str) -> dict:
    cfg = get_config()
    resp = get_bedrock_agent_client().get_ingestion_job(
        knowledgeBaseId=cfg["knowledge_base_id"],
        dataSourceId=cfg["data_source_id"],
        ingestionJobId=job_id,
    )
    job = resp["ingestionJob"]
    return {"status": job["status"], "failure_reasons": job.get("failureReasons", [])}


def _vector_search_configuration() -> dict:
    rerank_model_arn = get_config()["rerank_model_arn"]
    if not rerank_model_arn:
        return {"numberOfResults": NUM_RESULTS}
    return {
        "numberOfResults": NUM_CANDIDATES,
        "rerankingConfiguration": {
            "type": "BEDROCK_RERANKING_MODEL",
            "bedrockRerankingConfiguration": {
                "modelConfiguration": {"modelArn": rerank_model_arn},
                "numberOfRerankedResults": NUM_RESULTS,
            },
        },
    }

def query(question: str) -> dict:
    cfg = get_config()
    resp = get_bedrock_agent_runtime_client().retrieve_and_generate(
        input={"text": question},
        retrieveAndGenerateConfiguration={
            "type": "KNOWLEDGE_BASE",
            "knowledgeBaseConfiguration": {
                "knowledgeBaseId": cfg["knowledge_base_id"],
                "modelArn": cfg["generation_model_arn"],
                "retrievalConfiguration": {
                    "vectorSearchConfiguration": _vector_search_configuration(),
                },
            },
        },
    )

    citations = []
    for citation in resp.get("citations", []):
        for ref in citation.get("retrievedReferences", []):
            uri = ref.get("location", {}).get("s3Location", {}).get("uri")
            if uri:
                citations.append(uri)

    return {"answer": resp["output"]["text"], "citations": citations}

Query Agentic from agentcore_ops.py:

def query_agentic(question: str, session_id: str) -> dict:
    cfg = get_config()
    if not is_agentic_configured():
        raise RuntimeError(
            "AGENT_RUNTIME_ARN is not a Bedrock AgentCore runtime ARN. Deploy the agent "
            "with ./deploy.sh; it writes the ARN into app/.env."
        )

    # The same runtimeSessionId for every question in one browser session, so
    # AgentCore Observability groups them as one session.
    started = time.monotonic()
    resp = get_bedrock_agentcore_client().invoke_agent_runtime(
        agentRuntimeArn=cfg["agent_runtime_arn"],
        runtimeSessionId=session_id,
        qualifier="DEFAULT",
        payload=json.dumps({"question": question}).encode(),
    )
    result = json.loads(resp["response"].read())
    # Proof of execution on AgentCore: IDs returned by the AWS API, plus what
    # the agent reported from inside its container.
    result["agentcore"] = {
        "runtime_arn": cfg["agent_runtime_arn"],
        "session_id": resp.get("runtimeSessionId", session_id),
        "trace_id": resp.get("traceId"),
        "request_id": resp["ResponseMetadata"]["RequestId"],
        "status_code": resp.get("statusCode"),
        "latency_s": round(time.monotonic() - started, 1),
        "container": result.pop("runtime", None),
        "console_url": (
            f"https://{cfg['region']}.console.aws.amazon.com/bedrock-agentcore/observability"
            f"?region={cfg['region']}"
        ),
    }
    return result

All Code & Demo

GitHub Link: Project on GitHub

Deploy.sh includes image build, push to ECR. You can see details: deploy.sh
Run:

./deploy.sh

All images with better resolution

After applying the TF, view/copy outputs to link with app.py:
Images3

Uploading files (popular latest GenAI papers under docs/pdf):
Imageuploadfiles

Simple vs Agentic RAG:
Imagerag

Monitoring Agent:
Imagemonitoring

Querying & Retrieval Data:
Imageretrieval

DON't Forget to delete Resources:

Run:

./deploy.sh destroy

Conclusion

In this post, we mentioned:

  • how to create Agentic RAG using Bedrock AgentCore, Agent DockerImage, ECR, Bedrock KnowledgeBase, S3, OpenSearch Serverless with Terraform,
  • how to digest, query using simple streamlit app with embedding and reranker models,
  • how agent calls agent-as-tool for critique evaluation,
  • how to enable with permission roles,
  • how to use AWS Claude models (e.g. Sonnet).

If you found the tutorial interesting, I’d love to hear your thoughts in the blog post comments. Feel free to share your reactions or leave a comment. I truly value your input and engagement πŸ˜‰

For other posts πŸ‘‰ https://dev.to/omerberatsezer 🧐

References

Your comments πŸ€”
I’d love to hear your experience and know-how.

  • Do you know the detail of RAG that handled by Bedrock Knowledge Base
  • How do you implement your RAG in production (e.g. Bedrock, LlamaIndex, LangChain, etc.)?

Drop your thoughts, feedback, or improvements in the comments, always curious to learn from different approaches. ☺️

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.