Skip to content

Commit acd84e8

Browse files
committed
feat: integrate MLflow and DVC for experiment tracking and model versioning
1 parent 391faf7 commit acd84e8

File tree

5 files changed

+115
-210
lines changed

5 files changed

+115
-210
lines changed

.github/workflows/ci-cd.yml

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI/CD
1+
name: CI/CD with DVC and MLflow
22

33
on:
44
push:
@@ -7,39 +7,57 @@ on:
77
branches: [mlops-concepts, mlops-market-tools]
88

99
jobs:
10-
build-train:
10+
build-train-log:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- name: Checkout
1414
uses: actions/checkout@v4
1515

16+
- name: Setup Python
17+
uses: actions/setup-python@v4
18+
with:
19+
python-version: '3.9'
20+
1621
- name: Setup Bun
1722
uses: oven-sh/setup-bun@v1
1823
with:
1924
bun-version: latest
2025

21-
- name: Install dependencies (workspaces)
22-
run: bun install
26+
- name: Install Python Dependencies
27+
run: pip install "dvc[s3]" mlflow
2328

24-
- name: Generate DB migrations
25-
run: bun --cwd inference run db:generate
26-
27-
- name: Run DB migrations
28-
run: bun --cwd inference run db:migrate
29+
- name: Install JS Dependencies
30+
run: bun install
2931

30-
- name: Train model
32+
- name: Configure DVC and S3 Credentials
33+
env:
34+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
35+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
36+
run: |
37+
# Comente/descomente e configure conforme seu provedor S3
38+
# dvc remote modify my-remote endpointurl s3.amazonaws.com
39+
echo "DVC remote configured."
40+
41+
- name: Pull Data from DVC
42+
run: dvc pull -r my-remote
43+
44+
- name: Train model and Log to MLflow
45+
env:
46+
# Segredos para o MLflow se conectar ao seu servidor na Fly.io
47+
MLFLOW_TRACKING_URI: ${{ secrets.MLFLOW_TRACKING_URI }}
48+
# Se seu servidor MLflow precisar de autenticação, configure aqui
49+
# MLFLOW_TRACKING_USERNAME: ${{ secrets.MLFLOW_USERNAME }}
50+
# MLFLOW_TRACKING_PASSWORD: ${{ secrets.MLFLOW_PASSWORD }}
3151
run: bun --cwd training run train
3252

33-
- name: Build web
53+
- name: Push Artifacts to DVC
54+
run: dvc push -r my-remote
55+
56+
- name: Build Dashboard
3457
run: bun --cwd dashboard run build
3558

36-
- name: Upload web artifact
59+
- name: Upload Dashboard Artifact
3760
uses: actions/upload-artifact@v4
3861
with:
3962
name: dashboard-dist
4063
path: dashboard/dist
41-
42-
- name: Deploy (placeholder)
43-
if: ${{ secrets.FLY_API_TOKEN != '' }}
44-
run: |
45-
echo "FLY_API_TOKEN detected. Add deployment steps here (e.g., Fly.io)."
Lines changed: 21 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,27 @@
1-
import React, { useEffect, useMemo, useState } from 'react';
2-
import { Card, Group, Loader, Table, Text, Title, Badge } from '@mantine/core';
3-
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
1+
import React from 'react';
2+
import { Card, Title, Text, Button, Center } from '@mantine/core';
43

