langfun 0.0.2.dev20240429__py3-none-any.whl → 0.1.2.dev202501140804__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.
Files changed (144) hide show
  1. langfun/__init__.py +20 -2
  2. langfun/core/__init__.py +16 -5
  3. langfun/core/agentic/__init__.py +30 -0
  4. langfun/core/agentic/action.py +854 -0
  5. langfun/core/agentic/action_eval.py +150 -0
  6. langfun/core/agentic/action_eval_test.py +109 -0
  7. langfun/core/agentic/action_test.py +136 -0
  8. langfun/core/coding/python/__init__.py +5 -11
  9. langfun/core/coding/python/correction.py +37 -21
  10. langfun/core/coding/python/correction_test.py +29 -3
  11. langfun/core/coding/python/execution.py +40 -216
  12. langfun/core/coding/python/execution_test.py +29 -89
  13. langfun/core/coding/python/generation.py +21 -11
  14. langfun/core/coding/python/generation_test.py +2 -2
  15. langfun/core/coding/python/parsing.py +108 -193
  16. langfun/core/coding/python/parsing_test.py +2 -105
  17. langfun/core/component.py +63 -2
  18. langfun/core/component_test.py +53 -0
  19. langfun/core/concurrent.py +414 -117
  20. langfun/core/concurrent_test.py +111 -24
  21. langfun/core/console.py +18 -5
  22. langfun/core/console_test.py +17 -0
  23. langfun/core/eval/__init__.py +16 -1
  24. langfun/core/eval/base.py +622 -174
  25. langfun/core/eval/base_test.py +200 -54
  26. langfun/core/eval/matching.py +63 -76
  27. langfun/core/eval/matching_test.py +17 -8
  28. langfun/core/eval/patching.py +130 -0
  29. langfun/core/eval/patching_test.py +170 -0
  30. langfun/core/eval/scoring.py +26 -26
  31. langfun/core/eval/scoring_test.py +19 -2
  32. langfun/core/eval/v2/__init__.py +42 -0
  33. langfun/core/eval/v2/checkpointing.py +380 -0
  34. langfun/core/eval/v2/checkpointing_test.py +228 -0
  35. langfun/core/eval/v2/eval_test_helper.py +136 -0
  36. langfun/core/eval/v2/evaluation.py +725 -0
  37. langfun/core/eval/v2/evaluation_test.py +180 -0
  38. langfun/core/eval/v2/example.py +305 -0
  39. langfun/core/eval/v2/example_test.py +128 -0
  40. langfun/core/eval/v2/experiment.py +1048 -0
  41. langfun/core/eval/v2/experiment_test.py +433 -0
  42. langfun/core/eval/v2/metric_values.py +156 -0
  43. langfun/core/eval/v2/metric_values_test.py +80 -0
  44. langfun/core/eval/v2/metrics.py +357 -0
  45. langfun/core/eval/v2/metrics_test.py +203 -0
  46. langfun/core/eval/v2/progress.py +348 -0
  47. langfun/core/eval/v2/progress_test.py +82 -0
  48. langfun/core/eval/v2/progress_tracking.py +210 -0
  49. langfun/core/eval/v2/progress_tracking_test.py +66 -0
  50. langfun/core/eval/v2/reporting.py +270 -0
  51. langfun/core/eval/v2/reporting_test.py +158 -0
  52. langfun/core/eval/v2/runners.py +488 -0
  53. langfun/core/eval/v2/runners_test.py +334 -0
  54. langfun/core/langfunc.py +4 -17
  55. langfun/core/langfunc_test.py +22 -6
  56. langfun/core/language_model.py +577 -39
  57. langfun/core/language_model_test.py +470 -56
  58. langfun/core/llms/__init__.py +87 -16
  59. langfun/core/llms/anthropic.py +312 -87
  60. langfun/core/llms/anthropic_test.py +71 -3
  61. langfun/core/llms/cache/base.py +21 -2
  62. langfun/core/llms/cache/in_memory.py +13 -0
  63. langfun/core/llms/cache/in_memory_test.py +53 -2
  64. langfun/core/llms/compositional.py +101 -0
  65. langfun/core/llms/compositional_test.py +73 -0
  66. langfun/core/llms/deepseek.py +117 -0
  67. langfun/core/llms/deepseek_test.py +61 -0
  68. langfun/core/llms/fake.py +11 -7
  69. langfun/core/llms/fake_test.py +14 -0
  70. langfun/core/llms/gemini.py +507 -0
  71. langfun/core/llms/gemini_test.py +195 -0
  72. langfun/core/llms/google_genai.py +62 -218
  73. langfun/core/llms/google_genai_test.py +9 -202
  74. langfun/core/llms/groq.py +160 -144
  75. langfun/core/llms/groq_test.py +31 -137
  76. langfun/core/llms/llama_cpp.py +15 -42
  77. langfun/core/llms/llama_cpp_test.py +4 -30
  78. langfun/core/llms/openai.py +395 -203
  79. langfun/core/llms/openai_compatible.py +179 -0
  80. langfun/core/llms/openai_compatible_test.py +495 -0
  81. langfun/core/llms/openai_test.py +30 -395
  82. langfun/core/llms/rest.py +113 -0
  83. langfun/core/llms/rest_test.py +111 -0
  84. langfun/core/llms/vertexai.py +192 -0
  85. langfun/core/llms/vertexai_test.py +52 -0
  86. langfun/core/logging.py +284 -0
  87. langfun/core/logging_test.py +125 -0
  88. langfun/core/message.py +319 -9
  89. langfun/core/message_test.py +190 -13
  90. langfun/core/modalities/__init__.py +6 -2
  91. langfun/core/modalities/audio.py +30 -0
  92. langfun/core/modalities/audio_test.py +63 -0
  93. langfun/core/modalities/image.py +39 -20
  94. langfun/core/modalities/image_test.py +52 -9
  95. langfun/core/modalities/mime.py +206 -29
  96. langfun/core/modalities/mime_test.py +90 -9
  97. langfun/core/modalities/ms_office.py +117 -0
  98. langfun/core/modalities/ms_office_test.py +389 -0
  99. langfun/core/modalities/pdf.py +22 -0
  100. langfun/core/modalities/pdf_test.py +57 -0
  101. langfun/core/modalities/video.py +9 -26
  102. langfun/core/modalities/video_test.py +3 -3
  103. langfun/core/modality.py +26 -3
  104. langfun/core/modality_test.py +2 -2
  105. langfun/core/sampling.py +11 -11
  106. langfun/core/structured/__init__.py +12 -16
  107. langfun/core/structured/completion.py +32 -5
  108. langfun/core/structured/completion_test.py +7 -6
  109. langfun/core/structured/description.py +2 -2
  110. langfun/core/structured/description_test.py +3 -3
  111. langfun/core/structured/function_generation.py +60 -27
  112. langfun/core/structured/function_generation_test.py +72 -2
  113. langfun/core/structured/mapping.py +97 -47
  114. langfun/core/structured/mapping_test.py +90 -2
  115. langfun/core/structured/parsing.py +33 -21
  116. langfun/core/structured/parsing_test.py +53 -9
  117. langfun/core/structured/querying.py +746 -0
  118. langfun/core/structured/{prompting_test.py → querying_test.py} +469 -51
  119. langfun/core/structured/schema.py +204 -97
  120. langfun/core/structured/schema_generation.py +1 -1
  121. langfun/core/structured/schema_test.py +130 -29
  122. langfun/core/structured/scoring.py +125 -19
  123. langfun/core/structured/scoring_test.py +30 -0
  124. langfun/core/structured/tokenization.py +64 -0
  125. langfun/core/structured/tokenization_test.py +48 -0
  126. langfun/core/template.py +115 -1
  127. langfun/core/template_test.py +71 -1
  128. langfun/core/templates/conversation.py +9 -0
  129. langfun/core/templates/conversation_test.py +4 -3
  130. langfun/core/templates/selfplay_test.py +10 -2
  131. langfun-0.1.2.dev202501140804.dist-info/METADATA +225 -0
  132. langfun-0.1.2.dev202501140804.dist-info/RECORD +153 -0
  133. {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/WHEEL +1 -1
  134. langfun/core/coding/python/errors.py +0 -108
  135. langfun/core/coding/python/errors_test.py +0 -99
  136. langfun/core/coding/python/permissions.py +0 -90
  137. langfun/core/coding/python/permissions_test.py +0 -86
  138. langfun/core/structured/prompting.py +0 -238
  139. langfun/core/text_formatting.py +0 -162
  140. langfun/core/text_formatting_test.py +0 -47
  141. langfun-0.0.2.dev20240429.dist-info/METADATA +0 -100
  142. langfun-0.0.2.dev20240429.dist-info/RECORD +0 -108
  143. {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/LICENSE +0 -0
  144. {langfun-0.0.2.dev20240429.dist-info → langfun-0.1.2.dev202501140804.dist-info}/top_level.txt +0 -0
@@ -15,6 +15,7 @@
15
15
 
16
16
  import inspect
17
17
  import unittest
18
+ from langfun.core import language_model
18
19
  from langfun.core import message
19
20
  from langfun.core import modality
20
21
  import pyglove as pg
@@ -36,11 +37,19 @@ class MessageTest(unittest.TestCase):
36
37
 
37
38
  d = pg.Dict(x=A())
38
39
 
39
- m = message.UserMessage('hi', metadata=dict(x=1), x=pg.Ref(d.x), y=2)
40
+ m = message.UserMessage(
41
+ 'hi',
42
+ metadata=dict(x=1), x=pg.Ref(d.x),
43
+ y=2,
44
+ tags=['lm-input']
45
+ )
40
46
  self.assertEqual(m.metadata, {'x': pg.Ref(d.x), 'y': 2})
41
47
  self.assertEqual(m.sender, 'User')
42
48
  self.assertIs(m.x, d.x)
43
49
  self.assertEqual(m.y, 2)
50
+ self.assertTrue(m.has_tag('lm-input'))
51
+ self.assertTrue(m.has_tag(('lm-input', '')))
52
+ self.assertFalse(m.has_tag('lm-response'))
44
53
 
45
54
  with self.assertRaises(AttributeError):
46
55
  _ = m.z
@@ -54,7 +63,7 @@ class MessageTest(unittest.TestCase):
54
63
  self.assertTrue(
55
64
  pg.eq(
56
65
  message.UserMessage.from_value(CustomModality('foo')),
57
- message.UserMessage('{{object}}', object=CustomModality('foo')),
66
+ message.UserMessage('<<[[object]]>>', object=CustomModality('foo')),
58
67
  )
59
68
  )
60
69
  m = message.UserMessage('hi')
@@ -258,13 +267,16 @@ class MessageTest(unittest.TestCase):
258
267
 
259
268
  def test_referred_modalities(self):
260
269
  m1 = message.UserMessage(
261
- 'hi, this is a {{img1}} and {{x.img2}}',
270
+ 'hi, this is a <<[[img1]]>> and <<[[x.img2]]>>',
262
271
  img1=CustomModality('foo'),
263
272
  x=dict(img2=CustomModality('bar')),
264
273
  )
265
274
  m2 = message.SystemMessage('class Question:\n image={{img1}}', source=m1)
266
275
  m3 = message.AIMessage(
267
- 'This is the {{output_image}} based on {{x.img2}}, {{unknown_var}}',
276
+ (
277
+ 'This is the <<[[output_image]]>> based on <<[[x.img2]]>>, '
278
+ '{{unknown_var}}'
279
+ ),
268
280
  output_image=CustomModality('bar'),
269
281
  source=m2,
270
282
  )
@@ -276,11 +288,25 @@ class MessageTest(unittest.TestCase):
276
288
  },
277
289
  )
