From eae574b6e13de88a1125f5732119cbcd8e66a989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?th=E1=BB=8Bnh?= Date: Sun, 16 Mar 2025 17:42:03 +0700 Subject: [PATCH 1/5] Modeling the twitter* model; Creating facts right on verifing owner-ship; Migrating facts; Chat with Omi instead of your clone --- app/lib/pages/chat/clone_chat_page.dart | 2 +- backend/database/facts.py | 80 ++++++++++++ backend/routers/apps.py | 21 +++- backend/utils/apps.py | 32 ++--- backend/utils/memories/facts.py | 28 ++++- backend/utils/social.py | 161 +++++++++++++++++------- 6 files changed, 251 insertions(+), 73 deletions(-) diff --git a/app/lib/pages/chat/clone_chat_page.dart b/app/lib/pages/chat/clone_chat_page.dart index a35ec86f839..3cdd7bf5c0f 100644 --- a/app/lib/pages/chat/clone_chat_page.dart +++ b/app/lib/pages/chat/clone_chat_page.dart @@ -35,7 +35,7 @@ class CloneChatPageState extends State { var appProvider = Provider.of(context, listen: false); SharedPreferencesUtil().appsList = [selectedApp]; appProvider.setApps(); - appProvider.setSelectedChatAppId(selectedApp.id); + appProvider.setSelectedChatAppId(null); if (!selectedApp.enabled) { await appProvider.toggleApp(selectedApp.id, true, null); } diff --git a/backend/database/facts.py b/backend/database/facts.py index 8e44c0fa97c..2bdba15de0e 100644 --- a/backend/database/facts.py +++ b/backend/database/facts.py @@ -101,3 +101,83 @@ 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) + + +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) diff --git a/backend/routers/apps.py b/backend/routers/apps.py index 25879d034fa..f6c0932a2d2 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -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 @@ -534,9 +536,19 @@ 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 + } # By user persona first persona = get_user_persona_by_uid(uid) @@ -602,6 +614,9 @@ 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) + # Migrate facts from old user to new user + migrate_facts(old_id, uid) + # Start async task to update persona connected accounts asyncio.create_task(update_omi_persona_connected_accounts(uid)) diff --git a/backend/utils/apps.py b/backend/utils/apps.py index 501bede4e8e..99d22e06917 100644 --- a/backend/utils/apps.py +++ b/backend/utils/apps.py @@ -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 @@ -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 [] @@ -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) @@ -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 @@ -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 diff --git a/backend/utils/memories/facts.py b/backend/utils/memories/facts.py index defa22c2c73..36d543df6ed 100644 --- a/backend/utils/memories/facts.py +++ b/backend/utils/memories/facts.py @@ -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 @@ -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 diff --git a/backend/utils/social.py b/backend/utils/social.py index d02917aa598..06ff23a485c 100644 --- a/backend/utils/social.py +++ b/backend/utils/social.py @@ -1,19 +1,54 @@ import os import time from datetime import datetime, timezone -from typing import Dict, Any, Callable, TypeVar +from typing import Dict, Any, Callable, TypeVar, List, Optional, Tuple import httpx +from pydantic import BaseModel from ulid import ULID from database.apps import update_app_in_db, upsert_app_to_db, get_persona_by_id_db, get_persona_by_username_twitter_handle_db +from database.facts import create_fact from database.redis_db import delete_generic_cache, save_username, is_username_taken from utils.llm import condense_tweets, generate_twitter_persona_prompt - +from utils.memories.facts import process_twitter_facts rapid_api_host = os.getenv('RAPID_API_HOST') rapid_api_key = os.getenv('RAPID_API_KEY') defaultTimeoutSec = 15 +class TwitterTweet(BaseModel): + text: str + created_at: str + id: str + +class TwitterTimeline(BaseModel): + timeline: List[TwitterTweet] + +class TwitterProfile(BaseModel): + name: str + profile: str # Twitter handle + rest_id: str + avatar: str + desc: str # Bio description + friends: int # Following count + sub_count: int # Followers count + id: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TwitterProfile": + """Create a TwitterProfile instance from API response dictionary""" + return cls( + name=data.get("name", ""), + profile=data.get("profile", ""), + rest_id=data.get("rest_id", ""), + avatar=(data.get("avatar") or "").replace("_normal", ""), # Get full-size avatar + desc=data.get("desc", ""), + friends=data.get("friends", 0), + sub_count=data.get("sub_count", 0), + id=data.get("id", "") + ) + + T = TypeVar('T') def with_retry(operation_name: str, func: Callable[[], T]) -> T: max_retries = 5 @@ -31,7 +66,8 @@ def with_retry(operation_name: str, func: Callable[[], T]) -> T: time.sleep(delay) raise Exception("Maximum retries exceeded") -async def get_twitter_profile(handle: str) -> Dict[str, Any]: +async def get_twitter_profile(handle: str) -> TwitterProfile: + """Fetch Twitter profile for a user and return structured data""" url = f"https://{rapid_api_host}/screenname.php?screenname={handle}" headers = { @@ -45,13 +81,22 @@ def fetch_profile(): data = response.json() if data.get('status') == 'error': raise Exception(f"API returned error status: {data.get('message', 'Unknown error')}") - return data + return TwitterProfile.from_dict(data) # else response.raise_for_status() return with_retry(f"fetching Twitter profile for {handle}", fetch_profile) -async def get_twitter_timeline(handle: str) -> Dict[str, Any]: +def create_facts_from_twitter_tweets(uid: str, persona_id: str, tweets: List[TwitterTweet]) -> None: + """Create individual facts from tweets for more detailed persona information""" + # Combine tweets into a single text for fact extraction + combined_text = "\n".join([f"{tweet.text} (Posted: {tweet.created_at})" for tweet in tweets]) + + # Process tweets and extract facts using the dedicated function + process_twitter_facts(uid, combined_text, persona_id) + +async def get_twitter_timeline(handle: str) -> TwitterTimeline: + """Fetch Twitter timeline for a user and return structured data""" print(f"Fetching Twitter timeline for {handle}...") url = f"https://{rapid_api_host}/timeline.php?screenname={handle}" @@ -66,52 +111,77 @@ def fetch_timeline(): data = response.json() if data.get('status') == 'error': raise Exception(f"API returned error status: {data.get('message', 'Unknown error')}") - return data + + # Convert raw timeline to structured model + timeline_data = data.get('timeline', []) + tweets = [TwitterTweet( + text=tweet['text'], + created_at=tweet['created_at'], + id=tweet['id'] + ) for tweet in timeline_data] + + return TwitterTimeline(timeline=tweets) # else response.raise_for_status() return with_retry(f"fetching Twitter timeline for {handle}", fetch_timeline) async def verify_latest_tweet(username: str, handle: str) -> Dict[str, Any]: + """Verify if the latest tweet contains verification text""" print(f"Fetching latest tweet for {handle}, username {username}...") - url = f"https://{rapid_api_host}/timeline.php?screenname={handle}" - headers = { - "X-RapidAPI-Key": rapid_api_key, - "X-RapidAPI-Host": rapid_api_host - } + # Get timeline + timeline = await get_twitter_timeline(handle) - def verify_tweet(): - response = httpx.get(url, headers=headers, timeout=defaultTimeoutSec) - if response.status_code == 200: - data = response.json() - if data.get('status') == 'error': - raise Exception(f"API returned error status: {data.get('message', 'Unknown error')}") - # from the timeline, the first tweet is the latest - latest_tweet = None - timeline = data.get('timeline', []) - if len(timeline) > 0: - latest_tweet = timeline[0] - if f'Verifying my clone({username})' in latest_tweet['text']: - return {"tweet": latest_tweet['text'], 'verified': True} + # Check if there are any tweets + if not timeline.timeline: + return {"tweet": "", "verified": False} - return {"tweet": latest_tweet['text'] if latest_tweet else "", 'verified': False} + # Get the latest tweet (first in the timeline) + latest_tweet = timeline.timeline[0] - # else - response.raise_for_status() + # Check if it contains verification text + if f'Verifying my clone({username})' in latest_tweet.text: + return {"tweet": latest_tweet.text, "verified": True} - return with_retry(f"verifying latest tweet for {handle}", verify_tweet) + return {"tweet": latest_tweet.text, "verified": False} async def upsert_persona_from_twitter_profile(username: str, handle: str, uid: str) -> Dict[str, Any]: + """Create or update a persona based on Twitter profile and generate facts""" + # Get Twitter profile data profile = await get_twitter_profile(handle) - profile['avatar'] = (profile.get('avatar') or '').replace('_normal', '') + + # Get tweets + timeline = await get_twitter_timeline(handle) + + # Create or update persona + persona = _create_or_update_persona(profile, username, uid, handle) + + # Generate persona prompt from tweets + formatted_tweets = [{'tweet': tweet.text, 'posted_at': tweet.created_at} for tweet in timeline.timeline] + persona_prompt = generate_twitter_persona_prompt(formatted_tweets, persona["name"]) + persona['persona_prompt'] = persona_prompt + + # Save persona to database + upsert_app_to_db(persona) + save_username(username, uid) + delete_generic_cache('get_public_approved_apps_data') + + # Create facts from persona prompt and tweets + create_facts_from_twitter_tweets(uid, persona['id'], timeline.timeline) + + return persona + +def _create_or_update_persona(profile: TwitterProfile, username: str, uid: str, handle: str) -> Dict[str, Any]: + """Create a new persona or update an existing one""" persona = get_persona_by_username_twitter_handle_db(username, handle) + # Create new persona if it doesn't exist if not persona: persona = { - "name": profile["name"], - "author": profile['name'], + "name": profile.name, + "author": profile.name, "uid": uid, "id": str(ULID()), "deleted": False, @@ -119,46 +189,43 @@ async def upsert_persona_from_twitter_profile(username: str, handle: str, uid: s "capabilities": ["persona"], "username": username, "connected_accounts": ["twitter"], - "description": profile["desc"], - "image": profile["avatar"], + "description": profile.desc, + "image": profile.avatar, "category": "personality-emulation", "approved": True, "private": False, "created_at": datetime.now(timezone.utc), } - # update profle + # Update persona with Twitter data persona["twitter"] = { - "username": handle, - "avatar": profile["avatar"], + "username": profile.profile, + "avatar": profile.avatar, "connected_at": datetime.now(timezone.utc) } - # publish automatically + # Ensure persona is published persona["status"] = "approved" persona["approved"] = True persona["private"] = False - tweets = await get_twitter_timeline(handle) - tweets = [{'tweet': tweet['text'], 'posted_at': tweet['created_at']} for tweet in tweets['timeline']] - persona['persona_prompt'] = generate_twitter_persona_prompt(tweets, persona["name"]) - upsert_app_to_db(persona) - save_username(persona['username'], uid) - delete_generic_cache('get_public_approved_apps_data') return persona async def add_twitter_to_persona(handle: str, persona_id) -> Dict[str, Any]: + """Add Twitter account to an existing persona""" persona = get_persona_by_id_db(persona_id) - twitter = await get_twitter_profile(handle) - twitter['avatar'] = (twitter.get('avatar') or '').replace('_normal', '') + profile = await get_twitter_profile(handle) + if 'twitter' not in persona['connected_accounts']: persona['connected_accounts'].append('twitter') + persona['twitter'] = { - "username": handle, - "avatar": twitter["avatar"], + "username": profile.profile, + "avatar": profile.avatar, "connected_at": datetime.now(timezone.utc) } + update_app_in_db(persona) delete_generic_cache('get_public_approved_apps_data') return persona From 86d9148c5dd2f6d0e1e54459cd5fccf951841c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?th=E1=BB=8Bnh?= Date: Sun, 16 Mar 2025 18:06:35 +0700 Subject: [PATCH 2/5] Modeling the twitter* model; Creating facts right on verifing owner-ship; Migrating facts; Chat with Omi instead of your clone --- backend/database/facts.py | 40 --------------------------------------- backend/routers/apps.py | 3 ++- backend/utils/social.py | 9 ++++++++- 3 files changed, 10 insertions(+), 42 deletions(-) diff --git a/backend/database/facts.py b/backend/database/facts.py index 2bdba15de0e..1d92c144157 100644 --- a/backend/database/facts.py +++ b/backend/database/facts.py @@ -141,43 +141,3 @@ def migrate_facts(prev_uid: str, new_uid: str, app_id: str = None): batch.commit() print(f'Migrated {len(facts_to_migrate)} facts from {prev_uid} to {new_uid}') return len(facts_to_migrate) - - -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) diff --git a/backend/routers/apps.py b/backend/routers/apps.py index f6c0932a2d2..bd961d5dd8a 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -547,7 +547,8 @@ async def get_twitter_profile_data(handle: str, uid: str = Depends(auth.get_curr "desc": profile.desc, "friends": profile.friends, "sub_count": profile.sub_count, - "id": profile.id + "id": profile.id, + "status": profile.status, } # By user persona first diff --git a/backend/utils/social.py b/backend/utils/social.py index 06ff23a485c..a8a3b05ada8 100644 --- a/backend/utils/social.py +++ b/backend/utils/social.py @@ -33,6 +33,7 @@ class TwitterProfile(BaseModel): friends: int # Following count sub_count: int # Followers count id: str + status: str = "error" # Default status for successful profile fetch @classmethod def from_dict(cls, data: Dict[str, Any]) -> "TwitterProfile": @@ -45,7 +46,8 @@ def from_dict(cls, data: Dict[str, Any]) -> "TwitterProfile": desc=data.get("desc", ""), friends=data.get("friends", 0), sub_count=data.get("sub_count", 0), - id=data.get("id", "") + id=data.get("id", ""), + status=data.get("status", "error") ) @@ -81,6 +83,11 @@ def fetch_profile(): data = response.json() if data.get('status') == 'error': raise Exception(f"API returned error status: {data.get('message', 'Unknown error')}") + + # Ensure avatar URL is properly formatted (full size) + if 'avatar' in data and data['avatar'] and '_normal' in data['avatar']: + data['avatar'] = data['avatar'].replace('_normal', '') + return TwitterProfile.from_dict(data) # else response.raise_for_status() From 57ea17246db080f5fe761fc8d1c4587a5cc9b624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?th=E1=BB=8Bnh?= Date: Mon, 17 Mar 2025 06:01:22 +0700 Subject: [PATCH 3/5] Create facts whenever adding twitter to persona --- backend/utils/social.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/utils/social.py b/backend/utils/social.py index a8a3b05ada8..2f3ba556415 100644 --- a/backend/utils/social.py +++ b/backend/utils/social.py @@ -235,4 +235,12 @@ async def add_twitter_to_persona(handle: str, persona_id) -> Dict[str, Any]: update_app_in_db(persona) delete_generic_cache('get_public_approved_apps_data') + + # Get tweets from the Twitter timeline + timeline = await get_twitter_timeline(handle) + + # Create facts from the tweets + if timeline and timeline.timeline: + create_facts_from_twitter_tweets(persona['uid'], persona_id, timeline.timeline) + return persona From 8fb0e2978f84b1f4a96837bade69a52d49ef020d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?th=E1=BB=8Bnh?= Date: Mon, 17 Mar 2025 10:10:49 +0700 Subject: [PATCH 4/5] Clear user preference on signing out; add omi option on chatting with clone page --- app/lib/backend/preferences.dart | 35 +++++++++++++++++++ app/lib/pages/chat/clone_chat_page.dart | 1 + .../widgets/chat_apps_dropdown_widget.dart | 31 ++++++++++++++++ app/lib/pages/persona/persona_profile.dart | 3 +- app/lib/pages/settings/page.dart | 3 +- backend/utils/llm.py | 2 +- backend/utils/social.py | 2 +- 7 files changed, 71 insertions(+), 6 deletions(-) diff --git a/app/lib/backend/preferences.dart b/app/lib/backend/preferences.dart index c05aade31f2..5dd3a64851c 100644 --- a/app/lib/backend/preferences.dart +++ b/app/lib/backend/preferences.dart @@ -424,4 +424,39 @@ class SharedPreferencesUtil { Future clear() async { return await _preferences?.clear() ?? false; } + + /// Clears all user-related preferences when signing out + Future 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 + } } diff --git a/app/lib/pages/chat/clone_chat_page.dart b/app/lib/pages/chat/clone_chat_page.dart index 3cdd7bf5c0f..cbe624e28a6 100644 --- a/app/lib/pages/chat/clone_chat_page.dart +++ b/app/lib/pages/chat/clone_chat_page.dart @@ -35,6 +35,7 @@ class CloneChatPageState extends State { var appProvider = Provider.of(context, listen: false); SharedPreferencesUtil().appsList = [selectedApp]; appProvider.setApps(); + // Set to null to chat with Omi by default appProvider.setSelectedChatAppId(null); if (!selectedApp.enabled) { await appProvider.toggleApp(selectedApp.id, true, null); diff --git a/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart b/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart index cd4861dc566..c15d4df3dee 100644 --- a/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart +++ b/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart @@ -201,6 +201,37 @@ class ChatAppsDropdownWidget extends StatelessWidget { height: 1, child: Divider(height: 1), ), + // Add Omi option to the dropdown + PopupMenuItem( + 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>((App app) { return PopupMenuItem( height: 40, diff --git a/app/lib/pages/persona/persona_profile.dart b/app/lib/pages/persona/persona_profile.dart index 0a1b55012ad..78059666830 100644 --- a/app/lib/pages/persona/persona_profile.dart +++ b/app/lib/pages/persona/persona_profile.dart @@ -750,8 +750,7 @@ class _PersonaProfilePageState extends State { TextButton( onPressed: () async { Navigator.of(context).pop(); - SharedPreferencesUtil().hasOmiDevice = null; - SharedPreferencesUtil().verifiedPersonaId = null; + await SharedPreferencesUtil().clearUserPreferences(); Provider.of(context, listen: false).setRouting(PersonaProfileRouting.no_device); await signOut(); Navigator.of(context).pop(); diff --git a/app/lib/pages/settings/page.dart b/app/lib/pages/settings/page.dart index 1716173567e..8d50db0b57f 100644 --- a/app/lib/pages/settings/page.dart +++ b/app/lib/pages/settings/page.dart @@ -111,8 +111,7 @@ class _SettingsPageState extends State { return getDialog(context, () { Navigator.of(context).pop(); }, () async { - SharedPreferencesUtil().hasOmiDevice = null; - SharedPreferencesUtil().verifiedPersonaId = null; + await SharedPreferencesUtil().clearUserPreferences(); Provider.of(context, listen: false).setRouting(PersonaProfileRouting.no_device); await signOut(); Navigator.of(context).pop(); diff --git a/backend/utils/llm.py b/backend/utils/llm.py index 0d971d6d514..18685f3bfec 100644 --- a/backend/utils/llm.py +++ b/backend/utils/llm.py @@ -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: diff --git a/backend/utils/social.py b/backend/utils/social.py index 2f3ba556415..a9d7ff8114b 100644 --- a/backend/utils/social.py +++ b/backend/utils/social.py @@ -124,7 +124,7 @@ def fetch_timeline(): tweets = [TwitterTweet( text=tweet['text'], created_at=tweet['created_at'], - id=tweet['id'] + id=tweet['tweet_id'] ) for tweet in timeline_data] return TwitterTimeline(timeline=tweets) From 1b03153c65c9d561f9e2d6f3e237bbad27e2feab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?th=E1=BB=8Bnh=20=28aider=29?= Date: Mon, 17 Mar 2025 10:20:38 +0700 Subject: [PATCH 5/5] refactor: Run migrate_facts and update_omi_persona_connected_accounts in async tasks --- backend/routers/apps.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/routers/apps.py b/backend/routers/apps.py index bd961d5dd8a..5ebd684b5ea 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -615,10 +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) - # Migrate facts from old user to new user - migrate_facts(old_id, uid) - - # 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"}