Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
172 changes: 172 additions & 0 deletions .github/workflows/python-ec2-genai-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
## SPDX-License-Identifier: Apache-2.0

name: Python EC2 Gen AI Use Case
on:
workflow_dispatch: # be able to run the workflow on demand
push:
branches:
- genesis-release-test
workflow_call:
inputs:
caller-workflow-name:
required: true
type: string

cpu-architecture:
description: "Permitted values: x86_64 or arm64"
required: false
type: string
default: "x86_64"
staging-wheel-name:
required: false
default: 'aws-opentelemetry-distro'
type: string

permissions:
id-token: write
contents: read

env:
E2E_TEST_AWS_REGION: 'us-west-2'
E2E_TEST_ACCOUNT_ID: ${{ secrets.APPLICATION_SIGNALS_E2E_TEST_ACCOUNT_ID }}
E2E_TEST_ROLE_NAME: ${{ secrets.APPLICATION_SIGNALS_E2E_TEST_ROLE_NAME }}
ADOT_WHEEL_NAME: ${{ inputs.staging-wheel-name }}

METRIC_NAMESPACE: genesis
LOG_GROUP_NAME: test/genesis
TEST_RESOURCES_FOLDER: ${GITHUB_WORKSPACE}
SAMPLE_APP_ZIP: s3://aws-appsignals-sample-app-prod-us-east-1/python-genai-sample-app.zip

jobs:
python-ec2-adot-genai:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

- name: Set Get ADOT Wheel command environment variable
run: |
if [ "${{ github.event.repository.name }}" = "aws-otel-python-instrumentation" ]; then
# Reusing the adot-main-build-staging-jar bucket to store the python wheel file
echo GET_ADOT_WHEEL_COMMAND="aws s3 cp s3://adot-main-build-staging-jar/${{ env.ADOT_WHEEL_NAME }} ./${{ env.ADOT_WHEEL_NAME }} && python3.12 -m pip install ${{ env.ADOT_WHEEL_NAME }}" >> $GITHUB_ENV
elif [ "${{ env.OTEL_SOURCE }}" == "pypi" ]; then
echo GET_ADOT_WHEEL_COMMAND="python3.12 -m pip install ${{ env.ADOT_WHEEL_NAME }}" >> $GITHUB_ENV
else
latest_release_version=$(curl -sL https://github.com/aws-observability/aws-otel-python-instrumentation/releases/latest | grep -oP '/releases/tag/v\K[0-9]+\.[0-9]+\.[0-9]+' | head -n 1)
echo "The latest version is $latest_release_version"
echo GET_ADOT_WHEEL_COMMAND="wget -O ${{ env.ADOT_WHEEL_NAME }} https://github.com/aws-observability/aws-otel-python-instrumentation/releases/latest/download/aws_opentelemetry_distro-$latest_release_version-py3-none-any.whl \
&& python3.12 -m pip install ${{ env.ADOT_WHEEL_NAME }}" >> $GITHUB_ENV
fi

- name: Initiate Gradlew Daemon
uses: ./.github/workflows/actions/execute_and_retry
continue-on-error: true
with:
command: "./gradlew :validator:build"
cleanup: "./gradlew clean"
max_retry: 3
sleep_time: 60

- name: Generate testing id
run: echo TESTING_ID="${{ github.run_id }}-${{ github.run_number }}-${RANDOM}" >> $GITHUB_ENV

- name: Generate XRay and W3C trace ID
run: |
ID_1="$(printf '%08x' $(date +%s))"
ID_2="$(openssl rand -hex 12)"
W3C_TRACE_ID="${ID_1}${ID_2}"
XRAY_TRACE_ID="1-${ID_1}-${ID_2}"
PARENT_ID="$(openssl rand -hex 8)"
TRACE_ID_HEADER="Root=${XRAY_TRACE_ID};Parent=${PARENT_ID};Sampled=1"
echo "XRAY_TRACE_ID=${XRAY_TRACE_ID}" >> $GITHUB_ENV
echo "W3C_TRACE_ID=${W3C_TRACE_ID}" >> $GITHUB_ENV
echo "TRACE_ID_HEADER=${TRACE_ID_HEADER}" >> $GITHUB_ENV
echo "Generated XRay Trace ID: ${XRAY_TRACE_ID}"
echo "Generated W3C Trace ID: ${W3C_TRACE_ID}"
echo "Generated Trace ID Header: ${TRACE_ID_HEADER}"

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ env.E2E_TEST_ACCOUNT_ID }}:role/${{ env.E2E_TEST_ROLE_NAME }}
aws-region: ${{ env.E2E_TEST_AWS_REGION }}