54
export default function Dashboard() {
6-
const [runs, setRuns] = useState([]);
7-
const [loading, setLoading] = useState(true);
8-
const [error, setError] = useState('');
9-
10-
useEffect(() => {
11-
async function load() {
12-
try {
13-
const res = await fetch('http://localhost:3001/dashboard');
14-
const data = await res.json();
15-
setRuns(Array.isArray(data) ? data : []);
16-
} catch (e) {
17-
setError(e.message || 'Erro ao carregar');
18-
} finally {
19-
setLoading(false);
20-
}
21-
}
22-
load();
23-
}, []);
24-
25-
const chartData = useMemo(() => {
26-
const ordered = [...runs].reverse();
27-
return ordered.map((r, idx) => ({
28-
idx,
29-
accuracy: (r.metrics && r.metrics.accuracy) || 0,
30-
createdAt: r.createdAt ? new Date(r.createdAt).toLocaleString() : ''
31-
}));
32-
}, [runs]);
5+
// IMPORTANTE: Substitua pela URL do seu app MLflow na Fly.io
6+
const mlflowUiUrl = "https://spamguard-mlflow.fly.dev";
337

348
return (
35-
<>
36-
<Title order={4} mb="sm">Evolução de Métricas</Title>
37-
<Card withBorder mb="lg" style={{ width: '100%', height: 280 }}>
38-
{loading ? (
39-
<Group justify="center" align="center" style={{ height: '100%' }}>
40-
<Loader />
41-
</Group>
42-
) : error ? (
43-
<Text c="red">{error}</Text>
44-
) : (
45-
<ResponsiveContainer>
46-
<LineChart data={chartData}>
47-
<CartesianGrid strokeDasharray="3 3" />
48-
<XAxis dataKey="createdAt" interval={0} angle={-15} textAnchor="end" height={60} />
49-
<YAxis domain={[0, 1]} />
50-
<Tooltip formatter={(v) => (v*100).toFixed(1) + '%'} />
51-
<Line type="monotone" dataKey="accuracy" stroke="#10b981" strokeWidth={2} dot={{ r: 3 }} />
52-
</LineChart>
53-
</ResponsiveContainer>
54-
)}
55-
</Card>
56-
57-
<Title order={4} mb="sm">Execuções</Title>
58-
<Card withBorder>
59-
{loading ? (
60-
<Group justify="center" align="center"><Loader /></Group>
61-
) : (
62-
<Table highlightOnHover withTableBorder withColumnBorders>
63-
<Table.Thead>
64-
<Table.Tr>
65-
<Table.Th>ID</Table.Th>
66-
<Table.Th>Commit</Table.Th>
67-
<Table.Th>Acurácia</Table.Th>
68-
<Table.Th>F1</Table.Th>
69-
<Table.Th>Criado em</Table.Th>
70-
<Table.Th>Produção</Table.Th>
71-
</Table.Tr>
72-
</Table.Thead>
73-
<Table.Tbody>
74-
{runs.map((r) => (
75-
<Table.Tr key={r.id}>
76-
<Table.Td>{r.id}</Table.Td>
77-
<Table.Td><code>{r.gitCommit || '-'}</code></Table.Td>
78-
<Table.Td>{r.metrics ? (r.metrics.accuracy*100).toFixed(1)+'%' : '-'}</Table.Td>
79-
<Table.Td>{r.metrics ? (r.metrics.f1Score*100).toFixed(1)+'%' : '-'}</Table.Td>
80-
<Table.Td>{r.createdAt ? new Date(r.createdAt).toLocaleString() : '-'}</Table.Td>
81-
<Table.Td>
82-
{r.isProduction ? <Badge color="green">Sim</Badge> : <Badge variant="light">Não</Badge>}
83-
</Table.Td>
84-
</Table.Tr>
85-
))}
86-
</Table.Tbody>
87-
</Table>
88-
)}
89-
</Card>
90-
</>
9+
<Card withBorder>
10+
<Center style={{ flexDirection: 'column', textAlign: 'center', padding: '2rem' }}>
11+
<Title order={3}>MLOps Dashboard</Title>
12+
<Text c="dimmed" mt="md" maw={600}>
13+
O rastreamento de experimentos foi atualizado para MLflow, a ferramenta padrão da indústria. A UI abaixo foi substituída por um dashboard profissional e interativo hospedado no nosso próprio servidor MLflow.
14+
</Text>
15+
<Button
16+
component="a"
17+
href={mlflowUiUrl}
18+
target="_blank"
19+
mt="xl"
20+
size="md"
21+
>
22+
Abrir Dashboard MLflow
23+
</Button>
24+
</Center>
25+
</Card>
9126
);
9227
}

inference/src/index.js

