diff --git a/mobile/android/android-components/components/feature/summarize/src/main/java/mozilla/components/feature/summarize/ext/Content.kt b/mobile/android/android-components/components/feature/summarize/src/main/java/mozilla/components/feature/summarize/ext/Content.kt index 5e7df9dd54d97..25ec926ecb015 100644 --- a/mobile/android/android-components/components/feature/summarize/src/main/java/mozilla/components/feature/summarize/ext/Content.kt +++ b/mobile/android/android-components/components/feature/summarize/src/main/java/mozilla/components/feature/summarize/ext/Content.kt @@ -15,13 +15,16 @@ internal val PageMetadata.shouldUseReaderModeContent get() = isReaderable && !is private val PageMetadata.systemPrompt get() = if (isRecipe) { recipeInstructions(language) } else { - defaultInstructions() + defaultInstructions(language) } -internal fun defaultInstructions() = """ +internal fun defaultInstructions(language: String) = """ You are a Content Summarizer. You create mobile-optimized summaries by first understanding what users actually need from each type of content. + You MUST respond entirely in $language. Do not mix languages. + Translate all visible section headers and labels into $language. + Process: Step 1: Identify and Adapt. Use tree of thought to determine: What type of content is this? What would a mobile user want to extract? @@ -58,7 +61,7 @@ internal fun recipeInstructions(language: String) = """ You are an expert at creating mobile-optimized recipe summaries. You MUST respond entirely in $language. Do not mix languages. - Translate all visible section headers and labels into **{lang}**. + Translate all visible section headers and labels into $language. Output ONLY the formatted result. Do not add any closing phrases. If a field is null, empty, or missing, omit that section entirely. Always replace placeholders with actual values. diff --git a/mobile/android/android-components/components/feature/summarize/src/test/java/mozilla/components/feature/summarize/SummarizationStoreTest.kt b/mobile/android/android-components/components/feature/summarize/src/test/java/mozilla/components/feature/summarize/SummarizationStoreTest.kt index 0f5df2dd5869a..2d823d57ec58e 100644 --- a/mobile/android/android-components/components/feature/summarize/src/test/java/mozilla/components/feature/summarize/SummarizationStoreTest.kt +++ b/mobile/android/android-components/components/feature/summarize/src/test/java/mozilla/components/feature/summarize/SummarizationStoreTest.kt @@ -176,7 +176,95 @@ class SummarizationStoreTest { ) assertEquals(expected, states) - assertEquals(Prompt(content, defaultInstructions()), llm.lastPrompt) + assertEquals(Prompt(content, defaultInstructions("en")), llm.lastPrompt) + } + + @Test + fun `page language is forwarded to model for default case`() = runTest { + val llm = FakeLlm.successful + val provider = FakeCloudProvider(preparedState = CloudLlmProvider.State.Ready(llm)) + val content = "this is expected content." + val pageTitle = "Article Headline" + val language = "de" + val store = SummarizationStore( + initialState = Inert(true), + reducer = ::summarizationReducer, + middleware = listOf( + SummarizationMiddleware( + llmProvider = provider, + settings = SummarizationSettings.inMemory(hasConsentedToShake = true), + contentProvider = { Result.success(Content(PageMetadata(listOf("Article"), 0, language, pageTitle = pageTitle), content)) }, + errorReporter = noopReporter, + scope = backgroundScope, + dispatcher = StandardTestDispatcher(testScheduler), + ), + ), + ) + + val states = mutableListOf() + backgroundScope.launch { + store.stateFlow.toList(states) + } + testScheduler.advanceTimeBy(1.seconds) + + store.dispatch(ViewAppeared) + testScheduler.advanceTimeBy(15.seconds) + + val expected = listOf( + Inert(true), + Loading(provider.info), + Summarizing(provider.info, parser.parse("# $pageTitle\nThis is the article\n")), + Summarizing(provider.info, parser.parse("# $pageTitle\nThis is the article\nThis is some content...\n")), + Summarizing(provider.info, parser.parse("# $pageTitle\nThis is the article\nThis is some content...\nThis is some *bold* content.\n")), + Summarized(provider.info, parser.parse("# $pageTitle\nThis is the article\nThis is some content...\nThis is some *bold* content.\n")), + ) + + assertEquals(expected, states) + assertEquals(Prompt(content, defaultInstructions(language)), llm.lastPrompt) + } + + @Test + fun `page language is forwarded to model for recipe case`() = runTest { + val llm = FakeLlm.successful + val provider = FakeCloudProvider(preparedState = CloudLlmProvider.State.Ready(llm)) + val content = "this is expected content." + val pageTitle = "Article Headline" + val language = "de" + val store = SummarizationStore( + initialState = Inert(true), + reducer = ::summarizationReducer, + middleware = listOf( + SummarizationMiddleware( + llmProvider = provider, + settings = SummarizationSettings.inMemory(hasConsentedToShake = true), + contentProvider = { Result.success(Content(PageMetadata(listOf("Recipe"), 0, language, pageTitle = pageTitle), content)) }, + errorReporter = noopReporter, + scope = backgroundScope, + dispatcher = StandardTestDispatcher(testScheduler), + ), + ), + ) + + val states = mutableListOf() + backgroundScope.launch { + store.stateFlow.toList(states) + } + testScheduler.advanceTimeBy(1.seconds) + + store.dispatch(ViewAppeared) + testScheduler.advanceTimeBy(15.seconds) + + val expected = listOf( + Inert(true), + Loading(provider.info), + Summarizing(provider.info, parser.parse("# $pageTitle\nThis is the article\n")), + Summarizing(provider.info, parser.parse("# $pageTitle\nThis is the article\nThis is some content...\n")), + Summarizing(provider.info, parser.parse("# $pageTitle\nThis is the article\nThis is some content...\nThis is some *bold* content.\n")), + Summarized(provider.info, parser.parse("# $pageTitle\nThis is the article\nThis is some content...\nThis is some *bold* content.\n")), + ) + + assertEquals(expected, states) + assertEquals(Prompt(content, recipeInstructions(language)), llm.lastPrompt) } @Test @@ -322,7 +410,7 @@ class SummarizationStoreTest { assertTrue(usingReaderContent) assertEquals(expected, states) - assertEquals(Prompt(content, defaultInstructions()), llm.lastPrompt) + assertEquals(Prompt(content, defaultInstructions("en")), llm.lastPrompt) } @Test @@ -512,7 +600,7 @@ class SummarizationStoreTest { ) assertEquals(expected, states) - assertEquals(Prompt(content, defaultInstructions()), llm.lastPrompt) + assertEquals(Prompt(content, defaultInstructions("en")), llm.lastPrompt) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/summarization/eligibility/SummarizationEligibilityChecker.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/summarization/eligibility/SummarizationEligibilityChecker.kt index 686cee8f9a685..80f71dd70530f 100644 --- a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/summarization/eligibility/SummarizationEligibilityChecker.kt +++ b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/summarization/eligibility/SummarizationEligibilityChecker.kt @@ -72,7 +72,7 @@ internal class DefaultSummarizationEligibilityChecker : SummarizationEligibility return wordCount in WORD_COUNT_RANGE && language.inAcceptedLanguages() } - private fun String.inAcceptedLanguages() = listOf("en").any { acceptedLang -> + private fun String.inAcceptedLanguages() = listOf("en", "fr", "es", "pt", "de", "ja").any { acceptedLang -> this.contains(acceptedLang) }