langfun 0.0.2.dev20240531__py3-none-any.whl → 0.0.2.dev20240601__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of langfun might be problematic. Click here for more details.

langfun/__init__.py CHANGED
@@ -57,6 +57,8 @@ from langfun.core import memories
57
57
 
58
58
  from langfun.core import modalities
59
59
 
60
+ Mime = modalities.Mime
61
+ MimeType = Mime # For backwards compatibility.
60
62
  Image = modalities.Image
61
63
  Video = modalities.Video
62
64
  PDF = modalities.PDF
langfun/core/__init__.py CHANGED
@@ -94,6 +94,7 @@ from langfun.core.message import MemoryRecord
94
94
  # Interface for modality.
95
95
  from langfun.core.modality import Modality
96
96
  from langfun.core.modality import ModalityRef
97
+ from langfun.core.modality import ModalityError
97
98
 
98
99
  # Interfaces for languge models.
99
100
  from langfun.core.language_model import LanguageModel
@@ -49,9 +49,10 @@ class GenAI(lf.LanguageModel):
49
49
  ),
50
50
  ] = None
51
51
 
52
- multimodal: Annotated[bool, 'Whether this model has multimodal support.'] = (
53
- False
54
- )
52
+ supported_modalities: Annotated[
53
+ list[str],
54
+ 'A list of MIME types for supported modalities'
55
+ ] = []
55
56
 
56
57
  # Set the default max concurrency to 8 workers.
57
58
  max_concurrency = 8
@@ -118,14 +119,27 @@ class GenAI(lf.LanguageModel):
118
119
  chunks = []
119
120
  for lf_chunk in formatted.chunk():
120
121
  if isinstance(lf_chunk, str):
121
- chunk = lf_chunk
122
- elif self.multimodal and isinstance(lf_chunk, lf_modalities.MimeType):
123
- chunk = genai.types.BlobDict(
124
- data=lf_chunk.to_bytes(), mime_type=lf_chunk.mime_type
125
- )
122
+ chunks.append(lf_chunk)
123
+ elif isinstance(lf_chunk, lf_modalities.Mime):
124
+ try:
125
+ modalities = lf_chunk.make_compatible(
126
+ self.supported_modalities + ['text/plain']
127
+ )
128
+ if isinstance(modalities, lf_modalities.Mime):
129
+ modalities = [modalities]
130
+ for modality in modalities:
131
+ if modality.is_text:
132
+ chunk = modality.to_text()
133
+ else:
134
+ chunk = genai.types.BlobDict(
135
+ data=modality.to_bytes(),
136
+ mime_type=modality.mime_type
137
+ )
138
+ chunks.append(chunk)
139
+ except lf.ModalityError as e:
140
+ raise lf.ModalityError(f'Unsupported modality: {lf_chunk!r}') from e
126
141
  else:
127
- raise ValueError(f'Unsupported modality: {lf_chunk!r}')
128
- chunks.append(chunk)
142
+ raise lf.ModalityError(f'Unsupported modality: {lf_chunk!r}')
129
143
  return chunks
130
144
 
131
145
  def _response_to_result(
@@ -264,18 +278,57 @@ _GOOGLE_GENAI_MODEL_HUB = _ModelHub()
264
278
  #
265
279
 
266
280
 
281
+ _IMAGE_TYPES = [
282
+ 'image/png',
283
+ 'image/jpeg',
284
+ 'image/webp',
285
+ 'image/heic',
286
+ 'image/heif',
287
+ ]
288
+
289
+ _AUDIO_TYPES = [
290
+ 'audio/aac',
291
+ 'audio/flac',
292
+ 'audio/mp3',
293
+ 'audio/m4a',
294
+ 'audio/mpeg',
295
+ 'audio/mpga',
296
+ 'audio/mp4',
297
+ 'audio/opus',
298
+ 'audio/pcm',
299
+ 'audio/wav',
300
+ 'audio/webm'
301
+ ]
302
+
303
+ _VIDEO_TYPES = [
304
+ 'video/mov',
305
+ 'video/mpeg',
306
+ 'video/mpegps',
307
+ 'video/mpg',
308
+ 'video/mp4',
309
+ 'video/webm',
310
+ 'video/wmv',
311
+ 'video/x-flv',
312
+ 'video/3gpp',
313
+ ]
314
+
315
+ _PDF = [
316
+ 'application/pdf',
317
+ ]
318
+
319
+
267
320
  class GeminiPro1_5(GenAI): # pylint: disable=invalid-name
268
321
  """Gemini Pro latest model."""
269
322
 
270
323
  model = 'gemini-1.5-pro-latest'
271
- multimodal = True
324
+ supported_modalities = _PDF + _IMAGE_TYPES + _AUDIO_TYPES + _VIDEO_TYPES
272
325
 
273
326
 
274
327
  class GeminiFlash1_5(GenAI): # pylint: disable=invalid-name
275
328
  """Gemini Flash latest model."""
276
329
 
277
330
  model = 'gemini-1.5-flash-latest'
278
- multimodal = True
331
+ supported_modalities = _PDF + _IMAGE_TYPES + _AUDIO_TYPES + _VIDEO_TYPES
279
332
 
280
333
 
281
334
  class GeminiPro(GenAI):
@@ -288,7 +341,7 @@ class GeminiProVision(GenAI):
288
341
  """Gemini Pro vision model."""
289
342
 
290
343
  model = 'gemini-pro-vision'
291
- multimodal = True
344
+ supported_modalities = _IMAGE_TYPES + _VIDEO_TYPES
292
345
 
293
346
 
294
347
  class Palm2(GenAI):
@@ -107,7 +107,7 @@ class GenAITest(unittest.TestCase):
107
107
  )
108
108
 
109
109
  # Non-multimodal model.
110
- with self.assertRaisesRegex(ValueError, 'Unsupported modality'):
110
+ with self.assertRaisesRegex(lf.ModalityError, 'Unsupported modality'):
111
111
  google_genai.GeminiPro()._content_from_message(message)
112
112
 
113
113
  model = google_genai.GeminiProVision()
@@ -75,9 +75,10 @@ class VertexAI(lf.LanguageModel):
75
75
  ),
76
76
  ] = None
77
77
 
78
- multimodal: Annotated[bool, 'Whether this model has multimodal support.'] = (
79
- False
80
- )
78
+ supported_modalities: Annotated[
79
+ list[str],
80
+ 'A list of MIME types for supported modalities'
81
+ ] = []
81
82
 