Lines changed: 12 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,28 @@
11
import { Elysia, t } from 'elysia';
22
import { cors } from '@elysiajs/cors';
3-
import { Database } from 'bun:sqlite';
4-
import { drizzle } from 'drizzle-orm/bun-sqlite';
5-
import * as schema from './db/schema.js';
6-
import { eq } from 'drizzle-orm';
73
import natural from 'natural';
84

95
const { BayesClassifier } = natural;
10-
11-
// Resolve DB path relative to this file (independent of CWD)
12-
const dbPath = new URL('../main.db', import.meta.url).pathname;
13-
const sqlite = new Database(dbPath);
14-
const db = drizzle(sqlite, { schema });
15-
166
let classifier = null;
177

18-
async function loadProductionModel() {
19-
const prodRun = await db.query.runs.findFirst({ where: eq(schema.runs.isProduction, true) });
20-
if (prodRun && prodRun.modelArtifactPath) {
21-
// Resolve artifact path. If it's relative (e.g., 'artifacts/...'), resolve from the repo root.
22-
const artifactPath = prodRun.modelArtifactPath.startsWith('/')
23-
? prodRun.modelArtifactPath
24-
: new URL(`../../${prodRun.modelArtifactPath}`, import.meta.url).pathname;
25-
console.log(`Loading model: ${artifactPath}`);
26-
const modelJson = await Bun.file(artifactPath).text();
27-
classifier = BayesClassifier.restore(JSON.parse(modelJson));
28-
} else {
29-
console.log("No production model found.");
30-
}
31-
}
32-
33-
function parseMetrics(m) {
8+
// NOTE: In production you'd pull the model from MLflow Model Registry.
9+
// For this template, try to load a local demo artifact if present.
10+
async function loadDemoModel() {
3411
try {
35-
if (!m) return null;
36-
return typeof m === 'string' ? JSON.parse(m) : m;
37-
} catch {
38-
return null;
12+
const artifactPath = new URL(`../../artifacts/model_latest.json`, import.meta.url).pathname;
13+
console.log(`Loading demo model: ${artifactPath}`);
14+
const modelJson = await Bun.file(artifactPath).text();
15+
classifier = BayesClassifier.restore(JSON.parse(modelJson));
16+
} catch (e) {
17+
console.error('Could not load a demo model. Please run training to produce an artifact.', e.message);
3918
}
4019
}
4120

21+
4222
const app = new Elysia()
4323
.use(cors())
44-
.get('/dashboard', async () => {
45-
const rows = await db.query.runs.findMany({
46-
orderBy: (runs, { desc }) => [desc(runs.createdAt)],
47-
});
48-
return rows.map((r) => ({ ...r, metrics: parseMetrics(r.metrics) }));
49-
})
5024
.post('/predict', async ({ body }) => {
51-
if (!classifier) {
52-
await loadProductionModel();
53-
if(!classifier) return { error: 'Model is not loaded' };
54-
}
25+
if (!classifier) return { error: 'Model is not loaded on the server. The MLOps pipeline is the focus.' };
5526
const prediction = classifier.getClassifications(body.message);
5627
return { prediction };
5728
}, {
@@ -61,4 +32,4 @@ const app = new Elysia()
6132
.listen(3001);
6233

6334
console.log(`API running at http://${app.server?.hostname}:${app.server?.port}`);
64-
loadProductionModel();
35+
loadDemoModel();

training/src/log_mlflow.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import mlflow
2+
import sys
3+
import json
4+
5+
# Argumentos: 1=metricas_json, 2=caminho_artefatos_dir
6+
metrics_json = sys.argv[1]
7+
artifacts_dir = sys.argv[2]
8+
9+
metrics = json.loads(metrics_json)
10+
11+
# Inicia uma nova execução no MLflow
12+
with mlflow.start_run():
13+
print("MLflow: Logging metrics...")
14+
mlflow.log_metrics(metrics)
15+
print("MLflow: Logging model artifacts...")
16+
mlflow.log_artifacts(artifacts_dir, artifact_path="model")
17+
print("MLflow: Logged successfully.")

0 commit comments

Comments
 (0)