- name: Set up terraform
uses: ./.github/workflows/actions/execute_and_retry
with:
command: "wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg"
post-command: 'echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
&& sudo apt update && sudo apt install terraform'

- name: Initiate Terraform
uses: ./.github/workflows/actions/execute_and_retry
with:
command: "cd ${{ env.TEST_RESOURCES_FOLDER }}/terraform/python/ec2/adot-genai && terraform init && terraform validate"
cleanup: "rm -rf .terraform && rm -rf .terraform.lock.hcl"
max_retry: 6

- name: Deploy service via terraform
working-directory: terraform/python/ec2/adot-genai
run: |
terraform apply -auto-approve \
-var="aws_region=${{ env.E2E_TEST_AWS_REGION }}" \
-var="test_id=${{ env.TESTING_ID }}" \
-var="service_zip_url=${{ env.SAMPLE_APP_ZIP }}" \
-var="trace_id=${{ env.TRACE_ID_HEADER }}" \
-var="get_adot_wheel_command=${{ env.GET_ADOT_WHEEL_COMMAND }}" \


- name: Get deployment info
working-directory: terraform/python/ec2/adot-genai
run: |
echo "INSTANCE_IP=$(terraform output langchain_service_public_ip)" >> $GITHUB_ENV
echo "INSTANCE_ID=$(terraform output langchain_service_instance_id)" >> $GITHUB_ENV

- name: Waiting 5 Minutes for Gen AI service to be ready and emit logs, traces, and metrics
run: sleep 300

- name: Validate generated logs
run: ./gradlew validator:run --args='-c python/ec2/adot-genai/log-validation.yml
--testing-id ${{ env.TESTING_ID }}
--endpoint http://${{ env.INSTANCE_IP }}:8000
--region ${{ env.E2E_TEST_AWS_REGION }}
--metric-namespace ${{ env.METRIC_NAMESPACE }}
--log-group ${{ env.LOG_GROUP_NAME }}
--service-name langchain-traceloop-app
--instance-id ${{ env.INSTANCE_ID }}
--trace-id ${{ env.W3C_TRACE_ID }}'

- name: Validate generated traces
if: (success() || failure()) && !cancelled()
run: ./gradlew validator:run --args='-c python/ec2/adot-genai/trace-validation.yml
--testing-id ${{ env.TESTING_ID }}
--endpoint http://${{ env.INSTANCE_IP }}:8000
--region ${{ env.E2E_TEST_AWS_REGION }}
--metric-namespace ${{ env.METRIC_NAMESPACE }}
--service-name langchain-traceloop-app
--instance-id ${{ env.INSTANCE_ID }}
--trace-id ${{ env.XRAY_TRACE_ID }}'

- name: Validate generated metrics
if: (success() || failure()) && !cancelled()
run: ./gradlew validator:run --args='-c python/ec2/adot-genai/metric-validation.yml
--testing-id ${{ env.TESTING_ID }}
--endpoint http://${{ env.INSTANCE_IP }}:8000
--region ${{ env.E2E_TEST_AWS_REGION }}
--metric-namespace ${{ env.METRIC_NAMESPACE }}
--log-group ${{ env.LOG_GROUP_NAME }}
--service-name langchain-traceloop-app
--instance-id ${{ env.INSTANCE_ID }}'