82
83
  def _on_bound(self):
83
84
  super()._on_bound()
@@ -142,16 +143,29 @@ class VertexAI(lf.LanguageModel):
142
143
  """Gets generation input from langfun message."""
143
144
  from vertexai import generative_models
144
145
  chunks = []
146
+
145
147
  for lf_chunk in prompt.chunk():
146
148
  if isinstance(lf_chunk, str):
147
- chunk = lf_chunk
148
- elif self.multimodal and isinstance(lf_chunk, lf_modalities.MimeType):
149
- chunk = generative_models.Part.from_data(
150
- lf_chunk.to_bytes(), lf_chunk.mime_type
151
- )
149
+ chunks.append(lf_chunk)
150
+ elif isinstance(lf_chunk, lf_modalities.Mime):
151
+ try:
152
+ modalities = lf_chunk.make_compatible(
153
+ self.supported_modalities + ['text/plain']
154
+ )
155
+ if isinstance(modalities, lf_modalities.Mime):
156
+ modalities = [modalities]
157
+ for modality in modalities:
158
+ if modality.is_text:
159
+ chunk = modality.to_text()
160
+ else:
161
+ chunk = generative_models.Part.from_data(
162
+ modality.to_bytes(), modality.mime_type
163
+ )
164
+ chunks.append(chunk)
165
+ except lf.ModalityError as e:
166
+ raise lf.ModalityError(f'Unsupported modality: {lf_chunk!r}') from e
152
167
  else:
153
- raise ValueError(f'Unsupported modality: {lf_chunk!r}')
154
- chunks.append(chunk)
168
+ raise lf.ModalityError(f'Unsupported modality: {lf_chunk!r}')
155
169
  return chunks
156
170
 
157
171
  def _generation_response_to_message(
@@ -265,25 +279,64 @@ class _ModelHub:
265
279
  _VERTEXAI_MODEL_HUB = _ModelHub()
266
280
 
267
281
 
282
+ _IMAGE_TYPES = [
283
+ 'image/png',
284
+ 'image/jpeg',
285
+ 'image/webp',
286
+ 'image/heic',
287
+ 'image/heif',
288
+ ]
289
+
290
+ _AUDIO_TYPES = [
291
+ 'audio/aac',
292
+ 'audio/flac',
293
+ 'audio/mp3',
294
+ 'audio/m4a',
295
+ 'audio/mpeg',
296
+ 'audio/mpga',
297
+ 'audio/mp4',
298
+ 'audio/opus',
299
+ 'audio/pcm',
300
+ 'audio/wav',
301
+ 'audio/webm'
302
+ ]
303
+
304
+ _VIDEO_TYPES = [
305
+ 'video/mov',
306
+ 'video/mpeg',
307
+ 'video/mpegps',
308
+ 'video/mpg',
309
+ 'video/mp4',
310
+ 'video/webm',
311
+ 'video/wmv',
312
+ 'video/x-flv',
313
+ 'video/3gpp',
314
+ ]
315
+
316
+ _PDF = [
317
+ 'application/pdf',
318
+ ]
319
+
320
+
268
321
  class VertexAIGeminiPro1_5(VertexAI): # pylint: disable=invalid-name
269
322
  """Vertex AI Gemini 1.5 Pro model."""
270
323
 
271
324
  model = 'gemini-1.5-pro-preview-0514'
272
- multimodal = True
325
+ supported_modalities = _PDF + _IMAGE_TYPES + _AUDIO_TYPES + _VIDEO_TYPES
273
326
 
274
327
 
275
328
  class VertexAIGeminiPro1_5_0409(VertexAI): # pylint: disable=invalid-name
276
329
  """Vertex AI Gemini 1.5 Pro model."""
277
330
 
278
331
  model = 'gemini-1.5-pro-preview-0409'
279
- multimodal = True
332
+ supported_modalities = _PDF + _IMAGE_TYPES + _AUDIO_TYPES + _VIDEO_TYPES
280
333
 
281
334
 
282
335
  class VertexAIGeminiFlash1_5(VertexAI): # pylint: disable=invalid-name
283
336
  """Vertex AI Gemini 1.5 Flash model."""
284
337
 
285
338
  model = 'gemini-1.5-flash-preview-0514'
286
- multimodal = True
339
+ supported_modalities = _PDF + _IMAGE_TYPES + _AUDIO_TYPES + _VIDEO_TYPES
287
340
 
288
341
 
289
342
  class VertexAIGeminiPro1(VertexAI): # pylint: disable=invalid-name
@@ -296,7 +349,7 @@ class VertexAIGeminiPro1Vision(VertexAI): # pylint: disable=invalid-name
296
349
  """Vertex AI Gemini 1.0 Pro model."""
297
350
 
298
351
  model = 'gemini-1.0-pro-vision'
299
- multimodal = True
352
+ supported_modalities = _IMAGE_TYPES + _VIDEO_TYPES
300
353
 
301
354
 
302
355
  class VertexAIPalm2(VertexAI): # pylint: disable=invalid-name
@@ -79,7 +79,7 @@ class VertexAITest(unittest.TestCase):
79
79
  )
80
80
 
81
81
  # Non-multimodal model.
82
- with self.assertRaisesRegex(ValueError, 'Unsupported modality'):
82
+ with self.assertRaisesRegex(lf.ModalityError, 'Unsupported modality'):
83
83
  vertexai.VertexAIGeminiPro1()._content_from_message(message)
84
84
 
85
85
  model = vertexai.VertexAIGeminiPro1Vision()
@@ -18,7 +18,7 @@
18
18
  # pylint: disable=g-import-not-at-top
19
19
 
20
20
  from langfun.core.modalities.audio import Audio
21
- from langfun.core.modalities.mime import MimeType
21
+ from langfun.core.modalities.mime import Mime
22
22
  from langfun.core.modalities.mime import Custom
23
23
  from langfun.core.modalities.ms_office import Docx
24
24
  from langfun.core.modalities.ms_office import Pptx
@@ -17,7 +17,7 @@ import functools
17
17
  from langfun.core.modalities import mime
18
18
 
19
19
 
20
- class Audio(mime.MimeType):
20
+ class Audio(mime.Mime):
21
21
  """Audio."""
22
22
 
23
23
  MIME_PREFIX = 'audio'
