Skip to content

Commit a0895c6

Browse files
author
Subho Mukherjee
committed
feat: Add demo.py, docker-compose.yml, and GitHub Actions CI for adapters
Added: 1. demo.py - Interactive demo script showing: - Simple auto-routing - Parallel ensemble (Groq + OpenAI + DeepSeek) - Code generation - Complex reasoning - Health check 2. docker-compose.yml - Instant deployment with: - A3M Router server - Optional Redis for distributed cache - Optional Prometheus + Grafana for monitoring 3. prometheus.yml - Metrics scraping config 4. GitHub Actions CI (adapters-ci.yml): - Tests on Python 3.9, 3.10, 3.11, 3.12 - Linting (black, isort, flake8) - Unit tests - Integration tests (requires server) - PyPI publish on tag - Docker build and push 5. Integration tests (test_integration.py) 6. Requirements files
1 parent 75eccbe commit a0895c6

7 files changed

Lines changed: 575 additions & 60 deletions

File tree

.github/workflows/adapters-ci.yml

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
name: Adapters CI
2+
3+
on:
4+
push:
5+
branches: [main, feat/*]
6+
paths:
7+
- 'adapters/**'
8+
- '.github/workflows/adapters-ci.yml'
9+
pull_request:
10+
branches: [main]
11+
paths:
12+
- 'adapters/**'
13+
- '.github/workflows/adapters-ci.yml'
14+
15+
jobs:
16+
test-adapters:
17+
runs-on: ubuntu-latest
18+
19+
strategy:
20+
matrix:
21+
python-version: ['3.9', '3.10', '3.11', '3.12']
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Set up Python ${{ matrix.python-version }}
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: ${{ matrix.python-version }}
30+
31+
- name: Cache pip packages
32+
uses: actions/cache@v4
33+
with:
34+
path: ~/.cache/pip
35+
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('adapters/**/requirements*.txt') }}
36+
37+
- name: Install dependencies
38+
run: |
39+
cd adapters
40+
pip install -e .
41+
pip install pytest pytest-asyncio black isort flake8
42+
43+
- name: Lint with black
44+
run: |
45+
cd adapters
46+
black --check a3m_adapter/ --exclude='/(\.git|\.venv|__pycache__)/'
47+
48+
- name: Lint with isort
49+
run: |
50+
cd adapters
51+
isort --check-only a3m_adapter/ --exclude='/(\.git|\.venv|__pycache__)/'
52+
53+
- name: Lint with flake8
54+
run: |
55+
cd adapters
56+
flake8 a3m_adapter/ --max-line-length=100 --exclude='/(\.git|\.venv|__pycache__)/'
57+
58+
- name: Run tests
59+
run: |
60+
cd adapters
61+
pytest a3m_adapter/tests/ -v --tb=short
62+
63+
test-integration:
64+
runs-on: ubuntu-latest
65+
needs: test-adapters
66+
67+
steps:
68+
- uses: actions/checkout@v4
69+
70+
- name: Set up Node.js
71+
uses: actions/setup-node@v4
72+
with:
73+
node-version: '20'
74+
75+
- name: Start A3M Router
76+
run: |
77+
npx a3m-router serve &
78+
sleep 5
79+
curl -f http://localhost:8787/health || exit 1
80+
81+
- name: Set up Python
82+
uses: actions/setup-python@v5
83+
with:
84+
python-version: '3.11'
85+
86+
- name: Install adapter and test deps
87+
run: |
88+
cd adapters
89+
pip install -e .
90+
pip install requests pytest pytest-asyncio
91+
92+
- name: Run integration tests
93+
run: |
94+
cd adapters
95+
pytest a3m_adapter/tests/ -v --tb=short -k "integration"
96+
97+
publish-adapters:
98+
runs-on: ubuntu-latest
99+
needs: test-integration
100+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
101+
102+
steps:
103+
- uses: actions/checkout@v4
104+
105+
- name: Set up Python
106+
uses: actions/setup-python@v5
107+
with:
108+
python-version: '3.11'
109+
110+
- name: Publish to PyPI
111+
env:
112+
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
113+
run: |
114+
cd adapters
115+
pip install build twine
116+
python -m build
117+
twine upload --token $PYPI_TOKEN dist/*
118+
119+
docker-build:
120+
runs-on: ubuntu-latest
121+
needs: test-integration
122+
if: github.event_name == 'push'
123+
124+
steps:
125+
- uses: actions/checkout@v4
126+
127+
- name: Build Docker image
128+
run: |
129+
docker build -t ghcr.io/das-rebel/a3m-router:latest .
130+
131+
- name: Run container health check
132+
run: |
133+
docker run -d --name a3m-test -p 8787:8787 ghcr.io/das-rebel/a3m-router:latest
134+
sleep 5
135+
curl -f http://localhost:8787/health
136+
docker stop a3m-test
137+
138+
- name: Push to GHCR
139+
if: github.event_name == 'push'
140+
run: |
141+
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
142+
docker push ghcr.io/das-rebel/a3m-router:latest
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Integration tests for A3M Router adapters.
3+
Requires A3M Router server running on localhost:8787
4+
"""
5+
6+
import pytest
7+
import os
8+
9+
10+
@pytest.fixture
11+
def a3m_server_url():
12+
"""Get A3M Router server URL."""
13+
return os.environ.get("A3M_SERVER_URL", "http://localhost:8787")
14+
15+
16+
@pytest.fixture
17+
def skip_if_no_server():
18+
"""Skip test if server is not available."""
19+
import requests
20+
try:
21+
resp = requests.get("http://localhost:8787/health", timeout=2)
22+
if resp.status_code != 200:
23+
pytest.skip("A3M Router server not running")
24+
except:
25+
pytest.skip("A3M Router server not running")
26+
27+
28+
@pytest.mark.integration
29+
def test_simple_chat_completion(a3m_server_url, skip_if_no_server):
30+
"""Test simple chat completion via HTTP API."""
31+
import requests
32+
33+
response = requests.post(
34+
f"{a3m_server_url}/v1/chat/completions",
35+
json={
36+
"model": "auto",
37+
"messages": [{"role": "user", "content": "What is 2+2?"}]
38+
},
39+
timeout=30
40+
)
41+
42+
assert response.status_code == 200
43+
data = response.json()
44+
assert "choices" in data
45+
assert len(data["choices"]) > 0
46+
assert "message" in data["choices"][0]
47+
assert data["choices"][0]["message"]["content"]
48+
49+
50+
@pytest.mark.integration
51+
def test_parallel_ensemble(a3m_server_url, skip_if_no_server):
52+
"""Test parallel ensemble with multiple providers."""
53+
import requests
54+
55+
response = requests.post(
56+
f"{a3m_server_url}/v1/chat/completions",
57+
json={
58+
"model": "auto",
59+
"messages": [{"role": "user", "content": "Explain gravity"}],
60+
"parallel_ensemble": 3,
61+
},
62+
timeout=60
63+
)
64+
65+
assert response.status_code == 200
66+
data = response.json()
67+
assert "choices" in data
68+
assert "provider" in data
69+
70+
71+
@pytest.mark.integration
72+
def test_health_endpoint(a3m_server_url, skip_if_no_server):
73+
"""Test health endpoint."""
74+
import requests
75+
76+
response = requests.get(f"{a3m_server_url}/health", timeout=10)
77+
78+
assert response.status_code == 200
79+
data = response.json()
80+
assert "providers" in data or "status" in data

adapters/requirements-dev.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
-r requirements.txt
2+
black>=21.12
3+
isort>=5.10.1
4+
flake8>=3.9.0
5+
build>=0.10.0
6+
twine>=4.0.0

adapters/requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
requests>=2.25.1
2+
pydantic>=1.9.0
3+
pytest>=6.0
4+
pytest-asyncio>=0.21.0

0 commit comments

Comments
 (0)