langfun 0.0.2.dev20240527__py3-none-any.whl → 0.0.2.dev20240531__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.

@@ -16,6 +16,7 @@
16
16
  import abc
17
17
  import inspect
18
18
  import io
19
+ import re
19
20
  import textwrap
20
21
  import typing
21
22
  from typing import Any, Literal, Sequence, Type, Union
@@ -24,6 +25,17 @@ from langfun.core.coding.python import correction
24
25
  import pyglove as pg
25
26
 
26
27
 
28
+ def include_method_in_prompt(method):
29
+ """Decorator to include a method in the class definition of the prompt."""
30
+ setattr(method, '__show_in_prompt__', True)
31
+ return method
32
+
33
+
34
+ def should_include_method_in_prompt(method):
35
+ """Returns true if the method should be shown in the prompt."""
36
+ return getattr(method, '__show_in_prompt__', False)
37
+
38
+
27
39
  def parse_value_spec(value) -> pg.typing.ValueSpec:
28
40
  """Parses a PyGlove ValueSpec equivalence into a ValueSpec."""
29
41
  if isinstance(value, pg.typing.ValueSpec):
@@ -163,9 +175,12 @@ class Schema(lf.NaturalLanguageFormattable, pg.Object):
163
175
 
164
176
  def class_dependencies(
165
177
  self,
178
+ include_base_classes: bool = True,
166
179
  include_subclasses: bool = True) -> list[Type[Any]]:
167
180
  """Returns a list of class dependencies for current schema."""
168
- return class_dependencies(self.spec, include_subclasses)
181
+ return class_dependencies(
182
+ self.spec, include_base_classes, include_subclasses
183
+ )
169
184
 
170
185
  @classmethod
171
186
  def from_value(cls, value) -> 'Schema':
@@ -198,11 +213,12 @@ def class_dependencies(
198
213
  Type[pg.Object],
199
214
  tuple[Union[pg.typing.ValueSpec, Type[pg.Object]], ...],
200
215
  ],
216
+ include_base_classes: bool = True,
201
217
  include_subclasses: bool = True,
202
218
  ) -> list[Type[Any]]:
203
219
  """Returns a list of class dependencies from a value or specs."""
204
220
  if isinstance(value_or_spec, Schema):
205
- return value_or_spec.class_dependencies(include_subclasses)
221
+ value_or_spec = value_or_spec.spec
206
222
 