@@ -17,7 +17,7 @@ import functools
17
17
  from langfun.core.modalities import mime
18
18
 
19
19
 
20
- class Image(mime.MimeType):
20
+ class Image(mime.Mime):
21
21
  """Image."""
22
22
 
23
23
  MIME_PREFIX = 'image'
@@ -15,7 +15,9 @@
15
15
  import unittest
16
16
  from unittest import mock
17
17
 
18
+ import langfun.core as lf
18
19
  from langfun.core.modalities import image as image_lib
20
+ from langfun.core.modalities import mime as mime_lib
19
21
  import pyglove as pg
20
22
 
21
23
 
@@ -36,23 +38,29 @@ def mock_request(*args, **kwargs):
36
38
  return pg.Dict(content=image_content)
37
39
 
38
40
 
39
- class ImageContentTest(unittest.TestCase):
41
+ class ImageTest(unittest.TestCase):
40
42
 
41
- def test_image_content(self):
43
+ def test_from_bytes(self):
42
44
  image = image_lib.Image.from_bytes(image_content)
43
45
  self.assertEqual(image.image_format, 'png')
44
46
  self.assertIn('data:image/png;base64,', image._repr_html_())
45
47
  self.assertEqual(image.to_bytes(), image_content)
48
+ with self.assertRaisesRegex(
49
+ lf.ModalityError, '.* cannot be converted to text'
50
+ ):
51
+ image.to_text()
46
52
 
47
- def test_bad_image(self):
53
+ def test_from_bytes_invalid(self):
48
54
  image = image_lib.Image.from_bytes(b'bad')
49
55
  with self.assertRaisesRegex(ValueError, 'Expected MIME type'):
50
56
  _ = image.image_format
51
57
 
58
+ def test_from_bytes_base_cls(self):
59
+ self.assertIsInstance(
60
+ mime_lib.Mime.from_bytes(image_content), image_lib.Image
61
+ )
52
62
 
53
- class ImageFileTest(unittest.TestCase):
54
-
55
- def test_image_file(self):
63
+ def test_from_uri(self):
56
64
  image = image_lib.Image.from_uri('http://mock/web/a.png')
57
65
  with mock.patch('requests.get') as mock_requests_get:
58
66
  mock_requests_get.side_effect = mock_request
@@ -60,6 +68,15 @@ class ImageFileTest(unittest.TestCase):
60
68
  self.assertEqual(image._repr_html_(), '<img src="http://mock/web/a.png">')
61
69
  self.assertEqual(image.to_bytes(), image_content)
62
70
 
71
+ def test_from_uri_base_cls(self):
72
+ with mock.patch('requests.get') as mock_requests_get:
73
+ mock_requests_get.side_effect = mock_request
74
+ image = mime_lib.Mime.from_uri('http://mock/web/a.png')
75
+ self.assertIsInstance(image, image_lib.Image)
76
+ self.assertEqual(image.image_format, 'png')
77
+ self.assertEqual(image._repr_html_(), '<img src="http://mock/web/a.png">')
78
+ self.assertEqual(image.to_bytes(), image_content)
79
+
63
80
 
64
81
  if __name__ == '__main__':
65
82
  unittest.main()
@@ -15,15 +15,15 @@
15
15
 
16
16
  import base64
17
17
  import functools
18
- from typing import Annotated, Union
18
+ from typing import Annotated, Iterable, Type, Union
19
19
  import langfun.core as lf
20
20
  import magic
21
21
  import pyglove as pg
22
22
  import requests
23
23
 
24
24
 
25
- class MimeType(lf.Modality):
26
- """Base for MIME type data."""
25
+ class Mime(lf.Modality):
26
+ """Base for MIME data."""
27
27
 
28
28
  # The regular expression that describes the MIME type str.
29
29
  # If None, the MIME type is dynamic. Subclass could override.
@@ -39,12 +39,80 @@ class MimeType(lf.Modality):
39
39
  def mime_type(self) -> str:
40
40
  """Returns the MIME type."""
41
41
  mime = magic.from_buffer((self.to_bytes()), mime=True)
42
- if self.MIME_PREFIX and not mime.lower().startswith(self.MIME_PREFIX):
42
+ if (
43
+ self.MIME_PREFIX
44
+ and not mime.lower().startswith(self.MIME_PREFIX)
45
+ # NOTE(daiyip): libmagic fails to detect the MIME type of some binary
46
+ # files.
47
+ and mime != 'application/octet-stream'
48
+ ):
43
49
  raise ValueError(
44
50
  f'Expected MIME type: {self.MIME_PREFIX}, Encountered: {mime}'
45
51
  )
46
52
  return mime
47
53
 
54
+ @functools.cached_property
55
+ def is_text(self) -> bool:
56
+ return self.mime_type.startswith(
57
+ (
58
+ 'text/',
59
+ 'application/javascript',
60
+ 'application/json',
61
+ 'application/ld+json',
62
+ 'application/plain',
63
+ 'application/xhtml+xml',
64
+ 'application/xml',
65
+ 'application/x-tex',
66
+ 'application/x-yaml',
67
+ )
68
+ )
69
+
70
+ @property
71
+ def is_binary(self) -> bool:
72
+ """Returns True if the MIME type is a binary type."""
73
+ return not self.is_text
74
+
75
+ def to_text(self) -> str:
76
+ """Returns the text content of the MIME type."""
77
+ if not self.is_text:
78
+ raise lf.ModalityError(
79
+ f'MIME type {self.mime_type!r} cannot be converted to text.'
80
+ )
81
+ return self.to_bytes().decode()
82
+
83
+ def is_compatible(
84
+ self, mime_types: str | Iterable[str]
85
+ ) -> bool:
86
+ """Returns True if this object is compatible to any of the MIME types."""
87
+ if isinstance(mime_types, str):
88
+ mime_types = {mime_types}
89
+ return self._is_compatible(mime_types)
90
+
91
+ def _is_compatible(self, mime_types: Iterable[str]):
92
+ return self.mime_type in mime_types
93
+
94
+ def make_compatible(
95
+ self,
96
+ mime_types: str | Iterable[str]
97
+ ) -> Union['Mime', list['Mime']]:
98
+ """Makes compatible MIME objects from this object."""
99
+ if isinstance(mime_types, str):
100
+ mime_types = {mime_types}
101
+ if not self._is_compatible(mime_types):
102
+ raise lf.ModalityError(
103
+ f'MIME type {self.mime_type!r} cannot be converted to supported '
104
+ f'types: {mime_types!r}.'
105
+ )
106
+ return self._make_compatible(mime_types)
107
+
108
+ def _make_compatible(
109
+ self,
110
+ mime_types: Iterable[str]
111
+ ) -> Union['Mime', list['Mime']]:
112
+ """Makes compatbile MIME objects from this object."""
113
+ del mime_types
114
+ return self
115
+
48
116
  def _on_bound(self):
