ktg-plugin-marketplace/plugins/ms-ai-architect/skills/ms-ai-infrastructure/references/hybrid-edge/offline-first-ai-applications.md
Kjell Tore Guttormsen 9ea5a2e6c6 chore(privacy): scrub real-org references from plugin internals (phase 2)
Same bulk replacement applied to plugin-internal KB, examples, fixtures,
tests, and docs. Real organization names, persona names, internal system
identifiers, and domain-specific terms replaced with fictional generic
public-sector entity (DDT) and generic terminology.

Scope:
- okr/ — examples, governance, framework, integrations, sources
- ms-ai-architect/ — KB references (engineering, governance, security,
  infrastructure, advisor), tests/fixtures, agents, docs
- linkedin-thought-leadership/ — voice samples, network-builder,
  examples (genericized identifying headlines to "[your organization]")
- llm-security/ — research notes, scan report

Manual genericization beyond bulk replace:
- okr SKILL.md "Primary user / Domain" — generic Norwegian public sector
- linkedin-voice SKILL.md headline placeholder
- network-builder.md headline placeholder
- high-engagement-posts.md voice sample employer line + hashtag

Phase 3 (factual-attribution review) remains: a few KB files attribute
publicly known transport-sector docs/datasets (e.g. håndbok V440, NVDB)
to the fictional DDT after bulk replace. Needs manual semantic review
to either remove or restore correct citation without re-introducing
affiliation references.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 04:28:15 +02:00

19 KiB

Offline-First AI Application Patterns

Last updated: 2026-02 Status: GA Category: Hybrid Cloud & Edge AI


Introduksjon

Offline-first AI-applikasjoner er designet for a fungere primaert lokalt og synkronisere med skyen nar tilkobling er tilgjengelig. Dette monsteret snur den tradisjonelle sky-forst-tilnaermingen pa hodet: i stedet for a feile nar nettverket er nede, er applikasjonen designet for a operere uavhengig med lokal AI-inferens og datalagring.

For norsk offentlig sektor er offline-first sarlig relevant i felt-scenarioer: vegarbeidere som inspiserer infrastruktur i omrader uten dekning, ambulansepersonell som trenger AI-stoette i rurale omrader, beredskapspersonell under krisesituasjoner der kommunikasjonsinfrastruktur kan vaere nede, og maritime inspeksjoner langs kysten.

Microsoft tilbyr flere byggeklosser for offline-first AI: ONNX Runtime for lokal inferens, Azure IoT Edge for container-basert edge-prosessering med utvidet offline-stoette, Azure Container Storage for lokal persistens med automatisk sky-synkronisering, og Phi-modeller for lokale SLM-kapabiliteter.


Kjernekomponenter

Komponent Formal Teknologi
ONNX Runtime Lokal AI-inferens uten sky Cross-platform
Azure IoT Edge Utvidet offline-kapabilitet Container runtime
Azure Container Storage Lokal lagring med sky-sync Arc-enabled
Phi-3/4 SLM Lokal sprakmodell MIT-lisens
SQLite/LiteDB Lokal database for offline-data Embedded DB
CRDTs Konfliktfri replikert datatype Datastruktur
Azure Cosmos DB Sky-database med offline SDK Multi-model DB

Local-First Data Models

Arkitektur for lokal-forst AI

┌─────────────────────────────────────────────┐
│              Offline-First App               │
│                                              │
│  ┌──────────┐  ┌───────────┐  ┌───────────┐ │
│  │ UI Layer │  │ AI Engine │  │ Data Layer│ │
│  │          │  │           │  │           │ │
│  │ - Input  │←→│ - ONNX RT │←→│ - SQLite  │ │
│  │ - Output │  │ - Phi SLM │  │ - VectorDB│ │
│  │ - Status │  │ - Scoring │  │ - File    │ │
│  └──────────┘  └───────────┘  └───────────┘ │
│                                     ↕        │
│                              ┌────────────┐  │
│                              │ Sync Engine│  │
│                              │            │  │
│                              │ - Queue    │  │
│                              │ - Delta    │  │
│                              │ - Conflict │  │
│                              └──────┬─────┘  │
└─────────────────────────────────────┼────────┘
                                      ↕
                              [Sky (nar tilgjengelig)]