278
290
 
291
+ def test_text_with_modality_hash(self):
292
+ m = message.UserMessage(
293
+ 'hi, this is a <<[[img1]]>> and <<[[x.img2]]>>',
294
+ img1=CustomModality('foo'),
295
+ x=dict(img2=CustomModality('bar')),
296
+ )
297
+ self.assertEqual(
298
+ m.text_with_modality_hash,
299
+ (
300
+ 'hi, this is a <<[[img1]]>> and <<[[x.img2]]>>'
301
+ '<img1>acbd18db</img1><x.img2>37b51d19</x.img2>'
302
+ )
303
+ )
304
+
279
305
  def test_chunking(self):
280
306
  m = message.UserMessage(
281
307
  inspect.cleandoc("""
282
- Hi, this is {{a}} and this is {{b}}.
283
- {{x.c}} {{something else
308
+ Hi, this is <<[[a]]>> and this is {{b}}.
309
+ <<[[x.c]]>> {{something else
284
310
  """),
285
311
  a=CustomModality('foo'),
286
312
  x=dict(c=CustomModality('bar')),
@@ -292,9 +318,9 @@ class MessageTest(unittest.TestCase):
292
318
  [
293
319
  'Hi, this is',
294
320
  CustomModality('foo'),
295
- 'and this is {{b}}.',
321
+ 'and this is {{b}}.\n',
296
322
  CustomModality('bar'),
297
- ' {{something else',
323
+ '{{something else',
298
324
  ],
299
325
  )