49
117
  super()._on_bound()
50
118
  if self.uri is None and self.content is None:
@@ -54,15 +122,7 @@ class MimeType(lf.Modality):
54
122
  if self.content is not None:
55
123
  return self.content
56
124
 
57
- assert self.uri is not None
58
- if self.uri.lower().startswith(('http:', 'https:', 'ftp:')):
59
- content = requests.get(
60
- self.uri,
61
- headers={'User-Agent': 'Langfun'},
62
- ).content
63
- else:
64
- content = pg.io.readfile(self.uri, mode='rb')
65
- self.rebind(content=content, skip_notification=True)
125
+ self.rebind(content=self.download(self.uri), skip_notification=True)
66
126
  return self.content
67
127
 
68
128
  @property
@@ -71,13 +131,42 @@ class MimeType(lf.Modality):
71
131
  return f'data:{self.mime_type};base64,{base64_content}'
72
132
 
73
133
  @classmethod
74
- def from_uri(cls, uri: str, **kwargs) -> 'MimeType':
134
+ def from_uri(cls, uri: str, **kwargs) -> 'Mime':
135
+ if cls is Mime:
136
+ content = cls.download(uri)
137
+ mime = magic.from_buffer(content, mime=True).lower()
138
+ return cls.class_from_mime_type(mime)(uri=uri, content=content, **kwargs)
75
139
  return cls(uri=uri, content=None, **kwargs)
76
140
 
77
141
  @classmethod
78
- def from_bytes(cls, content: bytes | str, **kwargs) -> 'MimeType':
142
+ def from_bytes(cls, content: bytes | str, **kwargs) -> 'Mime':
143
+ if cls is Mime:
144
+ mime = magic.from_buffer(content, mime=True).lower()
145
+ return cls.class_from_mime_type(mime)(content=content, **kwargs)
79
146
  return cls(content=content, **kwargs)
80
147
 
148
+ @classmethod
149
+ def class_from_mime_type(cls, mime_type: str) -> Type['Mime']:
150
+ """Subclass from the given MIME type."""
151
+ for subcls in cls.__subclasses__():
152
+ if subcls.MIME_PREFIX is not None and mime_type.startswith(
153
+ subcls.MIME_PREFIX):
154
+ return subcls
155
+ return cls
156
+
157
+ @classmethod
158
+ def download(cls, uri: str) -> bytes | str:
159
+ """Downloads the content of the given URI."""
160
+ if uri.lower().startswith(('http:', 'https:', 'ftp:')):
161
+ return requests.get(
162
+ uri,
163
+ headers={'User-Agent': 'Mozilla/5.0'},
164
+ ).content
165
+ else:
166
+ content = pg.io.readfile(uri, mode='rb')
167
+ assert content is not None
168
+ return content
169
+
81
170
  def _repr_html_(self) -> str:
82
171
  if self.uri and self.uri.lower().startswith(('http:', 'https:', 'ftp:')):
83
172
  uri = self.uri
@@ -90,7 +179,7 @@ class MimeType(lf.Modality):
90
179
 
91
180
 
92
181
  @pg.use_init_args(['mime', 'content', 'uri'])
93
- class Custom(MimeType):
182
+ class Custom(Mime):
94
183
  """Custom MIME data."""
95
184
 