Event-sourcing for offline data

# Event-sourced datamodell for offline-first AI
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import json
import sqlite3
import uuid

@dataclass
class Event:
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
    type: str = ""
    entity_id: str = ""
    data: dict = field(default_factory=dict)
    synced: bool = False
    device_id: str = ""

class OfflineEventStore:
    def __init__(self, db_path: str, device_id: str):
        self.device_id = device_id
        self.conn = sqlite3.connect(db_path)
        self._init_schema()

    def _init_schema(self):
        self.conn.executescript("""
            CREATE TABLE IF NOT EXISTS events (
                id TEXT PRIMARY KEY,
                timestamp TEXT NOT NULL,
                type TEXT NOT NULL,
                entity_id TEXT NOT NULL,
                data TEXT NOT NULL,
                synced INTEGER DEFAULT 0,
                device_id TEXT NOT NULL
            );

            CREATE TABLE IF NOT EXISTS ai_results (
                id TEXT PRIMARY KEY,
                event_id TEXT REFERENCES events(id),
                model_version TEXT,
                result TEXT,
                confidence REAL,
                created_at TEXT,
                synced INTEGER DEFAULT 0
            );

            CREATE INDEX IF NOT EXISTS idx_events_synced ON events(synced);
            CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_id);
        """)

    def append_event(self, event_type: str, entity_id: str, data: dict) -> Event:
        """Legg til hendelse i lokal event store"""
        event = Event(
            type=event_type,
            entity_id=entity_id,
            data=data,
            device_id=self.device_id
        )
        self.conn.execute(
            "INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?)",
            (event.id, event.timestamp, event.type, event.entity_id,
             json.dumps(event.data), 0, event.device_id)
        )
        self.conn.commit()
        return event

    def store_ai_result(self, event_id: str, model_version: str,
                        result: dict, confidence: float):
        """Lagre AI-inferensresultat lokalt"""
        self.conn.execute(
            "INSERT INTO ai_results VALUES (?, ?, ?, ?, ?, ?, ?)",
            (str(uuid.uuid4()), event_id, model_version,
             json.dumps(result), confidence,
             datetime.utcnow().isoformat(), 0)
        )
        self.conn.commit()

    def get_unsynced_events(self, limit: int = 100) -> list[Event]:
        """Hent hendelser som ikke er synkronisert"""
        cursor = self.conn.execute(
            "SELECT * FROM events WHERE synced = 0 ORDER BY timestamp LIMIT ?",
            (limit,)
        )
        return [Event(*row) for row in cursor.fetchall()]

    def mark_synced(self, event_ids: list[str]):
        """Marker hendelser som synkronisert"""
        placeholders = ",".join("?" * len(event_ids))
        self.conn.execute(
            f"UPDATE events SET synced = 1 WHERE id IN ({placeholders})",
            event_ids
        )
        self.conn.commit()

Conflict Resolution on Sync

Konflikthondteringsstrategier

Strategi Beskrivelse Best for
Last-Write-Wins (LWW) Siste endring vinner Enkle data, lav risiko
First-Write-Wins Forste endring vinner Uforanderlige hendelser
Merge Kombiner endringer automatisk Komplementaere felt
CRDT Konfliktfri replikert datatype Tallere, sett, tekst
Custom Resolution Applikasjonsspesifikk logikk Komplekse forretningsregler

Implementering av konflikthondtering

# Konflikthondtering for offline-first AI-applikasjon
from enum import Enum
from typing import Callable

class ConflictStrategy(Enum):
    LAST_WRITE_WINS = "lww"
    FIRST_WRITE_WINS = "fww"
    MERGE = "merge"
    MANUAL = "manual"

