Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: experimental local ai model #254

Merged
merged 13 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/easypid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ The following standards and specifications were implemented.
- Fixed an issue where the PIN screen would get stuck in a loading state when an incorrect PIN was entered [commit](https://github.com/animo/paradym-wallet/commit/0f65ef98f5f26c3afc0968e4f848bf538a86cfd7)
- Fixed an issue with redirect based auth flow if the authorization flow left the in-app browser (e.g. when requiring authentication using the native AusweisApp with the eID card) [commit](https://github.com/animo/paradym-wallet/commit/eb333b81fe5662cc2f010e1ee9bbdc83a7e19aa3)
- Fixed an issue where the PID setup would get stuck if you skipped it during onboarding [commit](https://github.com/animo/openid4vc-playground-funke/commit/65178e776bc421b9ca413542ea0e86db4ad1ead4)
- Added support for on-device local AI model for oversharing detection on higher-end devices (can be enabled in the settings) [commit](https://github.com/animo/paradym-wallet/commit)



#### 28-11-2024

Expand Down
6 changes: 6 additions & 0 deletions apps/easypid/app.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ const config = {
],
},
associatedDomains: associatedDomains.map((host) => `applinks:${host}`),
entitlements: {
'com.apple.developer.kernel.increased-memory-limit': true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any thing we also need to configure in appstoreconnect?

},
},
android: {
adaptiveIcon: {
Expand Down Expand Up @@ -145,6 +148,9 @@ const config = {
}))
),
],
config: {
largeHeap: true,
},
},
experiments: {
tsconfigPaths: true,
Expand Down
2 changes: 2 additions & 0 deletions apps/easypid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"expo-blur": "^13.0.2",
"expo-constants": "~16.0.2",
"expo-dev-client": "~4.0.16",
"expo-device": "~6.0.2",
"expo-font": "~12.0.7",
"expo-haptics": "~13.0.1",
"expo-image": "~1.13.0",
Expand All @@ -55,6 +56,7 @@
"react": "catalog:",
"react-native": "catalog:",
"react-native-argon2": "^2.0.1",
"react-native-executorch": "^0.1.2",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.16.2",
"react-native-get-random-values": "~1.11.0",
Expand Down
2 changes: 2 additions & 0 deletions apps/easypid/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { Slot } from 'expo-router'
import * as SplashScreen from 'expo-splash-screen'

import { useCheckIncompleteDownload } from '@easypid/llm'
import tamaguiConfig from '../../tamagui.config'

void SplashScreen.preventAutoHideAsync()
Expand All @@ -17,6 +18,7 @@ export const unstable_settings = {

export default function RootLayout() {
useTransparentNavigationBar()
useCheckIncompleteDownload()

return (
<Provider config={tamaguiConfig}>
Expand Down
41 changes: 17 additions & 24 deletions apps/easypid/src/features/menu/FunkeSettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { Button, FlexPage, Heading, HeroIcons, Paragraph, ScrollView, Stack, XStack, YStack } from '@package/ui'
import { FlexPage, Heading, HeroIcons, ScrollView, Stack, Switch, YStack } from '@package/ui'
import React from 'react'
import { useRouter } from 'solito/router'
import { Label, Switch } from 'tamagui'

import { TextBackButton } from 'packages/app/src'
import { LocalAiContainer } from './components/LocalAiContainer'

import { useScrollViewPosition } from '@package/app/src/hooks'
import { useDevelopmentMode } from '../../hooks/useDevelopmentMode'

export function FunkeSettingsScreen() {
const { handleScroll, isScrolledByOffset, scrollEventThrottle } = useScrollViewPosition()
const router = useRouter()
const [isDevelpomentModeEnabled, setIsDevelopmentModeEnabled] = useDevelopmentMode()
const [isDevelopmentModeEnabled, setIsDevelopmentModeEnabled] = useDevelopmentMode()

return (
<FlexPage gap="$0" paddingHorizontal="$0">
Expand All @@ -27,26 +27,19 @@ export function FunkeSettingsScreen() {
contentContainerStyle={{ minHeight: '85%' }}
>
<YStack fg={1} px="$4" jc="space-between">
<YStack>
<Paragraph color="$grey-700" py="$4">
This page is under construction. More options will be added.
</Paragraph>
<XStack jc="space-between" ai="center">
<Label>Development Mode</Label>
<Switch
size="$5"
checked={isDevelpomentModeEnabled}
onCheckedChange={setIsDevelopmentModeEnabled}
animation="quick"
backgroundColor={isDevelpomentModeEnabled ? '$primary-500' : '$primary-300'}
>
<Switch.Thumb animation="quick" backgroundColor="$grey-200" />
</Switch>
</XStack>
<YStack gap="$4" py="$2">
<Switch
id="development-mode"
label="Development Mode"
icon={<HeroIcons.CommandLineFilled />}
value={isDevelopmentModeEnabled ?? false}
onChange={setIsDevelopmentModeEnabled}
/>
<LocalAiContainer />
</YStack>
<YStack btw="$0.5" borderColor="$grey-200" pt="$4" mx="$-4" px="$4" bg="$background">
<TextBackButton />
</YStack>
<Button.Text color="$primary-500" fontWeight="$semiBold" fontSize="$4" onPress={() => router.back()}>
<HeroIcons.ArrowLeft mr={-4} color="$primary-500" size={20} /> Back
</Button.Text>
</YStack>
</ScrollView>
</FlexPage>
Expand Down
99 changes: 99 additions & 0 deletions apps/easypid/src/features/menu/components/LocalAiContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { HeroIcons } from '@package/ui/src/content/Icon'

import { Switch } from '@package/ui/src/base/Switch'

import { useIsDeviceCapable, useLLM } from '@easypid/llm'
import { ConfirmationSheet } from '@package/app/src/components/ConfirmationSheet'
import { useHasInternetConnection, useIsConnectedToWifi } from 'packages/app/src/hooks'
import { useToastController } from 'packages/ui/src'
import React, { useState } from 'react'

export function LocalAiContainer() {
const toast = useToastController()
const isConnectedToWifi = useIsConnectedToWifi()
const hasInternetConnection = useHasInternetConnection()
const isDeviceCapable = useIsDeviceCapable()

const [isAiModelConfirmationOpen, setIsAiModelConfirmationOpen] = useState(false)
const { loadModel, isModelReady, downloadProgress, removeModel, isModelActivated, isModelDownloading } = useLLM()

const onActivateModel = () => {
if (!isDeviceCapable) {
toast.show('Device not supported', {
message: 'This device is not powerful enough to run local AI models',
customData: {
preset: 'warning',
},
})
setIsAiModelConfirmationOpen(false)
return
}
if (!isConnectedToWifi && !hasInternetConnection) {
toast.show('WiFi connection required', {
message: 'Please connect to WiFi to activate and download the model',
customData: {
preset: 'warning',
},
})
setIsAiModelConfirmationOpen(false)
return
}

setIsAiModelConfirmationOpen(false)
loadModel()
}

const handleAiModelChange = (value: boolean) => {
if (isModelDownloading) {
toast.show('Model download in progress', {
message: 'Force close the app to cancel the download',
customData: {
preset: 'warning',
},
})
return
}

if (value) {
setIsAiModelConfirmationOpen(true)
} else {
removeModel()
}
}

return (
<>
<Switch
id="local-ai-model"
label="Use local AI model"
icon={<HeroIcons.CpuChipFilled />}
value={isModelActivated}
description={
isModelActivated
? isModelReady
? 'Model active and ready to use'
: downloadProgress
? `Downloading model ${(downloadProgress * 100).toFixed(2)}%`
: 'Getting ready...'
: ''
}
onChange={handleAiModelChange}
beta
/>
<ConfirmationSheet
type="floating"
variant="regular"
isOpen={isAiModelConfirmationOpen}
setIsOpen={setIsAiModelConfirmationOpen}
title="Enable local AI model"
confirmText="Enable"
cancelText="Cancel"
description={[
'This will download a local AI model to your device which will take up to around 1.3GB of space.',
'This is an experimental feature. Only supported on high-end devices.',
]}
onConfirm={onActivateModel}
/>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,8 @@ export function FunkeCredentialNotificationScreen() {
logo={credentialsForRequest.verifier.logo}
submission={credentialsForRequest.formattedSubmission}
isAccepting={isSharingPresentation}
// Not supported for this flow atm
overAskingResponse={{ validRequest: 'could_not_determine', reason: '' }}
/>
),
}
Expand Down
23 changes: 14 additions & 9 deletions apps/easypid/src/features/receive/slides/CredentialErrorSlide.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { Button, Heading, HeroIcons, Paragraph, Stack, XStack, YStack } from '@package/ui'
import { Button, Heading, HeroIcons, Paragraph, ScrollView, Stack, XStack, YStack } from '@package/ui'
import { useState } from 'react'

interface CredentialErrorSlideProps {
reason?: string
onCancel: () => void
}

export const CredentialErrorSlide = ({ reason, onCancel }: CredentialErrorSlideProps) => {
const [scrollViewHeight, setScrollViewHeight] = useState(0)

return (
<YStack fg={1} jc="space-between">
<YStack gap="$6">
<YStack gap="$2">
<YStack gap="$6" fg={1} onLayout={(event) => setScrollViewHeight(event.nativeEvent.layout.height)}>
<ScrollView fg={1} maxHeight={scrollViewHeight} contentContainerStyle={{ gap: '$4' }}>
<YStack gap="$4">
<Heading>Something went wrong</Heading>
<Stack alignSelf="flex-start">
Expand All @@ -22,13 +25,15 @@ export const CredentialErrorSlide = ({ reason, onCancel }: CredentialErrorSlideP
again later.
</Paragraph>
</YStack>
{reason && (
<Paragraph variant="sub">
<Paragraph variant="caption">Reason: </Paragraph>
{reason}
</Paragraph>
{reason && scrollViewHeight !== 0 && (
<YStack>
<Paragraph variant="sub">
<Paragraph variant="caption">Reason: </Paragraph>
{reason}
</Paragraph>
</YStack>
)}
</YStack>
</ScrollView>
</YStack>
<Stack borderTopWidth="$0.5" borderColor="$grey-200" py="$4" mx="$-4" px="$4">
<Button.Solid scaleOnPress onPress={onCancel}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ import React, { useEffect, useState, useCallback } from 'react'

import { useAppAgent } from '@easypid/agent'
import { InvalidPinError } from '@easypid/crypto/error'
import { useOverAskingAi } from '@easypid/hooks'
import { useDevelopmentMode } from '@easypid/hooks'
import { analyzeVerification } from '@easypid/use-cases/ValidateVerification'
import type { VerificationAnalysisResponse } from '@easypid/use-cases/ValidateVerification'
import { usePushToWallet } from '@package/app/src/hooks/usePushToWallet'
import { setWalletServiceProviderPin } from '../../crypto/WalletServiceProviderClient'
import { useShouldUsePinForSubmission } from '../../hooks/useShouldUsePinForPresentation'
Expand Down Expand Up @@ -62,26 +61,24 @@ export function FunkeOpenIdPresentationNotificationScreen() {
})
}, [credentialsForRequest, params.data, params.uri, toast.show, agent, pushToWallet, toast, isDevelopmentModeEnabled])

const [verificationAnalysis, setVerificationAnalysis] = useState<{
isLoading: boolean
result: VerificationAnalysisResponse | undefined
}>({
isLoading: false,
result: undefined,
})
const { checkForOverAsking, isProcessingOverAsking, overAskingResponse, stopOverAsking } = useOverAskingAi()

useEffect(() => {
if (!credentialsForRequest?.formattedSubmission || !credentialsForRequest?.formattedSubmission.areAllSatisfied) {
return
}
setVerificationAnalysis((prev) => ({ ...prev, isLoading: true }))

if (isProcessingOverAsking || overAskingResponse) {
// Already generating or already has result
return
}

const submission = credentialsForRequest.formattedSubmission
const requestedCards = submission.entries
.filter((entry): entry is FormattedSubmissionEntrySatisfied => entry.isSatisfied)
.flatMap((entry) => entry.credentials)

analyzeVerification({
void checkForOverAsking({
verifier: {
name: credentialsForRequest.verifier.name ?? 'No name provided',
domain: credentialsForRequest.verifier.hostName ?? 'No domain provided',
Expand All @@ -93,8 +90,8 @@ export function FunkeOpenIdPresentationNotificationScreen() {
subtitle: credential.credential.display.description ?? 'Card description',
requestedAttributes: getDisclosedAttributeNamesForDisplay(credential),
})),
}).then((result) => setVerificationAnalysis((prev) => ({ ...prev, isLoading: false, result })))
}, [credentialsForRequest])
})
}, [credentialsForRequest, checkForOverAsking, isProcessingOverAsking, overAskingResponse])

const onProofAccept = useCallback(
async (pin?: string): Promise<PresentationRequestResult> => {
Expand All @@ -106,6 +103,7 @@ export function FunkeOpenIdPresentationNotificationScreen() {
},
}

stopOverAsking()
setIsSharing(true)

if (shouldUsePin) {
Expand Down Expand Up @@ -190,10 +188,11 @@ export function FunkeOpenIdPresentationNotificationScreen() {
}
}
},
[credentialsForRequest, agent, shouldUsePin, isDevelopmentModeEnabled]
[credentialsForRequest, agent, shouldUsePin, stopOverAsking, isDevelopmentModeEnabled]
)

const onProofDecline = async () => {
stopOverAsking()
if (credentialsForRequest) {
await addSharedActivityForCredentialsForRequest(
agent,
Expand All @@ -219,7 +218,7 @@ export function FunkeOpenIdPresentationNotificationScreen() {
trustedEntities={credentialsForRequest?.verifier.trustedEntities}
lastInteractionDate={lastInteractionDate}
onComplete={() => pushToWallet('replace')}
verificationAnalysis={verificationAnalysis}
overAskingResponse={overAskingResponse}
/>
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DisplayImage, FormattedSubmission, TrustedEntity } from '@package/agent'

import type { VerificationAnalysisResult } from '@easypid/use-cases/ValidateVerification'
import type { OverAskingResponse } from '@easypid/use-cases/OverAskingApi'
import { type SlideStep, SlideWizard } from '@package/app'
import { LoadingRequestSlide } from '../receive/slides/LoadingRequestSlide'
import { VerifyPartySlide } from '../receive/slides/VerifyPartySlide'
Expand All @@ -14,7 +14,7 @@ interface FunkePresentationNotificationScreenProps {
verifierName?: string
logo?: DisplayImage
lastInteractionDate?: string
verificationAnalysis: VerificationAnalysisResult
overAskingResponse?: OverAskingResponse
trustedEntities?: Array<TrustedEntity>
submission?: FormattedSubmission
usePin: boolean
Expand All @@ -35,7 +35,7 @@ export function FunkePresentationNotificationScreen({
isAccepting,
submission,
onComplete,
verificationAnalysis,
overAskingResponse,
trustedEntities,
}: FunkePresentationNotificationScreenProps) {
return (
Expand Down Expand Up @@ -74,7 +74,7 @@ export function FunkePresentationNotificationScreen({
logo={logo}
submission={submission}
isAccepting={isAccepting}
verificationAnalysis={verificationAnalysis}
overAskingResponse={overAskingResponse}
/>
),
},
Expand Down
Loading