96
185
  mime: Annotated[
@@ -15,6 +15,7 @@
15
15
  import unittest
16
16
  from unittest import mock
17
17
 
18
+ import langfun.core as lf
18
19
  from langfun.core.modalities import mime
19
20
  import pyglove as pg
20
21
 
@@ -31,10 +32,24 @@ def mock_readfile(*args, **kwargs):
31
32
 
32
33
  class CustomMimeTest(unittest.TestCase):
33
34
 
34
- def test_content(self):
35
- content = mime.Custom('text/plain', 'foo')
36
- self.assertEqual(content.to_bytes(), 'foo')
35
+ def test_from_byes(self):
36
+ content = mime.Mime.from_bytes(b'hello')
37
+ self.assertIs(content.__class__, mime.Mime)
38
+
39
+ content = mime.Custom('text/plain', b'foo')
40
+ self.assertEqual(content.to_bytes(), b'foo')
37
41
  self.assertEqual(content.mime_type, 'text/plain')
42
+ self.assertTrue(content.is_text)
43
+ self.assertFalse(content.is_binary)
44
+ self.assertEqual(content.to_text(), 'foo')
45
+ self.assertTrue(content.is_compatible('text/plain'))
46
+ self.assertFalse(content.is_compatible('text/xml'))
47
+ self.assertIs(content.make_compatible('text/plain'), content)
48
+
49
+ with self.assertRaisesRegex(
50
+ lf.ModalityError, '.* cannot be converted to supported types'
51
+ ):
52
+ content.make_compatible('application/pdf')
38
53
 
39
54
  with self.assertRaisesRegex(
40
55
  ValueError, 'Either uri or content must be provided.'
@@ -16,12 +16,13 @@
16
16
  import base64
17
17
  import io
18
18
  import os
19
+ from typing import Iterable
19
20
  from langfun.core.modalities import mime
20
21
  from langfun.core.modalities import pdf
21
22
  import requests
22
23
 
23
24
 
24
- class Xlsx(mime.MimeType):
25
+ class Xlsx(mime.Mime):
25
26
  """Xlsx file type."""
26
27
 
27
28
  MIME_PREFIX = (
@@ -37,8 +38,19 @@ class Xlsx(mime.MimeType):
37
38
  def _repr_html_(self) -> str:
38
39
  return self.to_html()
39
40
 
41
+ def _is_compatible(self, mime_types: Iterable[str]) -> bool:
42
+ return bool(set(mime_types).intersection([
43
+ 'text/html',
44
+ 'text/plain',
45
+ ]))
40
46
 
41
- class Docx(mime.MimeType):
47
+ def _make_compatible(self, mime_types: Iterable[str]) -> mime.Mime:
48
+ """Returns the MimeType of the converted file."""
49
+ del mime_types
50
+ return mime.Mime(uri=self.uri, content=self.to_html())
51
+
52
+
53
+ class Docx(mime.Mime):
42
54
  """Docx file type."""
43
55
 
44
56
  MIME_PREFIX = (
@@ -54,17 +66,26 @@ class Docx(mime.MimeType):
54
66
  def _repr_html_(self) -> str:
55
67
  return self.to_xml()
56
68
 
69
+ def _is_compatible(self, mime_types: Iterable[str]) -> bool:
70
+ return bool(set(mime_types).intersection([
71
+ 'application/xml',
72
+ 'text/xml',
73
+ 'text/plain',
74
+ ]))
75
+
76
+ def _make_compatible(self, mime_types: Iterable[str]) -> mime.Mime:
77
+ """Returns the MimeType of the converted file."""
78
+ del mime_types
79
+ return mime.Mime(uri=self.uri, content=self.to_xml())
80
+
57
81
 
58
- class Pptx(mime.MimeType):
82
+ class Pptx(mime.Mime):
59
83
  """Pptx file type."""
60
84
 
61
85
  MIME_PREFIX = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
62
86
  API_URL = 'https://v2.convertapi.com/convert/pptx/to/pdf'
63
87
 
64
88
  def to_pdf(self, convert_api_key: str | None = None) -> pdf.PDF:
65
- filename = os.path.basename(self.uri)
66
- file_bytes = self.to_bytes()
67
-
68
89
  api_key = convert_api_key or os.environ.get('CONVERT_API_KEY')
69
90
  url = f'{self.API_URL}?Secret={api_key}'
70
91
 
@@ -72,12 +93,19 @@ class Pptx(mime.MimeType):
72
93
  'Parameters': [{
73
94
  'Name': 'File',
74
95
  'FileValue': {
75
- 'Name': filename,
76
- 'Data': base64.b64encode(file_bytes),
96
+ 'Name': os.path.basename(self.uri) if self.uri else 'tmp.pptx',
97
+ 'Data': base64.b64encode(self.to_bytes()).decode('utf-8'),
77
98
  },
78
99
  }]
79
100
  }
80
101
  response = requests.post(url, json=json).json()
81
102
  base64_pdf = response['Files'][0]['FileData']
82
- pdf_bytes = base64.b64decode(base64_pdf)
83
- return pdf.PDF.from_bytes(content=pdf_bytes)
103
+ return pdf.PDF.from_bytes(base64.b64decode(base64_pdf))
104
+
105
+ def _is_compatible(self, mime_types: Iterable[str]) -> bool:
106
+ return 'application/pdf' in mime_types
107
+
108
+ def _make_compatible(self, mime_types: Iterable[str]) -> mime.Mime:
109
+ """Returns the MimeType of the converted file."""
110
+ del mime_types
111
+ return self.to_pdf()
@@ -12,11 +12,13 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  """Video tests."""
15
+ import base64
15
16
  import io
16
17
  import unittest
17
18
  from unittest import mock
18
19
 
19
20
  from langfun.core.modalities import ms_office as ms_office_lib
21
+ from langfun.core.modalities import pdf as pdf_lib
20
22
  import pyglove as pg
21
23
 
22
24
 
@@ -243,23 +245,72 @@ def pptx_mock_request(*args, **kwargs):
243
245
  return pg.Dict(content=pptx_bytes)
244
246
 
245
247
 
248
+ pdf_bytes = (
249
+ b'%PDF-1.1\n%\xc2\xa5\xc2\xb1\xc3\xab\n\n1 0 obj\n'
250
+ b'<< /Type /Catalog\n /Pages 2 0 R\n >>\nendobj\n\n2 0 obj\n '
251
+ b'<< /Type /Pages\n /Kids [3 0 R]\n '
252
+ b'/Count 1\n /MediaBox [0 0 300 144]\n '
253
+ b'>>\nendobj\n\n3 0 obj\n '
254
+ b'<< /Type /Page\n /Parent 2 0 R\n /Resources\n '
255
+ b'<< /Font\n'
256
+ b'<< /F1\n'
257
+ b'<< /Type /Font\n'
258
+ b'/Subtype /Type1\n'
259
+ b'/BaseFont /Times-Roman\n'
260
+ b'>>\n>>\n>>\n '
261
+ b'/Contents 4 0 R\n >>\nendobj\n\n4 0 obj\n '
262
+ b'<< /Length 55 >>\nstream\n BT\n /F1 18 Tf\n 0 0 Td\n '
263
+ b'(Hello World) Tj\n ET\nendstream\nendobj\n\nxref\n0 5\n0000000000 '
264
+ b'65535 f \n0000000018 00000 n \n0000000077 00000 n \n0000000178 00000 n '
265
+ b'\n0000000457 00000 n \ntrailer\n << /Root 1 0 R\n /Size 5\n '
266
+ b'>>\nstartxref\n565\n%%EOF\n'
267
+ )
268
+
269
+
270
+ def convert_mock_request(*args, **kwargs):
271
+ del args, kwargs
272
+
273
+ class Result:
274
+ def json(self):
275
+ return {
276
+ 'Files': [
277
+ {
278
+ 'FileData': base64.b64encode(pdf_bytes).decode()
279
+ }
280
+ ]
281
+ }
282
+ return Result()
283
+
284
+
246
285
  class DocxTest(unittest.TestCase):
247
286
 
248
- def test_content(self):
287
+ def test_from_bytes(self):
249
288
  content = ms_office_lib.Docx.from_bytes(docx_bytes)
250
- self.assertEqual(
289
+ self.assertIn(
251
290
  content.mime_type,
252
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
291
+ (
292
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
293
+ 'application/octet-stream',
294
+ ),
253
295
  )
254
296
  self.assertEqual(content.to_bytes(), docx_bytes)
297
+ self.assertTrue(content.is_compatible('text/plain'))
298
+ self.assertFalse(content.is_compatible('application/pdf'))
299
+ self.assertEqual(
300
+ content.make_compatible(['image/png', 'text/plain']).mime_type,
301
+ 'text/plain'
302
+ )
255
303
 
256
- def test_file(self):
304
+ def test_from_uri(self):
257
305
  content = ms_office_lib.Docx.from_uri('http://mock/web/a.docx')
258
306
  with mock.patch('requests.get') as mock_requests_get:
259
307
  mock_requests_get.side_effect = docx_mock_request
260
- self.assertEqual(
308
+ self.assertIn(
261
309
  content.mime_type,
262
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
310
+ (
311
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
312
+ 'application/octet-stream',
313
+ ),
263
314
  )
264
315
  self.assertEqual(content.to_bytes(), docx_bytes)
265
316
  self.assertEqual(content.to_xml(), expected_docx_xml)
@@ -267,21 +318,33 @@ class DocxTest(unittest.TestCase):
267
318
 
268
319
  class XlsxTest(unittest.TestCase):
269
320
 
270
- def test_content(self):
321
+ def test_from_bytes(self):
271
322
  content = ms_office_lib.Xlsx.from_bytes(xlsx_bytes)
272
- self.assertEqual(
323
+ self.assertIn(
273
324
  content.mime_type,
274
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
325
+ (
326
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
327
+ 'application/octet-stream',
328
+ ),
275
329
  )
276
330
  self.assertEqual(content.to_bytes(), xlsx_bytes)
331
+ self.assertTrue(content.is_compatible('text/plain'))
332
+ self.assertFalse(content.is_compatible('application/pdf'))
333
+ self.assertEqual(
334
+ content.make_compatible('text/plain').mime_type,
335
+ 'text/html'
336
+ )
277
337
 
278
- def test_file(self):
338
+ def test_from_uri(self):
279
339
  content = ms_office_lib.Xlsx.from_uri('http://mock/web/a.xlsx')
280
340
  with mock.patch('requests.get') as mock_requests_get:
281
341
  mock_requests_get.side_effect = xlsx_mock_request
282
- self.assertEqual(
342
+ self.assertIn(
283
343
  content.mime_type,
284
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
344
+ (
345
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
346
+ 'application/octet-stream',
347
+ ),
285
348
  )
286
349
  self.assertEqual(content.to_bytes(), xlsx_bytes)
287
350
  self.assertEqual(content.to_html(), expected_xlsx_html)
@@ -291,22 +354,36 @@ class PptxTest(unittest.TestCase):
291
354
 
292
355
  def test_content(self):
293
356
  content = ms_office_lib.Pptx.from_bytes(pptx_bytes)
294
- self.assertEqual(
357
+ self.assertIn(
295
358
  content.mime_type,
296
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
359
+ (
360
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
361
+ 'application/octet-stream',
362
+ ),
297
363
  )
298
364
  self.assertEqual(content.to_bytes(), pptx_bytes)
299
365
 
300
366
  def test_file(self):
301
367
  content = ms_office_lib.Pptx.from_uri('http://mock/web/a.pptx')
368
+ self.assertFalse(content.is_compatible('text/plain'))
369
+ self.assertTrue(content.is_compatible('application/pdf'))
302
370
  with mock.patch('requests.get') as mock_requests_get:
303
371
  mock_requests_get.side_effect = pptx_mock_request
304
- self.assertEqual(
372
+ self.assertIn(
305
373
  content.mime_type,
306
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
374
+ (
375
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
376
+ 'application/octet-stream',
377
+ ),
307
378
  )
308
379
  self.assertEqual(content.to_bytes(), pptx_bytes)
309
380
 
381
+ with mock.patch('requests.post') as mock_requests_post:
382
+ mock_requests_post.side_effect = convert_mock_request
383
+ self.assertIsInstance(
384
+ content.make_compatible('application/pdf'), pdf_lib.PDF
385
+ )
386
+
310
387
 
311
388
  if __name__ == '__main__':
312
389
  unittest.main()
@@ -16,7 +16,7 @@
16
16
  from langfun.core.modalities import mime
17
17
 
18
18
 
19
- class PDF(mime.MimeType):
19
+ class PDF(mime.Mime):
20
20
  """PDF document."""
21
21
 
22
22
  MIME_PREFIX = 'application/pdf'
@@ -17,7 +17,7 @@ import functools
17
17
  from langfun.core.modalities import mime
18
18
 
19
19
 
20
- class Video(mime.MimeType):
20
+ class Video(mime.Mime):
21
21
  """Video."""
22
22
 
23
23
  MIME_PREFIX = 'video'
langfun/core/modality.py CHANGED
@@ -108,3 +108,7 @@ class ModalityRef(pg.Object, pg.typing.CustomTyping):
108
108
  return ModalityRef(name=value.sym_path + k)
109
109
  return v
110
110
  return value.clone().rebind(_placehold, raise_on_no_change=False)
111
+
112
+
113
+ class ModalityError(RuntimeError): # pylint: disable=g-bad-exception-name
114
+ """Exception raised when modality is not supported."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langfun
3
- Version: 0.0.2.dev20240531
3
+ Version: 0.0.2.dev20240601
4
4
  Summary: Langfun: Language as Functions.
5
5
  Home-page: https://github.com/google/langfun
6
6
  Author: Langfun Authors
@@ -25,13 +25,14 @@ Requires-Dist: google-cloud-aiplatform >=1.5.0
25
25
  Requires-Dist: google-generativeai >=0.3.2
26
26
  Requires-Dist: jinja2 >=3.1.2
27
27
  Requires-Dist: openai ==0.27.2
28
+ Requires-Dist: openpyxl >=3.1.0
29
+ Requires-Dist: pandas >=2.1.4
28
30
  Requires-Dist: pyglove >=0.4.5.dev20240423
31
+ Requires-Dist: python-docx >=0.8.11
29
32
  Requires-Dist: python-magic >=0.4.27
30
33
  Requires-Dist: requests >=2.31.0
31
34
  Requires-Dist: termcolor ==1.1.0
32
35
  Requires-Dist: tqdm >=4.64.1
33
- Requires-Dist: python-docx >=0.8.11
34
- Requires-Dist: pandas >=2.1.4
35
36
 
36
37
  <div align="center">
37
38
  <img src="https://raw.githubusercontent.com/google/langfun/main/docs/_static/logo.svg" width="520px" alt="logo"></img>
@@ -1,5 +1,5 @@
1
- langfun/__init__.py,sha256=5RIeFKQLwyCk36n-NfZgbbfgdPLKI0ucy2O7b0B-384,2192
2
- langfun/core/__init__.py,sha256=6QEuXOZ9BXxm6TjpaMXuLwUBTYO3pkFDqn9QVBXyyPQ,4248
1
+ langfun/__init__.py,sha256=LFsDp22pTeJHmzzKEg2OLmSVOPAym00DyF38LmrL2n4,2263
2
+ langfun/core/__init__.py,sha256=nFJx6X7oB7IIWsAQqjbgZ_ScH-gsKg53YgAkuDvY0cw,4296
3
3
  langfun/core/component.py,sha256=oxesbC0BoE_TbtxwW5x-BAZWxZyyJbuPiX5S38RqCv0,9909
4
4
  langfun/core/component_test.py,sha256=uR-_Sz_42Jxc5qzLIB-f5_pXmNwnC01Xlbv5NOQSeSU,8021
5
5
  langfun/core/concurrent.py,sha256=TRc49pJ3HQro2kb5FtcWkHjhBm8UcgE8RJybU5cU3-0,24537
@@ -13,7 +13,7 @@ langfun/core/language_model_test.py,sha256=NZaSUls6cZdtxiqkqumWbtkx9zgNiJlsviYZO
13
13
  langfun/core/memory.py,sha256=f-asN1F7Vehgdn_fK84v73GrEUOxRtaW934keutTKjk,2416
14
14
  langfun/core/message.py,sha256=Rw3yC9HyGRjMhfDgyNjGlSCALEyDDbJ0_o6qTXeeDiQ,15738
15
15
  langfun/core/message_test.py,sha256=b6DDRoQ5j3uK-dc0QPSLelNTKaXX10MxJrRiI61iGX4,9574
16
- langfun/core/modality.py,sha256=-BZDYf5d4bmZnhZyS4QVGTSwvU7Xgs_55IOzeRmyacE,3378
16
+ langfun/core/modality.py,sha256=Tla4t86DUYHpbZ2G7dy1r19fTj_Ga5XOvlYp6lbWa-Q,3512
17
17
  langfun/core/modality_test.py,sha256=HyZ5xONKQ0Fw18SzoWAq-Ob9njOXIIjBo1hNtw-rudw,2400
18
18
  langfun/core/natural_language.py,sha256=3ynSnaYQnjE60LIPK5fyMgdIjubnPYZwzGq4rWPeloE,1177
19
19
  langfun/core/natural_language_test.py,sha256=LHGU_1ytbkGuSZQFIFP7vP3dBlcY4-A12fT6dbjUA0E,1424
@@ -53,16 +53,16 @@ langfun/core/llms/anthropic.py,sha256=7W9YdPN3SlAFhAIQlihMkrpo7tTY_4NvD0KIlCrqcs
53
53
  langfun/core/llms/anthropic_test.py,sha256=TMM30myyEhwF99Le4RvJEXOn8RYl0q1FRkt9Q9nl1jk,5540
54
54
  langfun/core/llms/fake.py,sha256=Dd7-6ka9pFf3fcWZyczamjOqQ91MOI-m7We3Oc9Ffmo,2927
55
55
  langfun/core/llms/fake_test.py,sha256=ipKfdOcuqVcJ8lDXVpnBVb9HHG0hAVkFkMoHpWjC2cI,7212
56
- langfun/core/llms/google_genai.py,sha256=H1GdarpoMb9RjQz7a4BqVF6loQf3S_mMv8G8TFYrCvw,8999
57
- langfun/core/llms/google_genai_test.py,sha256=VT_MMmyxHMe4sl4uK_UZzWyxKFFMlF3xc3v6SljJQE0,7529
56
+ langfun/core/llms/google_genai.py,sha256=Rl5a5CyF_6Y0BYYArKk8yMaenv1rH3MUQLy6b3dfMRI,10202
57
+ langfun/core/llms/google_genai_test.py,sha256=iTISk3tJ4-3gjWmzcKQhEbH3ke4AkEiCu8rAGtB7SvU,7535
58
58
  langfun/core/llms/groq.py,sha256=NaGItVL_pkOpqPpI4bPGU27xLFRoaeizZ49v2s-4ERs,7844
59
59
  langfun/core/llms/groq_test.py,sha256=M6GtlrsOvDun_j-sR8cPh4W_moHWZNSTiThu3kuwbbc,5281
60
60
  langfun/core/llms/llama_cpp.py,sha256=Y_KkMUf3Xfac49koMUtUslKl3h-HWp3-ntq7Jaa3bdo,2385
61
61
  langfun/core/llms/llama_cpp_test.py,sha256=ZxC6defGd_HX9SFRU9U4cJiQnBKundbOrchbXuC1Z2M,1683
62
62
  langfun/core/llms/openai.py,sha256=IN46gIqfY6aEEfxCPNmyH1hrep3oWBhJDwVFilfqNkM,13657
63
63
  langfun/core/llms/openai_test.py,sha256=QWDzTgi8F2Z9u9ip6alK4rDEp_YraVTxWlDX5XOsKJk,14858
64
- langfun/core/llms/vertexai.py,sha256=rrwHRtox-gayVBjrkR_lnko98b0iFIyxsRUPgB_09T8,9921
65
- langfun/core/llms/vertexai_test.py,sha256=PbkUTVYgbFhg5lDd3HgBJM0kr-OLVz10iph2C-SJblk,7645
64
+ langfun/core/llms/vertexai.py,sha256=eILbXoMSza5r4FLGlIdH6-eD8Ggy9Z4PdjLaBDxy29A,11162
65
+ langfun/core/llms/vertexai_test.py,sha256=G18BG36h5KvmX2zutDTLjtYCRjTuP_nWIFm4FMnLnyY,7651
66
66
  langfun/core/llms/cache/__init__.py,sha256=QAo3InUMDM_YpteNnVCSejI4zOsnjSMWKJKzkb3VY64,993
67
67
  langfun/core/llms/cache/base.py,sha256=cFfYvOIUae842pncqCAsRvqXCk2AnAsRYVx0mcIoAeY,3338
68
68
  langfun/core/llms/cache/in_memory.py,sha256=YfFyJEhLs73cUiB0ZfhMxYpdE8Iuxxw-dvMFwGHTSHw,4742
@@ -70,18 +70,18 @@ langfun/core/llms/cache/in_memory_test.py,sha256=D-n26h__rVXQO51WRFhRfq5sw1oifRL
70
70
  langfun/core/memories/__init__.py,sha256=HpghfZ-w1NQqzJXBx8Lz0daRhB2rcy2r9Xm491SBhC4,773
71
71
  langfun/core/memories/conversation_history.py,sha256=c9amD8hCxGFiZuVAzkP0dOMWSp8L90uvwkOejjuBqO0,1835
72
72
  langfun/core/memories/conversation_history_test.py,sha256=AaW8aNoFjxNusanwJDV0r3384Mg0eAweGmPx5DIkM0Y,2052
73
- langfun/core/modalities/__init__.py,sha256=0RYYHcjD-s-QHGuW0rkjh9F3c0qMDKtH9ZIQsMcKPXE,1273
74
- langfun/core/modalities/audio.py,sha256=Fbdt9iJOLwtMetfoI9hNjzAMrWfbGE7T3W7vKgL9B0o,956
73
+ langfun/core/modalities/__init__.py,sha256=F8P72IwFiTpEseTR2tYEJyQMlDW7fd9csvGJquLKJNg,1269
74
+ langfun/core/modalities/audio.py,sha256=Qxo7bYjLKQ1gVJVomr9RqR2SvxY826QgXhTzzk437Sk,952
75
75
  langfun/core/modalities/audio_test.py,sha256=gWCB9h3FyrdGqro3ajBXqkw0lU0W1sBjOOq6wZbl7Fg,2027
76
- langfun/core/modalities/image.py,sha256=hRapD4jTdzzQ23zz44K1HMaDpzXVEhrDNKgTXs7cy7k,930
77
- langfun/core/modalities/image_test.py,sha256=zyx1eyAdcWK3iwk4q4zgPPmkvKOH-1Los3orgQE9aqk,2295
78
- langfun/core/modalities/mime.py,sha256=W-rsMyCNeVdurEmnX2aogRrlsM4aCZ6hlXK1kTCoGQE,3056
79
- langfun/core/modalities/mime_test.py,sha256=pN6EHCf9qKMTZaVtQS7M8HWJcH68B3htiWvbq_tLh5I,1881
80
- langfun/core/modalities/ms_office.py,sha256=m_-PMuY0iB52POkp_0pRztaT4-O57ugzGNdI14APJEk,2384
81
- langfun/core/modalities/ms_office_test.py,sha256=D8ZGR6jy3wz7KmrCoB8FtsZ_Faknpws1TRxrZWIgp7o,85434
82
- langfun/core/modalities/pdf.py,sha256=A-lVHiXdELpbciedSVyhIgQVi1kI1QlE-YBjJFFh3oU,731
76
+ langfun/core/modalities/image.py,sha256=qi7B9uYLxBoKvMzApdOQNpVcp_dKaRwLzeshg2nvo9k,926
77
+ langfun/core/modalities/image_test.py,sha256=qU7G4ucUihIQ9ZB453FsUfcOipUYx5TnnuoMB1GIMfE,3034
78
+ langfun/core/modalities/mime.py,sha256=yMpbBAhf7MmEPJm9qj7tTn7_XionZQ4XkgTT8StA7io,5836
79
+ langfun/core/modalities/mime_test.py,sha256=ruEro7Joima2r-zOuQfO0NzBvmaweSQ6F6jsf-w4Bns,2468
80
+ langfun/core/modalities/ms_office.py,sha256=jOidMSdWCaV9RILpGz8VJkpTSpHJNoirD53jzQvcytM,3388
81
+ langfun/core/modalities/ms_office_test.py,sha256=d_NZ0QU23NydenYZgNj6YxgO5ZYzjg-HCbglsVJGp04,87866
82
+ langfun/core/modalities/pdf.py,sha256=mfaeCbUA4JslFVTARiJh8hW7imvL4tLVw9gUhO5bAZA,727
83
83
  langfun/core/modalities/pdf_test.py,sha256=KE40zJD3Whe6ty2OULkp1J8jwLmB4ZjGXlGekluTP48,1952
84
- langfun/core/modalities/video.py,sha256=6qADCwwv-pEtzxMs_1YvhWgX6NqTsjviQv6IZxQPDTY,959
84
+ langfun/core/modalities/video.py,sha256=sKcXxbx9S1ERjH8yEzkbtySpcRJD40QiPIQiIBy-U5I,955
85
85
  langfun/core/modalities/video_test.py,sha256=GbsoefSeO7y8kCYhTtp4s9E3ah_eYrb6Z-MXpS01RFc,2046
86
86
  langfun/core/structured/__init__.py,sha256=yp60yeDSVlyT0ElmLwbpBHnQtk_JX5udnjG1UGcsXKA,3776
87
87
  langfun/core/structured/completion.py,sha256=RzWdHyaqKj-tj6mGwpHXk0s8YbM0UEHSpyT2axmj-o8,7343
@@ -111,8 +111,8 @@ langfun/core/templates/demonstration.py,sha256=vCrgYubdZM5Umqcgp8NUVGXgr4P_c-fik
111
111
  langfun/core/templates/demonstration_test.py,sha256=SafcDQ0WgI7pw05EmPI2S4v1t3ABKzup8jReCljHeK4,2162
112
112
  langfun/core/templates/selfplay.py,sha256=yhgrJbiYwq47TgzThmHrDQTF4nDrTI09CWGhuQPNv-s,2273
113
113
  langfun/core/templates/selfplay_test.py,sha256=DYVrkk7uNKCqJGEHH31HssU2BPuMItU1vJLzfcXIlYg,2156
114
- langfun-0.0.2.dev20240531.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
115
- langfun-0.0.2.dev20240531.dist-info/METADATA,sha256=HoYUBYL46dYzSPalF8klTh42cE0-Mg7J2OGJRkokxYM,3518
116
- langfun-0.0.2.dev20240531.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
117
- langfun-0.0.2.dev20240531.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
118
- langfun-0.0.2.dev20240531.dist-info/RECORD,,
114
+ langfun-0.0.2.dev20240601.dist-info/LICENSE,sha256=WNHhf_5RCaeuKWyq_K39vmp9F28LxKsB4SpomwSZ2L0,11357
115
+ langfun-0.0.2.dev20240601.dist-info/METADATA,sha256=V6qAAPX1gt2gDEInFQKJIvYe48IbEztRw5qwpgq_QH0,3550
116
+ langfun-0.0.2.dev20240601.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
117
+ langfun-0.0.2.dev20240601.dist-info/top_level.txt,sha256=RhlEkHxs1qtzmmtWSwYoLVJAc1YrbPtxQ52uh8Z9VvY,8
118
+ langfun-0.0.2.dev20240601.dist-info/RECORD,,