300
326
  )
@@ -303,11 +329,8 @@ class MessageTest(unittest.TestCase):
303
329
  message.AIMessage.from_chunks(chunks),
304
330
  message.AIMessage(
305
331
  inspect.cleandoc("""
306
- Hi, this is
307
- {{obj0}}
308
- and this is {{b}}.
309
- {{obj1}}
310
- {{something else
332
+ Hi, this is <<[[obj0]]>> and this is {{b}}.
333
+ <<[[obj1]]>> {{something else
311
334
  """),
312
335
  obj0=pg.Ref(m.a),
313
336
  obj1=pg.Ref(m.x.c),
@@ -315,6 +338,160 @@ class MessageTest(unittest.TestCase):
315
338
  )
316
339
  )
317
340
 
341
+ def assert_html_content(self, html, expected):
342
+ expected = inspect.cleandoc(expected).strip()
343
+ actual = html.content.strip()
344
+ if actual != expected:
345
+ print(actual)
346
+ self.assertEqual(actual, expected)
347
+
348
+ def test_html_style(self):
349
+ self.assertIn(
350
+ inspect.cleandoc(
351
+ """
352
+ /* Langfun Message styles.*/
353
+ [class^="message-"] > details {
354
+ margin: 0px 0px 5px 0px;
355
+ border: 1px solid #EEE;
356
+ }
357
+ .lf-message.summary-title::after {
358
+ content: ' 💬';
359
+ }
360
+ details.pyglove.ai-message {
361
+ border: 1px solid blue;
362
+ color: blue;
363
+ }
364
+ details.pyglove.user-message {
365
+ border: 1px solid green;
366
+ color: green;
367
+ }
368
+ .message-tags {
369
+ margin: 5px 0px 5px 0px;
370
+ font-size: .8em;
371
+ }
372
+ .message-tags > span {
373
+ border-radius: 5px;
374
+ background-color: #CCC;
375
+ padding: 3px;
376
+ margin: 0px 2px 0px 2px;
377
+ color: white;
378
+ }
379
+ .message-text {
380
+ padding: 20px;
381
+ margin: 10px 5px 10px 5px;
382
+ font-style: italic;
383
+ white-space: pre-wrap;
384
+ border: 1px solid #EEE;
385
+ border-radius: 5px;
386
+ background-color: #EEE;
387
+ }
388
+ .modality-in-text {
389
+ display: inline-block;
390
+ }
391
+ .modality-in-text > details.pyglove {
392
+ display: inline-block;
393
+ font-size: 0.8em;
394
+ border: 0;
395
+ background-color: #A6F1A6;
396
+ margin: 0px 5px 0px 5px;
397
+ }
398
+ .message-result {
399
+ color: dodgerblue;
400
+ }
401
+ .message-usage {
402
+ color: orange;
403
+ }
404
+ .message-usage .object-key.str {
405
+ border: 1px solid orange;
406
+ background-color: orange;
407
+ color: white;
408
+ }
409
+ """
410
+ ),
411
+ message.UserMessage('hi').to_html().style_section,
412
+ )
413
+
414
+ def test_html_user_message(self):
415
+ self.assert_html_content(
416
+ message.UserMessage(
417
+ 'what is a <div>'
418
+ ).to_html(enable_summary_tooltip=False),
419
+ """
420
+ <details open class="pyglove user-message lf-message"><summary><div class="summary-title lf-message">UserMessage(...)</div></summary><div class="complex_value"><div class="message-tags"></div><div class="message-text">what is a &lt;div&gt;</div><div class="message-metadata"><details open class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></div></div></details>
421
+ """
422
+ )
423
+ self.assert_html_content(
424
+ message.UserMessage(
425
+ 'what is this <<[[image]]>>',
426
+ tags=['lm-input'],
427
+ image=CustomModality('bird')
428
+ ).to_html(
429
+ enable_summary_tooltip=False,
430
+ extra_flags=dict(include_message_metadata=False)
431
+ ),
432
+ """
433
+ <details open class="pyglove user-message lf-message"><summary><div class="summary-title lf-message">UserMessage(...)</div></summary><div class="complex_value"><div class="message-tags"><span>lm-input</span></div><div class="message-text">what is this<div class="modality-in-text"><details class="pyglove custom-modality"><summary><div class="summary-name">image<span class="tooltip">metadata.image</span></div><div class="summary-title">CustomModality(...)</div></summary><div class="complex-value custom-modality"><details open class="pyglove str"><summary><div class="summary-name">content<span class="tooltip">metadata.image.content</span></div><div class="summary-title">str</div></summary><span class="simple-value str">&#x27;bird&#x27;</span></details></div></details></div></div></div></details>
434
+ """
435
+ )
436
+
437
+ def test_html_ai_message(self):
438
+ image = CustomModality('foo')
439
+ user_message = message.UserMessage(
440
+ 'What is in this image? <<[[image]]>> this is a test',
441
+ metadata=dict(image=image),
442
+ source=message.UserMessage('User input'),
443
+ tags=['lm-input']
444
+ )
445
+ ai_message = message.AIMessage(
446
+ 'My name is Gemini',
447
+ metadata=dict(
448
+ result=pg.Dict(x=1, y=2, z=pg.Dict(a=[12, 323])),
449
+ usage=language_model.LMSamplingUsage(10, 2, 12)
450
+ ),
451
+ tags=['lm-response', 'lm-output'],
452
+ source=user_message,
453
+ )
454
+ self.assert_html_content(
455
+ ai_message.to_html(enable_summary_tooltip=False),
456
+ """
457
+ <details open class="pyglove ai-message lf-message"><summary><div class="summary-title lf-message">AIMessage(...)</div></summary><div class="complex_value"><div class="message-tags"><span>lm-response</span><span>lm-output</span></div><div class="message-text">My name is Gemini</div><div class="message-result"><details open class="pyglove dict"><summary><div class="summary-name">result<span class="tooltip">metadata.result</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><details open class="pyglove int"><summary><div class="summary-name">x<span class="tooltip">metadata.result.x</span></div><div class="summary-title">int</div></summary><span class="simple-value int">1</span></details><details open class="pyglove int"><summary><div class="summary-name">y<span class="tooltip">metadata.result.y</span></div><div class="summary-title">int</div></summary><span class="simple-value int">2</span></details><details class="pyglove dict"><summary><div class="summary-name">z<span class="tooltip">metadata.result.z</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><details class="pyglove list"><summary><div class="summary-name">a<span class="tooltip">metadata.result.z.a</span></div><div class="summary-title">List(...)</div></summary><div class="complex-value list"><table><tr><td><span class="object-key int">0</span><span class="tooltip">metadata.result.z.a[0]</span></td><td><span class="simple-value int">12</span></td></tr><tr><td><span class="object-key int">1</span><span class="tooltip">metadata.result.z.a[1]</span></td><td><span class="simple-value int">323</span></td></tr></table></div></details></div></details></div></details></div><div class="message-usage"><details open class="pyglove lm-sampling-usage"><summary><div class="summary-name">llm usage<span class="tooltip">metadata.usage</span></div><div class="summary-title">LMSamplingUsage(...)</div></summary><div class="complex-value lm-sampling-usage"><table><tr><td><span class="object-key str">prompt_tokens</span><span class="tooltip">metadata.usage.prompt_tokens</span></td><td><span class="simple-value int">10</span></td></tr><tr><td><span class="object-key str">completion_tokens</span><span class="tooltip">metadata.usage.completion_tokens</span></td><td><span class="simple-value int">2</span></td></tr><tr><td><span class="object-key str">total_tokens</span><span class="tooltip">metadata.usage.total_tokens</span></td><td><span class="simple-value int">12</span></td></tr><tr><td><span class="object-key str">num_requests</span><span class="tooltip">metadata.usage.num_requests</span></td><td><span class="simple-value int">1</span></td></tr><tr><td><span class="object-key str">estimated_cost</span><span class="tooltip">metadata.usage.estimated_cost</span></td><td><span class="simple-value none-type">None</span></td></tr><tr><td><span class="object-key str">retry_stats</span><span class="tooltip">metadata.usage.retry_stats</span></td><td><details class="pyglove retry-stats"><summary><div class="summary-title">RetryStats(...)</div></summary><div class="complex-value retry-stats"><table><tr><td><span class="object-key str">num_occurences</span><span class="tooltip">metadata.usage.retry_stats.num_occurences</span></td><td><span class="simple-value int">0</span></td></tr><tr><td><span class="object-key str">total_wait_interval</span><span class="tooltip">metadata.usage.retry_stats.total_wait_interval</span></td><td><span class="simple-value float">0.0</span></td></tr><tr><td><span class="object-key str">total_call_interval</span><span class="tooltip">metadata.usage.retry_stats.total_call_interval</span></td><td><span class="simple-value float">0.0</span></td></tr><tr><td><span class="object-key str">errors</span><span class="tooltip">metadata.usage.retry_stats.errors</span></td><td><details class="pyglove dict"><summary><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></td></tr></table></div></details></td></tr></table></div></details></div><div class="message-metadata"><details open class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></div><details open class="pyglove user-message lf-message"><summary><div class="summary-name lf-message">source<span class="tooltip lf-message">source</span></div><div class="summary-title lf-message">UserMessage(...)</div></summary><div class="complex_value"><div class="message-tags"><span>lm-input</span></div><div class="message-text">What is in this image?<div class="modality-in-text"><details class="pyglove custom-modality"><summary><div class="summary-name">image<span class="tooltip">source.metadata.image</span></div><div class="summary-title">CustomModality(...)</div></summary><div class="complex-value custom-modality"><details open class="pyglove str"><summary><div class="summary-name">content<span class="tooltip">source.metadata.image.content</span></div><div class="summary-title">str</div></summary><span class="simple-value str">&#x27;foo&#x27;</span></details></div></details></div>this is a test</div><div class="message-metadata"><details open class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">source.metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><details class="pyglove custom-modality"><summary><div class="summary-name">image<span class="tooltip">source.metadata.image</span></div><div class="summary-title">CustomModality(...)</div></summary><div class="complex-value custom-modality"><details open class="pyglove str"><summary><div class="summary-name">content<span class="tooltip">source.metadata.image.content</span></div><div class="summary-title">str</div></summary><span class="simple-value str">&#x27;foo&#x27;</span></details></div></details></div></details></div></div></details></div></details>
458
+ """
459
+ )
460
+ self.assert_html_content(
461
+ ai_message.to_html(
462
+ key_style='label',
463
+ enable_summary_tooltip=False,
464
+ extra_flags=dict(
465
+ collapse_modalities_in_text=False,
466
+ collapse_llm_usage=True,
467
+ collapse_message_result_level=0,
468
+ collapse_message_metadata_level=0,
469
+ collapse_source_message_level=0,
470
+ source_tag=None,
471
+ ),
472
+ ),
473
+ """
474
+ <details open class="pyglove ai-message lf-message"><summary><div class="summary-title lf-message">AIMessage(...)</div></summary><div class="complex_value"><div class="message-tags"><span>lm-response</span><span>lm-output</span></div><div class="message-text">My name is Gemini</div><div class="message-result"><details class="pyglove dict"><summary><div class="summary-name">result<span class="tooltip">metadata.result</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><table><tr><td><span class="object-key str">x</span><span class="tooltip">metadata.result.x</span></td><td><span class="simple-value int">1</span></td></tr><tr><td><span class="object-key str">y</span><span class="tooltip">metadata.result.y</span></td><td><span class="simple-value int">2</span></td></tr><tr><td><span class="object-key str">z</span><span class="tooltip">metadata.result.z</span></td><td><details class="pyglove dict"><summary><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><table><tr><td><span class="object-key str">a</span><span class="tooltip">metadata.result.z.a</span></td><td><details class="pyglove list"><summary><div class="summary-title">List(...)</div></summary><div class="complex-value list"><table><tr><td><span class="object-key int">0</span><span class="tooltip">metadata.result.z.a[0]</span></td><td><span class="simple-value int">12</span></td></tr><tr><td><span class="object-key int">1</span><span class="tooltip">metadata.result.z.a[1]</span></td><td><span class="simple-value int">323</span></td></tr></table></div></details></td></tr></table></div></details></td></tr></table></div></details></div><div class="message-usage"><details class="pyglove lm-sampling-usage"><summary><div class="summary-name">llm usage<span class="tooltip">metadata.usage</span></div><div class="summary-title">LMSamplingUsage(...)</div></summary><div class="complex-value lm-sampling-usage"><table><tr><td><span class="object-key str">prompt_tokens</span><span class="tooltip">metadata.usage.prompt_tokens</span></td><td><span class="simple-value int">10</span></td></tr><tr><td><span class="object-key str">completion_tokens</span><span class="tooltip">metadata.usage.completion_tokens</span></td><td><span class="simple-value int">2</span></td></tr><tr><td><span class="object-key str">total_tokens</span><span class="tooltip">metadata.usage.total_tokens</span></td><td><span class="simple-value int">12</span></td></tr><tr><td><span class="object-key str">num_requests</span><span class="tooltip">metadata.usage.num_requests</span></td><td><span class="simple-value int">1</span></td></tr><tr><td><span class="object-key str">estimated_cost</span><span class="tooltip">metadata.usage.estimated_cost</span></td><td><span class="simple-value none-type">None</span></td></tr><tr><td><span class="object-key str">retry_stats</span><span class="tooltip">metadata.usage.retry_stats</span></td><td><details class="pyglove retry-stats"><summary><div class="summary-title">RetryStats(...)</div></summary><div class="complex-value retry-stats"><table><tr><td><span class="object-key str">num_occurences</span><span class="tooltip">metadata.usage.retry_stats.num_occurences</span></td><td><span class="simple-value int">0</span></td></tr><tr><td><span class="object-key str">total_wait_interval</span><span class="tooltip">metadata.usage.retry_stats.total_wait_interval</span></td><td><span class="simple-value float">0.0</span></td></tr><tr><td><span class="object-key str">total_call_interval</span><span class="tooltip">metadata.usage.retry_stats.total_call_interval</span></td><td><span class="simple-value float">0.0</span></td></tr><tr><td><span class="object-key str">errors</span><span class="tooltip">metadata.usage.retry_stats.errors</span></td><td><details class="pyglove dict"><summary><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></td></tr></table></div></details></td></tr></table></div></details></div><div class="message-metadata"><details class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></div><details class="pyglove user-message lf-message"><summary><div class="summary-name lf-message">source<span class="tooltip lf-message">source</span></div><div class="summary-title lf-message">UserMessage(...)</div></summary><div class="complex_value"><div class="message-tags"><span>lm-input</span></div><div class="message-text">What is in this image?<div class="modality-in-text"><details open class="pyglove custom-modality"><summary><div class="summary-name">image<span class="tooltip">source.metadata.image</span></div><div class="summary-title">CustomModality(...)</div></summary><div class="complex-value custom-modality"><table><tr><td><span class="object-key str">content</span><span class="tooltip">source.metadata.image.content</span></td><td><span class="simple-value str">&#x27;foo&#x27;</span></td></tr></table></div></details></div>this is a test</div><div class="message-metadata"><details class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">source.metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><table><tr><td><span class="object-key str">image</span><span class="tooltip">source.metadata.image</span></td><td><details class="pyglove custom-modality"><summary><div class="summary-title">CustomModality(...)</div></summary><div class="complex-value custom-modality"><table><tr><td><span class="object-key str">content</span><span class="tooltip">source.metadata.image.content</span></td><td><span class="simple-value str">&#x27;foo&#x27;</span></td></tr></table></div></details></td></tr></table></div></details></div><details class="pyglove user-message lf-message"><summary><div class="summary-name lf-message">source<span class="tooltip lf-message">source.source</span></div><div class="summary-title lf-message">UserMessage(...)</div></summary><div class="complex_value"><div class="message-tags"></div><div class="message-text">User input</div><div class="message-metadata"><details class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">source.source.metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></div></div></details></div></details></div></details>
475
+ """
476
+ )
477
+ self.assert_html_content(
478
+ ai_message.to_html(
479
+ key_style='label',
480
+ enable_summary_tooltip=False,
481
+ extra_flags=dict(
482
+ collapse_modalities_in_text=True,
483
+ collapse_llm_usage=False,
484
+ collapse_message_result_level=1,
485
+ collapse_message_metadata_level=1,
486
+ collapse_source_message_level=2,
487
+ source_tag=None,
488
+ ),
489
+ ),
490
+ """
491
+ <details open class="pyglove ai-message lf-message"><summary><div class="summary-title lf-message">AIMessage(...)</div></summary><div class="complex_value"><div class="message-tags"><span>lm-response</span><span>lm-output</span></div><div class="message-text">My name is Gemini</div><div class="message-result"><details open class="pyglove dict"><summary><div class="summary-name">result<span class="tooltip">metadata.result</span></div><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><table><tr><td><span class="object-key str">x</span><span class="tooltip">metadata.result.x</span></td><td><span class="simple-value int">1</span></td></tr><tr><td><span class="object-key str">y</span><span class="tooltip">metadata.result.y</span></td><td><span class="simple-value int">2</span></td></tr><tr><td><span class="object-key str">z</span><span class="tooltip">metadata.result.z</span></td><td><details class="pyglove dict"><summary><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><table><tr><td><span class="object-key str">a</span><span class="tooltip">metadata.result.z.a</span></td><td><details class="pyglove list"><summary><div class="summary-title">List(...)</div></summary><div class="complex-value list"><table><tr><td><span class="object-key int">0</span><span class="tooltip">metadata.result.z.a[0]</span></td><td><span class="simple-value int">12</span></td></tr><tr><td><span class="object-key int">1</span><span class="tooltip">metadata.result.z.a[1]</span></td><td><span class="simple-value int">323</span></td></tr></table></div></details></td></tr></table></div></details></td></tr></table></div></details></div><div class="message-usage"><details open class="pyglove lm-sampling-usage"><summary><div class="summary-name">llm usage<span class="tooltip">metadata.usage</span></div><div class="summary-title">LMSamplingUsage(...)</div></summary><div class="complex-value lm-sampling-usage"><table><tr><td><span class="object-key str">prompt_tokens</span><span class="tooltip">metadata.usage.prompt_tokens</span></td><td><span class="simple-value int">10</span></td></tr><tr><td><span class="object-key str">completion_tokens</span><span class="tooltip">metadata.usage.completion_tokens</span></td><td><span class="simple-value int">2</span></td></tr><tr><td><span class="object-key str">total_tokens</span><span class="tooltip">metadata.usage.total_tokens</span></td><td><span class="simple-value int">12</span></td></tr><tr><td><span class="object-key str">num_requests</span><span class="tooltip">metadata.usage.num_requests</span></td><td><span class="simple-value int">1</span></td></tr><tr><td><span class="object-key str">estimated_cost</span><span class="tooltip">metadata.usage.estimated_cost</span></td><td><span class="simple-value none-type">None</span></td></tr><tr><td><span class="object-key str">retry_stats</span><span class="tooltip">metadata.usage.retry_stats</span></td><td><details class="pyglove retry-stats"><summary><div class="summary-title">RetryStats(...)</div></summary><div class="complex-value retry-stats"><table><tr><td><span class="object-key str">num_occurences</span><span class="tooltip">metadata.usage.retry_stats.num_occurences</span></td><td><span class="simple-value int">0</span></td></tr><tr><td><span class="object-key str">total_wait_interval</span><span class="tooltip">metadata.usage.retry_stats.total_wait_interval</span></td><td><span class="simple-value float">0.0</span></td></tr><tr><td><span class="object-key str">total_call_interval</span><span class="tooltip">metadata.usage.retry_stats.total_call_interval</span></td><td><span class="simple-value float">0.0</span></td></tr><tr><td><span class="object-key str">errors</span><span class="tooltip">metadata.usage.retry_stats.errors</span></td><td><details class="pyglove dict"><summary><div class="summary-title">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></td></tr></table></div></details></td></tr></table></div></details></div><div class="message-metadata"><details open class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></div><details open class="pyglove user-message lf-message"><summary><div class="summary-name lf-message">source<span class="tooltip lf-message">source</span></div><div class="summary-title lf-message">UserMessage(...)</div></summary><div class="complex_value"><div class="message-tags"><span>lm-input</span></div><div class="message-text">What is in this image?<div class="modality-in-text"><details class="pyglove custom-modality"><summary><div class="summary-name">image<span class="tooltip">source.metadata.image</span></div><div class="summary-title">CustomModality(...)</div></summary><div class="complex-value custom-modality"><table><tr><td><span class="object-key str">content</span><span class="tooltip">source.metadata.image.content</span></td><td><span class="simple-value str">&#x27;foo&#x27;</span></td></tr></table></div></details></div>this is a test</div><div class="message-metadata"><details open class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">source.metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><table><tr><td><span class="object-key str">image</span><span class="tooltip">source.metadata.image</span></td><td><details class="pyglove custom-modality"><summary><div class="summary-title">CustomModality(...)</div></summary><div class="complex-value custom-modality"><table><tr><td><span class="object-key str">content</span><span class="tooltip">source.metadata.image.content</span></td><td><span class="simple-value str">&#x27;foo&#x27;</span></td></tr></table></div></details></td></tr></table></div></details></div><details open class="pyglove user-message lf-message"><summary><div class="summary-name lf-message">source<span class="tooltip lf-message">source.source</span></div><div class="summary-title lf-message">UserMessage(...)</div></summary><div class="complex_value"><div class="message-tags"></div><div class="message-text">User input</div><div class="message-metadata"><details open class="pyglove dict message-metadata"><summary><div class="summary-name message-metadata">metadata<span class="tooltip message-metadata">source.source.metadata</span></div><div class="summary-title message-metadata">Dict(...)</div></summary><div class="complex-value dict"><span class="empty-container"></span></div></details></div></div></details></div></details></div></details>
492
+ """
493
+ )
494
+
318
495
 