class SyncConflictResolver:
    def __init__(self, strategy: ConflictStrategy = ConflictStrategy.LAST_WRITE_WINS):
        self.strategy = strategy
        self.custom_resolvers: dict[str, Callable] = {}

    def register_resolver(self, entity_type: str, resolver: Callable):
        """Registrer egendefinert konfliktloeser for en entitetstype"""
        self.custom_resolvers[entity_type] = resolver

    def resolve(self, local_event: dict, remote_event: dict) -> dict:
        """Los konflikt mellom lokal og fjern hendelse"""
        entity_type = local_event.get("type", "")

        # Egendefinert resolver har forrang
        if entity_type in self.custom_resolvers:
            return self.custom_resolvers[entity_type](local_event, remote_event)

        if self.strategy == ConflictStrategy.LAST_WRITE_WINS:
            return self._last_write_wins(local_event, remote_event)
        elif self.strategy == ConflictStrategy.FIRST_WRITE_WINS:
            return self._first_write_wins(local_event, remote_event)
        elif self.strategy == ConflictStrategy.MERGE:
            return self._merge(local_event, remote_event)
        else:
            return {"conflict": True, "local": local_event, "remote": remote_event}

    def _last_write_wins(self, local: dict, remote: dict) -> dict:
        local_ts = local.get("timestamp", "")
        remote_ts = remote.get("timestamp", "")
        return local if local_ts >= remote_ts else remote

    def _merge(self, local: dict, remote: dict) -> dict:
        """Merge ved a kombinere ikke-overlappende felt"""
        merged = {**remote.get("data", {})}
        for key, value in local.get("data", {}).items():
            if key not in merged or merged[key] is None:
                merged[key] = value
            elif key in merged and value != merged[key]:
                # Begge har endret — behold begge med suffix
                merged[f"{key}_local"] = value
                merged[f"{key}_remote"] = merged[key]
        return {"data": merged, "merge_status": "auto_merged"}


# Eksempel: Konflikthondtering for AI-inspeksjonsresultater
resolver = SyncConflictResolver(ConflictStrategy.MERGE)

def resolve_inspection(local, remote):
    """Inspeksjoner: Behold den med hoeyest AI-confidence"""
    local_conf = local.get("data", {}).get("ai_confidence", 0)
    remote_conf = remote.get("data", {}).get("ai_confidence", 0)
    winner = local if local_conf >= remote_conf else remote
    winner["data"]["conflict_resolved"] = True
    winner["data"]["alternative_confidence"] = min(local_conf, remote_conf)
    return winner

resolver.register_resolver("inspection_result", resolve_inspection)

Progressive Enhancement

Progressiv AI-kapabilitet

# Progressiv enhancement: Eskalerer AI-kapabilitet basert pa tilkobling
from enum import Enum
import asyncio

class ConnectivityLevel(Enum):
    OFFLINE = 0      # Ingen tilkobling
    LOW_BANDWIDTH = 1 # < 1 Mbps
    CONNECTED = 2     # Normal tilkobling
    HIGH_BANDWIDTH = 3 # > 10 Mbps

class ProgressiveAIService:
    def __init__(self):
        self.local_model = None   # Phi-3 Mini INT4 (alltid tilgjengelig)
        self.medium_model = None  # Phi-3 Small (krever > 16 GB RAM)
        self.cloud_client = None  # Azure OpenAI (krever tilkobling)

    async def classify_document(self, text: str) -> dict:
        """Klassifiser dokument med best tilgjengelig AI"""
        connectivity = await self.check_connectivity()

        if connectivity >= ConnectivityLevel.HIGH_BANDWIDTH:
            # Nivaa 3: Full sky-AI med GPT-4o
            return await self._classify_cloud(text, model="gpt-4o")

        elif connectivity >= ConnectivityLevel.CONNECTED:
            # Nivaa 2: Sky-AI med lettere modell
            return await self._classify_cloud(text, model="gpt-4o-mini")

        elif connectivity >= ConnectivityLevel.LOW_BANDWIDTH:
            # Nivaa 1: Lokal medium modell med sky-validering
            local_result = self._classify_local(text, self.medium_model)
            # Asynkron validering i bakgrunn nar mulig
            asyncio.create_task(self._validate_in_background(text, local_result))
            return local_result

        else:
            # Nivaa 0: Full offline med lokal mini-modell
            return self._classify_local(text, self.local_model)

    def _classify_local(self, text: str, model) -> dict:
        """Lokal klassifisering med ONNX-modell"""
        result = model.predict(text)
        return {
            "classification": result["label"],
            "confidence": result["score"],
            "model": "local",
            "connectivity": "offline",
            "note": "Resultat fra lokal modell — verifiseres ved tilkobling"
        }

    async def check_connectivity(self) -> ConnectivityLevel:
        """Sjekk navaerende tilkoblingsniva"""
        try:
            import aiohttp
            async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=3)) as session:
                async with session.get("https://management.azure.com/health") as resp:
                    if resp.status == 200:
                        # Estimer bandwidth
                        return ConnectivityLevel.HIGH_BANDWIDTH
        except Exception:
            pass

        try:
            # Proeving med minimal data
            import socket
            socket.create_connection(("8.8.8.8", 53), timeout=2)
            return ConnectivityLevel.LOW_BANDWIDTH
        except Exception:
            return ConnectivityLevel.OFFLINE

