ayni_sdk 0.2.0
ayni_sdk: ^0.2.0 copied to clipboard
Offline-first SDK primitives for Ayni Flutter integrations on Android and iOS. Minimums: Android API 26+, iOS 11+.
ayni_sdk #
Offline-first SDK primitives for Ayni Flutter integrations.
El paquete ayni_sdk permite integrar modelos de IA en aplicaciones Flutter con arquitectura offline-first. Los workflows versionados se sincronizan desde la plataforma y se ejecutan localmente en el dispositivo sin conexión obligatoria a internet ni dependencias en tiempo de compilación con el dashboard.
Instalación #
Usa Flutter 3.32.0 o posterior. Android 8.0 (API 26) o posterior es obligatorio;
configura minSdkVersion 26 en la app Flutter.
Agrega ayni_sdk a las dependencias de tu proyecto Flutter en pubspec.yaml:
dependencies:
ayni_sdk: ^0.2.0
Resuelve las dependencias desde la carpeta de tu aplicación:
flutter pub get
Si pub.dev no puede resolver la versión solicitada, corrige la restricción antes
de compilar: No se pudo resolver la versión solicitada de ayni_sdk. La app
de ejemplo está en example/app: solicita una credencial de prueba, inicializa
el SDK, sincroniza un workflow publicado y ejecuta la imagen seleccionada. Sus
instrucciones están en el
README del ejemplo
y usa una dependencia local path: ../.. para verificar este checkout antes
de publicar. Una aplicación consumidora debe resolver ^0.2.0 desde pub.dev.
Consulta las notas de cada versión en el changelog del repositorio.
API Pública #
Importa la librería pública oficial:
import 'package:ayni_sdk/ayni_sdk.dart';
Nota: No importes archivos src internos. La interfaz pública se expone a través de
package:ayni_sdk/ayni_sdk.dart.
La API pública estable que consume tu aplicación se compone de AyniSdk.initialize para configurar el SDK, sync() para actualizar workflows y modelos y enviar trazas técnicas pendientes cuando la política vigente lo permite, run() para ejecutar localmente, SyncResult para consultar la sincronización y WorkflowResult y WorkflowError para la ejecución. Las clases internas de lib/src/ no forman parte del contrato de integración y pueden cambiar sin previo aviso.
Configuración genérica e inicialización #
El paquete mantiene una configuración completamente genérica e independiente. No contiene secretos, tokens ni configuración de una aplicación concreta.
El SDK recibe su configuración dinámicamente en tiempo de ejecución mediante AyniSdk.initialize(AyniConfig(...)) (US-049). La aplicación host obtiene la credencial y la configuración desde almacenamiento seguro o su entorno de ejecución en runtime, manteniendo el paquete desacoplado y libre de secretos hardcodeados:
final result = AyniSdk.initialize(
AyniConfig(
serverUrl: Uri.parse('https://tu-servidor-ayni.example'),
credential: credential,
storageDirectory: storageDirectory,
),
);
if (!result.isReady) {
showMessage(result.message);
return null;
}
return AyniSdk.instance;
El ejemplo completo, que dart analyze comprueba, está en example/reference/initialize.dart.
Una vez completada la inicialización de forma exitosa (result.isReady), se accede a la instancia compartida a través de AyniSdk.instance. Revisa result.status si necesitas distinguir ready, incompleteConfiguration, error o unsupportedPlatform; en cualquier estado distinto de ready, muestra result.message y no uses la instancia. Si la configuración es incompleta o inválida, el SDK no queda en un estado parcialmente operativo.
Perfil técnico del dispositivo #
La app puede consultar el perfil técnico disponible para describir el dispositivo en sus propios reportes de prueba o en una traza:
final DeviceProfile profile = await sdk.getDeviceProfile();
final Map<String, Object> fields = profile.toJson();
El perfil versionado solo incluye plataforma, SO/API, modelo, rango de RAM y
SoC cuando la plataforma los expone. No incluye el nombre asignado al
dispositivo, seriales, identificadores de proveedor ni otros datos del plugin.
La respuesta de getDeviceProfile() no se persiste ni se envía; si esos campos
se incluyen en una traza habilitada de run(), quedan guardados dentro de la
outbox local y no se transmiten. Los campos ausentes se omiten. La consulta se
resuelve localmente y queda cacheada en esa instancia.
Sincronización #
Después de inicializar, sincroniza para instalar nuevas versiones publicadas. SyncResult.status indica el resultado general: updated, upToDate, offline o error. Ante offline, conserva y usa los workflows ya instalados. Ante error, no des por descartados todos los cambios: puede haber recursos actualizados junto con recursos fallidos. Recorre result.resources y maneja el status de cada recurso; la lista puede estar vacía si el fallo ocurrió antes de compararlos con el servidor.
final result = await sdk.sync();
switch (result.status) {
case SyncStatus.offline:
showMessage('Sin conexión. Se usarán los workflows instalados.');
case SyncStatus.error:
showMessage(
result.resources.isNotEmpty
? 'La sincronización terminó con errores. Revisa cada recurso.'
: 'La sincronización terminó con errores.',
);
case SyncStatus.updated:
case SyncStatus.upToDate:
break;
}
for (final resource in result.resources) {
switch (resource.status) {
case SyncResourceStatus.updated:
showMessage('Recurso actualizado.');
case SyncResourceStatus.upToDate:
showMessage('El recurso ya está actualizado.');
case SyncResourceStatus.invalidRemoteResource:
case SyncResourceStatus.invalidWorkflow:
case SyncResourceStatus.unsupportedWorkflowVersion:
case SyncResourceStatus.installationFailed:
case SyncResourceStatus.dependencyFailed:
case SyncResourceStatus.workflowUnavailable:
showMessage(resource.message ?? 'No se pudo actualizar un recurso.');
}
}
result.resources contiene los resultados por recurso cuando la sincronización alcanza la comparación con el servidor. El ejemplo completo, que dart analyze comprueba, está en example/reference/sync.dart.
Trazas técnicas locales y envío #
Cada sync() intenta actualizar la política de captura técnica de la
aplicación. Una respuesta válida se guarda en
storageDirectory/diagnostics/telemetry-policy.json; una falla mantiene la
última política válida. Sin una política válida habilitada, el SDK no crea
trazas. La política de telemetría y el consentimiento opcional sdkImprovement
son controles separados: la app debe mantener su switch apagado por defecto y
pasar traceContext solo cuando la política y el permiso o consentimiento que
aplique al uso de esa traza permitan capturar datos. sdkImprovement no autoriza
usar trazas para validar modelos.
Cuando está habilitada la captura, run() adjunta una traza tipada a
WorkflowResult.trace o WorkflowError.trace:
final result = await sdk.run(
workflowId,
imageBytes,
traceContext: WorkflowTraceContext(
runId: experimentRunId,
repetition: repetition,
condition: 'tratamiento',
caseId: 'caso-01',
measurements: [
TraceMeasurement(
name: 'latency',
value: elapsedMs,
unit: 'ms',
method: 'stopwatch',
source: 'host-app',
phase: 'workflow',
),
],
),
);
final Map<String, Object?>? trace = result.trace?.toJson();
runId y repetition los declara la app. Las mediciones y el contexto que
aporta la app quedan marcados como clientReported; el SDK no inventa datos
experimentales. La traza solo incluye resultados públicos ya decodificados
(clasificaciones, detecciones y booleanos), metadatos de versiones, perfil
allowlisted y errores tipados sanitizados. Nunca serializa bytes de imagen ni
tensores arbitrarios. run() guarda la traza, antes de devolver el resultado o
lanzar el error, en una outbox local durable dentro de
storageDirectory/diagnostics/trace-outbox/. Cada traceId queda pendiente una
sola vez y sobrevive a reinicios. En sync(), el SDK consulta la política vigente
antes de cada POST /sdk/traces y elimina la traza local solo tras validar el
acuse del servidor. Si la política no se obtiene, está deshabilitada, la red
falla o el servidor rechaza el contenido, queda pendiente para otro intento. Si
la escritura falla, la ejecución conserva su resultado o error original y
WorkflowResult.tracePersistenceFailed o
WorkflowError.tracePersistenceFailed indica que la evidencia no quedó
encolada; la traza sigue disponible en trace mientras vive el resultado o
error. Deshabilitar la política impide crear nuevas trazas, pero conserva las
pendientes. retentionDays no vence los archivos locales de la outbox; la app
puede descartarlos al borrar storageDirectory. En el servidor, cada traza vence
según retentionDays (7, 30 o 90 días); los datos reportados por el cliente se
guardan como clientReported y no se verifican de forma independiente. El
servidor acepta cuerpos de hasta 2 MiB. sync() procesa la outbox dentro de
un presupuesto opcional que reserva tiempo para actualizar workflows y modelos;
si una traza individual se rechaza, conserva esa entrada y continúa con las
siguientes mientras quede presupuesto.
La outbox contiene trazas de validación autorizadas por la app, no resultados
ni capturas de la aplicación; no son datos de
la finalidad sdkImprovement. Al retirar el permiso o consentimiento de
validación, desactiva primero la captura, espera a que terminen los run() en
curso y llama a await sdk.clearPendingTraces(). El SDK pausa los envíos y
elimina la outbox inmediatamente; la llamada termina cuando acaba un sync()
activo. Una ejecución posterior con traceContext reanuda la captura cuando la
app vuelva a autorizarla.
Para una ejecución de control hecha fuera de AyniSdk.run(), la misma instancia
puede construir el mismo tipo de registro con
sdk.createClientExecutionTrace(...). Pasa el contexto, workflow, versiones,
duración y resultados tipados. Devuelve null si no hay una política válida
habilitada y no hace inferencia ni red; estas trazas de control siguen en
memoria y no se añaden a la outbox de run().
Preferencias de privacidad #
La app integradora debe mostrar el aviso a sus usuarios y construir dos switches independientes, apagados por defecto: contribuir imágenes y etiquetas para mejorar modelos, y contribuir trazas técnicas para mejorar el SDK. La aceptación de los términos no los activa. La recolección necesaria para prestar el servicio se informa y se basa por separado.
Genera un UUID v4 opaco y aleatorio para cada persona dentro de esta aplicación; guarda la relación en la app cliente y no envíes nombre, correo, teléfono ni hashes de esos datos. Al cambiar un switch, registra la acción:
final result = await sdk.recordConsent(
subjectId: opaqueUserId,
purpose: ConsentPurpose.modelImprovement,
decision: enabled ? ConsentDecision.accepted : ConsentDecision.declined,
noticeVersion: displayedNoticeVersion,
);
showMessage(result.message);
El ejemplo comprobado está en example/reference/consent.dart.
Usa ConsentPurpose.sdkImprovement solo para la finalidad de mejora del SDK;
no representa el permiso de validación ni controla sus trazas.
synced confirma que Ayni recibió el recibo; pending significa que quedó
guardado localmente y se enviará antes del próximo manifiesto de sync; ante
error, deja esa finalidad desactivada. Para revocar, cambia primero el estado
local a desactivado y llama a recordConsent con
ConsentDecision.declined y la misma versión del aviso guardada al otorgarlo;
conserva la preferencia aunque el recibo quede pendiente. Una traza de
validación requiere el permiso o consentimiento separado que corresponda. Su
envío no espera el recibo sdkImprovement, y recordConsent() no purga ni
controla esa outbox; usa clearPendingTraces() al revocar la autorización de
validación.
Ejecución local de workflows #
Con el SDK inicializado y sincronizado, ejecuta un workflow instalado con run(). La ejecución es local: no hace peticiones de red y la aplicación nunca interpreta el JSON del DAG.
final sdk = AyniSdk.instance;
try {
// Devuelve la última versión instalada del workflow, sin conexión
final WorkflowResult result = await sdk.run(workflowId, imageBytes);
// Salidas publicadas: Map<String, WorkflowValue>
print(result.outputs);
} on WorkflowError catch (error) {
// error.category (WorkflowErrorCategory), error.nodeId, error.modelVersionId
print('Error de ejecución: ${error.category}');
}
El ejemplo completo, que dart analyze comprueba, está en example/reference/run_workflow.dart.
WorkflowResult expone executionId, workflowId, workflowVersion, outputs y usingOfflineCache. Cada valor de outputs es un WorkflowValue (ClassificationResult, DetectionResult, BooleanResult o CombinedWorkflowResult) bajo el nombre de salida publicado. Una salida con varias fuentes expone los valores disponibles en el orden declarado; los valores conservan sus IDs de nodo. Si la ejecución falla, run() lanza WorkflowError con su category, nodeId y modelVersionId; no devuelve el error como valor.
run() llama a onExecutionStarted con un ID antes de comenzar. Guárdalo para cancelar el análisis en curso:
late String executionId;
final pending = sdk.run(
workflowId,
imageBytes,
onExecutionStarted: (id) => executionId = id,
);
// Mientras `pending` sigue en curso:
sdk.cancelExecution(executionId);
La cancelación detiene los nodos pendientes después de que termine una inferencia que ya estaba en curso. run() completa con WorkflowErrorCategory.cancelled; un ID que ya no está activo produce executionNotFound.
Estructura del paquete #
pubspec.yaml: Especificación del paquete y dependencias genéricas.lib/ayni_sdk.dart: Librería pública oficial.lib/src/: Implementaciones internas del SDK.test/: Pruebas automatizadas.bin/package.dart: Herramienta de empaquetado y verificación de reglas de distribución.
Plataformas compatibles #
Plataformas compatibles con ayni_sdk 0.2.0: Android e iOS.
- Android 8.0 (API 26) o posterior.
- iOS 11.0 o posterior.
Estos mínimos corresponden al runtime de TensorFlow Lite usado por el SDK. Consulta también los requisitos de tflite_flutter al actualizar esa dependencia.
Configuración Android #
El paquete declara las dependencias requeridas por el runtime de TensorFlow Lite en Android a través de tflite_flutter.
-
Configura
minSdkVersion 26(Android 8.0) ycompileSdkVersion 36enandroid/app/build.gradle. -
Con Flutter 3.44 y
tflite_flutter0.12.1, alinea el target Java del subproyecto Android detflite_fluttercon Kotlin 17 enandroid/build.gradle.kts:gradle.projectsEvaluated { rootProject.subprojects .filter { it.name == "tflite_flutter" } .forEach { plugin -> plugin.tasks.withType<JavaCompile>().configureEach { sourceCompatibility = JavaVersion.VERSION_17.toString() targetCompatibility = JavaVersion.VERSION_17.toString() } } }Este ajuste deja de ser necesario cuando
tflite_flutteralinee ambos targets. -
La compilación correcta indica
Runtime Android listo.. -
Si la app se ejecuta en un dispositivo Android con una versión inferior al mínimo admitido (API < 26),
AyniSdk.initializedevuelveInitializationStatus.unsupportedPlatformcon el mensajeEste dispositivo Android no cumple el requisito mínimo del SDK.antes de intentar cargar cualquier modelo. -
La configuración no requiere modificar el código del workflow.
Configuración iOS #
El paquete declara las dependencias requeridas por el runtime de TensorFlow Lite en iOS a través de tflite_flutter y CocoaPods (TensorFlowLiteSwift).
- Configura iOS 11.0 o posterior (
platform :ios, '11.0') enios/Podfile. - La compilación correcta indica
Runtime iOS listo.. - Si la app se ejecuta en un dispositivo iOS con una versión inferior al mínimo admitido (< 11.0),
AyniSdk.initializedevuelveInitializationStatus.unsupportedPlatformcon el mensajeEste dispositivo iOS no cumple el requisito mínimo del SDK.antes de intentar cargar cualquier modelo. - Si se invoca la ejecución directa (
AyniSdk.run) en un dispositivo iOS no soportado (< 11.0), se rechaza conUnsupportedErrorcon el mensajeEste dispositivo iOS no cumple el requisito mínimo del SDK.antes de iniciar la ejecución o inferencia. - La configuración no requiere modificar el código del workflow (los workflows no requieren modificaciones).
AyniSdk.initialize rechaza otras plataformas con InitializationStatus.unsupportedPlatform y el mensaje Esta plataforma no es compatible con ayni_sdk.. El SDK no carga un modelo en esa situación.