319
496
  if __name__ == '__main__':
320
497
  unittest.main()
@@ -17,10 +17,14 @@
17
17
  # pylint: disable=g-bad-import-order
18
18
  # pylint: disable=g-import-not-at-top
19
19
 
20
- from langfun.core.modalities.mime import MimeType
20
+ from langfun.core.modalities.audio import Audio
21
+ from langfun.core.modalities.mime import Mime
21
22
  from langfun.core.modalities.mime import Custom
22
- from langfun.core.modalities.mime import PDF
23
+ from langfun.core.modalities.ms_office import Docx
24
+ from langfun.core.modalities.ms_office import Pptx
25
+ from langfun.core.modalities.ms_office import Xlsx
23
26
  from langfun.core.modalities.image import Image
27
+ from langfun.core.modalities.pdf import PDF
24
28
  from langfun.core.modalities.video import Video
25
29
 
26
30
  # pylint: enable=g-import-not-at-top
@@ -0,0 +1,30 @@
1
+ # Copyright 2024 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Audio types."""
15
+
16
+ import functools
17
+ from langfun.core.modalities import mime
18
+
19
+
20
+ class Audio(mime.Mime):
21
+ """Audio."""
22
+
23
+ MIME_PREFIX = 'audio'
24
+
25
+ @functools.cached_property
26
+ def audio_format(self) -> str:
27
+ return self.mime_type.removeprefix(self.MIME_PREFIX + '/')
28
+
29
+ def _mime_control_for(self, uri: str) -> str:
30
+ return f'<audio controls> <source src="{uri}"> </audio>'
@@ -0,0 +1,63 @@
1
+ # Copyright 2024 The Langfun Authors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Audio tests."""
15
+ import unittest
16
+ from unittest import mock
17
+
18
+ from langfun.core.modalities import audio as audio_lib
19
+ import pyglove as pg
20
+
21
+
22
+ content_bytes = (
23
+ b'RIFF$\x00\x00\x00WAVEfmt'
24
+ b' \x10\x00\x00\x00\x01\x00\x01\x00\x00\x04\x00\x00\x00\x04\x00\x00\x01\x00\x08\x00data\x00\x00\x00\x00'
25
+ )
26
+
27
+
28
+ def mock_request(*args, **kwargs):
29
+ del args, kwargs
30
+ return pg.Dict(content=content_bytes)
31
+
32
+
33
+ class AudioTest(unittest.TestCase):
34
+
35
+ def test_audio_content(self):
36
+ audio = audio_lib.Audio.from_bytes(content_bytes)
37
+ self.assertEqual(audio.mime_type, 'audio/x-wav')
38
+ self.assertEqual(audio.audio_format, 'x-wav')
39
+ self.assertEqual(audio.to_bytes(), content_bytes)
40
+
41
+ def test_bad_audio(self):
42
+ audio = audio_lib.Audio.from_bytes(b'bad')
43
+ with self.assertRaisesRegex(ValueError, 'Expected MIME type'):
44
+ _ = audio.audio_format
45
+
46
+
47
+ class AudioFileTest(unittest.TestCase):
48
+
49
+ def test_audio_file(self):
50
+ audio = audio_lib.Audio.from_uri('http://mock/web/a.wav')
51
+ with mock.patch('requests.get') as mock_requests_get:
52
+ mock_requests_get.side_effect = mock_request
53
+ self.assertEqual(audio.audio_format, 'x-wav')
54
+ self.assertEqual(audio.mime_type, 'audio/x-wav')
55
+ self.assertEqual(
56
+ audio._raw_html(),
57
+ '<audio controls> <source src="http://mock/web/a.wav"> </audio>',
58
+ )
59
+ self.assertEqual(audio.to_bytes(), content_bytes)
60
+
61
+
62
+ if __name__ == '__main__':
63
+ unittest.main()
@@ -13,28 +13,47 @@
13
13
  # limitations under the License.