UI-indikasjon av AI-nivaa

# Statusindikator for progressive AI
AI_LEVEL_INFO = {
    ConnectivityLevel.OFFLINE: {
        "label": "Offline-modus",
        "description": "Bruker lokal AI-modell. Resultater synkroniseres ved tilkobling.",
        "icon": "offline",
        "accuracy": "God (lokal modell)",
        "features": ["Klassifisering", "Oppsummering", "Uttrekking"]
    },
    ConnectivityLevel.LOW_BANDWIDTH: {
        "label": "Begrenset tilkobling",
        "description": "Lokal AI med bakgrunns-validering.",
        "icon": "low_signal",
        "accuracy": "God+ (validert i bakgrunn)",
        "features": ["Klassifisering", "Oppsummering", "Uttrekking", "Bakgrunns-validering"]
    },
    ConnectivityLevel.CONNECTED: {
        "label": "Tilkoblet",
        "description": "Sky-AI med standard modell.",
        "icon": "connected",
        "accuracy": "Hoey",
        "features": ["Alle funksjoner", "RAG", "Avansert analyse"]
    },
    ConnectivityLevel.HIGH_BANDWIDTH: {
        "label": "Full tilkobling",
        "description": "Sky-AI med avansert modell.",
        "icon": "full_signal",
        "accuracy": "Hoeyest",
        "features": ["Alle funksjoner", "RAG", "Avansert analyse", "Bildeanalyse"]
    }
}

Offline Capability Testing

Testrammeverk for offline AI

# Testrammeverk for offline-first AI-applikasjon
import pytest
from unittest.mock import patch, AsyncMock

