-
-
Notifications
You must be signed in to change notification settings - Fork 0
fix: voice STT full transcript and German device tools #2
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,6 +30,7 @@ import 'package:nova_assistant/services/platform_adaptation_service.dart'; | |
| import 'package:nova_assistant/utils/alarm_time_parser.dart'; | ||
| import 'package:nova_assistant/utils/agent_debug_log.dart'; | ||
| import 'package:nova_assistant/utils/message_limits.dart'; | ||
| import 'package:nova_assistant/utils/open_app_intent_parser.dart'; | ||
| import 'package:nova_assistant/services/memory_diagnostics_service.dart'; | ||
| import 'package:nova_assistant/services/session_history_reinjection.dart'; | ||
|
|
||
|
|
@@ -1578,6 +1579,29 @@ class ModelOrchestrator { | |
| return; | ||
| } | ||
|
|
||
| // Deterministic open-app shortcut (YouTube, Settings, etc.). | ||
| final openPackage = OpenAppIntentParser.tryParsePackage(query); | ||
| if (openPackage != null) { | ||
| _statusController.add('Opening app...'); | ||
| final result = await ToolExecutorService.instance.openApp(openPackage); | ||
| final ok = result['success'] == true; | ||
| yield InferenceResult( | ||
| text: ok | ||
| ? 'Opened $openPackage.' | ||
| : 'Could not open the app: ${result['error'] ?? 'unknown error'}', | ||
| model: selector.primaryHeavy, | ||
| isStreaming: false, | ||
| toolCalls: [ | ||
| { | ||
| 'name': 'open_app', | ||
| 'args': {'package': openPackage}, | ||
| 'result': result, | ||
| }, | ||
| ], | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| // Check if there are image attachments that need vision processing | ||
| final hasImageAttachments = attachments.any((att) { | ||
| if (att.type != AttachedDataType.file) return false; | ||
|
|
@@ -1702,6 +1726,7 @@ class ModelOrchestrator { | |
| } | ||
|
|
||
| final chatTools = _toolsForCreateChat(model, tools); | ||
| final textToolPrompt = chatTools.isEmpty && tools.isNotEmpty; | ||
| final wasNull = _activeChat == null; | ||
| if (wasNull) { | ||
| _isLoadingModel = true; | ||
|
|
@@ -1713,6 +1738,7 @@ class ModelOrchestrator { | |
| model, | ||
| ragContext, | ||
| _capContextInjection(attachmentContext), | ||
| textToolPrompt ? tools : null, | ||
| ), | ||
| tools: chatTools, | ||
| supportImage: model.hasVision && _activeModelSupportsImage, | ||
|
|
@@ -2283,6 +2309,7 @@ class ModelOrchestrator { | |
| NovaModel model, [ | ||
| String? ragContext, | ||
| String? attachmentContext, | ||
| List<Tool>? textTools, | ||
| ]) async { | ||
| final compact = | ||
| !kIsWeb && | ||
|
|
@@ -2312,6 +2339,15 @@ class ModelOrchestrator { | |
| buffer.write('\n\n${await _languageInstruction()}'); | ||
| } | ||
|
|
||
| // Compact Android prompts drop the role's tool list — restore capability | ||
| // so the model does not claim it cannot open apps / set alarms. | ||
| // Gate to Android only: web FC-off must not get "Android device" wording. | ||
| final isAndroid = | ||
| !kIsWeb && defaultTargetPlatform == TargetPlatform.android; | ||
| if (isAndroid && (compact || (textTools != null && textTools.isNotEmpty))) { | ||
| buffer.write(_deviceToolsCapabilitySuffix(textTools)); | ||
| } | ||
|
|
||
| if (_adultModeEnabled) { | ||
| buffer.write(AdultModePolicy.systemPromptSuffix(compact: compact)); | ||
| } | ||
|
|
@@ -2342,6 +2378,27 @@ class ModelOrchestrator { | |
| return buffer.toString(); | ||
| } | ||
|
|
||
| /// Short capability block for on-device tools (and text-JSON FC when native FC | ||
| /// is unavailable on Android Gemma 4). | ||
| String _deviceToolsCapabilitySuffix(List<Tool>? textTools) { | ||
| final names = (textTools ?? const <Tool>[]) | ||
| .map((t) => t.name) | ||
| .where((n) => n.isNotEmpty) | ||
| .toList(); | ||
| final listed = names.isEmpty | ||
| ? 'get_time, set_alarm, cancel_alarm, open_app, open_settings, search_web' | ||
| : names.join(', '); | ||
|
|
||
| return ' You control this Android device via tools: $listed. ' | ||
| 'When the user asks to open an app, set/cancel an alarm, open settings, ' | ||
| 'or search the web, call the tool immediately — do not say you lack ' | ||
| 'device access. ' | ||
| 'open_app packages: com.google.android.youtube, com.android.settings, ' | ||
| 'com.android.chrome. ' | ||
| 'If native function calling is unavailable, reply with only JSON like ' | ||
| '{"name":"open_app","arguments":{"package":"com.google.android.youtube"}}.'; | ||
| } | ||
|
Comment on lines
+2383
to
+2400
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The capability suffix prompt contains hardcoded instructions telling the model to call specific tools (like String _deviceToolsCapabilitySuffix(List<Tool>? textTools) {
final names = (textTools ?? const <Tool>[])
.map((t) => t.name)
.where((n) => n.isNotEmpty)
.toList();
final hasTools = names.isNotEmpty;
final listed = hasTools
? names.join(', ')
: 'get_time, set_alarm, cancel_alarm, open_app, open_settings, search_web';
final buffer = StringBuffer(' You control this Android device via tools: $listed. ');
final actions = <String>[];
if (!hasTools || names.contains('open_app')) actions.add('open an app');
if (!hasTools || names.contains('set_alarm') || names.contains('cancel_alarm')) actions.add('set/cancel an alarm');
if (!hasTools || names.contains('open_settings')) actions.add('open settings');
if (!hasTools || names.contains('search_web')) actions.add('search the web');
if (actions.isNotEmpty) {
final actionStr = actions.join(', ');
buffer.write('When the user asks to $actionStr, call the tool immediately — do not say you lack device access. ');
}
if (!hasTools || names.contains('open_app')) {
buffer.write('open_app packages: com.google.android.youtube, com.android.settings, com.android.chrome. ');
}
buffer.write('If native function calling is unavailable, reply with only JSON like '
'\'{"name":"open_app","arguments":{"package":"com.google.android.youtube"}}\'.');
return buffer.toString();
} |
||
|
|
||
| static AgentIdentity? _cachedIdentity; | ||
|
|
||
| static AgentIdentity? _getCachedIdentity() => _cachedIdentity; | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,72 @@ | ||||||||||||||||||||||||
| /// Resolves natural-language "open app" requests to Android package names. | ||||||||||||||||||||||||
| class OpenAppIntentParser { | ||||||||||||||||||||||||
| OpenAppIntentParser._(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// ASCII open verbs (safe with `\b`). Umlaut forms use normalized matching. | ||||||||||||||||||||||||
| static final RegExp _asciiOpenIntent = RegExp( | ||||||||||||||||||||||||
| r'\b(open|launch|start|starte|oeffne|oeffnen|aufmachen)\b', | ||||||||||||||||||||||||
| caseSensitive: false, | ||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: The This regex won’t match phrases with extra words between |
||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| static final RegExp _machAufIntent = RegExp( | ||||||||||||||||||||||||
| r'\bmach(?:en)?\s+\S+\s+auf\b', | ||||||||||||||||||||||||
| caseSensitive: false, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Common app aliases → package names. | ||||||||||||||||||||||||
| static const Map<String, String> knownApps = { | ||||||||||||||||||||||||
| 'youtube': 'com.google.android.youtube', | ||||||||||||||||||||||||
| 'yt': 'com.google.android.youtube', | ||||||||||||||||||||||||
| 'settings': 'com.android.settings', | ||||||||||||||||||||||||
| 'einstellungen': 'com.android.settings', | ||||||||||||||||||||||||
| 'chrome': 'com.android.chrome', | ||||||||||||||||||||||||
| 'maps': 'com.google.android.apps.maps', | ||||||||||||||||||||||||
| 'google maps': 'com.google.android.apps.maps', | ||||||||||||||||||||||||
| 'kamera': 'com.android.camera', | ||||||||||||||||||||||||
| 'camera': 'com.android.camera', | ||||||||||||||||||||||||
|
Comment on lines
+17
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Review alias list and matching strategy for coverage and accidental matches. Two small follow‑ups:
Suggested implementation: I can’t see the call site that uses
This preserves the current behavior for most aliases while reducing accidental matches for names like |
||||||||||||||||||||||||
| 'photos': 'com.google.android.apps.photos', | ||||||||||||||||||||||||
| 'galeria': 'com.google.android.apps.photos', | ||||||||||||||||||||||||
| 'gallery': 'com.google.android.apps.photos', | ||||||||||||||||||||||||
|
Comment on lines
+28
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The German word for gallery is "Galerie", but it is currently missing from
Suggested change
|
||||||||||||||||||||||||
| 'whatsapp': 'com.whatsapp', | ||||||||||||||||||||||||
| 'spotify': 'com.spotify.music', | ||||||||||||||||||||||||
| 'clock': 'com.android.deskclock', | ||||||||||||||||||||||||
| 'uhr': 'com.android.deskclock', | ||||||||||||||||||||||||
| 'wecker': 'com.android.deskclock', | ||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Normalize German umlauts so ASCII `\b` works ("öffne" → "oeffne"). | ||||||||||||||||||||||||
| /// Also prevents "geöffnet" → "geoeffnet" from matching `\boeffne\b`. | ||||||||||||||||||||||||
| static String _normalize(String query) { | ||||||||||||||||||||||||
| return query | ||||||||||||||||||||||||
| .toLowerCase() | ||||||||||||||||||||||||
| .replaceAll('ö', 'oe') | ||||||||||||||||||||||||
| .replaceAll('ä', 'ae') | ||||||||||||||||||||||||
| .replaceAll('ü', 'ue') | ||||||||||||||||||||||||
| .replaceAll('ß', 'ss'); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| static bool _hasOpenIntent(String query) { | ||||||||||||||||||||||||
| final normalized = _normalize(query); | ||||||||||||||||||||||||
| if (_asciiOpenIntent.hasMatch(normalized)) return true; | ||||||||||||||||||||||||
| if (_machAufIntent.hasMatch(normalized)) return true; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Returns a package name when [query] clearly asks to open a known app. | ||||||||||||||||||||||||
| static String? tryParsePackage(String query) { | ||||||||||||||||||||||||
| if (!_hasOpenIntent(query)) return null; | ||||||||||||||||||||||||
|
Comment on lines
+57
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The deterministic open-app shortcut currently matches queries even if they contain negations (e.g., "Don't open YouTube" or "öffne YouTube nicht"). This results in the app being opened when the user explicitly requested the opposite. We should add a negation check to return
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| final lower = query.toLowerCase(); | ||||||||||||||||||||||||
| // Prefer longer aliases first (e.g. "google maps" before "maps"). | ||||||||||||||||||||||||
| final aliases = knownApps.keys.toList() | ||||||||||||||||||||||||
| ..sort((a, b) => b.length.compareTo(a.length)); | ||||||||||||||||||||||||
| for (final alias in aliases) { | ||||||||||||||||||||||||
| if (lower.contains(alias)) { | ||||||||||||||||||||||||
| return knownApps[alias]; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+64
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Open-app shortcut currently bypasses any confirmation or disambiguation, which may be surprising on ambiguous queries.
Because this shortcut fires on any query that parses as both an open intent and a known alias, it will also trigger on informational prompts like “Explain how to open YouTube settings”, issuing an
open_appcall instead of letting the model respond. Consider tightening the heuristic (e.g., require the intent and alias to be close/within the same clause, or skip obvious “how to”/explanatory phrasing) so purely informational queries don’t invoke the tool.