Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
9da8845
Add focus analysis handler for desktop screen_frame messages (#5396)
beastoin Mar 7, 2026
4206abe
Add FocusResultEvent message type for desktop proactive AI (#5396)
beastoin Mar 7, 2026
dc9d765
Add screen_frame dispatcher to /v4/listen for desktop focus analysis …
beastoin Mar 7, 2026
f102156
Add 26 unit tests for desktop focus analysis (#5396)
beastoin Mar 7, 2026
e8690fa
Merge focus handler (kai backend) into #5396 trunk
beastoin Mar 7, 2026
616ca11
Add task extraction handler for desktop screen analysis
beastoin Mar 7, 2026
a0da068
Add memory extraction handler for desktop screen analysis
beastoin Mar 7, 2026
8d2b0f8
Add contextual advice handler for desktop screen analysis
beastoin Mar 7, 2026
51ed561
Add live notes handler for desktop transcript processing
beastoin Mar 7, 2026
4e3968f
Add user profile generation handler for desktop
beastoin Mar 7, 2026
a2138ca
Add task reranking and deduplication handlers for desktop
beastoin Mar 7, 2026
ecf3523
Add message event classes for all desktop handler types
beastoin Mar 7, 2026
3cac01d
Add full desktop dispatcher for screen_frame and text message types
beastoin Mar 7, 2026
5875ba7
Add unit tests for task extraction handler (18 tests)
beastoin Mar 7, 2026
aa330be
Add unit tests for memory extraction handler (14 tests)
beastoin Mar 7, 2026
8c0ebbb
Add unit tests for advice handler (14 tests)
beastoin Mar 7, 2026
5a32e04
Add unit tests for live notes handler (10 tests)
beastoin Mar 7, 2026
bbfe287
Add unit tests for profile handler (9 tests)
beastoin Mar 7, 2026
10112df
Add unit tests for task rerank and dedup handlers (16 tests)
beastoin Mar 7, 2026
7dd8fa5
Add all desktop handler tests to test.sh
beastoin Mar 7, 2026
cc43138
Add BackendProactiveService for server-side proactive AI (#5396)
beastoin Mar 8, 2026
056a352
Wire FocusAssistant to BackendProactiveService instead of GeminiClien…
beastoin Mar 8, 2026
a3a8dfa
Create BackendProactiveService in ProactiveAssistantsPlugin lifecycle…
beastoin Mar 8, 2026
01e323d
Update FocusTestRunnerWindow for new FocusAssistant init signature (#…
beastoin Mar 8, 2026
bc3abaf
Add all 8 message types to BackendProactiveService (#5396)
beastoin Mar 8, 2026
d1fcb80
Wire TaskAssistant thin client for Phase 2 (#5396)
beastoin Mar 8, 2026
cc33cbd
Wire MemoryAssistant thin client for Phase 2 (#5396)
beastoin Mar 8, 2026
0456a62
Wire AdviceAssistant thin client for Phase 2 (#5396)
beastoin Mar 8, 2026
2bad746
Wire TaskDeduplicationService thin client for Phase 2 (#5396)
beastoin Mar 8, 2026
0e0492b
Wire TaskPrioritizationService thin client for Phase 2 (#5396)
beastoin Mar 8, 2026
289e41a
Wire AIUserProfileService thin client for Phase 2 (#5396)
beastoin Mar 8, 2026
4c92d5b
Wire ProactiveAssistantsPlugin to pass backendService to all assistan…
beastoin Mar 8, 2026
aba6be4
Wire LiveNotesMonitor thin client for Phase 2 (#5396)
beastoin Mar 9, 2026
b985003
Wire LiveNotesMonitor in ProactiveAssistantsPlugin (#5396)
beastoin Mar 9, 2026
61ee9c3
Swap dev plist to based-hardware-dev Firebase project
beastoin Mar 10, 2026
7796471
Read Firebase API key from plist at runtime instead of hardcoding
beastoin Mar 10, 2026
9e8c3a0
Fix dev.sh to copy dev Firebase plist instead of prod
beastoin Mar 10, 2026
e3cfbf1
Fix reset-and-run.sh to copy dev Firebase plist instead of prod
beastoin Mar 10, 2026
00ad7c8
Log fatal warning when dev build falls back to prod Firebase key
beastoin Mar 10, 2026
6d8b57e
Crash dev builds when Firebase plist is missing instead of falling ba…
beastoin Mar 10, 2026
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
99 changes: 99 additions & 0 deletions backend/models/message_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,102 @@ def to_json(self):
j["type"] = self.event_type
del j["event_type"]
return j


# Desktop proactive AI events (Phase 2 — #5396)


class FocusResultEvent(MessageEvent):
event_type: str = "focus_result"
frame_id: str
status: str
app_or_site: str
description: str
message: Optional[str] = None

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j


class TasksExtractedEvent(MessageEvent):
event_type: str = "tasks_extracted"
frame_id: str
tasks: List = []

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j


class MemoriesExtractedEvent(MessageEvent):
event_type: str = "memories_extracted"
frame_id: str
memories: List = []

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j


class AdviceExtractedEvent(MessageEvent):
event_type: str = "advice_extracted"
frame_id: str
advice: Optional[Any] = None

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j


class LiveNoteEvent(MessageEvent):
event_type: str = "live_note"
text: str

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j


class ProfileUpdatedEvent(MessageEvent):
event_type: str = "profile_updated"
profile_text: str

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j


class RerankCompleteEvent(MessageEvent):
event_type: str = "rerank_complete"
updated_tasks: List = []

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j


class DedupCompleteEvent(MessageEvent):
event_type: str = "dedup_complete"
deleted_ids: List = []
reason: str = ""

def to_json(self):
j = self.model_dump(mode="json")
j["type"] = self.event_type
del j["event_type"]
return j
107 changes: 107 additions & 0 deletions backend/routers/transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,24 @@
TranscriptSegment,
)
from models.message_event import (
AdviceExtractedEvent,
ConversationEvent,
DedupCompleteEvent,
FocusResultEvent,
FREEMIUM_ACTION_SETUP_ON_DEVICE_STT,
FreemiumThresholdReachedEvent,
LastConversationEvent,
LiveNoteEvent,
MemoriesExtractedEvent,
MessageEvent,
MessageServiceStatusEvent,
PhotoDescribedEvent,
PhotoProcessingEvent,
ProfileUpdatedEvent,
RerankCompleteEvent,
SegmentsDeletedEvent,
SpeakerLabelSuggestionEvent,
TasksExtractedEvent,
TranslationEvent,
)
from models.transcript_segment import Translation
Expand Down Expand Up @@ -100,6 +108,13 @@
SPEAKER_MATCH_THRESHOLD,
)
from utils.speaker_sample_migration import maybe_migrate_person_samples
from utils.desktop.advice import generate_advice
from utils.desktop.focus import analyze_focus
from utils.desktop.live_notes import generate_live_note
from utils.desktop.memories import extract_memories
from utils.desktop.profile import generate_profile
from utils.desktop.task_ops import dedup_tasks, rerank_tasks
from utils.desktop.tasks import extract_tasks
from utils.log_sanitizer import sanitize, sanitize_pii

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -2127,6 +2142,98 @@ async def close_soniox_profile():
logger.info(
f"Speaker assignment ignored: missing speaker_id/person_id/person_name. {uid} {session_id}"
)
# Desktop proactive AI — screen_frame analysis (#5396)
elif json_data.get('type') == 'screen_frame':
frame_id = json_data.get('frame_id', '')
image_b64 = json_data.get('image_b64', '')
analyze_types = json_data.get('analyze', [])
sf_app = json_data.get('app_name', '')
sf_wtitle = json_data.get('window_title', '')
if not image_b64:
logger.warning(f"screen_frame missing image_b64 {uid} {session_id}")
else:
# Fan out to parallel handlers per analyze type
if 'focus' in analyze_types:
async def _handle_focus(fid, img, app, wtitle):
try:
result = await analyze_focus(uid=uid, image_b64=img, app_name=app, window_title=wtitle)
_send_message_event(FocusResultEvent(
frame_id=fid, status=result['status'], app_or_site=result['app_or_site'],
description=result['description'], message=result.get('message'),
))
except Exception as e:
logger.error(f"Focus analysis failed: {e} {uid} {session_id}")
spawn(_handle_focus(frame_id, image_b64, sf_app, sf_wtitle))

if 'tasks' in analyze_types:
async def _handle_tasks(fid, img, app, wtitle):
try:
result = await extract_tasks(uid=uid, image_b64=img, app_name=app, window_title=wtitle)
_send_message_event(TasksExtractedEvent(frame_id=fid, tasks=result.get('tasks', [])))
except Exception as e:
logger.error(f"Task extraction failed: {e} {uid} {session_id}")
spawn(_handle_tasks(frame_id, image_b64, sf_app, sf_wtitle))

if 'memories' in analyze_types:
async def _handle_memories(fid, img, app, wtitle):
try:
result = await extract_memories(uid=uid, image_b64=img, app_name=app, window_title=wtitle)
_send_message_event(MemoriesExtractedEvent(frame_id=fid, memories=result.get('memories', [])))
except Exception as e:
logger.error(f"Memory extraction failed: {e} {uid} {session_id}")
spawn(_handle_memories(frame_id, image_b64, sf_app, sf_wtitle))

if 'advice' in analyze_types:
async def _handle_advice(fid, img, app, wtitle):
try:
result = await generate_advice(uid=uid, image_b64=img, app_name=app, window_title=wtitle)
_send_message_event(AdviceExtractedEvent(
frame_id=fid, advice=result.get('advice'),
))
except Exception as e:
logger.error(f"Advice generation failed: {e} {uid} {session_id}")
spawn(_handle_advice(frame_id, image_b64, sf_app, sf_wtitle))

# Desktop proactive AI — text-only message types (#5396)
elif json_data.get('type') == 'live_notes_text':
async def _handle_live_notes(text, ctx):
try:
result = await generate_live_note(text=text, session_context=ctx)
if result.get('text'):
_send_message_event(LiveNoteEvent(text=result['text']))
except Exception as e:
logger.error(f"Live note generation failed: {e} {uid} {session_id}")
spawn(_handle_live_notes(json_data.get('text', ''), json_data.get('session_context', '')))

elif json_data.get('type') == 'profile_request':
async def _handle_profile():
try:
result = await generate_profile(uid=uid)
_send_message_event(ProfileUpdatedEvent(profile_text=result['profile_text']))
except Exception as e:
logger.error(f"Profile generation failed: {e} {uid} {session_id}")
spawn(_handle_profile())

elif json_data.get('type') == 'task_rerank':
async def _handle_rerank():
try:
result = await rerank_tasks(uid=uid)
_send_message_event(RerankCompleteEvent(updated_tasks=result['updated_tasks']))
except Exception as e:
logger.error(f"Task reranking failed: {e} {uid} {session_id}")
spawn(_handle_rerank())

elif json_data.get('type') == 'task_dedup':
async def _handle_dedup():
try:
result = await dedup_tasks(uid=uid)
_send_message_event(DedupCompleteEvent(
deleted_ids=result['deleted_ids'], reason=result['reason'],
))
except Exception as e:
logger.error(f"Task dedup failed: {e} {uid} {session_id}")
spawn(_handle_dedup())

except json.JSONDecodeError:
logger.info(
f"Received non-json text message: {sanitize(message.get('text'))} {uid} {session_id}"
Expand Down
7 changes: 7 additions & 0 deletions backend/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ pytest tests/unit/test_pusher_heartbeat.py -v
pytest tests/unit/test_desktop_updates.py -v
pytest tests/unit/test_translation_optimization.py -v
pytest tests/unit/test_conversation_source_unknown.py -v
pytest tests/unit/test_desktop_focus.py -v
pytest tests/unit/test_desktop_tasks.py -v
pytest tests/unit/test_desktop_memories.py -v
pytest tests/unit/test_desktop_advice.py -v
pytest tests/unit/test_desktop_live_notes.py -v
pytest tests/unit/test_desktop_profile.py -v
pytest tests/unit/test_desktop_task_ops.py -v
Loading