Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06de7c7ab0 | |||
| e3c7547c75 | |||
| 314780d59a | |||
| 091787a6da | |||
| 9119474e71 |
32
.env.example
32
.env.example
@@ -10,18 +10,34 @@ JWT_ALGORITHM=HS256
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
|
||||
|
||||
# ── OpenAI ────────────────────────────────────────────────────────────────────
|
||||
OPENAI_API_KEY=sk-...
|
||||
# ── LLM ───────────────────────────────────────────────────────────────────────
|
||||
# LiteLLM model identifiers — change to swap providers without code changes.
|
||||
# Examples: gpt-4o, anthropic/claude-sonnet-4-20250514, gemini/gemini-pro, ollama/llama3
|
||||
OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
GOOGLE_API_KEY=
|
||||
LLM_MODEL=gpt-4o
|
||||
LLM_ROUTER_MODEL=gpt-4o-mini
|
||||
|
||||
# ── Stripe ────────────────────────────────────────────────────────────────────
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||
# ── Stripe (leave empty to stub billing) ──────────────────────────────────────
|
||||
STRIPE_SECRET_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# ── AWS / S3 ──────────────────────────────────────────────────────────────────
|
||||
S3_BUCKET=adiuva-backups
|
||||
S3_BUCKET=adiuva
|
||||
S3_REGION=us-east-1
|
||||
AWS_ACCESS_KEY_ID=AKIA...
|
||||
AWS_SECRET_ACCESS_KEY=...
|
||||
S3_ENDPOINT_URL=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
# For MinIO (homelab): S3_ENDPOINT_URL=http://minio:9000
|
||||
|
||||
# ── Vector Store ──────────────────────────────────────────────────────────────
|
||||
# Pinecone is used when PINECONE_API_KEY is set; otherwise falls back to Qdrant.
|
||||
PINECONE_API_KEY=
|
||||
PINECONE_INDEX=adiuva
|
||||
QDRANT_URL=
|
||||
QDRANT_API_KEY=
|
||||
# For local Qdrant (homelab): QDRANT_URL=http://qdrant:6333
|
||||
|
||||
# ── CORS ──────────────────────────────────────────────────────────────────────
|
||||
# Comma-separated list parsed by Settings (override default if needed)
|
||||
|
||||
@@ -3,10 +3,8 @@ run-name: ${{ gitea.ref_name }} → Docker LXC
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
branches: [main]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
# ── 1. Run tests in an isolated Python container ──────────────────
|
||||
@@ -16,8 +14,15 @@ jobs:
|
||||
image: python:3.12-slim
|
||||
|
||||
steps:
|
||||
- name: Install git
|
||||
run: apt-get update && apt-get install -y --no-install-recommends git
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
run: |
|
||||
git clone --depth 1 --branch "${GITHUB_REF_NAME}" \
|
||||
"http://10.0.0.119:3000/${GITHUB_REPOSITORY}.git" . || \
|
||||
git clone --depth 1 "http://10.0.0.119:3000/${GITHUB_REPOSITORY}.git" . && \
|
||||
git checkout "${GITHUB_SHA}"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pip install --no-cache-dir -r requirements.txt
|
||||
@@ -28,69 +33,61 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: pytest tests/ -v --tb=short
|
||||
|
||||
# ── 2. Deploy to Docker LXC (only main branch & tags) ─────────────
|
||||
# ── 2. Deploy to Docker LXC via SSH ─────────────────────────────────
|
||||
deploy:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.event_name == 'push'
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Sync to deploy directory
|
||||
run: |
|
||||
- name: Deploy via SSH
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
with:
|
||||
host: ${{ secrets.SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
script: |
|
||||
set -e
|
||||
DEPLOY_DIR="/opt/adiuva-api"
|
||||
mkdir -p "$DEPLOY_DIR"
|
||||
REPO_URL="http://10.0.0.119:3000/${{ gitea.repository }}.git"
|
||||
TAG="${{ gitea.ref_name }}"
|
||||
|
||||
# Sync source, preserve .env and volumes
|
||||
cp -rf app/ alembic/ alembic.ini Dockerfile docker-compose.yml requirements.txt "$DEPLOY_DIR/"
|
||||
# ── Pull latest code ──
|
||||
cd /tmp && rm -rf adiuva-api-deploy
|
||||
git clone --depth 1 --branch "${TAG}" "${REPO_URL}" adiuva-api-deploy
|
||||
|
||||
- name: Build & restart services
|
||||
run: |
|
||||
cd /opt/adiuva-api
|
||||
docker compose up -d --build --remove-orphans
|
||||
# ── Sync source (preserve .env) ──
|
||||
cp -rf /tmp/adiuva-api-deploy/app/ \
|
||||
/tmp/adiuva-api-deploy/alembic/ \
|
||||
/tmp/adiuva-api-deploy/alembic.ini \
|
||||
/tmp/adiuva-api-deploy/Dockerfile \
|
||||
/tmp/adiuva-api-deploy/docker-compose.yml \
|
||||
/tmp/adiuva-api-deploy/requirements.txt \
|
||||
"$DEPLOY_DIR/"
|
||||
rm -rf /tmp/adiuva-api-deploy
|
||||
|
||||
- name: Run database migrations
|
||||
run: |
|
||||
cd /opt/adiuva-api
|
||||
# ── Verify .env ──
|
||||
if [ ! -f "$DEPLOY_DIR/.env" ]; then
|
||||
echo "❌ $DEPLOY_DIR/.env not found. Create it before deploying."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Build & restart ──
|
||||
cd "$DEPLOY_DIR"
|
||||
docker compose down --remove-orphans || true
|
||||
docker compose up -d --build
|
||||
|
||||
# ── Migrations ──
|
||||
docker compose exec -T app alembic upgrade head
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
echo "Waiting for app to be ready..."
|
||||
# ── Health check ──
|
||||
echo "Waiting for app..."
|
||||
sleep 5
|
||||
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/v1/health)
|
||||
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health)
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
echo "✅ API is healthy (HTTP ${HTTP_CODE})"
|
||||
else
|
||||
echo "❌ Health check failed (HTTP ${HTTP_CODE})"
|
||||
docker compose -f /opt/adiuva-api/docker-compose.yml logs app --tail=50
|
||||
docker compose logs app --tail=50
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Gitea Release (tags only)
|
||||
if: startsWith(gitea.ref, 'refs/tags/')
|
||||
run: |
|
||||
GITEA_URL="http://10.0.0.119:3000"
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
REPO="${GITHUB_REPOSITORY}"
|
||||
TOKEN="${{ gitea.token }}"
|
||||
|
||||
RELEASE_ID=$(curl -sf \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}" \
|
||||
| grep -o '"id":[0-9]*' | head -1 | cut -d: -f2)
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
curl -sf \
|
||||
-X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"Adiuva API ${TAG}\",\"body\":\"Deployed to Docker LXC\"}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases"
|
||||
echo "✅ Release ${TAG} created"
|
||||
else
|
||||
echo "ℹ️ Release ${TAG} already exists (ID: ${RELEASE_ID})"
|
||||
fi
|
||||
@@ -40,7 +40,7 @@ def upgrade() -> None:
|
||||
sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False),
|
||||
sa.Column("email", sa.String(255), nullable=False),
|
||||
sa.Column("password_hash", sa.String(255), nullable=False),
|
||||
sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier"), nullable=False, server_default="free"),
|
||||
sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier", create_type=False), nullable=False, server_default="free"),
|
||||
sa.Column("stripe_customer_id", sa.String(255), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
@@ -70,7 +70,7 @@ def upgrade() -> None:
|
||||
sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False),
|
||||
sa.Column("user_id", postgresql.UUID(as_uuid=False), nullable=False),
|
||||
sa.Column("stripe_subscription_id", sa.String(255), nullable=True),
|
||||
sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier"), nullable=False, server_default="free"),
|
||||
sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier", create_type=False), nullable=False, server_default="free"),
|
||||
sa.Column("status", sa.String(50), nullable=False, server_default="free"),
|
||||
sa.Column("current_period_end", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
@@ -125,7 +125,7 @@ def upgrade() -> None:
|
||||
sa.Column("category", sa.String(100), nullable=False, server_default=""),
|
||||
sa.Column("price_cents", sa.Integer, nullable=False, server_default="0"),
|
||||
sa.Column("permissions", sa.Text, nullable=False, server_default="[]"),
|
||||
sa.Column("status", sa.Enum("pending_review", "approved", "rejected", name="plugin_status"), nullable=False, server_default="pending_review"),
|
||||
sa.Column("status", sa.Enum("pending_review", "approved", "rejected", name="plugin_status", create_type=False), nullable=False, server_default="pending_review"),
|
||||
sa.Column("s3_package_key", sa.String(500), nullable=True),
|
||||
sa.Column("install_count", sa.Integer, nullable=False, server_default="0"),
|
||||
sa.Column("avg_rating", sa.Float, nullable=False, server_default="0.0"),
|
||||
@@ -157,7 +157,7 @@ def upgrade() -> None:
|
||||
sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False),
|
||||
sa.Column("plugin_id", sa.String(255), nullable=False),
|
||||
sa.Column("reviewer_id", postgresql.UUID(as_uuid=False), nullable=True),
|
||||
sa.Column("decision", sa.Enum("approved", "rejected", name="review_decision"), nullable=False),
|
||||
sa.Column("decision", sa.Enum("approved", "rejected", name="review_decision", create_type=False), nullable=False),
|
||||
sa.Column("notes", sa.Text, nullable=True),
|
||||
sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
|
||||
|
||||
@@ -7,7 +7,6 @@ PostgreSQL ``storage_records`` table.
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -24,6 +24,8 @@ class Settings(BaseSettings):
|
||||
QDRANT_API_KEY: str = ""
|
||||
|
||||
OPENAI_API_KEY: str = ""
|
||||
ANTHROPIC_API_KEY: str = ""
|
||||
GOOGLE_API_KEY: str = ""
|
||||
|
||||
LLM_MODEL: str = "gpt-4o"
|
||||
LLM_ROUTER_MODEL: str = "gpt-4o-mini"
|
||||
|
||||
@@ -26,9 +26,9 @@ from app.config.settings import settings
|
||||
def _api_key_for_model(model: str) -> str | None:
|
||||
"""Return the most appropriate API key for the given LiteLLM model string."""
|
||||
if model.startswith("anthropic/"):
|
||||
return getattr(settings, "ANTHROPIC_API_KEY", None) or None
|
||||
return settings.ANTHROPIC_API_KEY or None
|
||||
if model.startswith("gemini/") or model.startswith("google/"):
|
||||
return getattr(settings, "GOOGLE_API_KEY", None) or None
|
||||
return settings.GOOGLE_API_KEY or None
|
||||
# Default: OpenAI-compatible (covers plain model names like "gpt-4o")
|
||||
return settings.OPENAI_API_KEY or None
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@ from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import (
|
||||
BigInteger,
|
||||
Boolean,
|
||||
DateTime,
|
||||
Enum,
|
||||
Float,
|
||||
|
||||
@@ -9,7 +9,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
from app.config.settings import settings
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "8080:8000"
|
||||
env_file:
|
||||
- .env
|
||||
- path: .env
|
||||
required: false
|
||||
environment:
|
||||
DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/adiuva
|
||||
depends_on:
|
||||
|
||||
@@ -6,7 +6,6 @@ a per-test session, and a FastAPI ``TestClient`` wired to use it.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
@@ -8,11 +8,10 @@ from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from jose import jwt
|
||||
|
||||
from app.config.settings import settings
|
||||
from tests.conftest import auth_header, make_jwt, TEST_USER_IDS
|
||||
from tests.conftest import auth_header, TEST_USER_IDS
|
||||
|
||||
|
||||
# ── TestRegister ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -8,7 +8,6 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import auth_header, TEST_USER_IDS
|
||||
|
||||
@@ -168,7 +167,7 @@ class TestDeleteBackup:
|
||||
def _get_backup_id(self, client, tier="power") -> str:
|
||||
"""Upload a backup and return its DB id from history."""
|
||||
_upload(client, tier=tier)
|
||||
history = client.get(
|
||||
client.get(
|
||||
"/api/v1/backup/history", headers=auth_header(tier)
|
||||
).json()
|
||||
# History returns BackupMetadata schema which doesn't have `id`.
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.core.orchestrator import (
|
||||
route_pipeline,
|
||||
route_single,
|
||||
)
|
||||
from app.schemas import ChatContext, ChatRequest, ChatResponse, ExecutionPlan
|
||||
from app.schemas import ChatRequest, ChatResponse, ExecutionPlan
|
||||
|
||||
|
||||
# ── Stub agents ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -9,11 +9,9 @@ Covers:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
Reference in New Issue
Block a user