14
14
  """Image modality."""
15
15
 
16
- import base64
17
- import imghdr
18
- from typing import cast
16
+ import functools
17
+ import io
18
+ from typing import Any
19
+
19
20
  from langfun.core.modalities import mime
20
21
 
22
+ try:
23
+ from PIL import Image as pil_image # pylint: disable=g-import-not-at-top
24
+ PILImage = pil_image.Image
25
+ pil_open = pil_image.open
26
+ except ImportError:
27
+ PILImage = Any
28
+
29
+ def pil_open(*unused_args, **unused_kwargs):
30
+ raise RuntimeError(
31
+ 'Please install "langfun[mime-pil]" to enable PIL image support.'
32
+ )
33
+
34
+
35
+ class Image(mime.Mime):
36
+ """Image."""
21
37
 
22
- class Image(mime.MimeType):
23
- """Base class for image."""
38
+ MIME_PREFIX = 'image'
24
39
 
25
- @property
40
+ @functools.cached_property
26
41
  def image_format(self) -> str:
27
- iformat = imghdr.what(None, self.to_bytes())
28
- if iformat not in ['png', 'jpeg']:
29
- raise ValueError(f'Unsupported image format: {iformat!r}.')
30
- return cast(str, iformat)
31
-
32
- @property
33
- def mime_type(self) -> str:
34
- return f'image/{self.image_format}'
35
-
36
- def _repr_html_(self) -> str:
37
- if self.uri and self.uri.lower().startswith(('http:', 'https:', 'ftp:')):
38
- return f'<img src="{self.uri}">'
39
- image_raw = base64.b64encode(self.to_bytes()).decode()
40
- return f'<img src="data:image/{self.image_format};base64,{image_raw}">'
42
+ return self.mime_type.removeprefix(self.MIME_PREFIX + '/')
43
+
44
+ def _mime_control_for(self, uri: str) -> str:
45
+ return f'<img src="{uri}">'
46
+
47
+ @functools.cached_property
48
+ def size(self) -> tuple[int, int]:
49
+ img = pil_open(io.BytesIO(self.to_bytes()))
50
+ return img.size
51
+
52
+ def to_pil_image(self) -> PILImage: # pytype: disable=invalid-annotation
53
+ return pil_open(io.BytesIO(self.to_bytes()))
54
+
55
+ @classmethod
56
+ def from_pil_image(cls, img: PILImage) -> 'Image': # pytype: disable=invalid-annotation
57
+ buf = io.BytesIO()
58
+ img.save(buf, format='PNG')
59
+ return cls.from_bytes(buf.getvalue())
@@ -12,10 +12,14 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  """Image tests."""
15
+ import io
15
16
  import unittest
