Skip to content
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
35 changes: 35 additions & 0 deletions app/lib/backend/preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -424,4 +424,39 @@ class SharedPreferencesUtil {
Future<bool> clear() async {
return await _preferences?.clear() ?? false;
}

/// Clears all user-related preferences when signing out
Future<void> clearUserPreferences() async {
// Remove authentication related data
await remove('authToken');
await remove('tokenExpirationTime');
await remove('email');
await remove('givenName');
await remove('familyName');

// Remove device related data
await remove('hasOmiDevice');
await remove('verifiedPersonaId');

// Remove cached data
await remove('cachedConversations');
await remove('cachedMessages');
await remove('cachedPeople');
await remove('modifiedConversationDetails');

// Remove app related data
await remove('selectedChatAppId2');

// Remove Twitter connection data
await remove('twitterProfile');
await remove('twitterTimeline');

// Remove calendar data
await remove('calendarEnabled');
await remove('calendarId');
await remove('calendarType2');

// Keep settings like language, analytics opt-in, etc.
// as these are user preferences that should persist across logins
}
}
3 changes: 2 additions & 1 deletion app/lib/pages/chat/clone_chat_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ class CloneChatPageState extends State<CloneChatPage> {
var appProvider = Provider.of<AppProvider>(context, listen: false);
SharedPreferencesUtil().appsList = [selectedApp];
appProvider.setApps();
appProvider.setSelectedChatAppId(selectedApp.id);
// Set to null to chat with Omi by default
appProvider.setSelectedChatAppId(null);
if (!selectedApp.enabled) {
await appProvider.toggleApp(selectedApp.id, true, null);
}
Expand Down
31 changes: 31 additions & 0 deletions app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,37 @@ class ChatAppsDropdownWidget extends StatelessWidget {
height: 1,
child: Divider(height: 1),
),
// Add Omi option to the dropdown
PopupMenuItem<String>(
height: 40,
value: 'no_selected',
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getOmiAvatar(),
const SizedBox(width: 10),
Expanded(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Omi",
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w500, fontSize: 16),
),
selectedApp == null
? const SizedBox(
width: 24,
child: Icon(Icons.check, color: Colors.white60, size: 16),
)
: const SizedBox.shrink(),
],
),
),
),
],
),
),
...provider.apps.where((p) => p.enabled && p.worksWithChat()).map<PopupMenuItem<String>>((App app) {
return PopupMenuItem<String>(
height: 40,
Expand Down
3 changes: 1 addition & 2 deletions app/lib/pages/persona/persona_profile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -750,8 +750,7 @@ class _PersonaProfilePageState extends State<PersonaProfilePage> {
TextButton(
onPressed: () async {
Navigator.of(context).pop();
SharedPreferencesUtil().hasOmiDevice = null;
SharedPreferencesUtil().verifiedPersonaId = null;
await SharedPreferencesUtil().clearUserPreferences();
Provider.of<PersonaProvider>(context, listen: false).setRouting(PersonaProfileRouting.no_device);
await signOut();
Navigator.of(context).pop();
Expand Down
3 changes: 1 addition & 2 deletions app/lib/pages/settings/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,7 @@ class _SettingsPageState extends State<SettingsPage> {
return getDialog(context, () {
Navigator.of(context).pop();
}, () async {
SharedPreferencesUtil().hasOmiDevice = null;
SharedPreferencesUtil().verifiedPersonaId = null;
await SharedPreferencesUtil().clearUserPreferences();
Provider.of<PersonaProvider>(context, listen: false).setRouting(PersonaProfileRouting.no_device);
await signOut();
Navigator.of(context).pop();
Expand Down
40 changes: 40 additions & 0 deletions backend/database/facts.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,43 @@ def delete_facts_for_memory(uid: str, memory_id: str):
removed_ids.append(doc.id)
batch.commit()
print('delete_facts_for_memory', memory_id, len(removed_ids))


def migrate_facts(prev_uid: str, new_uid: str, app_id: str = None):
"""
Migrate facts from one user to another.
If app_id is provided, only migrate facts related to that app.
"""
print(f'Migrating facts from {prev_uid} to {new_uid}')

# Get source facts
prev_user_ref = db.collection('users').document(prev_uid)
prev_facts_ref = prev_user_ref.collection('facts')

# Apply app_id filter if provided
if app_id:
query = prev_facts_ref.where(filter=FieldFilter('app_id', '==', app_id))
else:
query = prev_facts_ref

# Get facts to migrate
facts_to_migrate = [doc.to_dict() for doc in query.stream()]

if not facts_to_migrate:
print(f'No facts to migrate for user {prev_uid}')
return 0

# Create batch for destination user
batch = db.batch()
new_user_ref = db.collection('users').document(new_uid)
new_facts_ref = new_user_ref.collection('facts')

# Add facts to batch
for fact in facts_to_migrate:
fact_ref = new_facts_ref.document(fact['id'])
batch.set(fact_ref, fact)

# Commit batch
batch.commit()
print(f'Migrated {len(facts_to_migrate)} facts from {prev_uid} to {new_uid}')
return len(facts_to_migrate)
22 changes: 18 additions & 4 deletions backend/routers/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
is_permit_payment_plan_get, generate_persona_prompt, generate_persona_desc, get_persona_by_uid, \
increment_username, generate_api_key

from database.facts import migrate_facts

from utils.llm import generate_description, generate_persona_intro_message

from utils.notifications import send_notification
Expand Down Expand Up @@ -534,9 +536,20 @@ def generate_description_endpoint(data: dict, uid: str = Depends(auth.get_curren
async def get_twitter_profile_data(handle: str, uid: str = Depends(auth.get_current_user_uid)):
if handle.startswith('@'):
handle = handle[1:]
res = await get_twitter_profile(handle)
if res['avatar']:
res['avatar'] = res['avatar'].replace('_normal', '')
profile = await get_twitter_profile(handle)

# Convert TwitterProfile to dict for response
res = {
"name": profile.name,
"profile": profile.profile,
"rest_id": profile.rest_id,
"avatar": profile.avatar,
"desc": profile.desc,
"friends": profile.friends,
"sub_count": profile.sub_count,
"id": profile.id,
"status": profile.status,
}

# By user persona first
persona = get_user_persona_by_uid(uid)
Expand Down Expand Up @@ -602,7 +615,8 @@ async def migrate_app_owner(old_id, uid: str = Depends(auth.get_current_user_uid
# Migrate app ownership in the database
migrate_app_owner_id_db(uid, old_id)

# Start async task to update persona connected accounts
# Start async tasks to migrate facts and update persona connected accounts
asyncio.create_task(migrate_facts(old_id, uid))
asyncio.create_task(update_omi_persona_connected_accounts(uid))

return {"status": "ok", "message": "Migration started"}
Expand Down
32 changes: 11 additions & 21 deletions backend/utils/apps.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
from collections import defaultdict
from datetime import datetime, timezone
from typing import List, Tuple
from typing import List, Tuple, Dict, Any
import hashlib
import secrets

Expand All @@ -25,7 +25,7 @@
from models.memory import Memory
from utils import stripe
from utils.llm import condense_conversations, condense_facts, generate_persona_description, condense_tweets
from utils.social import get_twitter_timeline
from utils.social import get_twitter_timeline, TwitterProfile, get_twitter_profile

MarketplaceAppReviewUIDs = os.getenv('MARKETPLACE_APP_REVIEWERS').split(',') if os.getenv(
'MARKETPLACE_APP_REVIEWERS') else []
Expand Down Expand Up @@ -396,8 +396,8 @@ async def generate_persona_prompt(uid: str, persona: dict):
if "twitter" in persona['connected_accounts']:
print("twitter in connected accounts---------------------------")
# Get latest tweets
tweets = await get_twitter_timeline(persona['twitter']['username'])
tweets = [{'tweet': tweet['text'], 'posted_at': tweet['created_at']} for tweet in tweets['timeline']]
timeline = await get_twitter_timeline(persona['twitter']['username'])
tweets = [{'tweet': tweet.text, 'posted_at': tweet.created_at} for tweet in timeline.timeline]

# Condense facts
facts_text = condense_facts([fact['content'] for fact in facts if not fact['deleted']], user_name)
Expand Down Expand Up @@ -494,8 +494,8 @@ async def update_persona_prompt(persona: dict):
# Condense tweets
if "twitter" in persona['connected_accounts'] and 'twitter' in persona:
# Get latest tweets
tweets = await get_twitter_timeline(persona['twitter']['username'])
tweets = [tweet['text'] for tweet in tweets['timeline']]
timeline = await get_twitter_timeline(persona['twitter']['username'])
tweets = [tweet.text for tweet in timeline.timeline]
condensed_tweets = condense_tweets(tweets, persona['name'])

# Condense facts
Expand Down Expand Up @@ -585,27 +585,17 @@ def verify_api_key(app_id: str, api_key: str) -> bool:
stored_key = get_api_key_by_hash_db(app_id, hashed_key)
return stored_key is not None

def app_has_action(app: App, action_name: str) -> bool:
"""Check if an app has a specific action capability"""
if not app:
return False

if not app.get('external_integration') or not app.get('external_integration').get('actions'):
return False

for action in app.get('external_integration').get('actions'):
if action.get('action') == action_name:
return True

return False
def app_has_action(app: dict, action_name: str) -> bool:
"""Check if an app has a specific action capability."""
if not app or not isinstance(app, dict):
return False

if not app.get('external_integration'):
return False

actions = app['external_integration'].get('actions', [])
for action in actions:
if action.get('action') == action_name:
return True

return False
2 changes: 1 addition & 1 deletion backend/utils/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -1371,7 +1371,7 @@ def extract_facts_from_text(
if user_name is None or facts_str is None:
user_name, facts_str = get_prompt_facts(uid)

if not text or len(text) < 25: # less than 5 words, probably nothing
if not text or len(text) == 0:
return []

try:
Expand Down
28 changes: 27 additions & 1 deletion backend/utils/memories/facts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import List, Tuple, Optional
from datetime import datetime, timezone

import database.facts as facts_db
from models.facts import FactDB
from models.facts import FactDB, Fact, CategoryEnum
from models.integrations import ExternalIntegrationCreateFact
from utils.llm import extract_facts_from_text

Expand All @@ -28,3 +29,28 @@ def process_external_integration_fact(uid: str, fact_data: ExternalIntegrationCr
facts_db.save_facts(uid, [fact_db.dict() for fact_db in saved_facts])

return saved_facts

def process_twitter_facts(uid: str, tweets_text: str, persona_id: str) -> List[FactDB]:
# Extract facts from tweets using the LLM
extracted_facts = extract_facts_from_text(
uid,
tweets_text,
"twitter_tweets"
)

if not extracted_facts or len(extracted_facts) == 0:
print(f"No facts extracted from tweets for user {uid}")
return []

# Convert extracted facts to database format
saved_facts = []
for fact in extracted_facts:
fact_db = FactDB.from_fact(fact, uid, None, None)
fact_db.manually_added = False
fact_db.app_id = persona_id
saved_facts.append(fact_db)

# Save all facts in batch
facts_db.save_facts(uid, [fact_db.dict() for fact_db in saved_facts])

return saved_facts
Loading