Skip to content

Commit 0a05ae6

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/onboarding-knowledge-graph-flow
2 parents 85b2422 + 4eb2446 commit 0a05ae6

File tree

8 files changed

+195
-13
lines changed

8 files changed

+195
-13
lines changed

app/lib/main.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,6 @@ Future _init() async {
145145
}
146146
}
147147

148-
// Local agent proxy testing — port-forward svc/prod-omi-agent-proxy 8082:8080
149-
// Comment out when agent.omi.me DNS is configured.
150-
if (kDebugMode) {
151-
Env.overrideAgentProxyWsUrl('ws://localhost:8082/v1/agent/ws');
152-
}
153-
154148
FlutterForegroundTask.initCommunicationPort();
155149

156150
// Service manager

app/lib/pages/chat/page.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ class ChatPageState extends State<ChatPage> with AutomaticKeepAliveClientMixin {
278278
final child = AIMessage(
279279
showTypingIndicator: provider.showTypingIndicator &&
280280
chatIndex == provider.messages.length - 1,
281+
showThinkingAfterText: provider.agentThinkingAfterText,
281282
message: message,
282283
sendMessage: _sendMessageUtil,
283284
onAskOmi: (text) {

app/lib/pages/chat/widgets/ai_message.dart

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ Widget _buildThinkingIconWidget(String thinkingText, {double size = 15, Color co
182182

183183
class AIMessage extends StatefulWidget {
184184
final bool showTypingIndicator;
185+
final bool showThinkingAfterText;
185186
final ServerMessage message;
186187
final Function(String) sendMessage;
187188
final Function(String)? onAskOmi;
@@ -200,6 +201,7 @@ class AIMessage extends StatefulWidget {
200201
required this.setMessageNps,
201202
this.appSender,
202203
this.showTypingIndicator = false,
204+
this.showThinkingAfterText = false,
203205
});
204206

205207
@override
@@ -233,6 +235,7 @@ class _AIMessageState extends State<AIMessage> {
233235
widget.updateConversation,
234236
widget.setMessageNps,
235237
onAskOmi: widget.onAskOmi,
238+
showThinkingAfterText: widget.showThinkingAfterText,
236239
),
237240
),
238241
],
@@ -249,6 +252,7 @@ Widget buildMessageWidget(
249252
Function(ServerConversation) updateConversation,
250253
Function(int, {String? reason}) sendMessageNps, {
251254
Function(String)? onAskOmi,
255+
bool showThinkingAfterText = false,
252256
}) {
253257
if (message.memories.isNotEmpty) {
254258
return MemoriesMessageWidget(
@@ -273,6 +277,7 @@ Widget buildMessageWidget(
273277
} else {
274278
return NormalMessageWidget(
275279
showTypingIndicator: showTypingIndicator,
280+
showThinkingAfterText: showThinkingAfterText,
276281
thinkings: message.thinkings,
277282
messageText: message.text.decodeString,
278283
message: message,
@@ -423,6 +428,7 @@ class DaySummaryWidget extends StatelessWidget {
423428

424429
class NormalMessageWidget extends StatefulWidget {
425430
final bool showTypingIndicator;
431+
final bool showThinkingAfterText;
426432
final String messageText;
427433
final List<String> thinkings;
428434
final ServerMessage message;
@@ -437,6 +443,7 @@ class NormalMessageWidget extends StatefulWidget {
437443
required this.message,
438444
required this.setMessageNps,
439445
required this.createdAt,
446+
this.showThinkingAfterText = false,
440447
this.thinkings = const [],
441448
this.onAskOmi,
442449
});
@@ -591,6 +598,35 @@ class _NormalMessageWidgetState extends State<NormalMessageWidget> {
591598
},
592599
),
593600
),
601+
if (widget.showTypingIndicator && widget.messageText.isNotEmpty && widget.showThinkingAfterText)
602+
Padding(
603+
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
604+
child: Row(
605+
mainAxisSize: MainAxisSize.min,
606+
children: [
607+
if (currentAppId != null) ...[
608+
_buildAppIcon(context, currentAppId, size: 15),
609+
const SizedBox(width: 6),
610+
] else ...[
611+
_buildThinkingIconWidget(displayThinkingText, size: 15),
612+
const SizedBox(width: 6),
613+
],
614+
Flexible(
615+
child: ShimmerWithTimeout(
616+
baseColor: Colors.white,
617+
highlightColor: Colors.grey,
618+
child: Text(
619+
overflow: TextOverflow.fade,
620+
maxLines: 1,
621+
softWrap: false,
622+
displayThinkingText,
623+
style: const TextStyle(color: Colors.white, fontSize: 15),
624+
),
625+
),
626+
),
627+
],
628+
),
629+
),
594630
if (widget.message.chartData != null)
595631
Padding(
596632
padding: const EdgeInsets.symmetric(horizontal: 4),

app/lib/pages/onboarding/speech_profile_widget.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,9 @@ class _SpeechProfileWidgetState extends State<SpeechProfileWidget> with TickerPr
284284

285285
// Title
286286
Text(
287-
context.l10n.speechProfile,
287+
provider.startedRecording && !provider.profileCompleted
288+
? 'Answer with your voice:'
289+
: 'Please find a quiet place',
288290
style: const TextStyle(
289291
color: Colors.white,
290292
fontSize: 28,
@@ -301,7 +303,7 @@ class _SpeechProfileWidgetState extends State<SpeechProfileWidget> with TickerPr
301303
if (!provider.startedRecording) ...[
302304
// Intro text
303305
Text(
304-
context.l10n.speechProfileIntro,
306+
'Omi needs to learn your goals and your voice. Answer questions with your voice. You\'ll be able to modify it later.',
305307
textAlign: TextAlign.center,
306308
style: TextStyle(
307309
color: Colors.white.withValues(alpha: 0.6),
@@ -481,10 +483,10 @@ class _SpeechProfileWidgetState extends State<SpeechProfileWidget> with TickerPr
481483
provider.currentQuestion,
482484
style: const TextStyle(
483485
color: Colors.white,
484-
fontSize: 18,
486+
fontSize: 24,
485487
height: 1.3,
486488
fontFamily: 'Manrope',
487-
fontWeight: FontWeight.w500,
489+
fontWeight: FontWeight.w600,
488490
),
489491
textAlign: TextAlign.center,
490492
),

app/lib/providers/message_provider.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class MessageProvider extends ChangeNotifier {
5050
bool showTypingIndicator = false;
5151
bool sendingMessage = false;
5252
double aiStreamProgress = 1.0;
53+
bool agentThinkingAfterText = false;
5354

5455
String firstTimeLoadingText = '';
5556

@@ -678,9 +679,20 @@ class MessageProvider extends ChangeNotifier {
678679
}
679680
}
680681

681-
await for (var event in _agentChatService.sendQuery(text)) {
682+
// Build prompt with recent conversation history so the agent has context
683+
final history = messages.where((m) => m.text.isNotEmpty).toList().reversed.take(10).toList().reversed.toList();
684+
final historyLines =
685+
history.map((m) => '${m.sender == MessageSender.human ? "User" : "Assistant"}: ${m.text}').join('\n');
686+
final prompt = historyLines.isEmpty ? text : '$historyLines\n\nUser: $text';
687+
688+
await for (var event in _agentChatService.sendQuery(prompt)) {
682689
switch (event.type) {
683690
case AgentChatEventType.textDelta:
691+
if (agentThinkingAfterText) {
692+
textBuffer += '\n\n';
693+
agentThinkingAfterText = false;
694+
notifyListeners();
695+
}
684696
textBuffer += event.text;
685697
timer ??= Timer.periodic(const Duration(milliseconds: 100), (_) {
686698
flushBuffer();
@@ -689,6 +701,9 @@ class MessageProvider extends ChangeNotifier {
689701
case AgentChatEventType.toolActivity:
690702
// Show tool activity as thinking
691703
flushBuffer();
704+
if (message.text.isNotEmpty) {
705+
agentThinkingAfterText = true;
706+
}
692707
message.thinkings.add(event.text);
693708
notifyListeners();
694709
break;
@@ -717,6 +732,7 @@ class MessageProvider extends ChangeNotifier {
717732
} finally {
718733
timer?.cancel();
719734
flushBuffer();
735+
agentThinkingAfterText = false;
720736
aiStreamProgress = 1.0;
721737
setShowTypingIndicator(false);
722738
setSendingMessage(false);

backend/agent-proxy/main.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@
33
44
Auth: Firebase ID token in Authorization header (Bearer <token>) during WS upgrade.
55
Flow: validate token → fetch VM from Firestore → connect to VM WS → bidirectional pump.
6+
History: fetches last 10 agent messages from Firestore and prepends to prompt.
67
"""
78

89
import asyncio
10+
import json
911
import logging
1012
import os
13+
import uuid
14+
from datetime import datetime, timezone
1115

1216
import firebase_admin
1317
import websockets
1418
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
1519
from firebase_admin import auth, credentials, firestore
20+
from google.cloud.firestore_v1 import Query
1621

1722
logging.basicConfig(level=logging.INFO)
1823
logger = logging.getLogger(__name__)
@@ -28,6 +33,9 @@
2833

2934
app = FastAPI()
3035

36+
AGENT_PLUGIN_ID = '__agent__'
37+
HISTORY_LIMIT = 10
38+
3139

3240
@app.get("/health")
3341
def health():
@@ -41,6 +49,59 @@ def _get_agent_vm(uid: str) -> dict | None:
4149
return None
4250

4351

52+
def _fetch_chat_history(uid: str) -> list:
53+
"""Fetch last N agent messages from Firestore, returned oldest-first."""
54+
messages_ref = (
55+
db.collection('users')
56+
.document(uid)
57+
.collection('messages')
58+
.where('plugin_id', '==', AGENT_PLUGIN_ID)
59+
.order_by('created_at', direction=Query.DESCENDING)
60+
.limit(HISTORY_LIMIT)
61+
)
62+
messages = []
63+
for doc in messages_ref.stream():
64+
data = doc.to_dict()
65+
messages.append(
66+
{
67+
'sender': data.get('sender', ''),
68+
'text': data.get('text', ''),
69+
}
70+
)
71+
return list(reversed(messages))
72+
73+
74+
def _save_message(uid: str, text: str, sender: str):
75+
"""Save a message to Firestore under the agent plugin_id."""
76+
msg_data = {
77+
'id': str(uuid.uuid4()),
78+
'text': text,
79+
'created_at': datetime.now(timezone.utc),
80+
'sender': sender,
81+
'plugin_id': AGENT_PLUGIN_ID,
82+
'type': 'text',
83+
'from_external_integration': False,
84+
'memories_id': [],
85+
'files_id': [],
86+
}
87+
db.collection('users').document(uid).collection('messages').add(msg_data)
88+
89+
90+
def _build_prompt_with_history(prompt: str, history: list) -> str:
91+
"""Prepend conversation history to the current prompt."""
92+
if not history:
93+
return prompt
94+
95+
lines = ["<conversation_history>"]
96+
for msg in history:
97+
role = "Human" if msg['sender'] == 'human' else "Assistant"
98+
lines.append(f"{role}: {msg['text']}")
99+
lines.append("</conversation_history>")
100+
lines.append("")
101+
lines.append(prompt)
102+
return "\n".join(lines)
103+
104+
44105
@app.websocket("/v1/agent/ws")
45106
async def agent_ws(websocket: WebSocket):
46107
# Validate Firebase token from Authorization header
@@ -76,17 +137,50 @@ async def agent_ws(websocket: WebSocket):
76137
async def phone_to_vm():
77138
try:
78139
async for msg in websocket.iter_text():
140+
try:
141+
data = json.loads(msg)
142+
if data.get('type') == 'query':
143+
prompt = data.get('prompt', '')
144+
# Fetch history before saving new message
145+
history = await asyncio.to_thread(_fetch_chat_history, uid)
146+
data['prompt'] = _build_prompt_with_history(prompt, history)
147+
msg = json.dumps(data)
148+
# Save user message
149+
await asyncio.to_thread(_save_message, uid, prompt, 'human')
150+
logger.info(f"[agent-proxy] uid={uid} query with {len(history)} history messages")
151+
except (json.JSONDecodeError, Exception) as e:
152+
logger.warning(f"[agent-proxy] failed to process message: {e}")
79153
await vm_ws.send(msg)
80154
except (WebSocketDisconnect, Exception):
81155
pass
82156

83157
async def vm_to_phone():
158+
response_text = ''
84159
try:
85160
async for msg in vm_ws:
86161
text = msg if isinstance(msg, str) else msg.decode()
87162
await websocket.send_text(text)
163+
# Collect response for saving
164+
try:
165+
event = json.loads(text)
166+
evt_type = event.get('type')
167+
evt_text = event.get('text', '') or event.get('content', '') or ''
168+
if evt_type == 'text_delta':
169+
response_text += evt_text
170+
elif evt_type == 'result' and evt_text and not response_text:
171+
response_text = evt_text
172+
except json.JSONDecodeError:
173+
pass
88174
except Exception:
89175
pass
176+
finally:
177+
# Save AI response
178+
if response_text.strip():
179+
try:
180+
await asyncio.to_thread(_save_message, uid, response_text.strip(), 'ai')
181+
logger.info(f"[agent-proxy] uid={uid} saved AI response ({len(response_text)} chars)")
182+
except Exception as e:
183+
logger.warning(f"[agent-proxy] failed to save AI response: {e}")
90184

91185
t1 = asyncio.create_task(phone_to_vm())
92186
t2 = asyncio.create_task(vm_to_phone())

backend/routers/users.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@
6363
delete_user_person_speech_sample,
6464
)
6565
from utils.webhooks import webhook_first_time_setup
66+
from database.action_items import get_action_items as get_standalone_action_items
67+
from google.cloud import firestore as cloud_firestore
6668
import logging
6769

6870
logger = logging.getLogger(__name__)
@@ -1133,3 +1135,40 @@ def get_llm_top_features(
11331135
Returns the top N features sorted by total token consumption.
11341136
"""
11351137
return llm_usage_db.get_top_features(uid, days=days, limit=limit)
1138+
1139+
1140+
@router.get('/v1/users/export', tags=['v1'])
1141+
def export_all_user_data(uid: str = Depends(auth.get_current_user_uid)):
1142+
"""Export all user data for GDPR/CCPA compliance."""
1143+
profile = get_user_profile(uid)
1144+
1145+
conversations = conversations_db.get_conversations(uid, limit=10000, offset=0, include_discarded=True)
1146+
1147+
memories_list = memories_db.get_memories(uid, limit=10000, offset=0)
1148+
1149+
people = get_people(uid)
1150+
1151+
action_items = get_standalone_action_items(uid, limit=10000, offset=0)
1152+
1153+
# Get chat messages (all, no app_id filter)
1154+
chat_messages = []
1155+
try:
1156+
user_ref = chat_db.db.collection('users').document(uid)
1157+
msgs_ref = user_ref.collection('messages').order_by(
1158+
'created_at', direction=cloud_firestore.Query.DESCENDING
1159+
).limit(10000)
1160+
for doc in msgs_ref.stream():
1161+
msg = doc.to_dict()
1162+
msg['id'] = doc.id
1163+
chat_messages.append(msg)
1164+
except Exception as e:
1165+
logger.warning(f'export_all_user_data: failed to fetch chat messages: {e}')
1166+
1167+
return {
1168+
'profile': profile if profile else {},
1169+
'conversations': conversations,
1170+
'memories': memories_list,
1171+
'people': people,
1172+
'action_items': action_items,
1173+
'chat_messages': chat_messages,
1174+
}

web/app/src/lib/api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,8 +1743,8 @@ export async function deleteMcpApiKey(keyId: string): Promise<void> {
17431743
/**
17441744
* Export all conversations as JSON
17451745
*/
1746-
export async function exportAllData(): Promise<{ conversations: unknown[] }> {
1747-
return fetchWithAuth<{ conversations: unknown[] }>('/v1/conversations?limit=10000&offset=0');
1746+
export async function exportAllData(): Promise<Record<string, unknown>> {
1747+
return fetchWithAuth<Record<string, unknown>>('/v1/users/export');
17481748
}
17491749

17501750
/**

0 commit comments

Comments
 (0)