207
223
  if inspect.isclass(value_or_spec) or isinstance(
208
224
  value_or_spec, pg.typing.ValueSpec
@@ -236,13 +252,14 @@ def class_dependencies(
236
252
  if vs.cls not in seen:
237
253
  seen.add(vs.cls)
238
254
 
239
- # Add base classes as dependencies.
240
- for base_cls in vs.cls.__bases__:
241
- # We only keep track of user-defined symbolic classes.
242
- if base_cls is not object and base_cls is not pg.Object:
243
- _fill_dependencies(
244
- pg.typing.Object(base_cls), include_subclasses=False
245
- )
255
+ if include_base_classes:
256
+ # Add base classes as dependencies.
257
+ for base_cls in vs.cls.__bases__:
258
+ # We only keep track of user-defined symbolic classes.
259
+ if base_cls is not object and base_cls is not pg.Object:
260
+ _fill_dependencies(
261
+ pg.typing.Object(base_cls), include_subclasses=False
262
+ )
246
263
 
247
264
  # Add members as dependencies.
248
265
  for field in _pg_schema(vs.cls).values():
@@ -301,7 +318,6 @@ class SchemaPythonRepr(SchemaRepr):
301
318
  schema: Schema,
302
319
  *,
303
320
  include_result_definition: bool = True,
304
- include_methods: bool = False,
305
321
  markdown: bool = True,
306
322
  **kwargs,
307
323
  ) -> str:
@@ -309,15 +325,27 @@ class SchemaPythonRepr(SchemaRepr):
309
325
  if include_result_definition:
310
326
  ret += self.result_definition(schema)
311
327
  class_definition_str = self.class_definitions(
312
- schema, markdown=markdown, include_methods=include_methods, **kwargs
328
+ schema, markdown=markdown, **kwargs
313
329
  )
314
330
  if class_definition_str:
315
331
  ret += f'\n\n{class_definition_str}'
316
332
  return ret.strip()
317
333
 
318
- def class_definitions(self, schema: Schema, **kwargs) -> str | None:
319
- deps = schema.class_dependencies(include_subclasses=True)
320
- return class_definitions(deps, **kwargs)
334
+ def class_definitions(
335
+ self,
336
+ schema: Schema,
337
+ additional_dependencies: list[Type[Any]] | None = None,
338
+ **kwargs
339
+ ) -> str | None:
340
+ """Returns a string containing of class definitions from a schema."""
341
+ deps = schema.class_dependencies(
342
+ include_base_classes=False, include_subclasses=True
343
+ )
344
+ allowed_dependencies = set(deps)
345
+ if additional_dependencies:
346
+ allowed_dependencies.update(additional_dependencies)
347
+ return class_definitions(
348
+ deps, allowed_dependencies=allowed_dependencies, **kwargs)
321
349
 
322
350
  def result_definition(self, schema: Schema) -> str:
323
351
  return annotation(schema.spec)
@@ -331,8 +359,7 @@ def source_form(value, markdown: bool = False) -> str:
331
359
  def class_definitions(
332
360
  classes: Sequence[Type[Any]],
333
361
  *,
334
- include_pg_object_as_base: bool = False,
335
- include_methods: bool = False,
362
+ allowed_dependencies: set[Type[Any]] | None = None,
336
363
  strict: bool = False,
337
364
  markdown: bool = False,
338
365
  ) -> str | None:
@@ -347,8 +374,7 @@ def class_definitions(
347
374
  class_definition(
348
375
  cls,
349
376
  strict=strict,
350
- include_pg_object_as_base=include_pg_object_as_base,
351
- include_methods=include_methods,
377
+ allowed_dependencies=allowed_dependencies,
352
378
  )
353
379
  )
354
380
  ret = def_str.getvalue()
@@ -360,8 +386,7 @@ def class_definitions(
360
386
  def class_definition(
361
387
  cls,
362
388
  strict: bool = False,
363
- include_pg_object_as_base: bool = False,
364
- include_methods: bool = False,
389
+ allowed_dependencies: set[Type[Any]] | None = None,
365
390
  ) -> str:
366
391
  """Returns the Python class definition."""
367
392
  out = io.StringIO()
@@ -369,7 +394,7 @@ def class_definition(
369
394
  eligible_bases = []
370
395
  for base_cls in cls.__bases__:
371
396
  if base_cls is not object:
372
- if include_pg_object_as_base or base_cls is not pg.Object:
397
+ if allowed_dependencies is None or base_cls in allowed_dependencies:
373
398
  eligible_bases.append(base_cls.__name__)
374
399
 
375
400
  if eligible_bases:
@@ -406,32 +431,41 @@ def class_definition(
406
431
  out.write(' # ')
407
432
  out.write(line)
408
433
  out.write('\n')
409
- out.write(f' {field.key}: {annotation(field.value, strict=strict)}')
410
- out.write('\n')
411
- empty_class = False
412
434
 
413
- if include_methods:
414
- for method in _iter_newly_defined_methods(cls):
415
- out.write('\n')
416
- out.write(
417
- textwrap.indent(
418
- inspect.cleandoc('\n' + inspect.getsource(method)), ' ' * 2)
435
+ annotation_str = annotation(
436
+ field.value, strict=strict, allowed_dependencies=allowed_dependencies
419
437
  )
438
+ out.write(f' {field.key}: {annotation_str}')
420
439
  out.write('\n')
421
440
  empty_class = False
422
441
 
442
+ for method in _iter_newly_defined_methods(cls, allowed_dependencies):
443
+ source = inspect.getsource(method)
444
+ # Remove decorators from the method definition.
445
+ source = re.sub(r'\s*@.*\.include_method_in_prompt.*\n', '', source)
446
+ out.write('\n')
447
+ out.write(
448
+ textwrap.indent(
449
+ inspect.cleandoc('\n' + source), ' ' * 2)
450
+ )
451
+ out.write('\n')
452
+ empty_class = False
453
+
423
454
  if empty_class:
424
455
  out.write(' pass\n')
425
456
  return out.getvalue()
426
457
 
427
458
 
428
- def _iter_newly_defined_methods(cls):
429
- names = set(dir(cls))
459
+ def _iter_newly_defined_methods(
460
+ cls, allowed_dependencies: set[Type[Any]] | None):
461
+ names = {attr_name: True for attr_name in dir(cls)}
430
462
  for base in cls.__bases__:
431
- names -= set(dir(base))
432
- for name in names:
463
+ if allowed_dependencies is None or base in allowed_dependencies:
464
+ for name in dir(base):
465
+ names.pop(name, None)
466
+ for name in names.keys():
433
467
  attr = getattr(cls, name)
434
- if callable(attr):
468
+ if callable(attr) and should_include_method_in_prompt(attr):
435
469
  yield attr
436
470
 
437
471
 
@@ -439,8 +473,12 @@ def annotation(
439
473
  vs: pg.typing.ValueSpec,
440
474
  annotate_optional: bool = True,
441
475
  strict: bool = False,
476
+ allowed_dependencies: set[Type[Any]] | None = None,
442
477
  ) -> str:
443
478
  """Returns the annotation string for a value spec."""
479
+ child_annotation_kwargs = dict(
480
+ strict=strict, allowed_dependencies=allowed_dependencies
481
+ )
444
482
  if isinstance(vs, pg.typing.Any):
445
483
  return 'Any'
446
484
  elif isinstance(vs, pg.typing.Enum):
@@ -449,7 +487,7 @@ def annotation(
449
487
  elif isinstance(vs, pg.typing.Union):
450
488
  candidate_str = ', '.join(
451
489
  [
452
- annotation(c, annotate_optional=False, strict=strict)
490
+ annotation(c, annotate_optional=False, **child_annotation_kwargs)
453
491
  for c in vs.candidates
454
492
  ]
455
493
  )
@@ -485,20 +523,23 @@ def annotation(
485
523
  )
486
524
  x += '(' + ', '.join(constraints) + ')'
487
525
  elif isinstance(vs, pg.typing.Object):
488
- x = vs.cls.__name__
526
+ if allowed_dependencies is None or vs.cls in allowed_dependencies:
527
+ x = vs.cls.__name__
528
+ else:
529
+ x = 'Any'
489
530
  elif isinstance(vs, pg.typing.List):
490
- item_str = annotation(vs.element.value, strict=strict)
531
+ item_str = annotation(vs.element.value, **child_annotation_kwargs)
491
532
  x = f'list[{item_str}]'
492
533
  elif isinstance(vs, pg.typing.Tuple):
493
534
  elem_str = ', '.join(
494
- [annotation(el.value, strict=strict) for el in vs.elements]
535
+ [annotation(el.value, **child_annotation_kwargs) for el in vs.elements]
495
536
  )
496
537
  x = f'tuple[{elem_str}]'
497
538
  elif isinstance(vs, pg.typing.Dict):
498
539
  kv_pairs = None
499
540
  if vs.schema is not None:
500
541
  kv_pairs = [
501
- (k, annotation(f.value, strict=strict))
542
+ (k, annotation(f.value, **child_annotation_kwargs))
502
543
  for k, f in vs.schema.items()
503
544
  if isinstance(k, pg.typing.ConstStrKey)
504
545
  ]
@@ -509,7 +550,7 @@ def annotation(
509
550
  if strict:
510
551
  x = f'pg.typing.Dict({x})'
511
552
  elif vs.schema and vs.schema.dynamic_field:
512
- v = annotation(vs.schema.dynamic_field.value, strict=strict)
553
+ v = annotation(vs.schema.dynamic_field.value, **child_annotation_kwargs)
513
554
  x = f'dict[str, {v}]'
514
555
  else:
515
556
  x = 'dict[str, Any]'
@@ -604,7 +645,12 @@ class ValuePythonRepr(ValueRepr):
604
645
  cls_schema = Schema.from_value(value)
605
646
  if isinstance(cls_schema.spec, pg.typing.Object):
606
647
  object_code = SchemaPythonRepr().class_definitions(
607
- cls_schema, markdown=markdown, include_pg_object_as_base=True
648
+ cls_schema,
649
+ markdown=markdown,
650
+ # We add `pg.Object` as additional dependencies to the class
651
+ # definition so exemplars for class generation could show
652
+ # pg.Object as their bases.
653
+ additional_dependencies=[pg.Object]
608
654
  )
609
655
  assert object_code is not None
610
656
  return object_code
@@ -283,9 +283,10 @@ class SchemaPythonReprTest(unittest.TestCase):
283
283
  value_spec: pg.typing.ValueSpec,
284
284
  expected_annotation: str,
285
285
  strict: bool = False,
286
+ **kwargs,
286
287
  ) -> None:
287
288
  self.assertEqual(
288
- schema_lib.annotation(value_spec, strict=strict),
289
+ schema_lib.annotation(value_spec, strict=strict, **kwargs),
289
290
  expected_annotation,
290
291
  )
291
292
 
@@ -361,11 +362,27 @@ class SchemaPythonReprTest(unittest.TestCase):
361
362
  self.assert_annotation(
362
363
  pg.typing.Object(Activity).noneable(), 'Activity | None'
363
364
  )
365
+ self.assert_annotation(
366
+ pg.typing.Object(Activity).noneable(), 'Activity | None',
367
+ allowed_dependencies=set([Activity]),
368
+ )
369
+ self.assert_annotation(
370
+ pg.typing.Object(Activity).noneable(), 'Any | None',
371
+ allowed_dependencies=set(),
372
+ )
364
373
 
365
374
  # List.
366
375
  self.assert_annotation(
367
376
  pg.typing.List(pg.typing.Object(Activity)), 'list[Activity]'
368
377
  )
378
+ self.assert_annotation(
379
+ pg.typing.List(pg.typing.Object(Activity)), 'list[Activity]',
380
+ allowed_dependencies=set([Activity]),
381
+ )
382
+ self.assert_annotation(
383
+ pg.typing.List(pg.typing.Object(Activity)), 'list[Any]',
384
+ allowed_dependencies=set(),
385
+ )
369
386
  self.assert_annotation(
370
387
  pg.typing.List(pg.typing.Object(Activity)).noneable(),
371
388
  'list[Activity] | None',
@@ -377,16 +394,35 @@ class SchemaPythonReprTest(unittest.TestCase):
377
394
 
378
395
  # Tuple.
379
396
  self.assert_annotation(
380
- pg.typing.Tuple([pg.typing.Int(), pg.typing.Str()]), 'tuple[int, str]'
397
+ pg.typing.Tuple([Activity, pg.typing.Str()]), 'tuple[Activity, str]'
398
+ )
399
+ self.assert_annotation(
400
+ pg.typing.Tuple([Activity, pg.typing.Str()]), 'tuple[Activity, str]',
401
+ allowed_dependencies=set([Activity]),
381
402
  )
382
403
  self.assert_annotation(
383
- pg.typing.Tuple([pg.typing.Int(), pg.typing.Str()]).noneable(),
384
- 'tuple[int, str] | None',
404
+ pg.typing.Tuple([Activity, pg.typing.Str()]), 'tuple[Any, str]',
405
+ allowed_dependencies=set(),
406
+ )
407
+ self.assert_annotation(
408
+ pg.typing.Tuple([Activity, pg.typing.Str()]).noneable(),
409
+ 'tuple[Activity, str] | None',
385
410
  )
386
411
 
387
412
  # Dict.
388
413
  self.assert_annotation(
389
- pg.typing.Dict({'x': int, 'y': str}), '{\'x\': int, \'y\': str}'
414
+ pg.typing.Dict({'x': Activity, 'y': str}),
415
+ '{\'x\': Activity, \'y\': str}'
416
+ )
417
+ self.assert_annotation(
418
+ pg.typing.Dict({'x': Activity, 'y': str}),
419
+ '{\'x\': Activity, \'y\': str}',
420
+ allowed_dependencies=set([Activity]),
421
+ )
422
+ self.assert_annotation(
423
+ pg.typing.Dict({'x': Activity, 'y': str}),
424
+ '{\'x\': Any, \'y\': str}',
425
+ allowed_dependencies=set(),
390
426
  )
391
427
  self.assert_annotation(
392
428
  pg.typing.Dict({'x': int, 'y': str}),
@@ -420,6 +456,13 @@ class SchemaPythonReprTest(unittest.TestCase):
420
456
  ).noneable(),
421
457
  'Union[Activity, Itinerary, None]',
422
458
  )
459
+ self.assert_annotation(
460
+ pg.typing.Union(
461
+ [pg.typing.Object(Activity), pg.typing.Object(Itinerary)]
462
+ ).noneable(),
463
+ 'Union[Activity, Any, None]',
464
+ allowed_dependencies=set([Activity]),
465
+ )
423
466
 
424
467
  # Any.
425
468
  self.assert_annotation(pg.typing.Any(), 'Any')
@@ -427,13 +470,13 @@ class SchemaPythonReprTest(unittest.TestCase):
427
470
 
428
471
  def test_class_definition(self):
429
472
  self.assertEqual(
430
- schema_lib.class_definition(Activity),
473
+ schema_lib.class_definition(Activity, allowed_dependencies=set()),
431
474
  'class Activity:\n description: str\n',
432
475
  )
433
476
  self.assertEqual(
434
477
  schema_lib.class_definition(Itinerary),
435
478
  inspect.cleandoc("""
436
- class Itinerary:
479
+ class Itinerary(Object):
437
480
  \"\"\"A travel itinerary for a day.\"\"\"
438
481
  day: int(min=1)
439
482
  type: Literal['daytime', 'nighttime']
@@ -443,7 +486,9 @@ class SchemaPythonReprTest(unittest.TestCase):
443
486
  """) + '\n',
444
487
  )
445
488
  self.assertEqual(
446
- schema_lib.class_definition(PlaceOfInterest),
489
+ schema_lib.class_definition(
490
+ PlaceOfInterest, allowed_dependencies=set()
491
+ ),
447
492
  inspect.cleandoc("""
448
493
  class PlaceOfInterest:
449
494
  \"\"\"The name of a place of interest.
@@ -459,11 +504,11 @@ class SchemaPythonReprTest(unittest.TestCase):
459
504
  pass
460
505
 
461
506
  self.assertEqual(
462
- schema_lib.class_definition(A),
507
+ schema_lib.class_definition(A, allowed_dependencies=set()),
463
508
  'class A:\n pass\n',
464
509
  )
465
510
  self.assertEqual(
466
- schema_lib.class_definition(A, include_pg_object_as_base=True),
511
+ schema_lib.class_definition(A),
467
512
  'class A(Object):\n pass\n',
468
513
  )
469
514
 
@@ -471,18 +516,21 @@ class SchemaPythonReprTest(unittest.TestCase):
471
516
  x: str
472
517
  __kwargs__: typing.Any
473
518
 
474
- self.assertEqual(schema_lib.class_definition(C), 'class C:\n x: str\n')
519
+ self.assertEqual(
520
+ schema_lib.class_definition(C), 'class C(Object):\n x: str\n'
521
+ )
475
522
 
476
523
  class D(pg.Object):
477
524
  x: str
525
+ @schema_lib.include_method_in_prompt
478
526
  def __call__(self, y: int) -> int:
479
527
  return len(self.x) + y
480
528
 
481
529
  self.assertEqual(
482
- schema_lib.class_definition(D, include_methods=True),
530
+ schema_lib.class_definition(D),
483
531
  inspect.cleandoc(
484
532
  """
485
- class D:
533
+ class D(Object):
486
534
  x: str
487
535
 
488
536
  def __call__(self, y: int) -> int:
@@ -506,31 +554,28 @@ class SchemaPythonReprTest(unittest.TestCase):
506
554
  class A(pg.Object):
507
555
  foo: Foo
508
556
 
557
+ @schema_lib.include_method_in_prompt
509
558
  def foo_value(self) -> int:
510
559
  return self.foo.x
511
560
 
561
+ def baz_value(self) -> str:
562
+ return 'baz'
563
+
512
564
  class B(A):
513
565
  bar: Bar
514
566
  foo2: Foo
515
567
 
568
+ @schema_lib.include_method_in_prompt
516
569
  def bar_value(self) -> str:
517
570
  return self.bar.y
518
571
 
519
572
  schema = schema_lib.Schema([B])
520
573
  self.assertEqual(
521
- schema_lib.SchemaPythonRepr().class_definitions(
522
- schema, include_methods=True
523
- ),
574
+ schema_lib.SchemaPythonRepr().class_definitions(schema),
524
575
  inspect.cleandoc('''
525
576
  class Foo:
526
577
  x: int
527
578
 
528
- class A:
529
- foo: Foo
530
-
531
- def foo_value(self) -> int:
532
- return self.foo.x
533
-
534
579
  class Bar:
535
580
  """Class Bar."""
536
581
  y: str
@@ -539,13 +584,16 @@ class SchemaPythonReprTest(unittest.TestCase):
539
584
  """Baz(y: str)"""
540
585
  y: str
541
586
 
542
- class B(A):
587
+ class B:
543
588
  foo: Foo
544
589
  bar: Bar
545
590
  foo2: Foo
546
591
 
547
592
  def bar_value(self) -> str:
548
593
  return self.bar.y
594
+
595
+ def foo_value(self) -> int:
596
+ return self.foo.x
549
597
  ''') + '\n',
550
598
  )
551
599
 
@@ -562,9 +610,6 @@ class SchemaPythonReprTest(unittest.TestCase):
562
610
  class Foo:
563
611
  x: int
564
612
 
565
- class A:
566
- foo: Foo
567
-
568
613
  class Bar:
569
614
  """Class Bar."""
570
615
  y: str
@@ -573,10 +618,16 @@ class SchemaPythonReprTest(unittest.TestCase):
573
618
  """Baz(y: str)"""
574
619
  y: str
575
620
 
576
- class B(A):
621
+ class B:
577
622
  foo: Foo
578
623
  bar: Bar
579
624
  foo2: Foo
625
+
626
+ def bar_value(self) -> str:
627
+ return self.bar.y
628
+
629
+ def foo_value(self) -> int:
630
+ return self.foo.x
580
631
  ```
581
632
  '''),
582
633
  )
@@ -584,16 +635,12 @@ class SchemaPythonReprTest(unittest.TestCase):
584
635
  schema_lib.SchemaPythonRepr().repr(
585
636
  schema,
586
637
  include_result_definition=False,
587
- include_pg_object_as_base=True,
588
638
  markdown=False,
589
639
  ),
590
640
  inspect.cleandoc('''
591
- class Foo(Object):
641
+ class Foo:
592
642
  x: int
593
643
 
594
- class A(Object):
595
- foo: Foo
596
-
597
644
  class Bar:
598
645
  """Class Bar."""
599
646
  y: str
@@ -602,10 +649,16 @@ class SchemaPythonReprTest(unittest.TestCase):
602
649
  """Baz(y: str)"""
603
650
  y: str
604
651
 
605
- class B(A):
652
+ class B:
606
653
  foo: Foo
607
654
  bar: Bar
608
655
  foo2: Foo
656
+
657
+ def bar_value(self) -> str:
658
+ return self.bar.y
659
+
660
+ def foo_value(self) -> int:
661
+ return self.foo.x
609
662
  '''),
610
663
  )
611
664
 
@@ -653,7 +706,7 @@ class ValuePythonReprTest(unittest.TestCase):
653
706
  ```python
654
707
  class Foo(Object):
655
708
  x: int
656
-
709
+
657
710
  class A(Object):
658
711
  foo: list[Foo]
659
712
  y: str | None
@@ -309,7 +309,7 @@ class RenderTest(unittest.TestCase):
309
309
  Template(
310
310
  'This is {{ x }} and {{ a }}', x=1, a=CustomModality('foo')
311
311
  ).render(),
312
- 'This is 1 and {{a}}',
312
+ 'This is 1 and <<[[a]]>>',
313
313
  )
314
314
 
315
315
  def test_render_with_default(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langfun
3
- Version: 0.0.2.dev20240527
3
+ Version: 0.0.2.dev20240531
4
4
  Summary: Langfun: Language as Functions.
5
5
  Home-page: https://github.com/google/langfun
6
6
  Author: Langfun Authors
@@ -30,6 +30,8 @@ Requires-Dist: python-magic >=0.4.27
30
30
  Requires-Dist: requests >=2.31.0
31
31
  Requires-Dist: termcolor ==1.1.0
32
32
  Requires-Dist: tqdm >=4.64.1
33
+ Requires-Dist: python-docx >=0.8.11
34
+ Requires-Dist: pandas >=2.1.4
33
35
 
34
36
  <div align="center">
35
37
  <img src="https://raw.githubusercontent.com/google/langfun/main/docs/_static/logo.svg" width="520px" alt="logo"></img>