class TestOfflineAI:
    """Tester for offline-first AI-funksjonalitet"""

    @pytest.fixture
    def ai_service(self):
        return ProgressiveAIService()

    @pytest.fixture
    def event_store(self, tmp_path):
        return OfflineEventStore(str(tmp_path / "test.db"), "test-device")

    def test_offline_classification(self, ai_service):
        """AI-klassifisering skal fungere uten nettverkstilkobling"""
        with patch.object(ai_service, 'check_connectivity',
                         return_value=ConnectivityLevel.OFFLINE):
            result = asyncio.run(ai_service.classify_document(
                "Vedtak om avslag pa soeknad om byggetillatelse"
            ))
            assert result["classification"] is not None
            assert result["connectivity"] == "offline"
            assert result["confidence"] > 0.5

    def test_event_persistence_offline(self, event_store):
        """Hendelser skal lagres lokalt ved offline"""
        event = event_store.append_event(
            "inspection", "bridge-001",
            {"status": "ok", "notes": "Ingen synlige skader"}
        )
        assert event.synced is False
        assert event.device_id == "test-device"

        # Hent usynkroniserte hendelser
        unsynced = event_store.get_unsynced_events()
        assert len(unsynced) == 1

    def test_sync_after_reconnection(self, event_store):
        """Usynkroniserte hendelser skal koes for synkronisering"""
        # Simuler 10 offline-hendelser
        for i in range(10):
            event_store.append_event(
                "sensor_reading", f"sensor-{i}",
                {"value": i * 1.5}
            )

        unsynced = event_store.get_unsynced_events()
        assert len(unsynced) == 10

        # Simuler synkronisering
        synced_ids = [e.id for e in unsynced[:5]]
        event_store.mark_synced(synced_ids)

        remaining = event_store.get_unsynced_events()
        assert len(remaining) == 5

    def test_conflict_resolution(self):
        """Konflikter ved sync skal loses deterministisk"""
        resolver = SyncConflictResolver(ConflictStrategy.LAST_WRITE_WINS)

        local = {"timestamp": "2026-02-12T10:00:00", "data": {"status": "ok"}}
        remote = {"timestamp": "2026-02-12T09:00:00", "data": {"status": "warning"}}

        result = resolver.resolve(local, remote)
        assert result["data"]["status"] == "ok"  # Nyeste vinner

    def test_progressive_enhancement(self, ai_service):
        """AI-kvalitet skal oeke med bedre tilkobling"""
        results = {}
        for level in ConnectivityLevel:
            with patch.object(ai_service, 'check_connectivity',
                            return_value=level):
                result = asyncio.run(ai_service.classify_document("test"))
                results[level] = result

        # Verifiser at sky-resultat har hoeyere konfidensangivelse
        assert results[ConnectivityLevel.OFFLINE]["model"] == "local"

Norsk offentlig sektor

Felt-scenarier som krever offline-first

Scenario Etat Offline-varighet AI-funksjon
Vegfinspeksjon DDT Timer Skadeklassifisering
Ambulanse Helseetaten Minutter-timer Triagering
Beredskap DSB Dager Situasjonsanalyse
Maritime inspeksjoner Sjoefartsdir. Timer-dager Rapport-generering
Grensekontroll Politiet Minutter Dokumentverifisering
Skogsbrannberedskap 110-sentraler Timer Risikoanalyse

Krav til offline-first i offentlig sektor

  • Applikasjonen MA fungere uten nettverkstilkobling
  • Lokale AI-resultater MA vaere tydelig merket som "ikke-validert"
  • Synkronisering MA skje automatisk ved tilkobling
  • Konflikthondtering MA vaere deterministisk og sporbar
  • Data MA vaere kryptert lokalt (BitLocker/LUKS)

Beslutningsrammeverk

Scenario Anbefaling Begrunnelse
Felt-app med periodisk tilkobling Full offline-first med event sourcing Data bevares alltid lokalt
Sanntids-AI med fallback Progressiv enhancement Best mulig kvalitet per tilstand
Multi-enhet med sync CRDTs + event store Konfliktfri synkronisering
Kritisk infrastruktur Azure IoT Edge extended offline Uavhengig drift i uker
Klient-app pa PC SQLite + ONNX RT + bakgrunns-sync Enkel, palitelig arkitektur
Beredskapsapplikasjon Full offline med manuell sync Ingen skyavhengighet

For Cosmo

  • Offline-first er et designprinsipp, ikke en feilhaandterings-strategi — applikasjonen MA designes for a fungere lokalt foerst, med sky som en berikelse nar tilgjengelig
  • Event sourcing er det foretrukne datamoensteeret for offline-first AI — alle hendelser og AI-resultater lagres lokalt som uforanderlige events og synkroniseres inkrementelt
  • Progressiv enhancement gir graceful degradation — definer tydelige AI-kapabilitetsnivaaer (offline/begrenset/tilkoblet/full) og kommuniser dette til brukeren
  • Konflikthondtering maa vaere deterministisk og sporbar — bruk Last-Write-Wins som standard, med custom resolvers for doemenespesifikke regler (f.eks. hoeyest AI-confidence vinner)
  • For norsk offentlig sektor: Test offline-scenarioer som foersteklasses testcase — ikke anta tilkobling, og sooerg for at felt-personell kan fullfoere sine oppgaver uavhengig av nettverksstatus