16
17
  from unittest import mock
17
18
 
19
+ import langfun.core as lf
18
20
  from langfun.core.modalities import image as image_lib
21
+ from langfun.core.modalities import mime as mime_lib
22
+ import PIL.Image as pil_image
19
23
  import pyglove as pg
20
24
 
21
25
 
@@ -36,30 +40,69 @@ def mock_request(*args, **kwargs):
36
40
  return pg.Dict(content=image_content)
37
41
 
38
42
 
39
- class ImageContentTest(unittest.TestCase):
43
+ class ImageTest(unittest.TestCase):
40
44
 
41
- def test_image_content(self):
45
+ def test_from_bytes(self):
42
46
  image = image_lib.Image.from_bytes(image_content)
43
47
  self.assertEqual(image.image_format, 'png')
44
- self.assertIn('data:image/png;base64,', image._repr_html_())
48
+ self.assertIn('data:image/png;base64,', image._raw_html())
45
49
  self.assertEqual(image.to_bytes(), image_content)
50
+ with self.assertRaisesRegex(
51
+ lf.ModalityError, '.* cannot be converted to text'
52
+ ):
53
+ image.to_text()
46
54
 
47
- def test_bad_image(self):
55
+ def test_from_bytes_invalid(self):
48
56
  image = image_lib.Image.from_bytes(b'bad')
