Repository navigation
feat(core): add LowercaseTextProcessor, TrimWhitespaceProcessor, and LanguageDetectProcessor #170
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
|
|
@@ -495,6 +495,82 @@ async def call( | |
| raise ValueError(f'Unsupported cleaning mode: {self._cleaning_mode}') | ||
|
|
||
|
|
||
| class LowercaseTextProcessor(processor.PartProcessor): | ||
| """PartProcessor that lowercases text parts.""" | ||
|
|
||
| def match(self, part: content_api.ProcessorPart) -> bool: | ||
| return content_api.is_text(part.mimetype) | ||
|
|
||
| async def call( | ||
| self, part: content_api.ProcessorPart | ||
| ) -> AsyncIterable[content_api.ProcessorPartTypes]: | ||
| yield content_api.ProcessorPart( | ||
| part.text.lower(), | ||
| mimetype=part.mimetype, | ||
| role=part.role, | ||
| substream_name=part.substream_name, | ||
| metadata=part.metadata, | ||
| ) | ||
|
|
||
|
|
||
| class TrimWhitespaceProcessor(processor.PartProcessor): | ||
| """PartProcessor that trims leading and trailing whitespace from text parts.""" | ||
|
|
||
| def match(self, part: content_api.ProcessorPart) -> bool: | ||
| return content_api.is_text(part.mimetype) | ||
|
|
||
| async def call( | ||
| self, part: content_api.ProcessorPart | ||
| ) -> AsyncIterable[content_api.ProcessorPartTypes]: | ||
| yield content_api.ProcessorPart( | ||
| part.text.strip(), | ||
| mimetype=part.mimetype, | ||
| role=part.role, | ||
| substream_name=part.substream_name, | ||
| metadata=part.metadata, | ||
| ) | ||
|
|
||
|
|
||
| class LanguageDetectProcessor(processor.PartProcessor): | ||
| """PartProcessor that automatically detects language of text parts and adds it to metadata. | ||
|
|
||
| Uses the `langdetect` library to detect the language. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, *, metadata_key: str = 'language', fallback_lang: str | None = None | ||
| ): | ||
| self._metadata_key = metadata_key | ||
| self._fallback_lang = fallback_lang | ||
|
|
||
| def match(self, part: content_api.ProcessorPart) -> bool: | ||
| return content_api.is_text(part.mimetype) | ||
|
|
||
| async def call( | ||
| self, part: content_api.ProcessorPart | ||
| ) -> AsyncIterable[content_api.ProcessorPartTypes]: | ||
| import langdetect | ||
|
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. Since It is highly recommended to move the References
|
||
|
|
||
| lang = self._fallback_lang | ||
| if part.text: | ||
| try: | ||
| lang = langdetect.detect(part.text) | ||
| except Exception: | ||
| pass | ||
|
|
||
| meta = dict(part.metadata or {}) | ||
| if lang: | ||
| meta[self._metadata_key] = lang | ||
|
|
||
| yield content_api.ProcessorPart( | ||
| part.text, | ||
| mimetype=part.mimetype, | ||
| role=part.role, | ||
| substream_name=part.substream_name, | ||
| metadata=meta, | ||
| ) | ||
|
|
||
|
|
||
| @processor.source() | ||
| async def terminal_input( | ||
| prompt: str = '', | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -719,5 +719,68 @@ async def test_html_cleaner_html_mode(self, cleaning_mode, expected_output): | |
| ) | ||
|
|
||
|
|
||
| class LowercaseTextProcessorTest( | ||
| parameterized.TestCase, unittest.IsolatedAsyncioTestCase | ||
| ): | ||
|
|
||
| async def test_lowercase_text(self): | ||
| processor = text.LowercaseTextProcessor() | ||
| input_part = content_api.ProcessorPart( | ||
| 'HELLO World!', mimetype='text/plain', role='user' | ||
| ) | ||
| self.assertSequenceEqual( | ||
| await processor(input_part).gather(), | ||
| [ | ||
| content_api.ProcessorPart( | ||
| 'hello world!', mimetype='text/plain', role='user' | ||
| ) | ||
| ], | ||
| ) | ||
|
|
||
|
|
||
| class TrimWhitespaceProcessorTest( | ||
| parameterized.TestCase, unittest.IsolatedAsyncioTestCase | ||
| ): | ||
|
|
||
| async def test_trim_whitespace(self): | ||
| processor = text.TrimWhitespaceProcessor() | ||
| input_part = content_api.ProcessorPart( | ||
| ' \n hello \t ', mimetype='text/plain', role='user' | ||
| ) | ||
| self.assertSequenceEqual( | ||
| await processor(input_part).gather(), | ||
| [ | ||
| content_api.ProcessorPart( | ||
| 'hello', mimetype='text/plain', role='user' | ||
| ) | ||
| ], | ||
| ) | ||
|
|
||
|
|
||
| class LanguageDetectProcessorTest( | ||
| parameterized.TestCase, unittest.IsolatedAsyncioTestCase | ||
| ): | ||
|
|
||
| async def test_detect_english(self): | ||
| processor = text.LanguageDetectProcessor() | ||
| input_part = content_api.ProcessorPart( | ||
| 'This is a simple english text to detect language.', | ||
| mimetype='text/plain', | ||
| ) | ||
| results = await processor(input_part).gather() | ||
| self.assertEqual(len(results), 1) | ||
| self.assertEqual(results[0].metadata.get('language'), 'en') | ||
|
|
||
| async def test_detect_spanish(self): | ||
| processor = text.LanguageDetectProcessor() | ||
| input_part = content_api.ProcessorPart( | ||
| 'Hola, esto es un texto simple en español para detectar el idioma.', | ||
| mimetype='text/plain', | ||
| ) | ||
| results = await processor(input_part).gather() | ||
| self.assertEqual(len(results), 1) | ||
| self.assertEqual(results[0].metadata.get('language'), 'es') | ||
|
Comment on lines
+780
to
+782
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. To ensure the robustness of
results = await processor(input_part).gather()
self.assertEqual(len(results), 1)
self.assertEqual(results[0].metadata.get('language'), 'es')
async def test_detect_fallback_on_empty_text(self):
processor = text.LanguageDetectProcessor(fallback_lang='fr')
input_part = content_api.ProcessorPart('', mimetype='text/plain')
results = await processor(input_part).gather()
self.assertEqual(len(results), 1)
self.assertEqual(results[0].metadata.get('language'), 'fr')
async def test_detect_custom_metadata_key(self):
processor = text.LanguageDetectProcessor(metadata_key='detected_lang')
input_part = content_api.ProcessorPart(
'This is a simple english text.', mimetype='text/plain'
)
results = await processor(input_part).gather()
self.assertEqual(len(results), 1)
self.assertEqual(results[0].metadata.get('detected_lang'), 'en')
async def test_detect_non_text_passed_through(self):
processor = text.LanguageDetectProcessor()
input_part = content_api.ProcessorPart(b'image_bytes', mimetype='image/png')
results = await processor(input_part).gather()
self.assertEqual(len(results), 1)
self.assertEqual(results[0], input_part) |
||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| absltest.main() | ||
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.
The
langdetectlibrary's detection algorithm is non-deterministic by default because it uses a randomized initialization. This can lead to inconsistent language detection results for the same input text across different runs, which can cause flaky tests or unpredictable behavior in production.To ensure deterministic and consistent results, initialize the
DetectorFactoryseed to0during the processor's initialization.