diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/ModelRequiredOverlay.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/ModelRequiredOverlay.kt index 67e0779d8..9f99a03e1 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/ModelRequiredOverlay.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/components/ModelRequiredOverlay.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.AutoAwesome import androidx.compose.material.icons.filled.GraphicEq import androidx.compose.material.icons.filled.Lock diff --git a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt index 06f063ee1..d9cad72bf 100644 --- a/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt +++ b/examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/settings/SettingsScreen.kt @@ -36,6 +36,8 @@ import androidx.compose.material.icons.outlined.RestartAlt import androidx.compose.material.icons.outlined.Visibility import androidx.compose.material.icons.outlined.VisibilityOff import androidx.compose.material.icons.outlined.Warning +import androidx.compose.material.icons.outlined.Build +import androidx.compose.material.icons.outlined.Add import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -809,7 +811,7 @@ fun ToolSettingsSection() { val application = context.applicationContext as Application val toolViewModel = remember { ToolSettingsViewModel.getInstance(application) } val toolState by toolViewModel.uiState.collectAsStateWithLifecycle() - + SettingsSection(title = "Tool Calling") { // Enable/Disable Toggle Row( @@ -839,10 +841,10 @@ fun ToolSettingsSection() { ), ) } - + if (toolState.toolCallingEnabled) { HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - + // Registered Tools Count Row( modifier = Modifier @@ -873,7 +875,7 @@ fun ToolSettingsSection() { color = AppColors.primaryAccent, ) } - + // Tool List (if any) if (toolState.registeredTools.isNotEmpty()) { toolState.registeredTools.forEach { tool -> @@ -891,9 +893,9 @@ fun ToolSettingsSection() { } } } - + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - + // Action Buttons Row( modifier = Modifier.fillMaxWidth(), @@ -915,7 +917,7 @@ fun ToolSettingsSection() { Spacer(modifier = Modifier.width(4.dp)) Text(if (toolState.isLoading) "Loading..." else "Add Demo Tools") } - + if (toolState.registeredTools.isNotEmpty()) { OutlinedButton( onClick = { toolViewModel.clearAllTools() }, @@ -934,7 +936,7 @@ fun ToolSettingsSection() { } } } - + Spacer(modifier = Modifier.height(8.dp)) Text( text = "Demo tools: get_weather (Open-Meteo API), get_current_time, calculate", @@ -943,4 +945,4 @@ fun ToolSettingsSection() { ) } } -} \ No newline at end of file +} diff --git a/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart b/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart index e89928b2f..1bd27ecd9 100644 --- a/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart +++ b/examples/flutter/RunAnywhereAI/lib/features/chat/chat_interface_view.dart @@ -16,6 +16,7 @@ import 'package:runanywhere_ai/features/models/model_selection_sheet.dart'; import 'package:runanywhere_ai/features/models/model_status_components.dart'; import 'package:runanywhere_ai/features/models/model_types.dart'; import 'package:runanywhere_ai/features/settings/tool_settings_view_model.dart'; +import 'package:runanywhere_ai/features/structured_output/structured_output_view.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// ChatInterfaceView (mirroring iOS ChatInterfaceView.swift) @@ -452,7 +453,18 @@ class _ChatInterfaceViewState extends State { return Scaffold( appBar: AppBar( title: const Text('Chat'), - actions: [ + actions: [ + IconButton( + icon: const Icon(Icons.data_object), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const StructuredOutputView(), + ), + ); + }, + tooltip: 'Structured Output Examples', + ), if (_messages.isNotEmpty) IconButton( icon: const Icon(Icons.delete_outline), diff --git a/examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart b/examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart new file mode 100644 index 000000000..3dac1e924 --- /dev/null +++ b/examples/flutter/RunAnywhereAI/lib/features/structured_output/structured_output_view.dart @@ -0,0 +1,685 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:runanywhere/runanywhere.dart' as sdk; +import 'package:runanywhere_ai/core/design_system/app_colors.dart'; +import 'package:runanywhere_ai/core/design_system/app_spacing.dart'; +import 'package:runanywhere_ai/core/design_system/typography.dart'; + +/// StructuredOutputView - Demonstrates structured output functionality +/// +/// Provides examples for both generate() and generateStream() with +/// various JSON schema templates and prompt templates. +class StructuredOutputView extends StatefulWidget { + const StructuredOutputView({super.key}); + + @override + State createState() => _StructuredOutputViewState(); +} + +class _StructuredOutputViewState extends State { + final TextEditingController _promptController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + // Selected example + int _selectedExampleIndex = 0; + bool _useStream = false; + + // State + bool _isGenerating = false; + String? _errorMessage; + String? _rawResponse; + Map? _structuredData; + + // Examples with both schema and prompt templates + final List _examples = [ + StructuredOutputExample( + name: 'Recipe', + typeName: 'Recipe', + schema: '''{ + "type": "object", + "properties": { + "name": { "type": "string" }, + "ingredients": { "type": "array", "items": { "type": "string" } }, + "steps": { "type": "array", "items": { "type": "string" } }, + "cookingTime": { "type": "integer" } + }, + "required": ["name", "ingredients", "steps", "cookingTime"] + }''', + promptTemplates: [ + 'Generate a recipe for homemade pasta', + 'Give me a quick breakfast recipe with eggs', + 'Create a vegan dessert recipe', + ], + ), + StructuredOutputExample( + name: 'User Profile', + typeName: 'User', + schema: '''{ + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" }, + "email": { "type": "string" }, + "location": { "type": "string" } + }, + "required": ["name", "age"] + }''', + promptTemplates: [ + 'Create a user profile for John', + 'Generate a fictional user from New York', + 'Make a profile for a software developer', + ], + ), + StructuredOutputExample( + name: 'Weather', + typeName: 'WeatherResponse', + schema: '''{ + "type": "object", + "properties": { + "temperature": { "type": "number" }, + "condition": { "type": "string" }, + "humidity": { "type": "integer" }, + "windSpeed": { "type": "number" } + }, + "required": ["temperature", "condition"] + }''', + promptTemplates: [ + 'What is the weather in Paris?', + 'Give me weather info for Tokyo', + 'Weather forecast for London today', + ], + ), + StructuredOutputExample( + name: 'Product List', + typeName: 'ProductList', + schema: '''{ + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "price": { "type": "number" }, + "category": { "type": "string" } + } + } + }''', + promptTemplates: [ + 'List 3 electronics products with prices', + 'Generate 5 grocery items', + 'Create a list of 4 books with prices', + ], + ), + StructuredOutputExample( + name: 'Book Summary', + typeName: 'BookSummary', + schema: '''{ + "type": "object", + "properties": { + "title": { "type": "string" }, + "author": { "type": "string" }, + "genre": { "type": "string" }, + "year": { "type": "integer" }, + "rating": { "type": "number" } + }, + "required": ["title", "author"] + }''', + promptTemplates: [ + 'Summarize the book 1984', + 'Give me info about Harry Potter', + 'Create a summary for The Great Gatsby', + ], + ), + StructuredOutputExample( + name: 'Task', + typeName: 'Task', + schema: '''{ + "type": "object", + "properties": { + "id": { "type": "string" }, + "title": { "type": "string" }, + "completed": { "type": "boolean" }, + "priority": { "type": "string", "enum": ["low", "medium", "high"] }, + "dueDate": { "type": "string" } + }, + "required": ["id", "title", "completed"] + }''', + promptTemplates: [ + 'Create a task to buy groceries', + 'Generate a high priority task', + 'Make a task for meeting tomorrow', + ], + ), + ]; + + @override + void initState() { + super.initState(); + _promptController.text = _examples[0].promptTemplates[0]; + } + + @override + void dispose() { + _promptController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _onExampleChanged(int? index) { + if (index != null) { + setState(() { + _selectedExampleIndex = index; + _promptController.text = _examples[index].promptTemplates[0]; + _rawResponse = null; + _structuredData = null; + _errorMessage = null; + }); + } + } + + void _onPromptTemplateSelected(String prompt) { + setState(() { + _promptController.text = prompt; + }); + } + + Future _generate() async { + if (_promptController.text.isEmpty) { + setState(() { + _errorMessage = 'Please enter a prompt'; + }); + return; + } + + if (!sdk.RunAnywhere.isModelLoaded) { + setState(() { + _errorMessage = 'LLM model not loaded. Please load a model first.'; + }); + return; + } + + setState(() { + _isGenerating = true; + _errorMessage = null; + _rawResponse = null; + _structuredData = null; + }); + + try { + final example = _examples[_selectedExampleIndex]; + + if (_useStream) { + await _generateStream(example); + } else { + await _generateNonStream(example); + } + } catch (e) { + setState(() { + _errorMessage = e.toString(); + }); + } finally { + setState(() { + _isGenerating = false; + }); + } + } + + Future _generateNonStream(StructuredOutputExample example) async { + final result = await sdk.RunAnywhere.generate( + _promptController.text, + options: sdk.LLMGenerationOptions( + maxTokens: 1000, + temperature: 0.7, + structuredOutput: sdk.StructuredOutputConfig( + typeName: example.typeName, + schema: example.schema, + ), + ), + ); + + setState(() { + _rawResponse = result.text; + _structuredData = result.structuredData; + }); + } + + Future _generateStream(StructuredOutputExample example) async { + final streamResult = await sdk.RunAnywhere.generateStream( + _promptController.text, + options: sdk.LLMGenerationOptions( + maxTokens: 1000, + temperature: 0.7, + structuredOutput: sdk.StructuredOutputConfig( + typeName: example.typeName, + schema: example.schema, + ), + ), + ); + + final buffer = StringBuffer(); + + await for (final token in streamResult.stream) { + buffer.write(token); + setState(() { + _rawResponse = buffer.toString(); + }); + } + + final finalResult = await streamResult.result; + setState(() { + _structuredData = finalResult.structuredData; + }); + } + + void _copyToClipboard(String text) { + Clipboard.setData(ClipboardData(text: text)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied to clipboard')), + ); + } + + @override + Widget build(BuildContext context) { + final example = _examples[_selectedExampleIndex]; + + return Scaffold( + appBar: AppBar( + title: const Text('Structured Output'), + actions: [ + IconButton( + icon: const Icon(Icons.copy), + onPressed: _rawResponse != null + ? () => _copyToClipboard(_rawResponse!) + : null, + tooltip: 'Copy raw response', + ), + ], + ), + body: SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.only(bottom: AppSpacing.large), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Example selector + _buildExampleSelector(), + + // Prompt templates + _buildPromptTemplates(example), + + // Prompt input + _buildPromptInput(), + + // Schema preview + _buildSchemaPreview(), + + const SizedBox(height: AppSpacing.large), + + // Stream toggle and generate button + _buildGenerateControls(), + + const SizedBox(height: AppSpacing.large), + Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.large), + child: Row( + children: [ + Expanded( + child: FilledButton.icon( + onPressed: _isGenerating ? null : _generate, + icon: _isGenerating + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.auto_awesome), + label: Text(_isGenerating ? 'Generating...' : 'Generate'), + ), + ) + ], + ), + ), + + // Error message + if (_errorMessage != null) _buildErrorBanner(), + + // Results + _buildResults() + ], + ), + ), + ); + } + + Widget _buildExampleSelector() { + return Container( + padding: const EdgeInsets.all(AppSpacing.large), + child: DropdownButtonFormField( + value: _selectedExampleIndex, + decoration: InputDecoration( + labelText: 'Example', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppSpacing.cornerRadiusRegular), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: AppSpacing.large, + vertical: AppSpacing.mediumLarge, + ), + ), + items: List.generate(_examples.length, (index) { + return DropdownMenuItem( + value: index, + child: Text(_examples[index].name), + ); + }), + onChanged: _onExampleChanged, + ), + ); + } + + Widget _buildPromptTemplates(StructuredOutputExample example) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.large), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Prompt Templates', + style: AppTypography.subheadline(context).copyWith( + color: AppColors.textSecondary(context), + ), + ), + const SizedBox(height: AppSpacing.smallMedium), + Wrap( + spacing: AppSpacing.smallMedium, + runSpacing: AppSpacing.smallMedium, + children: example.promptTemplates.map((template) { + return ActionChip( + label: Text( + template, + style: AppTypography.caption(context), + ), + onPressed: () => _onPromptTemplateSelected(template), + ); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildPromptInput() { + return Padding( + padding: const EdgeInsets.all(AppSpacing.large), + child: TextField( + controller: _promptController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Prompt', + hintText: 'Enter your prompt...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppSpacing.cornerRadiusRegular), + ), + contentPadding: const EdgeInsets.all(AppSpacing.large), + ), + ), + ); + } + + Widget _buildSchemaPreview() { + final example = _examples[_selectedExampleIndex]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.large), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.schema, + size: AppSpacing.iconRegular, + color: AppColors.textSecondary(context), + ), + const SizedBox(width: AppSpacing.smallMedium), + Text( + 'JSON Schema (${example.typeName})', + style: AppTypography.subheadline(context).copyWith( + color: AppColors.textSecondary(context), + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: AppSpacing.smallMedium), + Container( + width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.mediumLarge), + decoration: BoxDecoration( + color: AppColors.backgroundGray5(context), + borderRadius: + BorderRadius.circular(AppSpacing.cornerRadiusRegular), + border: Border.all( + color: AppColors.primaryBlue.withValues(alpha: 0.3), + ), + ), + child: SelectableText( + _formatJson(example.schema), + style: AppTypography.monospaced.copyWith( + fontSize: 12, + color: AppColors.primaryBlue, + ), + ), + ), + ], + ), + ); + } + + String _formatJson(String jsonString) { + try { + final decoded = jsonDecode(jsonString); + return const JsonEncoder.withIndent(' ').convert(decoded); + } catch (e) { + return jsonString; + } + } + + Widget _buildGenerateControls() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.large), + child: Row( + children: [ + Expanded( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: false, + label: Text('Non-Stream'), + icon: Icon(Icons.check_circle_outline), + ), + ButtonSegment( + value: true, + label: Text('Stream'), + icon: Icon(Icons.stream), + ), + ], + selected: {_useStream}, + onSelectionChanged: (selected) { + setState(() { + _useStream = selected.first; + }); + }, + ), + ), + ], + ), + ); + } + + Widget _buildErrorBanner() { + return Container( + margin: const EdgeInsets.all(AppSpacing.large), + padding: const EdgeInsets.all(AppSpacing.mediumLarge), + decoration: BoxDecoration( + color: AppColors.badgeRed, + borderRadius: BorderRadius.circular(AppSpacing.cornerRadiusRegular), + ), + child: Row( + children: [ + const Icon(Icons.error, color: Colors.red), + const SizedBox(width: AppSpacing.smallMedium), + Expanded( + child: Text( + _errorMessage!, + style: AppTypography.subheadline(context), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(() { + _errorMessage = null; + }); + }, + ), + ], + ), + ); + } + + Widget _buildResults() { + if (_rawResponse == null && !_isGenerating) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.data_object, + size: AppSpacing.iconXXLarge, + color: AppColors.textSecondary(context), + ), + const SizedBox(height: AppSpacing.large), + Text( + 'Generate a response', + style: AppTypography.title2(context), + ), + const SizedBox(height: AppSpacing.smallMedium), + Text( + 'Select an example and enter a prompt', + style: AppTypography.subheadline(context).copyWith( + color: AppColors.textSecondary(context), + ), + ), + ], + ), + ); + } + + return ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + padding: const EdgeInsets.all(AppSpacing.large), + children: [ + // Raw Response + if (_rawResponse != null) ...[ + _buildSectionHeader('Raw Response', Icons.text_snippet), + const SizedBox(height: AppSpacing.smallMedium), + Container( + width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.mediumLarge), + decoration: BoxDecoration( + color: AppColors.backgroundGray5(context), + borderRadius: + BorderRadius.circular(AppSpacing.cornerRadiusRegular), + ), + child: MarkdownBody( + data: _rawResponse!, + styleSheet: MarkdownStyleSheet( + p: AppTypography.body(context), + code: AppTypography.monospaced.copyWith( + backgroundColor: AppColors.backgroundGray6(context), + ), + ), + ), + ), + const SizedBox(height: AppSpacing.large), + ], + + // Structured Data + if (_structuredData != null) ...[ + _buildSectionHeader('Structured Data ', Icons.data_object), + const SizedBox(height: AppSpacing.smallMedium), + Container( + width: double.infinity, + padding: const EdgeInsets.all(AppSpacing.mediumLarge), + decoration: BoxDecoration( + color: AppColors.primaryBlue.withValues(alpha: 0.1), + borderRadius: + BorderRadius.circular(AppSpacing.cornerRadiusRegular), + border: Border.all( + color: AppColors.primaryBlue.withValues(alpha: 0.3), + ), + ), + child: SelectableText( + const JsonEncoder.withIndent(' ').convert(_structuredData), + style: AppTypography.monospaced.copyWith( + color: AppColors.primaryBlue, + ), + ), + ), + const SizedBox(height: AppSpacing.large), + ], + + // Loading indicator + if (_isGenerating) ...[ + const Center( + child: Padding( + padding: EdgeInsets.all(AppSpacing.large), + child: CircularProgressIndicator(), + ), + ), + ], + + const SizedBox(height: AppSpacing.large), + ], + ); + } + + Widget _buildSectionHeader(String title, IconData icon) { + return Row( + children: [ + Icon( + icon, + size: AppSpacing.iconRegular, + color: AppColors.textSecondary(context), + ), + const SizedBox(width: AppSpacing.smallMedium), + Text( + title, + style: AppTypography.title3(context), + ), + ], + ); + } +} + +/// Data class for structured output example +class StructuredOutputExample { + final String name; + final String typeName; + final String schema; + final List promptTemplates; + + const StructuredOutputExample({ + required this.name, + required this.typeName, + required this.schema, + required this.promptTemplates, + }); +} diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/generatable.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/generatable.dart index bcf5f47c0..caa5ed221 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/generatable.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/generatable.dart @@ -18,5 +18,5 @@ abstract class Generatable { Map toJson(); } -// Note: StructuredOutputConfig is now defined in structured_output_handler.dart -// to avoid duplication and maintain iOS parity with a more complete implementation +// Note: StructuredOutputConfig is defined in public/types/structured_output_types.dart +// and imported by structured_output_handler.dart diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/structured_output_handler.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/structured_output_handler.dart index 3de699c18..96f063731 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/structured_output_handler.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/features/llm/structured_output/structured_output_handler.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - import 'dart:convert'; import 'package:runanywhere/foundation/logging/sdk_logger.dart'; +import 'package:runanywhere/public/types/structured_output_types.dart'; /// Handles structured output generation and validation /// Matches iOS StructuredOutputHandler from Features/LLM/StructuredOutput/StructuredOutputHandler.swift @@ -50,9 +49,6 @@ Remember: Output ONLY the JSON object, nothing else. final instructions = ''' CRITICAL INSTRUCTION: You MUST respond with ONLY a valid JSON object. No other text is allowed. -JSON Schema: -${config.schema} - RULES: 1. Start your response with { and end with } 2. Include NO text before the opening { @@ -67,9 +63,12 @@ IMPORTANT: Your entire response must be valid JSON that can be parsed. Do not in return ''' System: You are a JSON generator. You must output only valid JSON. - +Convert this data: $originalPrompt +Use the following JSON Schema: +${config.schema} + $instructions Remember: Output ONLY the JSON object, nothing else. @@ -83,15 +82,22 @@ Remember: Output ONLY the JSON object, nothing else. final jsonString = extractJSON(text); // Parse JSON - final jsonData = jsonDecode(jsonString); + try { + final jsonData = jsonDecode(jsonString); - if (jsonData is! Map) { - throw StructuredOutputError.validationFailed( - 'Expected JSON object, got ${jsonData.runtimeType}', - ); - } + if (jsonData is! Map) { + throw StructuredOutputError.validationFailed( + 'Expected JSON object, got ${jsonData.runtimeType}', + ); + } - return fromJson(jsonData); + return fromJson(jsonData); + } on FormatException catch (e) { + throw StructuredOutputError.invalidJSON( + 'Invalid JSON format: ${e.message}'); + } catch (e) { + throw StructuredOutputError.invalidJSON(e.toString()); + } } /// Extract JSON from potentially mixed text @@ -241,88 +247,3 @@ class _BraceMatch { final int end; _BraceMatch({required this.start, required this.end}); } - -/// Structured output validation result -/// Matches iOS StructuredOutputValidation -class StructuredOutputValidation { - final bool isValid; - final bool containsJSON; - final String? error; - - const StructuredOutputValidation({ - required this.isValid, - required this.containsJSON, - this.error, - }); -} - -/// Structured output errors -/// Matches iOS StructuredOutputError -class StructuredOutputError implements Exception { - final String message; - - StructuredOutputError(this.message); - - factory StructuredOutputError.invalidJSON(String detail) { - return StructuredOutputError('Invalid JSON: $detail'); - } - - factory StructuredOutputError.validationFailed(String detail) { - return StructuredOutputError('Validation failed: $detail'); - } - - factory StructuredOutputError.extractionFailed(String detail) { - return StructuredOutputError( - 'Failed to extract structured output: $detail'); - } - - factory StructuredOutputError.unsupportedType(String type) { - return StructuredOutputError( - 'Unsupported type for structured output: $type'); - } - - @override - String toString() => message; -} - -/// Configuration for structured output generation -/// Matches iOS StructuredOutputConfig from Features/LLM/StructuredOutput/ -class StructuredOutputConfig { - /// The type being generated - final Type type; - - /// JSON schema describing the expected output - final String schema; - - /// Whether to include schema instructions in the prompt - final bool includeSchemaInPrompt; - - /// Name for the structured output (optional) - final String? name; - - /// Whether to enforce strict schema validation - final bool strict; - - const StructuredOutputConfig({ - required this.type, - required this.schema, - this.includeSchemaInPrompt = true, - this.name, - this.strict = false, - }); -} - -/// Result container for streaming structured output -/// Matches iOS StructuredOutputStreamResult from Features/LLM/StructuredOutput/ -class StructuredOutputStreamResult { - /// Stream of individual tokens as they are generated - final Stream stream; - - /// Future that resolves to the final parsed object - final Future result; - - const StructuredOutputStreamResult({ - required this.stream, - required this.result, - }); -} diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_structured_output.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_structured_output.dart new file mode 100644 index 000000000..7365a31ba --- /dev/null +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/dart_bridge_structured_output.dart @@ -0,0 +1,345 @@ +/// DartBridge+StructuredOutput +/// +/// Structured output FFI bindings - wraps C++ rac_structured_output_* APIs. +/// Mirrors Swift's CppBridge extensions for structured output. +library dart_bridge_structured_output; + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +import 'package:runanywhere/foundation/logging/sdk_logger.dart'; +import 'package:runanywhere/native/ffi_types.dart'; +import 'package:runanywhere/native/platform_loader.dart'; + +/// Structured output FFI bridge for C++ interop. +/// +/// Provides access to C++ structured output functions: +/// - rac_structured_output_get_system_prompt +/// - rac_structured_output_extract_json +/// - rac_structured_output_prepare_prompt +/// - rac_structured_output_validate +class DartBridgeStructuredOutput { + static final DartBridgeStructuredOutput shared = + DartBridgeStructuredOutput._(); + + DartBridgeStructuredOutput._(); + + final _logger = SDKLogger('DartBridge.StructuredOutput'); + + /// Get system prompt for structured output generation + /// Uses C++ rac_structured_output_get_system_prompt + String getSystemPrompt(String schema) { + final schemaPtr = schema.toNativeUtf8(); + final promptPtrPtr = calloc>(); + + try { + final lib = PlatformLoader.loadCommons(); + final getSystemPromptFn = lib.lookupFunction< + Int32 Function(Pointer, Pointer>), + int Function(Pointer, Pointer>)>( + 'rac_structured_output_get_system_prompt'); + + final result = getSystemPromptFn(schemaPtr, promptPtrPtr); + + if (result != RAC_SUCCESS) { + _logger.warning( + 'getSystemPrompt failed with code $result, using fallback'); + return _fallbackSystemPrompt(schema); + } + + final promptPtr = promptPtrPtr.value; + if (promptPtr == nullptr) { + return _fallbackSystemPrompt(schema); + } + + final prompt = promptPtr.toDartString(); + lib.lookupFunction), + void Function(Pointer)>('rac_free')(promptPtr.cast()); + + return prompt; + } catch (e) { + _logger.error('getSystemPrompt exception: $e'); + return _fallbackSystemPrompt(schema); + } finally { + calloc.free(schemaPtr); + calloc.free(promptPtrPtr); + } + } + + /// Fallback system prompt when C++ fails + String _fallbackSystemPrompt(String schema) { + return ''' +You are a JSON generator that outputs ONLY valid JSON without any additional text. + +CRITICAL RULES: +1. Your entire response must be valid JSON that can be parsed +2. Start with { and end with } +3. No text before the opening { +4. No text after the closing } +5. Follow the provided schema exactly +6. Include all required fields +7. Use proper JSON syntax (quotes, commas, etc.) + +Expected JSON Schema: +$schema + +Remember: Output ONLY the JSON object, nothing else. +'''; + } + + /// Extract JSON from generated text + /// Uses C++ rac_structured_output_extract_json + String? extractJson(String text) { + final textPtr = text.toNativeUtf8(); + final jsonPtrPtr = calloc>(); + + try { + final lib = PlatformLoader.loadCommons(); + final extractJsonFn = lib.lookupFunction< + Int32 Function(Pointer, Pointer>, Pointer), + int Function(Pointer, Pointer>, + Pointer)>('rac_structured_output_extract_json'); + + final result = extractJsonFn(textPtr, jsonPtrPtr, nullptr); + + if (result != RAC_SUCCESS) { + _logger.warning('extractJson failed with code $result'); + return _fallbackExtractJson(text); + } + + final jsonPtr = jsonPtrPtr.value; + if (jsonPtr == nullptr) { + return _fallbackExtractJson(text); + } + + final jsonString = jsonPtr.toDartString(); + lib.lookupFunction), + void Function(Pointer)>('rac_free')(jsonPtr.cast()); + + return jsonString; + } catch (e) { + _logger.error('extractJson exception: $e'); + return _fallbackExtractJson(text); + } finally { + calloc.free(textPtr); + calloc.free(jsonPtrPtr); + } + } + + /// Fallback JSON extraction when C++ fails + String? _fallbackExtractJson(String text) { + final trimmed = text.trim(); + + for (final pair in [ + ('{', '}'), + ('[', ']'), + ]) { + final open = pair.$1; + final close = pair.$2; + final startIndex = trimmed.indexOf(open); + if (startIndex == -1) continue; + + int depth = 0; + for (int i = startIndex; i < trimmed.length; i++) { + if (trimmed[i] == open) depth++; + if (trimmed[i] == close) depth--; + if (depth == 0) { + return trimmed.substring(startIndex, i + 1); + } + } + } + return null; + } + + /// Prepare prompt with structured output instructions + /// Uses C++ rac_structured_output_prepare_prompt + String preparePrompt(String originalPrompt, String schema, + {bool includeSchemaInPrompt = true}) { + final promptPtr = originalPrompt.toNativeUtf8(); + final schemaPtr = schema.toNativeUtf8(); + + // Build config struct + final configPtr = calloc(); + configPtr.ref.jsonSchema = schemaPtr; + configPtr.ref.includeSchemaInPrompt = includeSchemaInPrompt ? 1 : 0; + + final preparedPtrPtr = calloc>(); + + try { + final lib = PlatformLoader.loadCommons(); + final preparePromptFn = lib.lookupFunction< + Int32 Function(Pointer, + Pointer, Pointer>), + int Function(Pointer, Pointer, + Pointer>)>('rac_structured_output_prepare_prompt'); + + final result = preparePromptFn(promptPtr, configPtr, preparedPtrPtr); + + if (result != RAC_SUCCESS) { + _logger.warning('preparePrompt failed with code $result'); + return _fallbackPreparePrompt(originalPrompt, schema, + includeSchemaInPrompt: includeSchemaInPrompt); + } + + final preparedPtr = preparedPtrPtr.value; + if (preparedPtr == nullptr) { + return _fallbackPreparePrompt(originalPrompt, schema, + includeSchemaInPrompt: includeSchemaInPrompt); + } + + final prepared = preparedPtr.toDartString(); + lib.lookupFunction), + void Function(Pointer)>('rac_free')(preparedPtr.cast()); + + return prepared; + } catch (e) { + _logger.error('preparePrompt exception: $e'); + return _fallbackPreparePrompt(originalPrompt, schema, + includeSchemaInPrompt: includeSchemaInPrompt); + } finally { + calloc.free(promptPtr); + calloc.free(schemaPtr); + calloc.free(configPtr); + calloc.free(preparedPtrPtr); + } + } + + /// Fallback prepare prompt when C++ fails + String _fallbackPreparePrompt(String originalPrompt, String schema, + {bool includeSchemaInPrompt = true}) { + final schemaPart = + includeSchemaInPrompt ? '\n\nJSON Schema:\n$schema\n' : ''; + return ''' +System: You are a JSON generator. You must output only valid JSON. + +$originalPrompt + +CRITICAL INSTRUCTION: You MUST respond with ONLY a valid JSON object. No other text is allowed. + +$schemaPart + +RULES: +1. Start your response with { and end with } +2. Include NO text before the opening { +3. Include NO text after the closing } +4. Follow the schema exactly +5. All required fields must be present + +Remember: Output ONLY the JSON object, nothing else. +'''; + } + + /// Validate structured output + /// Uses C++ rac_structured_output_validate + StructuredOutputValidationResult validate(String text, String schema) { + final textPtr = text.toNativeUtf8(); + final schemaPtr = schema.toNativeUtf8(); + + final configPtr = calloc(); + configPtr.ref.jsonSchema = schemaPtr; + configPtr.ref.includeSchemaInPrompt = 1; + + final validationPtr = calloc(); + + try { + final lib = PlatformLoader.loadCommons(); + final validateFn = lib.lookupFunction< + Int32 Function( + Pointer, + Pointer, + Pointer), + int Function( + Pointer, + Pointer, + Pointer)>( + 'rac_structured_output_validate'); + + final result = validateFn(textPtr, configPtr, validationPtr); + + if (result != RAC_SUCCESS) { + return _fallbackValidate(text); + } + + final validation = validationPtr.ref; + final isValid = validation.isValid == 1; + final containsJson = validation.extractedJson != nullptr; + + String? errorMessage; + if (validation.errorMessage != nullptr) { + errorMessage = validation.errorMessage.toDartString(); + lib.lookupFunction), + void Function(Pointer)>('rac_free')( + validation.errorMessage.cast(), + ); + } + + if (validation.extractedJson != nullptr) { + lib.lookupFunction), + void Function(Pointer)>('rac_free')( + validation.extractedJson.cast(), + ); + } + + return StructuredOutputValidationResult( + isValid: isValid, + containsJSON: containsJson, + error: errorMessage, + ); + } catch (e) { + _logger.error('validate exception: $e'); + return _fallbackValidate(text); + } finally { + calloc.free(textPtr); + calloc.free(schemaPtr); + calloc.free(configPtr); + calloc.free(validationPtr); + } + } + + /// Fallback validation when C++ fails + StructuredOutputValidationResult _fallbackValidate(String text) { + try { + // Simple JSON validation + final trimmed = text.trim(); + if (trimmed.startsWith('{') && trimmed.endsWith('}')) { + return const StructuredOutputValidationResult( + isValid: true, + containsJSON: true, + error: null, + ); + } + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + return const StructuredOutputValidationResult( + isValid: true, + containsJSON: true, + error: null, + ); + } + return const StructuredOutputValidationResult( + isValid: false, + containsJSON: false, + error: 'No valid JSON found', + ); + } catch (e) { + return StructuredOutputValidationResult( + isValid: false, + containsJSON: false, + error: e.toString(), + ); + } + } +} + +/// Structured output validation result +class StructuredOutputValidationResult { + final bool isValid; + final bool containsJSON; + final String? error; + + const StructuredOutputValidationResult({ + required this.isValid, + required this.containsJSON, + this.error, + }); +} diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart index 35ce96fe5..94acff210 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/native/ffi_types.dart @@ -1085,6 +1085,28 @@ abstract class RacToolParamType { static const int array = 4; } +// ============================================================================= +// Structured Output FFI Types (from rac_llm_types.h) +// ============================================================================= + +/// Structured output config struct - matches rac_structured_output_config_t +final class RacStructuredOutputConfigStruct extends Struct { + external Pointer jsonSchema; + + @Int32() + external int includeSchemaInPrompt; +} + +/// Structured output validation struct - matches rac_structured_output_validation_t +final class RacStructuredOutputValidationStruct extends Struct { + @Int32() + external int isValid; + + external Pointer errorMessage; + + external Pointer extractedJson; +} + // ============================================================================= // Backward Compatibility Aliases // ============================================================================= diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart index ab6d24745..8fea1e9b6 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/runanywhere.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -20,6 +21,7 @@ import 'package:runanywhere/native/dart_bridge_device.dart'; import 'package:runanywhere/native/dart_bridge_model_paths.dart'; import 'package:runanywhere/native/dart_bridge_model_registry.dart' hide ModelInfo; +import 'package:runanywhere/native/dart_bridge_structured_output.dart'; import 'package:runanywhere/public/configuration/sdk_environment.dart'; import 'package:runanywhere/public/events/event_bus.dart'; import 'package:runanywhere/public/events/sdk_event.dart'; @@ -638,11 +640,12 @@ class RunAnywhere { logger.debug('Transcribing ${audioData.length} bytes of audio...'); final startTime = DateTime.now().millisecondsSinceEpoch; final modelId = currentSTTModelId ?? 'unknown'; - + // Get model name for telemetry - final modelInfo = await DartBridgeModelRegistry.instance.getPublicModel(modelId); + final modelInfo = + await DartBridgeModelRegistry.instance.getPublicModel(modelId); final modelName = modelInfo?.name; - + // Calculate audio duration from bytes (PCM16 at 16kHz mono) // Duration = bytes / 2 (16-bit = 2 bytes) / 16000 Hz * 1000 ms final calculatedDurationMs = (audioData.length / 32).round(); @@ -650,13 +653,14 @@ class RunAnywhere { try { final result = await DartBridge.stt.transcribe(audioData); final latencyMs = DateTime.now().millisecondsSinceEpoch - startTime; - + // Use calculated duration if C++ returns 0 - final audioDurationMs = result.durationMs > 0 ? result.durationMs : calculatedDurationMs; - + final audioDurationMs = + result.durationMs > 0 ? result.durationMs : calculatedDurationMs; + // Count words in transcription - final wordCount = result.text.trim().isEmpty - ? 0 + final wordCount = result.text.trim().isEmpty + ? 0 : result.text.trim().split(RegExp(r'\s+')).length; // Track transcription success with full metrics @@ -709,24 +713,26 @@ class RunAnywhere { logger.debug('Transcribing ${audioData.length} bytes with details...'); final startTime = DateTime.now().millisecondsSinceEpoch; final modelId = currentSTTModelId ?? 'unknown'; - + // Get model name for telemetry - final modelInfo = await DartBridgeModelRegistry.instance.getPublicModel(modelId); + final modelInfo = + await DartBridgeModelRegistry.instance.getPublicModel(modelId); final modelName = modelInfo?.name; - + // Calculate audio duration from bytes (PCM16 at 16kHz mono) final calculatedDurationMs = (audioData.length / 32).round(); try { final result = await DartBridge.stt.transcribe(audioData); final latencyMs = DateTime.now().millisecondsSinceEpoch - startTime; - + // Use calculated duration if C++ returns 0 - final audioDurationMs = result.durationMs > 0 ? result.durationMs : calculatedDurationMs; - + final audioDurationMs = + result.durationMs > 0 ? result.durationMs : calculatedDurationMs; + // Count words in transcription - final wordCount = result.text.trim().isEmpty - ? 0 + final wordCount = result.text.trim().isEmpty + ? 0 : result.text.trim().split(RegExp(r'\s+')).length; // Track transcription success with full metrics @@ -899,9 +905,10 @@ class RunAnywhere { 'Synthesizing: "${text.substring(0, text.length.clamp(0, 50))}..."'); final startTime = DateTime.now().millisecondsSinceEpoch; final voiceId = currentTTSVoiceId ?? 'unknown'; - + // Get model name for telemetry - final modelInfo = await DartBridgeModelRegistry.instance.getPublicModel(voiceId); + final modelInfo = + await DartBridgeModelRegistry.instance.getPublicModel(voiceId); final modelName = modelInfo?.name; try { @@ -912,7 +919,7 @@ class RunAnywhere { volume: volume, ); final latencyMs = DateTime.now().millisecondsSinceEpoch - startTime; - + // Calculate audio size in bytes (Float32 samples = 4 bytes each) final audioSizeBytes = result.samples.length * 4; @@ -1102,9 +1109,8 @@ class RunAnywhere { // Audio is already in WAV format (C++ voice agent converts Float32 TTS to WAV) // No conversion needed - pass directly to playback - final synthesizedAudio = result.audioWavData.isNotEmpty - ? result.audioWavData - : null; + final synthesizedAudio = + result.audioWavData.isNotEmpty ? result.audioWavData : null; logger.info( 'Voice turn complete: transcript="${result.transcription.substring(0, result.transcription.length.clamp(0, 50))}", ' @@ -1181,18 +1187,34 @@ class RunAnywhere { } final modelId = DartBridge.llm.currentModelId ?? 'unknown'; - + // Get model name from registry for telemetry - final modelInfo = await DartBridgeModelRegistry.instance.getPublicModel(modelId); + final modelInfo = + await DartBridgeModelRegistry.instance.getPublicModel(modelId); final modelName = modelInfo?.name; + // Determine effective system prompt - add JSON conversion instructions if structuredOutput is provided + String? effectiveSystemPrompt = opts.systemPrompt; + if (opts.structuredOutput != null) { + final jsonSystemPrompt = + DartBridgeStructuredOutput.shared.getSystemPrompt( + opts.structuredOutput!.schema, + ); + // If user already provided a system prompt, prepend the JSON instructions + if (effectiveSystemPrompt != null && effectiveSystemPrompt.isNotEmpty) { + effectiveSystemPrompt = '$jsonSystemPrompt\n\n$effectiveSystemPrompt'; + } else { + effectiveSystemPrompt = jsonSystemPrompt; + } + } + try { // Generate directly via DartBridgeLLM (calls rac_llm_component_generate) final result = await DartBridge.llm.generate( prompt, maxTokens: opts.maxTokens, temperature: opts.temperature, - systemPrompt: opts.systemPrompt, + systemPrompt: effectiveSystemPrompt, ); final endTime = DateTime.now(); @@ -1215,6 +1237,23 @@ class RunAnywhere { isStreaming: false, ); + // Extract structured data if structuredOutput is provided + Map? structuredData; + if (opts.structuredOutput != null) { + try { + final jsonString = + DartBridgeStructuredOutput.shared.extractJson(result.text); + if (jsonString != null) { + final parsed = jsonDecode(jsonString); + structuredData = _normalizeStructuredData(parsed); + } + } catch (e) { + // JSON extraction/parse failed — return text result without structured data + final logger = SDKLogger('StructuredOutputHandler'); + logger.info('JSON extraction/parse failed: $e'); + } + } + return LLMGenerationResult( text: result.text, inputTokens: result.promptTokens, @@ -1223,6 +1262,7 @@ class RunAnywhere { latencyMs: latencyMs, framework: 'llamacpp', tokensPerSecond: tokensPerSecond, + structuredData: structuredData, ); } catch (e) { // Track generation failure @@ -1279,11 +1319,27 @@ class RunAnywhere { } final modelId = DartBridge.llm.currentModelId ?? 'unknown'; - + // Get model name from registry for telemetry - final modelInfo = await DartBridgeModelRegistry.instance.getPublicModel(modelId); + final modelInfo = + await DartBridgeModelRegistry.instance.getPublicModel(modelId); final modelName = modelInfo?.name; + // Determine effective system prompt - add JSON conversion instructions if structuredOutput is provided + String? effectiveSystemPrompt = opts.systemPrompt; + if (opts.structuredOutput != null) { + final jsonSystemPrompt = + DartBridgeStructuredOutput.shared.getSystemPrompt( + opts.structuredOutput!.schema, + ); + // If user already provided a system prompt, prepend the JSON instructions + if (effectiveSystemPrompt != null && effectiveSystemPrompt.isNotEmpty) { + effectiveSystemPrompt = '$jsonSystemPrompt\n\n$effectiveSystemPrompt'; + } else { + effectiveSystemPrompt = jsonSystemPrompt; + } + } + // Create a broadcast stream controller for the tokens final controller = StreamController.broadcast(); final allTokens = []; @@ -1293,7 +1349,7 @@ class RunAnywhere { prompt, maxTokens: opts.maxTokens, temperature: opts.temperature, - systemPrompt: opts.systemPrompt, + systemPrompt: effectiveSystemPrompt, ); // Forward tokens and collect them, track subscription in bridge for cancellation @@ -1338,7 +1394,8 @@ class RunAnywhere { // Calculate time to first token int? timeToFirstTokenMs; if (firstTokenTime != null) { - timeToFirstTokenMs = firstTokenTime!.difference(startTime).inMilliseconds; + timeToFirstTokenMs = + firstTokenTime!.difference(startTime).inMilliseconds; } // Estimate tokens (~4 chars per token) @@ -1360,14 +1417,31 @@ class RunAnywhere { isStreaming: true, ); + // Extract structured data if structuredOutput is provided + Map? structuredData; + final fullText = allTokens.join(); + if (opts.structuredOutput != null) { + try { + final jsonString = + DartBridgeStructuredOutput.shared.extractJson(fullText); + if (jsonString != null) { + final parsed = jsonDecode(jsonString); + structuredData = _normalizeStructuredData(parsed); + } + } catch (_) { + // JSON extraction/parse failed — return text result without structured data + } + } + return LLMGenerationResult( - text: allTokens.join(), + text: fullText, inputTokens: promptTokens, tokensUsed: completionTokens, modelUsed: modelId, latencyMs: latencyMs, framework: 'llamacpp', tokensPerSecond: tokensPerSecond, + structuredData: structuredData, ); }); @@ -1425,7 +1499,8 @@ class RunAnywhere { } else if (progress.stage == ModelDownloadStage.extracting) { logger.info('Extracting model...'); } else if (progress.stage == ModelDownloadStage.completed) { - final downloadTimeMs = DateTime.now().millisecondsSinceEpoch - startTime; + final downloadTimeMs = + DateTime.now().millisecondsSinceEpoch - startTime; logger.info('✅ Download completed for model: $modelId'); // Track download success @@ -1800,4 +1875,29 @@ class RunAnywhere { if (lower.endsWith('.ort')) return ModelFormat.ort; return ModelFormat.unknown; } + + // ============================================================================ + // Structured Output Helpers + // ============================================================================ + + /// Normalizes parsed JSON to Map. + /// If the parsed result is a List, wraps it in a Map with 'items' key. + /// If it's already a Map, returns it directly. + /// Returns null if parsing fails. + static Map? _normalizeStructuredData(dynamic parsed) { + if (parsed is Map) { + return parsed; + } else if (parsed is List) { + // Wrap array in object with 'items' key + return {'items': parsed}; + } else if (parsed is Map) { + // Convert Map to Map; guard against non-String keys. + try { + return parsed.map((k, v) => MapEntry(k.toString(), v)); + } catch (_) { + return null; + } + } + return null; + } } diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/generation_types.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/generation_types.dart index dcf39885e..c4b09610c 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/generation_types.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/generation_types.dart @@ -7,6 +7,7 @@ library generation_types; import 'dart:typed_data'; import 'package:runanywhere/core/types/model_types.dart'; +import 'package:runanywhere/public/types/structured_output_types.dart'; /// Options for LLM text generation /// Matches Swift's LLMGenerationOptions @@ -18,6 +19,7 @@ class LLMGenerationOptions { final bool streamingEnabled; final InferenceFramework? preferredFramework; final String? systemPrompt; + final StructuredOutputConfig? structuredOutput; const LLMGenerationOptions({ this.maxTokens = 100, @@ -27,6 +29,7 @@ class LLMGenerationOptions { this.streamingEnabled = false, this.preferredFramework, this.systemPrompt, + this.structuredOutput, }); } @@ -44,6 +47,7 @@ class LLMGenerationResult { final double? timeToFirstTokenMs; final int thinkingTokens; final int responseTokens; + final Map? structuredData; const LLMGenerationResult({ required this.text, @@ -57,6 +61,7 @@ class LLMGenerationResult { this.timeToFirstTokenMs, this.thinkingTokens = 0, this.responseTokens = 0, + this.structuredData, }); } diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/structured_output_types.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/structured_output_types.dart new file mode 100644 index 000000000..fc2699149 --- /dev/null +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/structured_output_types.dart @@ -0,0 +1,99 @@ +/// Structured Output Types +/// +/// Types for structured output generation. +/// Mirrors Swift's Structured Output types. +library structured_output_types; + +/// Configuration for structured output generation +/// Mirrors Swift's StructuredOutputConfig +class StructuredOutputConfig { + /// The type name being generated + final String typeName; + + /// JSON schema describing the expected output + final String schema; + + /// Whether to include schema instructions in the prompt + final bool includeSchemaInPrompt; + + /// Name for the structured output (optional) + final String? name; + + /// Whether to enforce strict schema validation + final bool strict; + + const StructuredOutputConfig({ + required this.typeName, + required this.schema, + this.includeSchemaInPrompt = true, + this.name, + this.strict = false, + }); +} + +/// Structured output validation result +/// Mirrors Swift's StructuredOutputValidation +class StructuredOutputValidation { + final bool isValid; + final bool containsJSON; + final String? error; + + const StructuredOutputValidation({ + required this.isValid, + required this.containsJSON, + this.error, + }); +} + +/// Structured output errors +/// Mirrors Swift's StructuredOutputError +class StructuredOutputError implements Exception { + final String message; + + StructuredOutputError(this.message); + + factory StructuredOutputError.invalidJSON(String detail) { + return StructuredOutputError('Invalid JSON: $detail'); + } + + factory StructuredOutputError.validationFailed(String detail) { + return StructuredOutputError('Validation failed: $detail'); + } + + factory StructuredOutputError.extractionFailed(String detail) { + return StructuredOutputError( + 'Failed to extract structured output: $detail'); + } + + factory StructuredOutputError.unsupportedType(String type) { + return StructuredOutputError( + 'Unsupported type for structured output: $type'); + } + + @override + String toString() => message; +} + +/// Result for structured output generation with parsed result and metrics +class StructuredOutputResult { + /// The parsed structured output object + final T result; + + /// Raw text from generation + final String rawText; + + /// Generation metrics + final int inputTokens; + final int tokensUsed; + final double latencyMs; + final double tokensPerSecond; + + const StructuredOutputResult({ + required this.result, + required this.rawText, + required this.inputTokens, + required this.tokensUsed, + required this.latencyMs, + required this.tokensPerSecond, + }); +} diff --git a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/types.dart b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/types.dart index 1111ad66c..fb5c5cffc 100644 --- a/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/types.dart +++ b/sdk/runanywhere-flutter/packages/runanywhere/lib/public/types/types.dart @@ -8,5 +8,6 @@ export 'configuration_types.dart'; export 'download_types.dart'; export 'generation_types.dart'; export 'message_types.dart'; +export 'structured_output_types.dart'; export 'tool_calling_types.dart'; export 'voice_agent_types.dart';