5 Commits

Author SHA1 Message Date
06de7c7ab0 feat: deploy via SSH with port 8080, idempotent migrations
All checks were successful
Test & Deploy API / test (push) Successful in 1m39s
Test & Deploy API / deploy (push) Successful in 16s
2026-03-03 22:10:03 +01:00
e3c7547c75 Remove unused imports across multiple files to clean up the codebase
Some checks failed
Test & Deploy API / test (push) Successful in 1m39s
Test & Deploy API / deploy (push) Failing after 2s
2026-03-03 17:21:40 +01:00
314780d59a Add LLM configuration options and update deployment workflow
Some checks failed
Test & Deploy API / test (push) Failing after 1m14s
Test & Deploy API / deploy (push) Has been skipped
- Introduced new API keys for Anthropic and Google in .env.example and settings.py
- Updated llm.py to retrieve API keys directly from settings
- Modified deploy.yaml to streamline code checkout and improve deployment process
2026-03-03 16:52:56 +01:00
091787a6da Merge branch 'develop'
Some checks failed
Test & Deploy API / test (push) Failing after 5s
Test & Deploy API / deploy (push) Has been skipped
2026-03-03 16:09:31 +01:00
9119474e71 Update docker-compose.yml
Some checks failed
Deploy to Proxmox Docker / Deploy (push) Failing after 1m52s
2026-03-02 16:51:19 +00:00
14 changed files with 94 additions and 88 deletions

View File

@@ -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)

View File

@@ -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: 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"
REPO_URL="http://10.0.0.119:3000/${{ gitea.repository }}.git"
TAG="${{ gitea.ref_name }}"
- name: Sync to deploy directory
run: |
DEPLOY_DIR="/opt/adiuva-api"
mkdir -p "$DEPLOY_DIR"
# ── Pull latest code ──
cd /tmp && rm -rf adiuva-api-deploy
git clone --depth 1 --branch "${TAG}" "${REPO_URL}" adiuva-api-deploy
# Sync source, preserve .env and volumes
cp -rf app/ alembic/ alembic.ini Dockerfile docker-compose.yml requirements.txt "$DEPLOY_DIR/"
# ── 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: Build & restart services
run: |
cd /opt/adiuva-api
docker compose up -d --build --remove-orphans
# ── Verify .env ──
if [ ! -f "$DEPLOY_DIR/.env" ]; then
echo "❌ $DEPLOY_DIR/.env not found. Create it before deploying."
exit 1
fi
- name: Run database migrations
run: |
cd /opt/adiuva-api
docker compose exec -T app alembic upgrade head
# ── Build & restart ──
cd "$DEPLOY_DIR"
docker compose down --remove-orphans || true
docker compose up -d --build
- name: Verify deployment
run: |
echo "Waiting for app to be ready..."
sleep 5
# ── Migrations ──
docker compose exec -T app alembic upgrade head
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/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
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
# ── Health check ──
echo "Waiting for app..."
sleep 5
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 logs app --tail=50
exit 1
fi

View File

@@ -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()")),

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -23,7 +23,6 @@ from datetime import datetime, timezone
from sqlalchemy import (
BigInteger,
Boolean,
DateTime,
Enum,
Float,

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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 ──────────────────────────────────────────────────────

View File

@@ -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`.

View File

@@ -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 ──────────────────────────────────────────────────────

View File

@@ -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