49
- with self.assertRaisesRegex(ValueError, 'Unsupported image format'):
57
+ with self.assertRaisesRegex(ValueError, 'Expected MIME type'):
50
58
  _ = image.image_format
51
59
 
60
+ def test_from_bytes_base_cls(self):
61
+ self.assertIsInstance(
62
+ mime_lib.Mime.from_bytes(image_content), image_lib.Image
63
+ )
52
64
 
53
- class ImageFileTest(unittest.TestCase):
54
-
55
- def test_image_file(self):
65
+ def test_from_uri(self):
56
66
  image = image_lib.Image.from_uri('http://mock/web/a.png')
57
67
  with mock.patch('requests.get') as mock_requests_get:
58
68
  mock_requests_get.side_effect = mock_request
59
69
  self.assertEqual(image.image_format, 'png')
60
- self.assertEqual(image._repr_html_(), '<img src="http://mock/web/a.png">')
70
+ self.assertEqual(
71
+ image._raw_html(),
72
+ '<img src="http://mock/web/a.png">'
73
+ )
74
+ self.assertEqual(image.to_bytes(), image_content)
75
+
76
+ def test_from_uri_base_cls(self):
77
+ with mock.patch('requests.get') as mock_requests_get:
78
+ mock_requests_get.side_effect = mock_request
79
+ image = mime_lib.Mime.from_uri('http://mock/web/a.png')
80
+ self.assertIsInstance(image, image_lib.Image)
81
+ self.assertEqual(image.image_format, 'png')
82
+ self.assertEqual(
83
+ image._raw_html(),
84
+ '<img src="http://mock/web/a.png">'
85
+ )
61
86
  self.assertEqual(image.to_bytes(), image_content)
62
87
 
88
+ def test_image_size(self):
89
+ image = image_lib.Image.from_uri('http://mock/web/a.png')
90
+ with mock.patch('requests.get') as mock_requests_get:
91
+ mock_requests_get.side_effect = mock_request
92
+ self.assertEqual(image.size, (24, 24))
93
+
94
+ def test_to_pil_image(self):
95
+ image = image_lib.Image.from_uri('http://mock/web/a.png')
96
+ with mock.patch('requests.get') as mock_requests_get:
97
+ mock_requests_get.side_effect = mock_request
98
+ self.assertIsInstance(image.to_pil_image(), pil_image.Image)
99
+
100
+ def test_from_pil_image(self):
101
+ image = pil_image.open(io.BytesIO(image_content))
102
+ self.assertIsInstance(
103
+ image_lib.Image.from_pil_image(image), image_lib.Image
104
+ )
105
+
63
106
 
64
107
  if __name__ == '__main__':
65
108
  unittest.main()