- name: Cleanup
if: always()
continue-on-error: true
working-directory: terraform/python/ec2/adot-genai
run: |
terraform destroy -auto-approve \
-var="aws_region=${{ env.E2E_TEST_AWS_REGION }}" \
-var="test_id=${{ env.TESTING_ID }}" \
-var="service_zip_url=${{ env.SAMPLE_APP_ZIP }}" \
-var="trace_id=${{ env.TRACE_ID_HEADER }}" \
-var="get_adot_wheel_command=${{ env.GET_ADOT_WHEEL_COMMAND }}"
15 changes: 15 additions & 0 deletions sample-apps/python/genai_service/ec2-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
langchain
langchain-community
langchain_aws
opentelemetry-sdk
openinference-instrumentation-langchain
opentelemetry-api
opentelemetry-semantic-conventions
python-dotenv
openlit
botocore
setuptools
boto3
aws_opentelemetry_distro
fastapi
uvicorn[standard]
114 changes: 114 additions & 0 deletions sample-apps/python/genai_service/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import os
from typing import Dict, List
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from langchain_aws import ChatBedrock
from langchain.prompts import ChatPromptTemplate
from langchain.chains import LLMChain
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from openinference.instrumentation.langchain import LangChainInstrumentor

# Load environment variables
load_dotenv()

# Set up OpenTelemetry with BOTH exporters
tracer_provider = TracerProvider()

# Add Console exporter
console_exporter = ConsoleSpanExporter()
console_processor = BatchSpanProcessor(console_exporter)
tracer_provider.add_span_processor(console_processor)

# Add OTLP exporter
otlp_exporter = OTLPSpanExporter(endpoint="http://localhost:4318/v1/traces")
otlp_processor = BatchSpanProcessor(otlp_exporter)
tracer_provider.add_span_processor(otlp_processor)

# Set as global provider
trace.set_tracer_provider(tracer_provider)

# Instrument LangChain with OpenInference
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)

# Initialize FastAPI app
app = FastAPI(title="LangChain Bedrock OpenInference API", version="1.0.0")

# Initialize the LLM with AWS Bedrock
llm = ChatBedrock(
model_id="anthropic.claude-3-haiku-20240307-v1:0",
model_kwargs={
"temperature": 0.7,
"max_tokens": 500
},
region_name=os.getenv("AWS_DEFAULT_REGION", "us-west-2")
)

# Create a prompt template
prompt = ChatPromptTemplate.from_template(
"You are a helpful assistant. The user says: {input}. Provide a helpful response."
)

# Create a chain
chain = LLMChain(llm=llm, prompt=prompt)

# Request models
class ChatRequest(BaseModel):
message: str

class BatchChatRequest(BaseModel):
messages: List[str]

class ChatResponse(BaseModel):
response: str

class BatchChatResponse(BaseModel):
responses: List[Dict[str, str]]

# Sample prompts for testing
SAMPLE_PROMPTS = [
"What is the capital of France?",
"How do I make a cup of coffee?",
"What are the benefits of exercise?",
"Explain quantum computing in simple terms",
"What's the best way to learn programming?"
]

@app.get("/")
async def root():
return {
"message": "LangChain Bedrock OpenInference API is running!",
"endpoints": {
"/ai-chat": "Single message chat endpoint",
"/hello": "Simple hello endpoint"
}
}

@app.post("/ai-chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""
Chat endpoint that processes a single user message through AWS Bedrock
"""
try:
# Process the input through the chain
result = await chain.ainvoke({"input": request.message})
return ChatResponse(response=result["text"])
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "healthy", "llm": "AWS Bedrock Claude 3 Haiku"}

if __name__ == "__main__":
import uvicorn
print("Starting FastAPI server with AWS Bedrock and OpenInference instrumentation...")
print("Make sure AWS credentials are configured")
print("Server will run on http://localhost:8000")
print("API docs available at http://localhost:8000/docs")

uvicorn.run(app, host="0.0.0.0", port=8000)
Loading
Loading