Skip to content

[WIP] [Genesis] Add Python Release Test #424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
191 changes: 191 additions & 0 deletions .github/workflows/python-ec2-genai-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
## Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
## SPDX-License-Identifier: Apache-2.0

name: Python EC2 LangChain Service Deployment
on:
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 }}
E2E_TEST_ACCOUNT_ID: 571600841604
ADOT_WHEEL_NAME: ${{ inputs.staging-wheel-name }}
E2E_TEST_ROLE_NAME: github
METRIC_NAMESPACE: genesis
LOG_GROUP_NAME: test/genesis
TEST_RESOURCES_FOLDER: ${GITHUB_WORKSPACE}
SAMPLE_APP_ZIP: s3://sigv4perfresults/langchain-service.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 }} && sudo python${{ env.PYTHON_VERSION }} -m pip install ${{ env.ADOT_WHEEL_NAME }}" >> $GITHUB_ENV
# elif [ "${{ env.OTEL_SOURCE }}" == "pypi" ]; then
# echo GET_ADOT_WHEEL_COMMAND="sudo python${{ env.PYTHON_VERSION }} -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 \
# && sudo python${{ env.PYTHON_VERSION }} -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}"
echo "XRAY_TRACE_ID=${XRAY_TRACE_ID}" >> $GITHUB_ENV
echo "W3C_TRACE_ID=${W3C_TRACE_ID}" >> $GITHUB_ENV
echo "Generated XRay Trace ID: ${XRAY_TRACE_ID}"
echo "Generated W3C Trace ID: ${W3C_TRACE_ID}"

- 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 }}"

- 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: Wait for Gen AI Chatbot service to be ready
run: |
echo "Waiting for service to be ready at http://${{ env.INSTANCE_IP }}:8000"
for i in {1..60}; do
if curl -f -s "http://${{ env.INSTANCE_IP }}:8000/health" > /dev/null 2>&1; then
echo "Service is ready!"
break
fi
echo "Attempt $i: Service not ready yet, waiting 10 seconds..."
sleep 10
done

# Final check
if ! curl -f -s "http://${{ env.INSTANCE_IP }}:8000/health" > /dev/null 2>&1; then
echo "Service failed to become ready after 10 minutes"
exit 1
fi

- name: Generate traffic
run: |
cd sample-apps/traffic-generator/genai
chmod +x generate_traffic.sh
export SERVER_URL="http://${{ env.INSTANCE_IP }}:8000"
export NUM_REQUESTS="5"
export DELAY_SECONDS="5"
export TIMEOUT="30"
export TRACE_ID="Root=${XRAY_TRACE_ID};Parent=$(openssl rand -hex 8);Sampled=1"
./generate_traffic.sh

- 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: Wait for metrics to be published
if: (success() || failure()) && !cancelled()
run: |
echo "Waiting 60 seconds to ensure EMF metrics are published to CloudWatch"
sleep 60

- 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 }}"
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_genai_beta
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