main 36af51e19ef7 cached
20 files
340.3 KB
158.0k tokens
105 symbols
1 requests
Download .txt
Showing preview only (352K chars total). Download the full file or copy to clipboard to get everything.
Repository: google-research/talk-like-a-graph
Branch: main
Commit: 36af51e19ef7
Files: 20
Total size: 340.3 KB

Directory structure:
gitextract_nuchrmnk/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __init__.py
├── talk_like_a_graph/
│   ├── README.md
│   ├── __init__.py
│   ├── graph_generators.py
│   ├── graph_generators_runner.py
│   ├── graph_generators_test.py
│   ├── graph_metrics.py
│   ├── graph_metrics_test.py
│   ├── graph_tasks.py
│   ├── graph_tasks_generator.py
│   ├── graph_tasks_utils.py
│   ├── graph_text_encoders.py
│   ├── graph_text_encoders_test.py
│   └── name_dictionaries.py
└── tutorial/
    ├── KDD-Tutorial-1-Talk-Like-a-Graph.ipynb
    ├── KDD-Tutorial-2-Let-Your-Graph-Do-The-Talking.ipynb
    └── README.md

================================================
FILE CONTENTS
================================================

================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution,
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.

## Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.

## Community Guidelines

This project follows [Google's Open Source Community
Guidelines](https://opensource.google/conduct/).

================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
# Encoding Graphs and Structured Data in Language Models

This repository contains code for
[Talk like a Graph: Encoding Graphs for Large Language Models](https://arxiv.org/abs/2310.04560)
and
[Let Your Graph Do the Talking: Encoding Structured Data for LLMs](https://arxiv.org/abs/2402.05862).

## Cite us

If you use this package for published work, please cite the following.

For the "Talk like a Graph" project:

```
@inproceedigs{fatemi2024talk,
  title={Talk like a Graph: Encoding Graphs for Large Language Models},
  author={Bahare Fatemi and Jonathan Halcrow and Bryan Perozzi},
  booktitle={International Conference on Learning Representations (ICLR)},
  year={2024}
}
```

For Graph Token (a.k.a "Let Your Graph Do the Talking"):

```
@misc{perozzi2024letgraphtalkingencoding,
      title={Let Your Graph Do the Talking: Encoding Structured Data for LLMs}, 
      author={Bryan Perozzi and Bahare Fatemi and Dustin Zelle and Anton Tsitsulin and Mehran Kazemi and Rami Al-Rfou and Jonathan Halcrow},
      year={2024},
      eprint={2402.05862},
      archivePrefix={arXiv},
      primaryClass={cs.LG},
      url={https://arxiv.org/abs/2402.05862}, 
}
```

## Disclaimer

This is not an official Google product.


================================================
FILE: __init__.py
================================================


================================================
FILE: talk_like_a_graph/README.md
================================================
# Using Large Language Models to Solve Graph Problems

This repository contains the code to generate graph reasoning problems with
different graph generator algorithms and graph encoding methods, as well as
different prompting techniques.

The graph tasks are `edge existence`, `node degree`, `node count`, `edge count`,
`connected nodes`, `disconnected nodes`, `cycle check`, `reachability`,
`shortest path`, `maximum flow`, `node classification`, and `triangle counting`.

The datasets used here are proposed in our paper:
[Talk like a Graph: Encoding Graphs for Large Language Models](https://arxiv.org/abs/2310.04560).

### Generating graphs

```sh
./graphqa/graph_generator.sh
```

### Generating files for tasks

```sh
./graphqa/task_generator.sh
```

## Contact us

For questions or comments about the implementation, please contact
baharef@google.com.

## Cite us

If you use this package for published work, please cite the following:

```
@inproceedigs{fatemi2024talk,
  title={Talk like a Graph: Encoding Graphs for Large Language Models},
  author={Bahare Fatemi and Jonathan Halcrow and Bryan Perozzi},
  booktitle={International Conference on Learning Representations (ICLR)},
  year={2024}
}
```

## Disclaimer

This is not an official Google product.

# Placeholder for internal data notes.

================================================
FILE: talk_like_a_graph/__init__.py
================================================


================================================
FILE: talk_like_a_graph/graph_generators.py
================================================
r"""Random graph generation."""

import random

import networkx as nx
import numpy as np


_NUMBER_OF_NODES_RANGE = {
    "small": np.arange(5, 10),
    "medium": np.arange(10, 15),
    "large": np.arange(15, 20),
}
_NUMBER_OF_COMMUNITIES_RANGE = {
    "small": np.arange(2, 4),
    "medium": np.arange(2, 8),
    "large": np.arange(2, 10),
}


def generate_graphs(
    number_of_graphs: int,
    algorithm: str,
    directed: bool,
    random_seed: int = 1234,
    er_min_sparsity: float = 0.0,
    er_max_sparsity: float = 1.0,
) -> list[nx.Graph]:
  """Generating multiple graphs using the provided algorithms.

  Args:
    number_of_graphs: number of graphs to generate
    algorithm: the random graph generator algorithm
    directed: whether to generate directed or undirected graphs.
    random_seed: the random seed to generate graphs with.
    er_min_sparsity: minimum sparsity of er graphs.
    er_max_sparsity: maximum sparsity of er graphs.

  Returns:
    generated_graphs: a list of nx graphs.
  Raises:
    NotImplementedError: if the algorithm is not yet implemented.
  """

  random.seed(random_seed)
  np.random.seed(random_seed)

  generated_graphs = []
  graph_sizes = random.choices(
      list(_NUMBER_OF_NODES_RANGE.keys()), k=number_of_graphs
  )
  random_state = np.random.RandomState(random_seed)
  if algorithm == "er":
    for i in range(number_of_graphs):
      sparsity = random.uniform(er_min_sparsity, er_max_sparsity)
      number_of_nodes = random.choice(_NUMBER_OF_NODES_RANGE[graph_sizes[i]])
      generated_graphs.append(
          nx.erdos_renyi_graph(
              number_of_nodes, sparsity, seed=random_state, directed=directed
          )
      )
  elif algorithm == "ba":
    for i in range(number_of_graphs):
      number_of_nodes = random.choice(_NUMBER_OF_NODES_RANGE[graph_sizes[i]])
      m = random.randint(1, number_of_nodes - 1)
      generated_graph = nx.barabasi_albert_graph(
          number_of_nodes, m, seed=random_state
      )
      if directed:
        generated_graphs.append(randomize_directions(generated_graph))
      else:
        generated_graphs.append(generated_graph)
  elif algorithm == "sbm":
    for i in range(number_of_graphs):
      number_of_nodes = random.choice(_NUMBER_OF_NODES_RANGE[graph_sizes[i]])
      number_of_communities = random.choice(
          _NUMBER_OF_COMMUNITIES_RANGE[graph_sizes[i]]
      )
      # sizes forms number of nodes in communities.
      sizes = []
      for _ in range(number_of_communities - 1):
        sizes.append(
            random.randint(
                1,
                max(
                    1,
                    number_of_nodes - sum(sizes) - (number_of_communities - 1),
                ),
            )
        )
      sizes.append(number_of_nodes - sum(sizes))

      # p forms probabilities of communities connecting each other.
      p = np.random.uniform(size=(number_of_communities, number_of_communities))
      if random.uniform(0, 1) < 0.5:
        p = np.maximum(p, p.transpose())
      else:
        p = np.minimum(p, p.transpose())
      sbm_graph = nx.stochastic_block_model(
          sizes, p, seed=random_state, directed=directed
      )
      # sbm graph generator automatically adds dictionary attributes.
      sbm_graph = remove_graph_data(sbm_graph)
      generated_graphs.append(sbm_graph)
  elif algorithm == "sfn":
    for i in range(number_of_graphs):
      number_of_nodes = random.choice(_NUMBER_OF_NODES_RANGE[graph_sizes[i]])
      generated_graph = nx.scale_free_graph(number_of_nodes, seed=random_state)
      # sfn graphs are by defaukt directed.
      if not directed:
        generated_graphs.append(remove_directions(generated_graph))
      else:
        generated_graphs.append(generated_graph)
  elif algorithm == "complete":
    for i in range(number_of_graphs):
      number_of_nodes = random.choice(_NUMBER_OF_NODES_RANGE[graph_sizes[i]])
      create_using = nx.DiGraph if directed else nx.Graph
      generated_graphs.append(
          nx.complete_graph(number_of_nodes, create_using=create_using)
      )
  elif algorithm == "star":
    for i in range(number_of_graphs):
      number_of_nodes = random.choice(_NUMBER_OF_NODES_RANGE[graph_sizes[i]])
      # number_of_nodes for star is the input + a center node.
      generated_graph = nx.star_graph(number_of_nodes - 1)
      if directed:
        generated_graphs.append(randomize_directions(generated_graph))
      else:
        generated_graphs.append(generated_graph)
  elif algorithm == "path":
    for i in range(number_of_graphs):
      number_of_nodes = random.choice(_NUMBER_OF_NODES_RANGE[graph_sizes[i]])
      create_using = nx.DiGraph if directed else nx.Graph
      generated_graphs.append(
          nx.path_graph(number_of_nodes, create_using=create_using)
      )
  else:
    raise NotImplementedError()
  return generated_graphs


def remove_graph_data(graph: nx.Graph) -> nx.Graph:
  # GraphML writer does not support dictionary data for nodes or graphs.
  for ind in range((graph.number_of_nodes())):
    graph.nodes[ind].pop("block", None)
  graph_data_keys = list(graph.graph.keys())
  for _, node in enumerate(graph_data_keys):
    graph.graph.pop(node, None)
  return graph


def randomize_directions(graph: nx.Graph) -> nx.DiGraph:
  # Converting the undirected graph to a directed graph.
  directed_graph = graph.to_directed()
  # For each edge, randomly choose a direction.
  edges = list(graph.edges())
  for u, v in edges:
    if random.random() < 0.5:
      directed_graph.remove_edge(u, v)
    else:
      directed_graph.remove_edge(v, u)

  return directed_graph


def remove_directions(graph: nx.Graph) -> nx.Graph:
  # Converting the direted graph to an undirected one by removing directions.
  undirected_graph = nx.Graph()
  undirected_graph.add_nodes_from(graph.nodes())
  # Add edges between nodes, ignoring directions.
  for u, v in graph.edges():
    undirected_graph.add_edge(u, v)

  return undirected_graph


================================================
FILE: talk_like_a_graph/graph_generators_runner.py
================================================
r"""Random graph generation.

This code generates random graph using different algorithms.

# Placeholder for Google-internal comments.
"""

from collections.abc import Sequence
import os

from absl import app
from absl import flags
import networkx as nx

# Internal import.
from . import graph_generators

_ALGORITHM = flags.DEFINE_string(
    "algorithm",
    None,
    "The graph generating algorithm to use.",
    required=True,
)
_NUMBER_OF_GRAPHS = flags.DEFINE_integer(
    "number_of_graphs",
    None,
    "The number of graphs to generate.",
    required=True,
)
_DIRECTED = flags.DEFINE_bool(
    "directed", False, "Whether to generate directed graphs."
)
_OUTPUT_PATH = flags.DEFINE_string(
    "output_path", None, "The output path to write the graphs.", required=True
)
_SPLIT = flags.DEFINE_string(
    "split", None, "The dataset split to generate.", required=True
)
_MIN_SPARSITY = flags.DEFINE_float("min_sparsity", 0.0, "The minimum sparsity.")
_MAX_SPARSITY = flags.DEFINE_float("max_sparsity", 1.0, "The maximum sparsity.")


def write_graphs(graphs: list[nx.Graph], output_dir: str) -> None:
  if not os.path.isdir(output_dir):
    os.makedirs(output_dir)
  for ind, graph in enumerate(graphs):
    nx.write_graphml(
        graph,
        os.Open(
            os.path.join(output_dir, str(ind) + ".graphml"),
            "wb",
        ),
    )


def main(argv: Sequence[str]) -> None:
  if len(argv) > 1:
    raise app.UsageError("Too many command-line arguments.")

  if _SPLIT.value == "train":
    random_seed = 9876
  elif _SPLIT.value == "test":
    random_seed = 1234
  elif _SPLIT.value == "validation":
    random_seed = 5432
  else:
    raise NotImplementedError()

  generated_graphs = graph_generators.generate_graphs(
      number_of_graphs=_NUMBER_OF_GRAPHS.value,
      algorithm=_ALGORITHM.value,
      directed=_DIRECTED.value,
      random_seed=random_seed,
      er_min_sparsity=_MIN_SPARSITY.value,
      er_max_sparsity=_MAX_SPARSITY.value,
  )
  write_graphs(
      graphs=generated_graphs,
      output_dir=os.path.join(
          _OUTPUT_PATH.value,
          "directed" if _DIRECTED.value else "undirected",
          _ALGORITHM.value,
          _SPLIT.value,
      ),
  )


if __name__ == "__main__":
  app.run(main)


================================================
FILE: talk_like_a_graph/graph_generators_test.py
================================================
from absl.testing import parameterized
from . import graph_generators
from absl.testing import absltest


class GraphGenerationTest(absltest.TestCase, parameterized.TestCase):

  @parameterized.named_parameters(
      dict(
          testcase_name='er_undirected_1',
          algorithm='er',
          directed=False,
          k=1,
      ),
      dict(
          testcase_name='er_directed_1',
          algorithm='er',
          directed=True,
          k=1,
      ),
      dict(
          testcase_name='ba_undirected_5',
          algorithm='ba',
          directed=False,
          k=5,
      ),
      dict(
          testcase_name='ba_directed_5',
          algorithm='ba',
          directed=True,
          k=5,
      ),
  )
  def test_number_of_graphs(self, algorithm, directed, k):
    generated_graph = graph_generators.generate_graphs(k, algorithm, directed)
    self.assertLen(generated_graph, k)

  @parameterized.named_parameters(
      dict(
          testcase_name='er_undirected',
          algorithm='er',
          directed=False,
      ),
      dict(
          testcase_name='er_directed',
          algorithm='er',
          directed=True,
      ),
      dict(
          testcase_name='ba_undirected',
          algorithm='ba',
          directed=False,
      ),
      dict(
          testcase_name='ba_directed',
          algorithm='ba',
          directed=True,
      ),
      dict(
          testcase_name='sbm_undirected',
          algorithm='sbm',
          directed=False,
      ),
      dict(
          testcase_name='sbm_directed',
          algorithm='sbm',
          directed=True,
      ),
      dict(
          testcase_name='sfn_undirected',
          algorithm='sfn',
          directed=False,
      ),
      dict(
          testcase_name='sfn_directed',
          algorithm='sfn',
          directed=True,
      ),
      dict(
          testcase_name='complete_undirected',
          algorithm='complete',
          directed=False,
      ),
      dict(
          testcase_name='complete_directed',
          algorithm='complete',
          directed=True,
      ),
      dict(
          testcase_name='star_undirected',
          algorithm='star',
          directed=False,
      ),
      dict(
          testcase_name='star_directed',
          algorithm='star',
          directed=True,
      ),
      dict(
          testcase_name='path_undirected',
          algorithm='path',
          directed=False,
      ),
      dict(
          testcase_name='path_directed',
          algorithm='path',
          directed=True,
      ),
  )
  def test_directions(self, algorithm, directed):
    generated_graph = graph_generators.generate_graphs(1, algorithm, directed)
    self.assertEqual(generated_graph[0].is_directed(), directed)


if __name__ == '__main__':
  googletest.main()


================================================
FILE: talk_like_a_graph/graph_metrics.py
================================================
"""Metrics for seqio tasks over graph data.

This module contains definitions of metric_fns to be used for scoring
graph tasks from nlgraph and graphqa.
"""

from typing import Mapping, Sequence


def yes_no_accuracy(
    targets: Sequence[str], predictions: Sequence[str]
) -> Mapping[str, float]:
  """Assesses the accuracy of LLM outputs on Yes/No tasks.
  
  Targets must contain either the word 'yes' or the word 'no' but not both.
  
  Predictions are binarized by checking for 'yes' or 'no' in the first line.

  Args:
    targets: The expected output strings.
    predictions: The LLM outputs.

  Returns:
     Returns a dict of the following metrics:
      yes_no_accuracy: The % where the target and prediction match.
      yes_no_ambiguous: The % where the prediction contained yes and no
      yes_no_indeterminate: The % where the prediction contained neither yes nor
      no

  Raises:
    ValueError: If a target string contains 'yes' and 'no'
  """
  num_correct = 0
  num_ambiguous = 0
  num_indeterminate = 0
  for target, prediction in zip(targets, predictions):
    normalized_target = target.lower()
    binarized_target = 'yes' in normalized_target
    print(binarized_target)
    if binarized_target and 'no' in normalized_target:
      raise ValueError(f'Ambiguous target string, {target}')
    if not binarized_target and 'no' not in normalized_target:
      raise ValueError(f'Indeterminate target string, {target}')
    normalized_prediction = prediction.splitlines()
    if not normalized_prediction:
      normalized_prediction = ''
    else:
      normalized_prediction = normalized_prediction[0]
    normalized_prediction = normalized_prediction.lower()
    binarized_prediction = 'yes' in normalized_prediction.lower()
    print(normalized_prediction)
    if binarized_prediction and 'no' in normalized_prediction:
      num_ambiguous += 1
      continue
    if not binarized_prediction and 'no' not in normalized_prediction:
      num_indeterminate += 1
      continue
    if binarized_prediction == binarized_target:
      num_correct += 1
  return {
      'yes_no_accuracy': num_correct / len(targets),
      'yes_no_ambiguous': num_ambiguous / len(targets),
      'yes_no_indeterminate': num_indeterminate / len(targets),
  }


================================================
FILE: talk_like_a_graph/graph_metrics_test.py
================================================
from . import graph_metrics
from absl.testing import absltest


class GraphTasksTest(absltest.TestCase):

  def test_yes_no_correct(self):
    result = graph_metrics.yes_no_accuracy(
        targets=["Yes", "The answer is yes.", "No", "The answer is no.", "No"],
        predictions=[
            "yes",
            "Yes\nNo",
            "No\nYes",
            "That's gonna be no from me.",
            "yes",
        ],
    )
    self.assertEqual(result["yes_no_ambiguous"], 0)
    self.assertEqual(result["yes_no_indeterminate"], 0)
    self.assertAlmostEqual(result["yes_no_accuracy"], 0.8)

  def test_yes_no_ambiguous(self):
    result = graph_metrics.yes_no_accuracy(
        targets=["Yes", "The answer is yes.", "No", "The answer is no.", "No"],
        predictions=[
            "yes",
            "Yes No",
            "No Yes",
            "That's gonna be no from me.",
            "yes",
        ],
    )
    self.assertEqual(result["yes_no_ambiguous"], 0.4)
    self.assertEqual(result["yes_no_indeterminate"], 0)
    self.assertAlmostEqual(result["yes_no_accuracy"], 0.4)

  def test_yes_no_indeterminate(self):
    result = graph_metrics.yes_no_accuracy(
        targets=["Yes", "The answer is yes.", "No", "The answer is no.", "No"],
        predictions=[
            "yes",
            "\n No",
            "",
            "That's gonna be no from me.",
            "yes",
        ],
    )
    self.assertEqual(result["yes_no_ambiguous"], 0)
    self.assertEqual(result["yes_no_indeterminate"], 0.4)
    self.assertAlmostEqual(result["yes_no_accuracy"], 0.4)

  def test_yes_no_accuracy_raises_on_ambiguous_target(self):
    with self.assertRaises(ValueError):
      graph_metrics.yes_no_accuracy(
          targets=["Yes but maybe no"],
          predictions=["yes"],
      )

  def test_yes_no_accuracy_raises_on_indeterminate_target(self):
    with self.assertRaises(ValueError):
      graph_metrics.yes_no_accuracy(
          targets=["Hmm?"],
          predictions=["yes"],
      )


if __name__ == "__main__":
  googletest.main()


================================================
FILE: talk_like_a_graph/graph_tasks.py
================================================
"""The graph tasks to be tried with LLMs."""

import random

import networkx as nx
import numpy as np

from . import graph_text_encoders


class GraphTask:
  """The parent class for all the graph tasks."""

  def __init__(self):
    self.name = 'default'
    self.maximum_nnodes_cot_graph = 10

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    raise NotImplementedError()

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ):
    raise NotImplementedError()


class CycleCheck(GraphTask):
  """The graph task to check if there is at least one cycle or not."""

  def __init__(self):
    super().__init__()
    self.name = 'cycle_check'
    self._task_description = 'Q: Is there a cycle in this graph?\nA: '

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    for ind, graph in enumerate(graphs):
      question = (
          graph_text_encoders.encode_graph(graph, encoding_method)
          + self._task_description
      )
      try:
        nx.find_cycle(graph)
        answer = 'Yes, there is a cycle.'
      except nx.NetworkXNoCycle:
        answer = 'No, there is no cycle.'
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': self._task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [],
      }
    return examples_dict

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    """Create a few shot example w or w/o cot for the graph graph."""
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = (
        graph_text_encoders.encode_graph(graph, encoding_method)
        + self._task_description
    )
    try:
      cycle = nx.find_cycle(graph)
      cycle_text = ''
      answer = 'Yes, there is a cycle. '
      if cot:
        for pair in cycle:
          cycle_text += (
              name_dict[pair[0]]
              + ' is connected to '
              + name_dict[pair[1]]
              + ', '
          )
        cycle_cot = 'The cycle is: %s.' % cycle_text[:-2]
        answer += cycle_cot
    except nx.NetworkXNoCycle:
      answer = 'No, there is no cycle.'
    return question + answer

  def choose_few_shot_examples(
      self,
      few_shots_dict: dict[tuple[str, str], list[str]],
      encoding_method: str,
      k: int = 2,
  ) -> str:
    """Choose few shot examples for each algorithm."""
    pos_cycle_algorithms = ['er', 'ba', 'sbm', 'sfn', 'complete']
    neg_cycle_algorithms = ['star', 'path']
    few_shots_str = ''
    # choose k-1 shots for pos algorithms and one negative.
    positive_algorithms = random.choices(pos_cycle_algorithms, k=k - 1)
    for positive_algorithm in positive_algorithms:
      example_list = few_shots_dict[(positive_algorithm, encoding_method)]
      few_shots_str += 'Example: ' + random.choice(example_list) + '\n'
    negative_algorithm = random.choice(neg_cycle_algorithms)
    example_list = few_shots_dict[(negative_algorithm, encoding_method)]
    few_shots_str += 'Example: ' + random.choice(example_list) + '\n'
    return few_shots_str


class EdgeExistence(GraphTask):
  """The graph task to check if an edge exist in a graph or not."""

  def __init__(self):
    super().__init__()
    self.name = 'edge_existence'

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)

    for ind, graph in enumerate(graphs):
      source, target = random.sample(list(graph.nodes()), k=2)
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      task_description = 'Q: Is node %s connected to node %s?\nA: ' % (
          name_dict[source],
          name_dict[target],
      )
      question += task_description
      if ((source, target) in graph.edges()) or (
          (target, source) in graph.edges()
      ):
        answer = 'Yes.'
      else:
        answer = 'No.'
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [source, target],
      }
    return examples_dict

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    source, target = random.sample(list(graph.nodes()), k=2)
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    question += 'Q: Is node %s connected to node %s?\nA: ' % (
        name_dict[source],
        name_dict[target],
    )
    if ((source, target) in graph.edges()) or (
        (target, source) in graph.edges()
    ):
      answer = 'Yes.'
      if cot:
        answer += (
            ' Because, there is an edge from %s to %s in the graph description.'
            % (name_dict[source], name_dict[target])
        )
    else:
      answer = 'No.'
      if cot:
        answer += (
            ' Because, there is no edge from %s to %s in the graph description.'
            % (name_dict[source], name_dict[target])
        )
    return question + answer


class NodeCount(GraphTask):
  """The graph task for finding number of nodes in a graph."""

  def __init__(self):
    super().__init__()
    self.name = 'node_count'
    self._task_description = 'Q: How many nodes are in this graph?\nA: '

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    for ind, graph in enumerate(graphs):
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      question += self._task_description
      answer = ' %d.' % len(graph.nodes())
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': self._task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [],
      }
    return examples_dict

  def get_nodes_string(self, name_dict: dict[int, str], nnodes: int) -> str:
    node_string = ''
    for i in range(nnodes - 1):
      node_string += name_dict[i] + ', '
    node_string += 'and ' + name_dict[nnodes - 1]
    return node_string

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    question += self._task_description
    answer = '%d.' % len(graph.nodes())
    if cot:
      answer += ' The nodes are %s.' % self.get_nodes_string(
          name_dict, len(graph.nodes())
      )

    return question + answer


class NodeDegree(GraphTask):
  """The graph task for finding degree of a node in a graph."""

  def __init__(self):
    super().__init__()
    self.name = 'node_degree'

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)
    for ind, graph in enumerate(graphs):
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      source_node = random.sample(list(graph.nodes()), k=1)[0]
      task_description = (
          'Q: What is the degree of node %s?\nA: ' % name_dict[source_node]
      )
      question += task_description
      answer = '%d.' % graph.degree[source_node]
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [source_node],
      }
    return examples_dict

  def get_edge_string(
      self, name_dict: dict[int, str], graph: nx.Graph, source_node: int
  ) -> str:
    """Gets a string identifying the edges a given node is connected to."""
    edge_string = ''
    target_edges = graph.edges(source_node)
    target_nodes = []
    for edge in target_edges:
      target_nodes.append(edge[1])
    if target_nodes:
      for i in range(len(target_nodes) - 1):
        edge_string += name_dict[target_nodes[i]] + ', '
      edge_string += 'and ' + name_dict[target_nodes[-1]]
    else:
      edge_string = 'no nodes'
    return edge_string

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    source_node = random.sample(list(graph.nodes()), k=1)[0]
    question += (
        'Q: What is the degree of node %s?\nA: ' % name_dict[source_node]
    )
    answer = '%d.' % graph.degree[source_node]
    if cot:
      answer += ' This is because %s is connected to %s.' % (
          name_dict[source_node],
          self.get_edge_string(name_dict, graph, source_node),
      )
    return question + answer


class EdgeCount(GraphTask):
  """The graph task for finding number of edges in a graph."""

  def __init__(self):
    super().__init__()
    self.name = 'edge_count'
    self._task_description = 'Q: How many edges are in this graph?\nA: '

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    for ind, graph in enumerate(graphs):
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      question += self._task_description
      answer = ' %d.' % len(graph.edges())
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': self._task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [],
      }
    return examples_dict

  def get_edges_string(
      self, name_dict: dict[int, str], edges: list[tuple[int, int]]
  ) -> str:
    edges_string = ''
    for edge in edges:
      edges_string += (
          '(' + name_dict[edge[0]] + ', ' + name_dict[edge[1]] + '), '
      )
    return edges_string.strip()[:-1]

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    question += self._task_description
    answer = '%d.' % len(graph.edges())
    if cot:
      answer += ' The edges are %s.' % self.get_edges_string(
          name_dict, list(graph.edges())
      )
    return question + answer


class ConnectedNodes(GraphTask):
  """The graph task for finding connected nodes to a given node in a graph."""

  def __init__(self):
    super().__init__()
    self.name = 'connected_nodes'

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)
    for ind, graph in enumerate(graphs):
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      source_node = random.sample(list(graph.nodes()), k=1)[0]
      task_description = (
          'Q: List all the nodes connected to %s in alphabetical order.\nA: '
          % name_dict[source_node]
      )
      question += task_description
      outgoing_edges = list(graph.edges(source_node))
      if outgoing_edges:
        answer = self.get_connected_nodes(outgoing_edges, name_dict) + '.'
      else:
        answer = ' No nodes.'
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [source_node],
      }
    return examples_dict

  def get_connected_nodes(
      self, edges: list[tuple[int, int]], name_dict: dict[int, str]
  ) -> str:
    """Gets a string including all the nodes that are connected to source."""
    connected_nodes = []
    for edge in edges:
      connected_nodes.append(name_dict[edge[1]])
    connected_nodes_string = ''
    if connected_nodes:
      try:
        int(connected_nodes[0])
        connected_nodes_string = ', '.join(map(str, connected_nodes))
      except ValueError:
        # Check if these are not integers, sort
        connected_nodes_string = ', '.join(map(str, sorted(connected_nodes)))
    return connected_nodes_string

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    source_node = random.sample(list(graph.nodes()), k=1)[0]
    question += (
        'Q: List all the nodes connected to %s in alphabetical order.\nA: '
        % name_dict[source_node]
    )
    outgoing_edges = list(graph.edges(source_node))
    answer = ''
    if outgoing_edges:
      answer = self.get_connected_nodes(outgoing_edges, name_dict) + '.'
      if cot:
        answer += ' This is because there is an edge from %s to %s.' % (
            name_dict[source_node],
            answer,
        )
      else:
        answer = 'No nodes.'
        if cot:
          answer += (
              ' This is because %s is not connected to any node.'
              % name_dict[source_node]
          )
    return question + answer


class DisconnectedNodes(GraphTask):
  """The task for finding disconnected nodes for a given node in a graph."""

  def __init__(self):
    super().__init__()
    self.name = 'disconnected_nodes'

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)
    for ind, graph in enumerate(graphs):
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      source_node = random.sample(list(graph.nodes()), k=1)[0]
      task_description = (
          'Q: List all the nodes that are not connected to %s in alphabetical'
          ' order.\nA: '
          % name_dict[source_node]
      )
      question += task_description
      outgoing_edges = list(graph.edges(source_node))
      answer = self.get_disconnected_nodes(
          source_node, outgoing_edges, name_dict, list(graph.nodes())
      )
      if not answer:
        answer = 'No nodes'

      answer += '.'
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [source_node],
      }
    return examples_dict

  def get_disconnected_nodes(
      self,
      source: int,
      edges: list[tuple[int, int]],
      name_dict: dict[int, str],
      all_nodes: list[int],
  ) -> str:
    """Gets a string with all the nodes that are not connected to source."""
    for edge in edges:
      if edge[1] in all_nodes:
        all_nodes.remove(edge[1])
    if source in all_nodes:
      all_nodes.remove(source)
    all_nodes_names = []
    for node in all_nodes:
      all_nodes_names.append(name_dict[node])
    # sorted operation should be different for integers vs strings.
    if all_nodes_names:
      try:
        int(all_nodes_names[0])
        for ind, value in enumerate(all_nodes_names):
          all_nodes_names[ind] = int(value)
        all_nodes_names = sorted(all_nodes_names)
        for ind, value in enumerate(all_nodes_names):
          all_nodes_names[ind] = str(value)
      except ValueError:
        pass
    return ', '.join(map(str, sorted(all_nodes_names)))

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    source_node = random.sample(list(graph.nodes()), k=1)[0]
    question += (
        'Q: List all the nodes that are not connected to %s in alphabetical'
        ' order.\nA: '
        % name_dict[source_node]
    )
    outgoing_edges = list(graph.edges(source_node))
    answer = ''
    disconnected_nodes_string = self.get_disconnected_nodes(
        source_node, outgoing_edges, name_dict, list(graph.nodes())
    )
    if outgoing_edges:
      if not disconnected_nodes_string:
        disconnected_nodes_string = 'No nodes'
      answer = disconnected_nodes_string + '.'
      if cot:
        answer += ' This is because there is not an edge from %s to %s.' % (
            name_dict[source_node],
            answer,
        )
      else:
        answer = ' No nodes.'
        if cot:
          answer += (
              ' This is because %s is connected to all nodes.'
              % name_dict[source_node]
          )
    return question + answer


class Reachability(GraphTask):
  """The graph task to check if there is a path from a source to target."""

  def __init__(self):
    super().__init__()
    self.name = 'reachability'

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)

    for ind, graph in enumerate(graphs):
      source, target = random.sample(list(graph.nodes()), k=2)
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      task_description = 'Q: Is there a path from node %s to node %s?\nA: ' % (
          name_dict[source],
          name_dict[target],
      )
      question += task_description
      if nx.has_path(graph, source, target):
        answer = 'Yes.'
      else:
        answer = 'No.'
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [source, target],
      }
    return examples_dict

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    source, target = random.sample(list(graph.nodes()), k=2)
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    question += 'Q: Is there a path from node %s to node %s?\nA: ' % (
        name_dict[source],
        name_dict[target],
    )
    if nx.has_path(graph, source, target):
      answer = 'Yes.'
      if cot:
        path = nx.shortest_path(graph, source, target)
        explanation = ' Because'
        for i in range(len(path) - 1):
          # The only edge or the non-last edges in the path.
          if len(path) == 2 or i < len(path) - 2:
            sep = ','
          # The last edge in a path with more than one edge.
          else:
            sep = ', and'
          explanation += '%s there is an edge from node %d to node %d' % (
              sep,
              path[i],
              path[i + 1],
          )
        explanation += ' .'
        answer += explanation
    else:
      answer = 'No.'
      if cot:
        answer += (
            ' Because, there is no path connecting node %s to node %s based on'
            ' the graph description.' % (name_dict[source], name_dict[target])
        )
    return question + answer


class ShortestPath(GraphTask):
  """The graph task to check if there is a path from a source to target."""

  def __init__(self):
    super().__init__()
    self.name = 'shortest_path'

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)

    for ind, graph in enumerate(graphs):
      source, target = random.sample(list(graph.nodes()), k=2)
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      task_description = (
          'Q: What is the length of the shortest path from node %s to node'
          ' %s?\nA: '
          % (
              name_dict[source],
              name_dict[target],
          )
      )
      question += task_description
      try:
        path = nx.shortest_path(graph, source, target)
        answer = str(len(path) - 1) + '.'
      except nx.NetworkXNoPath:
        answer = 'There is no path from node %s to node %s.' % (
            name_dict[source],
            name_dict[target],
        )
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [source, target],
      }
    return examples_dict

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    source, target = random.sample(list(graph.nodes()), k=2)
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    question += (
        'Q: What is the length of the shortest path from node %s to node'
        ' %s?\nA: '
        % (
            name_dict[source],
            name_dict[target],
        )
    )
    if nx.has_path(graph, source, target):
      path = nx.shortest_path(graph, source, target)
      answer = str(len(path) - 1) + '.'
      if cot:
        explanation = ' Because'
        for i in range(len(path) - 1):
          # The only edge or the non-last edges in the path.
          if len(path) == 2 or i < len(path) - 2:
            sep = ','
          # The last edge in a path with more than one edge.
          else:
            sep = ', and'
          explanation += '%s there is an edge from node %d to node %d' % (
              sep,
              path[i],
              path[i + 1],
          )
        explanation += ' .'
        answer += explanation
    else:
      answer = 'There is no path from node %s to node %s.' % (
          name_dict[source],
          name_dict[target],
      )
      if cot:
        answer += (
            ' Because, there is no path connecting node %s to node %s based on'
            ' the graph description.' % (name_dict[source], name_dict[target])
        )
    return question + answer


class TriangleCounting(GraphTask):
  """The graph task to count the number of triangles in a graph."""

  def __init__(self):
    super().__init__()
    self.name = 'triangle_counting'
    self._task_description = 'Q: How many triangles are in this graph?\nA: '

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    for ind, graph in enumerate(graphs):
      question = (
          graph_text_encoders.encode_graph(graph, encoding_method)
          + self._task_description
      )
      ntriangles = int(np.sum(list(nx.triangles(graph).values())) / 3)

      answer = '%i.' % ntriangles
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': self._task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [],
      }
    return examples_dict

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    """Create a few shot example w or w/o cot for the graph graph."""
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = (
        graph_text_encoders.encode_graph(graph, encoding_method)
        + self._task_description
    )
    triangles_dict = nx.triangles(graph)
    ntriangles = int(np.sum(list(triangles_dict.values())) / 3)

    if ntriangles > 0:
      answer = '%i.' % ntriangles
      if cot:
        ntriangles_cot = ''
        for key, value in triangles_dict.items():
          if value > 0:
            if value == 1:
              ntriangles_cot += (
                  'There is %i triangle including node %s as a vertex.\n'
                  % (value, name_dict[key])
              )
            else:
              ntriangles_cot += (
                  'There are %i triangles including node %s as a vertex.\n'
                  % (value, name_dict[key])
              )
        ntriangles_cot += (
            'Summing the number of triangles for all nodes and dividing them by'
            ' three gives us %i triangles in total.' % ntriangles
        )
        answer += ntriangles_cot
    else:
      answer = '0.'
      if cot:
        ntriangles_cot = 'No three nodes form a triangle of edges.'
        answer += ntriangles_cot
    return question + answer


class MaximumFlow(GraphTask):
  """The graph task to compute the maximum flow from a source to a target."""

  def __init__(self):
    super().__init__()
    self.name = 'maximum_flow'

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)

    for ind, graph in enumerate(graphs):
      graph = add_edge_weight(graph)
      source, target = random.sample(list(graph.nodes()), k=2)
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      task_description = (
          'Q: What is the maximum capacity of the flow from node %s to node'
          ' %s?\nA: ' % (name_dict[source], name_dict[target])
      )
      question += task_description
      maximum_flow_value = nx.maximum_flow(
          graph, source, target, capacity='weight'
      )[0]
      answer = str(maximum_flow_value) + '.'
      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(len(graph.nodes())),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          'node_ids': [source, target],
      }
    return examples_dict

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    graph = add_edge_weight(graph)
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    source, target = random.sample(list(graph.nodes()), k=2)
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    question += (
        'Q: What is the maximum capacity of the flow from node %s to'
        ' node %s?\nA: ' % (name_dict[source], name_dict[target])
    )
    flow_value, flow_dict = nx.maximum_flow(
        graph, source, target, capacity='weight'
    )
    answer = str(flow_value) + '.'
    if flow_value > 0:
      if cot:
        explanation = ' This is because of the following edges: '
        for edge, capacity in flow_dict.items():
          for key, value in capacity.items():
            if value > 0:
              explanation += (
                  'the edge from node %i to node %i with capacity %i, '
                  % (
                      edge,
                      key,
                      value,
                  )
              )
        explanation = explanation.strip()[:-1] + '.'
        answer += explanation
    else:
      if cot:
        answer += (
            ' Because, there is no path connecting node %s to node %s based on'
            ' the graph description.' % (name_dict[source], name_dict[target])
        )
    return question + answer


def has_edge_weights(graph):
  for _, _, data in graph.edges(data=True):
    if 'weight' not in data:
      return False
  return True


def add_edge_weight(graph):
  if has_edge_weights(graph):
    return graph
  else:
    for edge in graph.edges():
      graph[edge[0]][edge[1]]['weight'] = random.randint(1, 10)
    return graph


class NodeClassification(GraphTask):
  """The graph task to classify a given node in the graph."""

  def __init__(self):
    super().__init__()
    self.name = 'node_classification'
    self.classes = [
        'soccer',
        'baseball',
        'tennis',
        'golf',
        'football',
        'surfing',
    ]

  def prepare_examples_dict(
      self,
      graphs: list[nx.Graph],
      generator_algorithms: list[str],
      encoding_method: str,
  ) -> dict[int, dict[str, str | list[int]]]:
    classes = random.sample(list(self.classes), k=2)
    examples_dict = {}
    name_dict = graph_text_encoders.get_tlag_node_encoder(None, encoding_method)
    for ind, graph in enumerate(graphs):
      question = graph_text_encoders.encode_graph(graph, encoding_method)
      nnodes = len(graph.nodes())
      # Sampling nnodes // 2 + 1 nodes.
      sampled_nodes = random.sample(
          list(graph.nodes(data=True)), k=nnodes // 2 + 1
      )
      # Adding the class of half of the nodes.
      for node_data in sampled_nodes[:-1]:
        node_class = classes[node_data[1]['block']]
        question += (
            'Node ' + name_dict[node_data[0]] + ' likes ' + node_class + '.\n'
        )
      # Reserving the last sampled node for the question.
      task_description = 'Q: Does node %s like %s or %s?\nA: ' % (
          name_dict[sampled_nodes[-1][0]],
          classes[0],
          classes[1],
      )
      question += task_description
      answer = classes[sampled_nodes[-1][1]['block']]

      examples_dict[ind] = {
          'question': question,
          'answer': answer,
          'nnodes': str(nnodes),
          'nedges': str(len(graph.edges())),
          'task_description': task_description,
          'graph': graph,
          'algorithm': generator_algorithms[ind],
          # id of the last samples node
          'node_ids': [sampled_nodes[-1][0]],
      }

    return examples_dict

  def create_few_shot_example(
      self, graph: nx.Graph, encoding_method: str, cot: bool
  ) -> str:
    classes = random.sample(list(self.classes), k=2)
    name_dict = graph_text_encoders.get_tlag_node_encoder(
        graph, encoding_method
    )
    question = graph_text_encoders.encode_graph(graph, encoding_method)
    nnodes = len(graph.nodes())
    sampled_nodes = random.sample(
        list(graph.nodes(data=True)), k=nnodes // 2 + 1
    )
    for node_data in sampled_nodes[:-1]:
      node_class = classes[node_data[1]['block']]
      question += (
          'Node ' + name_dict[node_data[0]] + ' likes ' + node_class + '.\n'
      )
    task_description = 'Q: Does node %s like %s or %s?\nA: ' % (
        name_dict[sampled_nodes[-1][0]],
        classes[0],
        classes[1],
    )
    question += task_description
    answer = classes[sampled_nodes[-1][1]['block']]

    if cot:
      explanation = (
          ' This is because most of the nodes that are connected to node %s'
          ' likes %s.'
          % (sampled_nodes[-1][0], classes[sampled_nodes[-1][1]['block']])
      )
      answer += explanation
    return question + answer


================================================
FILE: talk_like_a_graph/graph_tasks_generator.py
================================================
r"""The graph tasks to be tried with LLMs..

This code loads graphs and creates graph tasks and output them as tf examples in
a recordio file in the task directory provided.

# Placeholder for Google-internal comments.
"""

from collections.abc import Sequence
import os
import random

from absl import app
from absl import flags
import networkx as nx
import numpy as np

from . import graph_tasks
from . import graph_tasks_utils as utils

_TASK_DIR = flags.DEFINE_string(
    'task_dir', None, 'The directory to write tasks.', required=True
)
_GRAPHS_DIR = flags.DEFINE_string(
    'graphs_dir', None, 'The directory containing the graphs.', required=True
)
_RANDOM_SEED = flags.DEFINE_integer(
    'random_seed',
    None,
    'The random seed to use for task generation.',
    required=True,
)


def zero_shot(
    task: graph_tasks.GraphTask,
    graphs: list[nx.Graph],
    algorithms: list[str],
    text_encoders: list[str],
    cot: bool,
    random_seed: int,
    split: str,
) -> None:
  """Creating zero-shot or zero-cot examples for the given task.

  Args:
    task: the corresponding graph task.
    graphs: the list of graphs to use for the task.
    algorithms: the algorithm used to generate the graphs.
    text_encoders: the encoders to use in the tasks.
    cot: whether to apply cot or not.
    random_seed: the random seed to use in the process.
    split: whether we are creating a train or test split.
  """
  random.seed(random_seed)
  zero_shot_examples = utils.create_zero_shot_task(
      task, graphs, algorithms, text_encoders, cot=cot
  )

  file_name = task.name + ('_zero_cot_' if cot else '_zero_shot_')

  file_name += split + '.recordio'
  utils.write_examples(
      zero_shot_examples,
      os.path.join(_TASK_DIR.value, file_name),
  )


def few_shot(
    task: graph_tasks.GraphTask,
    graphs: list[nx.Graph],
    few_shot_graphs: list[nx.Graph],
    algorithms: list[str],
    text_encoders: list[str],
    cot: bool,
    bag: bool,
    random_seed: int,
) -> None:
  """Creating few-shot, cot, or cot-bag examples for the given task.

  Args:
    task: the corresponding graph task.
    graphs: the list of graphs to use for the task.
    few_shot_graphs: the list of graphs to generate few shot examples for.
    algorithms: the algorithm used to generate the graphs.
    text_encoders: the encoders to use in the tasks.
    cot: whether to apply cot or not.
    bag: whether to apply build-a-graph method or not.
    random_seed: the random seed to use in the process.
  """
  random.seed(random_seed)
  few_shot_examples = utils.create_few_shot_task(
      task,
      graphs,
      algorithms,
      few_shot_graphs,
      text_encoders,
      cot=cot,
      bag=bag,
      random_seed=random_seed,
  )
  file_name = task.name
  if cot and bag:
    file_name += '_few_shot_cot_bag_test.recordio'
  elif cot:
    file_name += '_few_shot_cot_test.recordio'
  else:
    file_name += '_few_shot_test.recordio'

  utils.write_examples(
      few_shot_examples,
      os.path.join(_TASK_DIR.value, file_name),
  )


def generate_random_sbm_graph(random_state: np.random.RandomState):
  # Sampling a small number as the probability of the two nodes in different
  # communities being connected.
  small_number = random.uniform(0, 0.05)
  # Sampling a large number as probability of the nodes in one community
  # being connected.
  large_number = random.uniform(0.6, 0.8)
  number_of_nodes = random.choice(np.arange(5, 20))
  sizes = [number_of_nodes // 2, number_of_nodes // 2]
  probs = [[large_number, small_number], [small_number, large_number]]
  return nx.stochastic_block_model(sizes, probs, seed=random_state)


def main(argv: Sequence[str]) -> None:
  if len(argv) > 1:
    raise app.UsageError('Too many command-line arguments.')

  algorithms = ['er']
  directions = ['undirected']
  text_encoders = ['adjacency']

  # Loading the graphs.
  graphs = []
  generator_algorithms = []
  for algorithm in algorithms:
    for direction in directions:
      loaded_graphs = utils.load_graphs(
          _GRAPHS_DIR.value,
          algorithm,
          'train',
          direction,
      )
      graphs += loaded_graphs
      generator_algorithms += [algorithm] * len(loaded_graphs)

  # Defining a task on the graphs
  task = graph_tasks.ShortestPath()

  if isinstance(task, graph_tasks.NodeClassification):
    # The node classification task requires SBM graphs. As it's not possible to
    # write graphs with data (e.g., blocks data as in SBM graphs), we regenerate
    # graphs.

    random_state = np.random.RandomState(_RANDOM_SEED.value)
    print('Generating sbm graphs')
    graphs = [
        generate_random_sbm_graph(random_state) for _ in range(len(graphs))
    ]

  zero_shot(
      task,
      graphs,
      generator_algorithms,
      text_encoders,
      cot=False,
      random_seed=_RANDOM_SEED.value,
      split='test',
  )
  zero_shot(
      task,
      graphs,
      generator_algorithms,
      text_encoders,
      cot=True,
      random_seed=_RANDOM_SEED.value,
      split='test',
  )

  # Loading few-shot graphs.
  few_shot_graphs = []
  for algorithm in algorithms:
    for direction in directions:
      few_shot_graphs += utils.load_graphs(
          _GRAPHS_DIR.value,
          algorithm,
          'train',
          direction,
      )

  if isinstance(task, graph_tasks.NodeClassification):
    # The node classification task requires SBM graphs. As it's not possible to
    # write graphs with data (e.g., blocks data as in SBM graphs), we regenerate
    # graphs.
    random_state = np.random.RandomState(_RANDOM_SEED.value + 1)
    print('Generating few shot sbm graphs')
    few_shot_graphs = [
        generate_random_sbm_graph(random_state)
        for _ in range(len(few_shot_graphs))
    ]

  few_shot(
      task,
      graphs,
      few_shot_graphs,
      generator_algorithms,
      text_encoders,
      cot=False,
      bag=False,
      random_seed=_RANDOM_SEED.value,
  )

  few_shot(
      task,
      graphs,
      few_shot_graphs,
      generator_algorithms,
      text_encoders,
      cot=True,
      bag=False,
      random_seed=_RANDOM_SEED.value,
  )

  few_shot(
      task,
      graphs,
      few_shot_graphs,
      generator_algorithms,
      text_encoders,
      cot=True,
      bag=True,
      random_seed=_RANDOM_SEED.value,
  )


if __name__ == '__main__':
  app.run(main)


================================================
FILE: talk_like_a_graph/graph_tasks_utils.py
================================================
"""The graph tasks to be tried with LLMs."""

import os
import random

import networkx as nx
import numpy as np
import seqio
import tensorflow as tf
import tensorflow_gnn as tfgnn

# Google-internal import(s).
# Internal import.
from . import graph_tasks
from tensorflow.core.example import example_pb2
from tensorflow.core.example import feature_pb2


def laplacian_pos_embedding(graph: nx.Graph, units: int = 4) -> nx.Graph:
  """Adds the laplacian positional encoding."""
  m = nx.normalized_laplacian_matrix(
      graph, nodelist=sorted(graph.nodes), weight=None
  ).astype(np.float32)
  u, _, _ = np.linalg.svd(m.todense(), compute_uv=True)
  if units > u.shape[1]:
    u = np.pad(u, ((0, 0), (0, units - u.shape[1])))
  nx.set_node_attributes(
      graph, dict(zip(sorted(graph.nodes), u[:, :units])), name='lpe'
  )
  return graph


def to_tfgnn(graph: nx.Graph, node_ids: list[int]) -> tfgnn.GraphTensor:
  """Convert a given nx graph to a tfgnn graph."""
  if graph.edges(data=True):
    s, t, w = zip(*[
        (s, t, (d['weight'] if d and 'weight' in d else None))
        for s, t, d in graph.edges(data=True)
    ])
  else:
    s, t, w = (), (), ()
  # tfgnn assumes graphs are directed. Adding the rev edges for an undirected
  # graph.
  if not graph.is_directed():
    s, t, w = s + t, t + s, w + w

  graph = laplacian_pos_embedding(graph, units=4)
  features = set(k for n in graph.nodes for k in graph.nodes[n].keys())  # pylint: disable=g-complex-comprehension
  node_features = {
      f: tf.convert_to_tensor([graph.nodes[n][f] for n in graph.nodes])
      for f in features
  }
  # If all edges have a non-trivial weight, then we record the weights.
  if all(w):
    edge_features = {'weights': tf.convert_to_tensor(w, dtype=tf.int32)}
    gt = tfgnn.homogeneous(
        tf.convert_to_tensor(s, dtype=tf.int32),
        tf.convert_to_tensor(t, dtype=tf.int32),
        node_features=node_features,
        edge_features=edge_features,
    )
  else:
    gt = tfgnn.homogeneous(
        tf.convert_to_tensor(s, dtype=tf.int32),
        tf.convert_to_tensor(t, dtype=tf.int32),
        node_features=node_features,
    )

  if not node_ids:
    # No node is mentioned in the task description.
    return gt
  node_sets = {
      **gt.node_sets,
      '_readout': tfgnn.NodeSet.from_fields(sizes=[1]),
  }
  if len(node_ids) == 1:
    # Tasks requiring only one node id e.g., computing node degree.
    edge_sets = {
        **gt.edge_sets,
        '_readout/node': tfgnn.EdgeSet.from_fields(
            sizes=[1],
            adjacency=tfgnn.Adjacency.from_indices(
                source=('nodes', node_ids),
                target=('_readout', [0]),
            ),
        ),
    }
  elif len(node_ids) == 2:
    # Tasks requiring two nodes e.g., shortest path from one node to the other.
    edge_sets = {
        **gt.edge_sets,
        '_readout/source': tfgnn.EdgeSet.from_fields(
            sizes=[1],
            adjacency=tfgnn.Adjacency.from_indices(
                source=('nodes', node_ids[:1]),
                target=('_readout', [0]),
            ),
        ),
        '_readout/target': tfgnn.EdgeSet.from_fields(
            sizes=[1],
            adjacency=tfgnn.Adjacency.from_indices(
                source=('nodes', node_ids[1:]),
                target=('_readout', [0]),
            ),
        ),
    }
  else:
    # Raising an error if more than two nodes are mentiones.
    raise ValueError(f'Invalid number of integers: {len(node_ids)}')

  return tfgnn.GraphTensor.from_pieces(
      context=gt.context, node_sets=node_sets, edge_sets=edge_sets
  )


def create_example_feature(
    key: int,
    question: str,
    answer: str,
    algorithm: str,
    encoding_method: str,
    nnodes: str,
    nedges: str,
    task_description: str,
    graph: nx.Graph,
    node_ids: list[int],
) -> example_pb2.Example:
  """Create a tensorflow example from a datapoint."""
  key_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[str(key).encode()])
  )
  question_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[question.encode()])
  )
  answer_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[answer.encode()])
  )
  algorithm_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[algorithm.encode()])
  )
  encoding_method_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[encoding_method.encode()])
  )
  nnodes_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[nnodes.encode()])
  )
  nedges_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[nedges.encode()])
  )
  task_description_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[task_description.encode()])
  )
  gt = to_tfgnn(graph, node_ids)
  graph_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(
          value=[tfgnn.write_example(gt).SerializeToString()]
      )
  )
  directed_feature = feature_pb2.Feature(
      bytes_list=tf.train.BytesList(value=[str(graph.is_directed()).encode()])
  )
  example_feats = tf.train.Features(
      feature={
          'id': key_feature,
          'question': question_feature,
          'answer': answer_feature,
          'algorithm': algorithm_feature,
          'text_encoding': encoding_method_feature,
          'nnodes': nnodes_feature,
          'nedges': nedges_feature,
          'task_description': task_description_feature,
          'graph': graph_feature,
          'directed': directed_feature,
      }
  )
  return example_pb2.Example(features=example_feats)


def load_graphs(
    base_path: str,
    algorithm: str,
    split: str,
    direction: str,
    max_nnodes: int = 20,
) -> list[nx.Graph]:
  """Load a list of graphs from a given algorithm and split."""
  graphs_path = os.path.join(
      base_path,
      direction,
      algorithm,
      split,
  )
  loaded_graphs = []
  all_files = gfile.ListDir(graphs_path)
  for file in all_files:
    if file.endswith('.graphml'):
      path = os.path.join(graphs_path, file)
      graph = nx.read_graphml(os.Open(path, 'rb'), node_type=int)
      if graph.number_of_nodes() <= max_nnodes:
        loaded_graphs.append(graph)
  return loaded_graphs


def prepare_examples(
    examples_dict: dict[int, dict[str, str | list[int]]],
    encoding_method: str,
) -> list[example_pb2.Example]:
  """Create a list of tf.train.Example from a dict of examples."""
  examples = []
  for key, value in examples_dict.items():
    (
        question,
        answer,
        nnodes,
        nedges,
        task_description,
        graph,
        algorithm,
        node_ids,
    ) = (
        value['question'],
        value['answer'],
        value['nnodes'],
        value['nedges'],
        value['task_description'],
        value['graph'],
        value['algorithm'],
        value['node_ids'],
    )
    examples.append(
        create_example_feature(
            key,
            question,
            answer,
            algorithm,
            encoding_method,
            nnodes,
            nedges,
            task_description,
            graph,
            node_ids,
        )
    )
  return examples


def create_zero_shot_task(
    task: graph_tasks.GraphTask,
    graphs: list[nx.Graph],
    generator_algorithms: list[str],
    text_encoders: list[str],
    cot: bool = False,
) -> list[example_pb2.Example]:
  """Create a recordio file with zero-shot examples for the task."""
  examples = []
  for encoding_method in text_encoders:
    examples_dict = task.prepare_examples_dict(
        graphs, generator_algorithms, encoding_method
    )
    if cot:
      for key in examples_dict.keys():
        examples_dict[key]['question'] += "Let's think step by step. "
    examples += prepare_examples(examples_dict, encoding_method)
  return examples


def write_examples(examples: list[example_pb2.Example], output_path: str):
  with recordio.RecordWriter(output_path) as output_file:
    for example in examples:
      output_file.WriteRecord(example.SerializeToString())


def prepare_few_shots(
    task: graph_tasks.GraphTask,
    graphs: list[nx.Graph],
    text_encoders: list[str],
    cot: bool,
) -> dict[str, list[str]]:
  """Create a dict of few-shot examples with their cot for the task."""
  few_shots_examples_dict = {}
  for encoding_method in text_encoders:
    if encoding_method not in few_shots_examples_dict:
      few_shots_examples_dict[(encoding_method)] = []
    for graph in graphs:
      few_shots_examples_dict[(encoding_method)].append(
          task.create_few_shot_example(graph, encoding_method, cot)
      )
  return few_shots_examples_dict


def choose_few_shot_examples(
    few_shots_dict: dict[str, list[str]],
    encoding_method: str,
    k: int = 2,
) -> str:
  """Choose few shot examples for each algorithm."""
  few_shots_str = ''
  for _ in range(k):
    example_list = few_shots_dict[encoding_method]
    few_shots_str += 'Example: ' + random.choice(example_list) + '\n'
  return few_shots_str


def create_few_shot_task(
    task: graph_tasks.GraphTask,
    graphs: list[nx.Graph],
    generator_algorithms: list[str],
    few_shots_graphs: list[nx.Graph],
    text_encoders: list[str],
    cot: bool,
    bag: bool,
    random_seed: int,
) -> list[example_pb2.Example]:
  """Create a recordio file with few-shot examples for the task."""
  # LINT.IfChange
  vocab_path = None
  # LINT.ThenChange(//research/graph/llm/graphqa/copy.bara.sky)
  # Loading the palm tokenizer to calculate number of tokens in the sequence.
  sp_vocab = seqio.SentencePieceVocabulary(vocab_path)
  number_of_tokens = {}
  examples = []
  print('prepare few shot task', 'cot', cot, 'bag', bag)
  few_shots_examples_dict = prepare_few_shots(
      task,
      few_shots_graphs,
      text_encoders,
      cot,
  )
  for encoding_method in text_encoders:
    random.seed(random_seed)
    examples_dict = task.prepare_examples_dict(
        graphs, generator_algorithms, encoding_method
    )
    for key in examples_dict.keys():
      few_shots_examples = choose_few_shot_examples(
          few_shots_examples_dict,
          encoding_method,
      )
      examples_dict[key]['question'] = (
          few_shots_examples + 'Example: ' + examples_dict[key]['question']
      )
      if bag:
        examples_dict[key]['question'] = examples_dict[key]['question'].replace(
            '\nQ: ',
            "\nLet's construct the graph with the nodes and edges first.\nQ: ",
        )  # pytype: disable=attribute-error
      if encoding_method not in number_of_tokens:
        number_of_tokens[encoding_method] = []
      number_of_tokens[encoding_method].append(
          len(sp_vocab.encode(examples_dict[key]['question']))
      )
    examples += prepare_examples(examples_dict, encoding_method)

  # Printing maximum number of tokens in the sequence.
  for key, value in number_of_tokens.items():
    print(key, np.max(value))

  return examples


================================================
FILE: talk_like_a_graph/graph_text_encoders.py
================================================
"""Library for encoding graphs in text."""

import networkx as nx

from . import name_dictionaries


def create_node_string(name_dict, nnodes: int) -> str:
  node_string = ""
  sorted_keys = list(sorted(name_dict.keys()))
  for i in sorted_keys[: nnodes - 1]:
    node_string += name_dict[i] + ", "
  node_string += "and " + name_dict[sorted_keys[nnodes - 1]]
  return node_string


def nx_encoder(graph: nx.Graph, _: dict[int, str], edge_type="id") -> str:
  """Encoding a graph as entries of an adjacency matrix."""
  if graph.is_directed():
    output = (
        "In a directed graph, (s,p,o) means that there is an edge from node s"
        " to node o of type p. "
    )
  else:
    output = (
        "In an undirected graph, (s,p,o) means that node s and node o are"
        " connected with an undirected edge of type p. "
    )

  name_dict = {x: str(x) for x in graph.nodes()}

  nodes_string = create_node_string(name_dict, nnodes=len(graph.nodes()))
  output += "G describes a graph among nodes %s.\n" % nodes_string
  if graph.edges():
    output += "The edges in G are: "
  for i, j in graph.edges():
    edge_type = graph.get_edge_data(i, j)[edge_type]
    if edge_type is None:
      edge_type = "linked"
    output += "(%s, %s, %s) " % (name_dict[i], edge_type, name_dict[j])
  return output.strip() + ".\n"


def adjacency_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  """Encoding a graph as entries of an adjacency matrix."""
  if graph.is_directed():
    output = (
        "In a directed graph, (i,j) means that there is an edge from node i to"
        " node j. "
    )
  else:
    output = (
        "In an undirected graph, (i,j) means that node i and node j are"
        " connected with an undirected edge. "
    )
  nodes_string = create_node_string(name_dict, len(graph.nodes()))
  output += "G describes a graph among nodes %s.\n" % nodes_string
  if graph.edges():
    output += "The edges in G are: "
  for i, j in graph.edges():
    output += "(%s, %s) " % (name_dict[i], name_dict[j])
  return output.strip() + ".\n"


def friendship_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  """Encoding a graph as a friendship graph."""
  if graph.is_directed():
    raise ValueError("Friendship encoder is not defined for directed graphs.")
  nodes_string = create_node_string(name_dict, len(graph.nodes()))
  output = (
      "G describes a friendship graph among nodes %s.\n" % nodes_string.strip()
  )
  if graph.edges():
    output += "We have the following edges in G:\n"
  for i, j in graph.edges():
    output += "%s and %s are friends.\n" % (name_dict[i], name_dict[j])
  return output


def coauthorship_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  """Encoding a graph as a coauthorship graph."""
  if graph.is_directed():
    raise ValueError("Coauthorship encoder is not defined for directed graphs.")
  nodes_string = create_node_string(name_dict, len(graph.nodes()))
  output = (
      "G describes a coauthorship graph among nodes %s.\n"
      % nodes_string.strip()
  )
  if graph.edges():
    output += "In this coauthorship graph:\n"
  for i, j in graph.edges():
    output += "%s and %s wrote a paper together.\n" % (
        name_dict[i],
        name_dict[j],
    )
  return output.strip() + ".\n"


def incident_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  """Encoding a graph with its incident lists."""
  nodes_string = create_node_string(name_dict, len(graph.nodes()))
  output = "G describes a graph among nodes %s.\n" % nodes_string
  if graph.edges():
    output += "In this graph:\n"
  for source_node in graph.nodes():
    target_nodes = graph.neighbors(source_node)
    target_nodes_str = ""
    nedges = 0
    for target_node in target_nodes:
      target_nodes_str += name_dict[target_node] + ", "
      nedges += 1
    if nedges > 1:
      output += "Node %s is connected to nodes %s.\n" % (
          source_node,
          target_nodes_str[:-2],
      )
    elif nedges == 1:
      output += "Node %d is connected to node %s.\n" % (
          source_node,
          target_nodes_str[:-2],
      )
  return output


def social_network_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  """Encoding a graph as a social network graph."""
  if graph.is_directed():
    raise ValueError(
        "Social network encoder is not defined for directed graphs."
    )
  nodes_string = create_node_string(name_dict, len(graph.nodes()))
  output = (
      "G describes a social network graph among nodes %s.\n"
      % nodes_string.strip()
  )
  if graph.edges():
    output += "We have the following edges in G:\n"
  for i, j in graph.edges():
    output += "%s and %s are connected.\n" % (name_dict[i], name_dict[j])
  return output


def expert_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  nodes_string = create_node_string(name_dict, len(graph.nodes()))
  output = (
      "You are a graph analyst and you have been given a graph G among nodes"
      " %s.\n"
      % nodes_string.strip()
  )
  output += "G has the following undirected edges:\n" if graph.edges() else ""
  for i, j in graph.edges():
    output += "%s -> %s\n" % (name_dict[i], name_dict[j])
  return output


def nodes_to_text(graph, encoding_type):
  """Get dictionary converting node ids to text."""
  if encoding_type == "integer":
    return name_dictionaries.create_name_dict(graph, "integer", nnodes=1000)
  elif encoding_type == "popular":
    return name_dictionaries.create_name_dict(graph, "popular")
  elif encoding_type == "alphabet":
    return name_dictionaries.create_name_dict(graph, "alphabet")
  elif encoding_type == "got":
    return name_dictionaries.create_name_dict(graph, "got")
  elif encoding_type == "south_park":
    return name_dictionaries.create_name_dict(graph, "south_park")
  elif encoding_type == "politician":
    return name_dictionaries.create_name_dict(graph, "politician")
  elif encoding_type == "random":
    return name_dictionaries.create_name_dict(
        graph, "random_integer", nnodes=1000
    )
  elif encoding_type == "nx_node_name":
    return name_dictionaries.create_name_dict(graph, "nx_node_name")
  else:
    raise ValueError("Unknown encoding type: %s" % encoding_type)


def get_tlag_node_encoder(graph, encoder_name):
  """Find the node encoder used in the 'Talk Like a Graph' paper."""
  if encoder_name == "adjacency":
    return nodes_to_text(graph, "integer")
  elif encoder_name == "incident":
    return nodes_to_text(graph, "integer")
  elif encoder_name == "friendship":
    return nodes_to_text(graph, "popular")
  elif encoder_name == "south_park":
    return nodes_to_text(graph, "south_park")
  elif encoder_name == "got":
    return nodes_to_text(graph, "got")
  elif encoder_name == "politician":
    return nodes_to_text(graph, "politician")
  elif encoder_name == "social_network":
    return nodes_to_text(graph, "popular")
  elif encoder_name == "expert":
    return nodes_to_text(graph, "expert")
  elif encoder_name == "coauthorship":
    return nodes_to_text(graph, "popular")
  elif encoder_name == "random":
    return nodes_to_text(graph, "random")
  elif encoder_name == "nx_node_name":
    return nodes_to_text(graph, "nx_node_name")
  else:
    raise ValueError("Unknown graph encoder strategy: %s" % encoder_name)


# A dictionary from edge encoder name to the corresponding function.
EDGE_ENCODER_FN = {
    "adjacency": adjacency_encoder,
    "incident": incident_encoder,
    "friendship": friendship_encoder,
    "south_park": friendship_encoder,
    "got": friendship_encoder,
    "politician": social_network_encoder,
    "social_network": social_network_encoder,
    "expert": expert_encoder,
    "coauthorship": coauthorship_encoder,
    "random": adjacency_encoder,
    "nx_edge_encoder": nx_encoder,
}


def with_ids(graph: nx.Graph, node_encoder: str) -> nx.Graph:
  nx.set_node_attributes(graph, nodes_to_text(graph, node_encoder), name="id")
  return graph


def encode_graph(
    graph: nx.Graph, graph_encoder=None, node_encoder=None, edge_encoder=None
) -> str:
  r"""Encodes a graph as text.

  This relies on choosing:
     a node_encoder and an edge_encoder:
     or
     a graph_encoder (a predefined pair of node and edge encoding strategies).

  Note that graph_encoders may assume that the graph has some properties
  (e.g. integer keys).

  Example usage:
  .. code-block:: python
  ```
  # Use a predefined graph encoder from the paper.
  >>> G = nx.karate_club_graph()
  >>> encode_graph(G, graph_encoder="adjacency")
  'In an undirected graph, (i,j) means that node i and node j are
  connected
  with an undirected edge. G describes a graph among nodes 0, 1, 2, 3, 4, 5,
  6,
  7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26,
  27, 28, 29, 30, 31, 32, and 33.\nThe edges in G are: (0, 1) (0, 2) (0, 3)
  ...'

  # Use the node's name in the graph as the node identifier.
  >>> G = nx.les_miserables_graph()
  >>> encode_graph(G, node_encoder="nx_node_name", edge_encoder="friendship")
  'G describes a friendship graph among nodes Anzelma, Babet, Bahorel,
  Bamatabois, BaronessT, Blacheville, Bossuet, Boulatruelle, Brevet, ...
  We have the following edges in G:
  Napoleon and Myriel are friends. Myriel and MlleBaptistine are friends...'

  # Use the `id` feature from the edges to describe the edge type.
  >>> G = nx.karate_club_graph()
  >>> encode_graph(G, node_encoder="nx_node_name", edge_encoder="nx_edge_id")
  'In an undirected graph, (s,p,o) means that node s and node o are connected
  with an undirected edge of type p. G describes a graph among nodes 0, 1, 2, 3,
  4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
  25, 26, 27, 28, 29, 30, 31, 32, and 33.
  The edges in G are: (0, linked, 1) (0, linked, 2) (0, linked, 3) ...'
  ```

  Args:
    graph: the graph to be encoded.
    graph_encoder: the name of the graph encoder to use.
    node_encoder: the name of the node encoder to use.
    edge_encoder: the name of the edge encoder to use.

  Returns:
    The encoded graph as a string.
  """

  # Check that only one of graph_encoder or (node_encoder, edge_encoder) is set.
  if graph_encoder and (node_encoder or edge_encoder):
    raise ValueError(
        "Only one of graph_encoder or (node_encoder, edge_encoder) can be set."
    )

  if graph_encoder:
    if isinstance(graph_encoder, str):
      node_encoder_dict = get_tlag_node_encoder(graph, graph_encoder)
      return EDGE_ENCODER_FN[graph_encoder](graph, node_encoder_dict)
    else:
      return graph_encoder(graph)

  else:
    node_encoder_dict = nodes_to_text(graph, node_encoder)
    return EDGE_ENCODER_FN[edge_encoder](graph, node_encoder_dict)


================================================
FILE: talk_like_a_graph/graph_text_encoders_test.py
================================================
"""Testing for graph_text_encoders.py."""

from absl.testing import parameterized
import networkx as nx

from . import graph_text_encoders
from absl.testing import absltest

_G = nx.Graph()
_G.add_node(0)
_G.add_node(1)
_G.add_node(2)
_G.add_node(3)
_G.add_edge(0, 1)
_G.add_edge(1, 2)
_G.add_edge(2, 3)
_G.add_edge(3, 0)


class GraphTextEncodersTest(absltest.TestCase, parameterized.TestCase):

  @parameterized.named_parameters(
      dict(
          testcase_name='adjacency_integer',
          encoding_method='adjacency',
          expected_result=(
              'In an undirected graph, (i,j) means that node i and node j are'
              ' connected with an undirected edge. G describes a graph among'
              ' nodes 0, 1, 2, and 3.\nThe edges in G are: (0, 1) (0, 3) (1, 2)'
              ' (2, 3).\n'
          ),
      ),
      dict(
          testcase_name='incident_integer',
          encoding_method='incident',
          expected_result=(
              'G describes a graph among nodes 0, 1, 2, and 3.\nIn this'
              ' graph:\nNode 0 is connected to nodes 1, 3.\nNode 1 is connected'
              ' to nodes 0, 2.\nNode 2 is connected to nodes 1, 3.\nNode 3 is'
              ' connected to nodes 2, 0.\n'
          ),
      ),
      dict(
          testcase_name='friendship_per_line_popular',
          encoding_method='friendship',
          expected_result=(
              'G describes a friendship graph among nodes James, Robert, John,'
              ' and Michael.\nWe have the following edges in G:\nJames and'
              ' Robert are friends.\nJames and Michael are friends.\nRobert and'
              ' John are friends.\nJohn and Michael are friends.\n'
          ),
      ),
      dict(
          testcase_name='social_network_politician',
          encoding_method='politician',
          expected_result=(
              'G describes a social network graph among nodes Barack, Jimmy,'
              ' Arnold, and Bernie.\nWe have the following edges in'
              ' G:\nBarack and Jimmy are connected.\nBarack and Bernie are'
              ' connected.\nJimmy and Arnold are connected.\nArnold and'
              ' Bernie are connected.\n'
          ),
      ),
  )
  def test_encoders(self, encoding_method, expected_result):
    self.assertEqual(
        graph_text_encoders.encode_graph(_G, encoding_method),
        expected_result,
    )


if __name__ == '__main__':
  googletest.main()


================================================
FILE: talk_like_a_graph/name_dictionaries.py
================================================
"""Creates a dictionary mapping integers to node names."""

import random

_RANDOM_SEED = 1234
random.seed(_RANDOM_SEED)

_INTEGER_NAMES = [str(x) for x in range(10000)]

_POPULAR_NAMES = [
    "James",
    "Robert",
    "John",
    "Michael",
    "David",
    "Mary",
    "Patricia",
    "Jennifer",
    "Linda",
    "Elizabeth",
    "William",
    "Richard",
    "Joseph",
    "Thomas",
    "Christopher",
    "Barbara",
    "Susan",
    "Jessica",
    "Sarah",
    "Karen",
    "Daniel",
    "Lisa",
    "Matthew",
    "Nancy",
    "Anthony",
    "Betty",
    "Mark",
    "Margaret",
    "Donald",
    "Sandra",
    "Steven",
    "Ashley",
    "Paul",
    "Kimberly",
    "Andrew",
    "Emily",
    "Joshua",
    "Donna",
    "Kenneth",
    "Michelle",
    "Kevin",
    "Carol",
    "Brian",
    "Amanda",
    "George",
    "Melissa",
    "Edward",
    "Deborah",
    "Ronald",
    "Stephanie",
    "Timothy",
    "Rebecca",
    "Jason",
    "Sharon",
    "Jeffrey",
    "Laura",
    "Ryan",
    "Cynthia",
    "Jacob",
    "Dorothy",
    "Gary",
    "Olivia",
    "Nicholas",
    "Emma",
    "Eric",
    "Sophia",
    "Jonathan",
    "Ava",
    "Stephen",
    "Isabella",
    "Scott",
    "Mia",
    "Justin",
    "Abigail",
    "Brandon",
    "Madison",
    "Frank",
    "Chloe",
    "Benjamin",
    "Victoria",
    "Samuel",
    "Lauren",
    "Gregory",
    "Hannah",
    "Alexander",
    "Grace",
    "Frank",
    "Alexis",
    "Raymond",
    "Alice",
    "Patrick",
    "Samantha",
    "Jack",
    "Natalie",
    "Dennis",
    "Anna",
    "Jerry",
    "Taylor",
    "Tyler",
    "Kayla",
    "Henry",
    "Hailey",
    "Douglas",
    "Jasmine",
    "Peter",
    "Nicole",
    "Adam",
    "Amy",
    "Nathan",
    "Christina",
    "Zachary",
    "Andrea",
    "Jose",
    "Leah",
    "Walter",
    "Angelina",
    "Harold",
    "Valerie",
    "Kyle",
    "Veronica",
    "Ethan",
    "Carl",
    "Arthur",
    "Roger",
    "Noah",
]


_SOUTH_PARK_NAMES = [
    "Eric",
    "Kenny",
    "Kyle",
    "Stan",
    "Tolkien",
    "Heidi",
    "Bebe",
    "Liane",
    "Sharon",
    "Linda",
    "Gerald",
    "Veronica",
    "Michael",
    "Jimbo",
    "Herbert",
    "Malcolm",
    "Gary",
    "Steve",
    "Chris",
    "Wendy",
]

_GOT_NAMES = [
    "Ned",
    "Cat",
    "Daenerys",
    "Jon",
    "Bran",
    "Sansa",
    "Arya",
    "Cersei",
    "Jaime",
    "Petyr",
    "Robert",
    "Jorah",
    "Viserys",
    "Joffrey",
    "Maester",
    "Theon",
    "Rodrik",
    "Lysa",
    "Stannis",
    "Osha",
]


_POLITICIAN_NAMES = [
    "Barack",
    "Jimmy",
    "Arnold",
    "Bernie",
    "Bill",
    "Kamala",
    "Hillary",
    "Elizabeth",
    "John",
    "Ben",
    "Joe",
    "Alexandria",
    "George",
    "Nancy",
    "Pete",
    "Madeleine",
    "Elijah",
    "Gabrielle",
    "Al",
]


_ALPHABET_NAMES = [
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "J",
    "K",
    "L",
    "M",
    "N",
    "O",
    "P",
    "Q",
    "R",
    "S",
    "T",
    "U",
    "V",
    "W",
    "X",
    "Y",
    "Z",
    "AA",
    "BB",
    "CC",
    "DD",
    "EE",
    "FF",
    "GG",
    "HH",
    "II",
    "JJ",
    "KK",
    "LL",
    "MM",
    "NN",
    "OO",
    "PP",
    "QQ",
    "RR",
    "SS",
    "TT",
    "UU",
    "VV",
    "WW",
    "XX",
    "YY",
    "ZZ",
]


def create_name_dict(graph, name: str, nnodes: int = 20) -> dict[int, str]:
  """The runner function to map integers to node names.

  Args:
    graph: the graph to be encoded.
    name: name of the approach for mapping.
    nnodes: optionally provide nnodes in the graph to be encoded.

  Returns:
    A dictionary from integers to strings.
  """
  if name == "alphabet":
    names_list = _ALPHABET_NAMES
  elif name == "integer":
    names_list = _INTEGER_NAMES
  elif name == "random_integer":
    names_list = []
    for _ in range(nnodes):
      names_list.append(str(random.randint(0, 1000000)))
  elif name == "popular":
    names_list = _POPULAR_NAMES
  elif name == "south_park":
    names_list = _SOUTH_PARK_NAMES
  elif name == "got":
    names_list = _GOT_NAMES
  elif name == "politician":
    names_list = _POLITICIAN_NAMES
  elif name == "nx_node_name":
    return {x: str(x) for x in graph.nodes()}
  else:
    raise ValueError(f"Unknown approach: {name}")
  name_dict = {}
  for ind, value in enumerate(names_list):
    name_dict[ind] = value

  return name_dict


================================================
FILE: tutorial/KDD-Tutorial-1-Talk-Like-a-Graph.ipynb
================================================
{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "EMeYjNCilDE_"
      },
      "source": [
        "This is a noteboook that illustrates how to encode structured data for use in Large Language Models.  It is Part 1 of a two part tutorial from **KDD'24**.\n",
        "\n",
        "If you find this tutorial useful or want to know more, please consider our publication:  \n",
        "[**Talk like a Graph: Encoding Graphs for Large Language Models**](https://openreview.net/pdf?id=IuXR1CCrSi)\n",
        "\n",
        "```\n",
        "@inproceedings{\n",
        "fatemi2024talk,\n",
        "  title={Talk like a Graph: Encoding Graphs for Large Language Models},\n",
        "  author={Bahare Fatemi and Jonathan Halcrow and Bryan Perozzi},\n",
        "  booktitle={The Twelfth International Conference on Learning Representations},\n",
        "  year={2024},\n",
        "  url={https://openreview.net/forum?id=IuXR1CCrSi}\n",
        "}\n",
        "```\n",
        "\n",
        "\n",
        "\n",
        "\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "5Pt0yWFcnIdy"
      },
      "source": [
        "## Tutorial Part I:  Text Encoding of Graph Information\n",
        "\n",
        "This first notebook focuses on using text serialization methods from **Talk Like a Graph** to perform graph reasoning tasks with off-the-shelf LLMs.  This notebook focuses on using static **Gemini** models (which have free API quota), but the techniques (and code) here are generally applicable to any LLM.\n",
        "\n",
        "\n",
        "Notebook Outline:\n",
        "\n",
        "1.   Setup (Install Dependencies, get Google Cloud API key)\n",
        "1.   Dataset creation\n",
        "1.   Graph-to-Text conversion\n",
        "1.   Evaluation\n",
        "1.   Exercise:  *Graph Encoding Challenge*\n",
        "1.   Exercise: DBLP Dataset\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "SAgPqF6SANGs"
      },
      "source": [
        "# Setup"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {
        "id": "N6cnEIIlXhCj"
      },
      "source": [
        "## Get Google AI Studio Key\n",
        "\n",
        "Go to [Google AI Studio](https://aistudio.google.com/app/u/1/apikey) and click on the `Create API Key` button.  Follow prompts in dialog to setup an API key with a new Google Cloud Project.\n",
        "\n",
        "Then add the API key to your colab secrets (using the side panel 🔑) as in the picture below:\n",
        "\n",
        "![colab_secrets.png](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAwwAAALmCAYAAADv1xMrAAAAAXNSR0IArs4c6QAAIABJREFUeF7snQWUXUX29Su4Q3AnBHfJ4O4+uLsPZHC34O4SGJzBfXCXBCc4DAT4A8E1WHDNt34132mqb19/93a/fr3PWlmEvHvrVu2ys49U9Ro5cuRIJxECQkAICAEhIASEgBAQAkKgxyPwf//3f26WWWZph0MvEYYePy4EgBAQAkJACAgBISAEhIAQ8AiIMGggCAEhIASEgBAQAkJACAgBIZCIgAiDBocQEAJCQAgIASEgBISAEBACIgwaA0JACAgBISAEhIAQEAJCQAgUR0AehuKY6Q0hIASEgBAQAkJACAgBIdBjEBBh6DFdrYYKASEgBISAEBACQkAICIHiCIgwFMdMbwgBISAEhIAQEAJCQAgIgR6DgAhDj+lqNVQICAEhIASEgBAQAkJACBRHQIShOGZ6QwgIASEgBISAEBACQkAI9BgERBh6TFeroUJACAgBISAEhIAQEAJCoDgCIgzFMdMbQkAICAEhIASEgBAQAkKgxyAgwtBjuloNFQJCQAgIASEgBISAEBACxREQYSiOmd4QAkJACAgBISAEhIAQEAI9BgERhh7T1WqoEBACQkAICAEhIASEgBAojoAIQ3HM9IYQEAJCQAgIASEgBISAEOgxCIgw9JiuVkOFgBAQAkJACAgBISAEhEBxBEQYimOmN4SAEBACQkAICAEhIASEQI9BQIShx3S1GioEhIAQEAJCQAgIASEgBIojIMJQHDO9IQSEgBAQAkJACAgBISAEegwCIgw9pqvVUCEgBISAEBACQkAICAEhUBwBEYbimOkNISAEhIAQEAJCQAgIASHQYxAQYegxXa2GCgEhIASEgBAQAkJACAiB4giIMBTHTG8IASEgBISAEBACQkAICIEeg4AIQ4/pajVUCAgBISAEhIAQEALNj8DIkSPd3Xff7QYNGuSGDx9ee4V79erlJp98crfaaqu5pZdeuvbvdccPiDB0x15TnYWAEBACQkAICAEh0KII3HHHHe7mm2/uktZtv/32boklluiSb9tHP/vsM09gIDKNyKeffuqmnHLKRopoe1eEoRIYVYgQEAJCQAgIASEgBIRAFQjsueeebsSIEVUUVbiMPn36uAEDBhR+r6oX3nnnHXfqqae6fv36uW233bY0aXj11Vfd2Wef7ZZffnm30UYbNVw9EYaGIVQBQkAICAEhIASEgBAQAlUg8Msvv7hddtmlraj+/fu7SSaZpHDRTz75pLv//vsLvzfuuON6Rbsr5P3333cnnnii++mnn/znl1xyyVKkwcjCr7/+6stZc8013XrrrddQk0QYGoJPLwsBISAEhIAQEAJCQAhUhUCUMBx77LFuqqmmKlz8Pffc466//vrC74099thu4MCBhd+r4gWIwimnnOKGDRvWVlxR0hAlC2OMMYbbbbfd3FxzzdVQFUUYGoJPLwsBISAEhIAQEAJCQAhUhUBPJgxg2AhpqIssUK/aCcPgwYMdLI9YtDnmmMNtvvnmrnfv3lWNK5UjBISAEBACQkAICAEh0CII9HTCkEQaSMTebrvtEnMa6iQLtROGRx991F166aXthjBupcMPP9zhIkE+//xz9/rrr1cyzMcZZxz3t7/9rZKyVIgQEAJCQAgIASEgBIRA5yIgwvA/vOM8DUmkoW6yUDthIMv8ww8/7DDS/vnPf7oFF1zQ/ztJKRdeeGElo5Gjo4477rhKylIhQkAICAEhIASEgBAQAp2LgAjDX3jnIQ2dQRZqJwz77LOP+/rrrzuMNI6JWmqppUQYOncO6mtCQAgIASEgBISAEGhqBKoiDCjSQ4YMydXWb7/91r388sv+2a5Meo6rbBppeO211/yJTnYaUlUJznH1qDWH4aKLLnJPPPFEu++OOuqojox3LqRA6KCbbropV4dmPTTZZJM5vBddKXTaAw884B5//HHHxRuESfXt29cfaTXzzDN3ZdX0bSEgBISAEBACQkAINDUCVRGGIo0kNP6kk05qSsJApeJIwzzzzOPeeOONTiEL1KFWwvDdd9+50047zb333nu+E0YbbTSf9LzMMssU6cdu8+x///tft/vuu7t33323Q51HGWUUt80227iDDz7Y8XeJEBACQkAICAEhIASEQHsERBjiR0QcabAn6/Qs2DdqJQx85M8///RJzZAHLOxlLt/oDpOJyzbWWWcd98033/jqcp03bf3xxx/9H5Mtt9zSHXnkkd2hSQ3XEU/S0KFDfTlXXnllw+WpACEgBISAEBACQqC1ERBhSO5fSAO5uh999FG7h/bee28399xz1zowaicMtda+iQqns2655RZfo5VWWsmfBDX11FO7P/74w919993uwAMPbCMOhGAtsMACTVT7eqqy6aabuqefftoXzlXnEiEgBISAEBACQkAIpCEgwpCMDpEs5Cz89ttv7R7KOnK1ihEnwlAFis75U5/wLpBH8dhjj7nRRx+9Xcm33nqrv71viy22cJtssombcMIJK/py8xYjwtC8faOaCQEhIASEgBBoRgREGOJ7JYks2NN1k4baCQMhSXyEkKSZZpqpJS9to42zzDKLGzlypJt99tndXXfd1aG3eQbJyl/48ssv3fPPP+/dTRNMMIGbccYZc3sjvv/+e/fMM8/4d8kXmWaaafy9FGT8h0I9cWshxL3x7M8//+yJDona3JWx/PLLd2jDCy+84N5++233ww8/eO9Jv3793MQTT9zhOQvBghgxwBH7L38fa6yxOuAwfPhw99RTT/l7OcYcc0w366yzehJGkrxECAgBISAEhIAQ6BkIiDB07GdOfDrrrLPaPAsYpXfYYQd/MfKwYcPaXqiTNNRKGFBgTz/99LbG0EBi+JdccsmWG/XLLbecT+6GEHA61LLLLluojSjwxx9/vLvhhhsckyUUyAh3WjAQ4oTnTz75ZHfttde2y5fg2YkmmshxjC2nR5FXgXA3xtJLL+3/TijVfPPN55Oxw5i4MISIE5+OPvpo9+abb7b7PGRjww039O8aKaEu3OidJnhbyO5HeJ6yr7vuOh++FQr3atDuVVddtRCWelgICAEhIASEgBDongiIMLTvtziysNtuu/mchTz3NFQ1CmolDBdffLE/XjQUrNnHHHNM27Gqr7zyivvPf/5TSXtIMu7fv38lZRUt5Oqrr3aHHnqofw3SsMEGG3hFfbbZZsssCmJFqJKdAWyKPgPByAOW9lNPPdX9/e9/b1cenhtOngot+HEfXGuttdyZZ57pfwoJw2qrreaPgY3GwxlhoG/233//NmUe0jfeeOO1u1+DQYvCD2koShh22WUXd++997bhxnG7I0aMaCM+kBzqzbG0EiEgBISAEBACQqC1ERBh+Kt/08iCPdVZpKFWwtDTLm5Dsb7xxhvbzWSs7eutt563xBNiFCf77bdf210UKPYHHHBAW8I0CvtRRx3lIBXjjjuudz8RamSC58BCoKabbjq31157+TAkJtygQYO8h8dChEiUWWONNdoRBisHIsK3UdAJidp33319+BGKOmXhqYDorbLKKj5MCG/EYYcd5r+BbL311j7Rm9Cr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lQiIMFSJpsoSAkJACAgBISAEhIAQEAIthoAIQ4t1qJojBISAEBACQkAICAEhIASqRECEoUo0VZYQEAJCQAgIASEgBISAEGgxBEQYWqxD1RwhIASEgBAQAkJACAgBIVAlAiIMVaKpsoSAEBACQkAICAEhIASEQIshIMLQYh2q5ggBISAEhIAQEAJCQAgIgSoREGGoEk2VJQSEgBAQAkJACAgBISAEWgwBEYYW61A1RwgIASEgBISAEBACQkAIVImACEOVaKosISAEhIAQEAJCQAgIASHQYgiIMLRYh6o5QkAICAEhIASEgBAQAkKgSgREGKpEU2UJASEgBISAEBACQkAICIEWQ0CEocU6VM0RAkJACAgBISAEhIAQEAJVIiDCUCWaKksICAEhIASEgBAQAkJACLQYAiIMLdahao4QEAJCQAgIASEgBISAEKgSARGGKtFUWUJACAgBISAEhIAQEAJCoMUQEGFosQ5Vc4SAEBACQkAICAEhIASEQJUIiDBUiabKEgJCQAgIASEgBISAEBACLYaACEOLdaiaIwSEgBAQAkJACAgBISAEqkSgywjDp59+6l5//XX3zTffuB9//NGNO+64buKJJ3Zzzjmnm2SSSapso8oSAkJACAgBISAEhIAQEAJCoCQCnUoYRo4c6Z599ll36623uo8//jixyn369HHrrruum2eeeUo2S68JASEgBISAEBACQkAICAEhUAUCnUYYfvjhB3feeee51157LXe9F154Ybfddtu5McYYI/c7elAICAEhIASEgBAQAkJACAiB6hDoFMLw7bffuhNOOMF99tln7Wreu3dvN8MMM7hxxhnHff/99+7dd991I0aMaPfMjDPO6Pbbbz831lhjVddqlSQEhIAQEAJCQAgIASEgBIRALgRqJwy//fabJwvDhg1rq1Dfvn3dRhtt5GadddZ2lSRk6ZVXXnHXX399u5Cl+eef3+22226uV69euRqlh4SAEBACQkAICAEhIASEgBCoBoHaCcN//vMfd/vtt7fVdplllnFbbrmlG2WUURJb8Ouvv7rzzz/fvfDCC23PbL311o53m10Ivfruu+8c3pMxxxyzy6tLrsipp57qBg8e7BPMJ510UgcBow+WWGKJLq+fKtB8CBxyyCGeuB999NFuvvnmq6yCb775ptt3333dLLPM4sekxLmvvvrKTTDBBG600UYTHAECGI+++OILN/nkkwuXHoRAFfNhgw02cOgQGB7zRCaQV3nUUUf5fZH/NqNQx7POOss9//zzvm1TTz21W3TRRd0uu+ziozRMfv75Z2+MZT25+eabm7Eppeuk/aM0dJW9WCthQHHef//93S+//OIrXMRT8Pvvv3vPxDvvvOPfnWiiidyJJ57oRh999MoaX1VBX3/9tbvkkkvcDTfc4D7//HNf7Kijjur69evn+vfv75ZaaqmqPlWonC+//NKtueaaPhSMhXO66abzoV+cUHXAAQe4nXbaqVB5erhnILDpppu6p59+2l1xxRWVkkoMAOuvv76be+653W233dYzwExpJSSeHC1yta655poej0cIwMknn+xz3vbYYw//R9L6CFQ1H2affXavVP/3v//14c5Z8tBDD7kddtjBLb744u7KK6/MerzTf3/qqae8ge+PP/5wE044oZtiiik8mUbvuOWWW9y8887bVidOnGR9Rf9AuWsl0f7R9b1ZK2EYNGiQu/zyy30rSVxG4WfA55UPPvjAHXHEEQ5rE7Lnnnu2mxx5y6nzOUKttt12W/f+++97r8IiiyziPQsAawneBx54YJco56eccoo799xz3d/+9jd34YUXtmEPgSC8S9a7OkdG9y27JxMGrFh4PwibhFTXKffff7/beeed/ZrGxi/5C4FjjjnGG2EwarB+SsojwBhDBg4c2NSerKrmQ6sRBjwmeBbQMw4++GBPBpA33njDr1OhEbUVCEPSeBVhKL8GVPVmrYQBF9qLL77o67rYYou5HXfcsXC9TzrpJH9fA7L88su7LbbYonAZdb1AfgYWfEBkUh955JFu7LHHbvvcPffc461jeEvwPiy44IJ1VSW23M0339w9+eSTnjSsuuqqnfptfaz7ItCTCQPWvM0228x7QzvDpY9RhDDBcN3oviOnuppjTcUIQ7hFWvhqdV9s3ZJQKpGhQ4c2RZhsGtJVzIdWIgx//vmnm2222bx34bnnnvNGyTRpBcKQNF5FGLp+jaqVMBAL/cknn/hW4npfcsklC7eY0AWzvuFq23vvvQuXUdcLl156qY/zRrm48cYbYze2448/3lv3V1llFe9iTxJIRdE4ZhYRNtOkZPC11167LYkcL0NRoXyzZhR9N+/zfIP6xykFLJZIZykMWXjmbVPac3jLaFcRXHme9/K+U7Qd0T7ISxiK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9EuvXr1yFZdVzzyEIWsORCuSNX6KzL9cjdRDQkAICAEhIAS6IQIiDA10GgoMytA111zjzjjjDHfLLbe44cOHu/HGG8+tvvrq7rDDDnPjjjtuhy8QDnPZZZe5oUOHul9++cVNN910Pixmp512cmOPPbZ//s4773RnnXWW++mnn9yHH37oxhprLP8cSii/hXL33Xe7Cy64wJeH0jXbbLM5lPBoqI0RhvPOO8+9/vrrPhb8k08+8XH6K664ojvwwAPdlFNO2aG+n376qTv33HPdvffe67744gtfR5KT//nPf7oFF1wwFsEXX3zR8R2s6CNGjHATTzyxW2GFFdyee+7ppppqqnbvJBGGJ554wh111FFujDHGcKeddpqbeeaZ/XsQhOuuu879+9//9kmwKH2GIR4DsEqS448/3g0aNMh9+eWX7quvvnK9e/d2k046qW8HvyEWkkTdL7zwwlz9Cu7gee211/p+QFkGy7XXXtvttttubbkQEAW+S5/St9NMM43/jb7htzg5/fTT3T333OPWXHNNX1YofJck7R9//NHngMw444z+5yJ9Bo5XX32192RtvfXW7cpnnOyxxx5+TDEes4R6nHPOOR4z6gBhIEGZ9q233nodXoeIXXzxxb4/P/jgA0+2F1poIbfXXnu5BRZYoN3zaYQh7xygQMYL7eXP22+/7fuKsbXVVlt5DJjTReZfFib6XQgIASEgBIRAd0dAhKGBHjTFknAhFCuUTpSP559/3v93lVVW8UpzKJxQdOWVV3qldokllvDKIooxCuw888zjrrrqKk84HnroIXfJJZd4ZZtET/5t3nnn9QoYCp4JJ8acf/75/neUeBQwFF2ICEpXqGAaYUCRRfGn3uOPP7574YUX3LfffuumnXZad+utt3ol2uSNN97wihTPzzLLLD7e/qOPPnLPPvus92ScdNJJHYjJbbfd5vbdd19PXhZeeGFPEGjDW2+95cumjWBnEkcYwJDv0g4UULA0OeKII9zll1/uydiSSy7p/xkMv/vuO58MC4FL8jgMHDjQPfnkk1455Q8K+wwzzODmnntuT5gQ61cU3WHDhmX2KwRm11139YSK9lmdHnvsMff11197DFBOwQuS9c0333g86Ns555zTTTTRRG7DDTf05CJOIDjbbbedJyCPP/54O4s9bdl8883d9NNP74kQUrTPyKcAl/79+ztO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CJFJmMNQB/AhYeAIVo5ilQgBISAEhIAQEAJCQAgIASHQOAK1EAZyEHbYYYe2PIVDDz3U9e3bt/HaJpRAvgPHtCJ2b0NtH1PBQkAICAEhIASEgBAQAkKgByFQC2EAPzwMeBqQuLsTqsKY41k5kYmL3ZD+/fu7fv36VVW8yhECQkAICAEhIASEgBAQAj0agdoIw6BBg9zll1/uweW251122cXNO++8lYLNfQ/nnHOOGzZsmC+3T58+7sADD4xNoq70wypMCAgBISAEhIAQEAJCQAj0EARqIwzgx2Vrt9xyi/vxxx89nFNOOaUbMGCAG2ussRqG94YbbnD33HNPW9jT/PPP77bddls3/vjjN1y2ChACQkAICAEhIASEgBAQAkLgfwjUShj4ADc7Dx061OcYcAfD6quv7kYfffSG8efI1S+++MJNOumkPj9i8sknb7hMFSAEhIAQEAJCQAgIASEgBIRAewRqJwwCXAgIASEgBISAEBACQkAICIHui4AIQ/ftO9VcCAgBISAEhIAQEAJCQAjUjoAIQ+0Q6wNCQAgIASEgBISAEBACQqD7IiDC0H37TjUXAkJACAgBISAEhIAQEAK1IyDCUDvE+oAQEAJCQAgIASEgBISAEOi+CIgwdN++U82FgBAQAkJACAgBISAEhEDtCIgw1A6xPiAEhIAQEAJCQAgIASEgBLovAiIM3bfvVHMhIASEgBAQAkJACAgBIVA7AiIMtUOsDwgBISAEhIAQEAJCQAgIge6LgAhD9+071VwICAEhIASEgBAQAkJACNSOgAhDhRB/9dVX7tdff3WTTjqpG2200SosWUUJASHQHRD47bff3Pfff+969+7dHaqrOgoBISAEhIAQyIWACEMumJIf+vrrr93ZZ5/tbrvtNgdhQEYZZRS38MILux122MEtv/zyDX5BrwsBIdBdENhggw3ciy++6K644gq32GKLdZdqq55CQAgIASEgBFIREGFoYIB8+OGHbqONNnKffvqptyguuOCCboIJJnDvvfeee+GFF9zIkSPdTjvt5A488MAGvtJ1r7755pvu1FNPdX379nUHHHBA11VEX25KBG6//XZ3xx13uFVXXdWtu+66TVnHPJVijDPWd999dzfXXHPleSXxmTXXXNO99tpr7pJLLnHLLrtsQ2XpZSEgBISAEBACzYKACEMDPbHNNtu4Rx55xK200kru9NNPd+OMM05baRCGrbfe2ocnXHjhhW6FFVZo4Etd8+pTTz3lNttsMzf//PO7m2++uWsqoa82LQJnnnmm488//vEPt//++zdtPbMqtummm7qnn37aXXrppW6ZZZbJejz19x9//NHhdZxmmmkaKkcvCwEhIASEgBBoJgREGEr2BorBvPPO6/788083aNAgN/3003co6YILLnAnnHCCtzRicYwT3kcIY8ojf/zxh3+2V69eeR53v//+e2I+Rda3qyIMxHWPPvrouepL+2hbXjwoNK0deHkoM29OCc+OOuqoueqa9lAa7tH3yrS56Djgm0X7IWucVUEYiuBEX9LXRfsnC6sihKEIhmnjg3bQnqJtCcskX2qMMcbIPVaL4ld07lCRIv2Ztf5EG1a0vbmB0YNCQAgIASGQiYAIQyZE8Q8MHz7c5ykgzz33XGyS4+uvv+49D5NPPrk7+uij2xV09913OwjF0KFDvUI722yzue233z42tOPbb791559/vs+TIPwJRa5fv35ut912c4svvnhbuXxvjz328ESG0IhjjjnGvf32226fffZx/fv3b3su69t33nmnO+uss9xPP/3kCLsaa6yx3HTTTeeVbn5LEvs+uBCKdeKJJ3oyBbmacsopvceFf4+Sne+++863j/AWvofMPPPMDg/OJpts0u5z11xzjbvssst8OdQHDN944w1f5txzz+1DpxZddFH3zDPPuDPOOMP3DYoG9ef72267bYfvowRefPHF7rrrrnMffPCBJzcLLbSQ22uvvdwCCyyQe4TgbaIdL730km/zFFNM4dZaay235557tvM+UWCRNh966KG+PfTJs88+69v/zjvveGWTOPkBAwa4mWaaqcM4KNoPv/zyiye2N954o3v//fd9eYTobLXVVm699dZrK//444/3/frll1/6vB3C8Uj0JySP37KEMTlw4EA3ePBgb40fb7zxPKlmnM4wwwwdXif0iTYzV6gjfUkIFGNg7LHHbvd8EawgCtSfMcdYxyuAl5Bxx28mTzzxhPcSDhkyxD9H2CGeiP32289NO+207b5PWBPj8bTTTmsLb7r++uv9+Npxxx39N1gT8ECiMDPvKWe55ZbLgs3/zvg85ZRTPHYjRozwc5N+5rvgb2JzEVxXWWUV/w5jh7WGuYVXKC6MjN+p77XXXuvxhgAwd9dee22/3oReVPtWkXHPO1nrTwhE3vbmAk8PCQEhIASEQGkERBhKQof1bemll3YfffSRV6iOOOKI3CWhSKNYoigtssgi3vJLSATKEEoqG7MJxATlBSULLwYK7P9j7zzApCi2NnzIIJKTgCiSBBHRa1ZMmDNerjl7zVkvKOacc06Yc46YMCfMWUEFQVGQnDPs/v9ba429w4TuCTuzO995Hh5gprq66u2amfqqzjk1adIkVx5jB2PPPfd0/2YSMnDgQDd5Q2TwY8/khuBrLxjC3Putt95yE0cmJN9//71rJyKECer999+ftJ/+/rQTVywm8X369LEpU6bYDz/84K5jcsuEzBuTTuJAxo4da6uttpoTQjNnznQTUtpPu5lIerv55pvdZIyJGuxxl6KP1A8rVlyZoDNBYsLOhAyRxQQKY0J52GGHxepbuHCh+z+7KUyMmHRR/ssvv3TCAQ6bbrpp2mdLuUsuucSJGEQczJig8ax69uzpJmG8hkXts3d949kTUNu3b19r0qSJfffdd44V7X7jjTdik7lMnsO8efPcOOZaxg9CBNHz4YcfGowYg/QPY7I/YsQIN3nlD5NgJvoItnTxOggfeHM/+sNYYWJKDAF9QhCuscYaMd6Ml4ceeshNjHkOTFiZwMOQsfXwww/HuHJRFFbHH3+848cYZ6xz3+bNm7vPExNkDDclxD7PFZGAOGJsINjghIiHv7fddtvN1cfzXm+99dzLt99+u1155ZXuM8Q4pc/wGj16tBMrfK6effZZxy+V8Rnh802bKUtsEe3gftTBd4pPsuDHAJ8BRFH79u2dUEAIcl/snHPOcQLaG99pxx57rL322muun/369XNvffDBB07YIUweeeSRSrt/UcY9dYX5/vHtidLftB9QFRABERABEciKgARDFvj4YeUHlh9aJrrsELCix+QmmTEZZwLfuXNnNxHq0KGDK8qElokKk7PXX3/dTZ4xJsysyO29995ux8C7MLCqx/2YIPPvVq1axQQD1w0YMMBNjlu2bBlrStR7R3VJ8pMUbki2GCZaDRo0cPcfOnSoXXrppa5fb775ZqxN5513nssow2rnVVddFZuMMCmjzxgruz5NpRcMTBy5zu8AsPILVyayGDsTF154YcwVyU/84M0EyBttom3bbbed25Hwz45VdvzyESZvv/12StcRJrxMFGkTuxQIBIwJN+OD58Pq8hlnnOFe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0NO1IiACIiACIiACIiACIlDDCUgw1PAHrO6JgAiIgAiIgAiIgAiIQDYEJBiyoadrRUAEREAEREAEREAERKCGE5BgqOEPWN0TAREQAREQAREQAREQgWwISDBkQ0/XioAIiIAIiIAIiIAIiEANJyDBUMMfsLonAiIgAiIgAiIgAiIgAtkQkGDIhp6uFQEREAEREAEREAEREIEaTqBKBEP54vm2+Kc3bdFPb1rZjN+tbMFMq9WwidVt29Ma9NzW6vfYyqx2nRqOWt0TAREQAREQAREQAREQgepHIL+CobzM5n801Oa9dqmVzZ2SlE6dNt2sye5XWIM1dqh+BNViERABERABERABERABEajBBPImGMoXzbVZD//XFn3/Ujh8tWpZ4/7/sxV3PFe7DeGIqZQIiIAIiIAIiIAIiIAI5J1AfgRD2VKbcfuutnj0e/90oHYdq99tc6u32sZWe4UWVjZ/hi0Z+7Et/uUds/KyWLlGGx9mTfe8Ke8d1w1EQAREQAREQAREQAREQATSE8iLYJjz/BCb/+4/k/56q65vTfe53eq267lci5b+NdJmPXiQLZ34Y+y9pvveYY3WPyB961VCBERABERABERABERABEQgrwRyLhiWTRltU69Y16xsqWt4/e5bWvMjnrZadRsm7Uj5glk2/cb+tnTSqJx2tu1lk6xWgxVzWqcqEwEREAEREAEREAEREIFSIpBzwTDn2UE2//3bHMPajVtaqzO+dS5I3pb88bUtmzra6rZfs9KOA2Jh2tUbmS1bkjP+Egw5Q6mKREAEREAEREAEREAESpRAzgXD1It727Lp4xxOApgbb3t6BdryMhcEvfDLJ2KoV9jiBGuy++Wx/8965Ahb+PkjOXsUEgw5Q6mKREAEREAEREAEREAESpRATgUD5y1MHtImhrLV/z6yuh37uv/P/+AOm/PMqcthbn7Y49ZgzV3c66ReXTZjfMaPgjMeZt63f+x6CYaMUepCERABERABERABERABEXAEcioYlk3/zaZevEYMbZuLxju3JGzmvfvYou9eXA77Cv2Otib/viYnj2Pp5J9t2uXrSDDkhKYqEQEREAEREAEREAEREIEcC4ayOZNsynldYlxbn/uz1Wne0f1/9mNH24JPH1yOeePtzrAVdzg7J89CgiEnGFWJCIiACIiACIiACIiACMQI5HSHwcqW2aTTWsYyJDU/4hlr0Gt7d7Mlv39h02/cyijjjQxGrQZ9bHVareZeWjTy9VAHvTVce6DV777F3/V+bgs+ecD9m2xLC79+Kla/XJI00kVABERABERABERABEQgOwK5FQxmNuOW7W3xmA9cqxqu/R9rdtD9sRYuHvWGzRl2ri2bOsbqtu9tTQZcafVWWS/2/vTrN3fCIp01GXCFrbD58a7Ywi8ec8HUiUyCIR1JvS8CIiACIiACIiACIiACqQnkXDCQUpXUqs5q1bIWRw+L7QakasqCEffY7CdPCPW8JBhCYVIhERABERABERABERABEciaQM4FQ/nShTbt0rVs2cw/XeM4gwHXpHqrbpC0sYtHDbeZd+9l5csWuzJ12nS3+l02qVR+0Y+vGjESWDLBUKthU2vYd4/YdU0GXme16jbIGpIqEAEREAEREAEREAEREIFSJZBzwQBIYhFmDh3ozl7AmLSvsPlx1mjTI61Oi04x1stm/mEL3r/N5r17Uyy2oVajZtbq1A+sTqt/gqe5YMYtO9jiMe+nFAx123a3VkO+LtVnqX6LgAiIgAiIgAiIgAiIQM4J5EUw0ErnmvTcYLPy8kqNrtN8Zau1QnMrnz/TEAxBq1WvkTU75BFr0Gu75ToaFAzUUXvFivMeyubPiB0UJ8GQ8/GhCkVABERABERABERABEqcQN4EA1wXfvOsS6davmhuWszsKDQ/9FGr22HNhGWDgiFZZRIMaTEvV+C0006zUaNGuddPPvlk69+/f/RK8nBFsbYrD11VlSIgAiIgAiIgAiJQ1ATyKhjcDsDcKTZv+JW28MvHrWzetOVgcE5Do40OsxW2ON5Is5rMilUwXHnllfb444+7Zjdv3tyef/55W3HF5fux/fbb29SpU125iy++2HbccceiGBj//ve/7euvK9y46Mt//vMftasoCKgRIiACIiACIiACIlAcBPIuGGLdLC+zJX98bWUzxlvZgplOHNRt18vqrtTLZVNKZ4t+GGZlsyamLIarE6lcq9LOO+88e/DBfw6kO+SQQ+zcc89drgkbbLBBTDBce+21NmDAgIybecstt9iIESPc9Uz4+ZOpSTBkSk7XiYAIiIAIiIAIiEBpEKg6wVBDecYLhjp16rhdhjXWWKNSj3MpGE488UR76aWXXP0nnXSS+5OpSTBkSk7XiYAIiIAIiIAIiEBpEJBgyPI5xwsGqltnnXXsqaeeslqBnZOwgqG8vNzKysoM4ZHMogqGpUuXWt26dRNWl0wwLFu2LGUbgpWFaXP8zam/du3alRgFyxSrkMlyuOhyERABERABERABEah2BCQYsnxkiQQDVV522WW29957x2pPJRgWL15s9913nz399NM2btw4Y4K/8sor20477WRHHXWUi43AXnvtNcOd6a+//rI5c+a411q2bGmtWrWy/fbbzw4++ODY/d577z0bOnSoi0+YO3euK0dAM7sRHTt2jJULTszPPvtsFwD98ssv24IFC6xTp0520EEH2aGHHrrcxD5sm4N4afddd91lr776qk2aNMnV2bNnT9trr71c+4MiKZlgoF+XXnqpIVKwI4880gYOHJjlU9TlIiACIiACIiACIiACyQhIMGQ5NoKCoVmzZjZr1ixXY4sWLeyNN95wf2PJBAMTfyb6PvA4vjkIh0cffdRN8gmuPuOMMxK2+LjjjrP//e9/7r0bb7zRrr/++oTlaM9jjz1m3bt3d+8HJ+b169c3hEC8HXbYYYaY8Balzf6aH3/80Yjv8IHf8ffYfPPNnZioV6/ecu3ywdg///yzC8pGAGG77rqr62dwJyfLx6nLRUAEREAEREAEREAE4ghIMGQ5JIKCYauttnK1vf322+5vVs4vv/xy9+9kgoH0obgvYUzYyZ7UoEEDtwo/e/Zs9/raa6/tdh9Gjhxpb731lg0bNsx++ukn997GG2/s/qy//vq24YYbGivwTMy9bbHFFtalSxd799137ddff3Uv9+3b15599tnlJua8sOWWW7ryBFVzP4wJ+YsvvhiLy4jSZq5FhOy88842ZswYV1/btm1t2223tWnTptnrr7/uXLAwdguGDBmyXLsQDPRjjz32sAkTJrj36S/B5jCTiYAIiIAIiIAIiIAI5I+ABEOWbOMFw/nnn2+kUF24cKGbaD/55JP2r3/9K6FgmDJlipvs+wnz3XffbV50sJrOJBtff+zhhx92ZbFUMQz77ruvffLJJ65ccGcAFyMm6X7CTdA0gdnBHQZcj8455xx3LZN83mNnAPvvf/9rZ511lmXSZgTOCSec4Opp0qSJc61aaaWV3P8feeSR2O5Fw4YN7fPPP7cVVlihUrsuuugix/Hbb7911yBoEFneVSvLR6jLRUAEREAEREAEREAEUhCQYMhyeMQLBib9N998s4s1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HbpABERABERABERABERABESgMoGiEgzZPJwrrrjCfvrpJ1fFvvvua9tuu2021elaERABERABERABERABERABM3dMQvfu3SuxqFWeyRJ/oIqoMQy5eBISDLmgqDpEQAREQAREQAREQAREQDsMGgMiIAIiIAIiIAIiIAIiIAIhCWiHISQoFRMBERABERABERABERCBUiQgwVCKT119FgEREAEREAEREAEREIGQBCQYQoJSMREQAREQAREQAREQAREoRQISDKX41NVnERABERABERABERABEQhJQIIhJCgVEwEREAEREAEREAEREIFSJCDBUIpPXX0WAREQAREQAREQAREQgZAEJBhCglIxERABERABERABERABEShFAhIMNeCpv/vjXPtk9Dz7YfxC+3XyIps8a4nNW1RuZeXleeld7Vq1rHGDWta2WT3r0raB9e7U0Dbs1ti2WGPFvNxPlYqACIiACIiACIiACBSOgARD4dhndefPx8y3x0bMsBc/n2lzFpZlVVeuLm7SsLbtul5z22fjFrZe1xVyVW3e6mHwy0SgOhPo3r17yuZrjFfnp6u2QyDdGBclERCBqiEgwVA1nHN2F4TCTa9OseHfzc5ZnfmoaNs+Te2EHdoUtXBINPjzwUJ1ikA+CIQZv2HK5KNtqlMEckFA4zcXFFWHCOSGgARDbjhWSS3nPTnR7npzapXcK1c3OWLr1nbBnu1zVV1O69GPUU5xqrIqJhBm/IYpU8XN1u1EIDQBjd/QqFRQBPJOQIIh74izv8HIPxfaKff/Yd/+viD7ygpQw1qrNLLrDl7ZenVsWIC7J7+lfoyK6nGoMREJhBm/YcpEvK2Ki0CVEdD4rTLUGd1oyZIlNnnyZFu2bFnk62vVqmWtW7e2Ro0aRb5WFxSGQF4Ew7PPPmsvvvii61Hbtm1tjTXWyHvvvvrqK5s1a5a7z0EHHWRbbrll3u9ZFTcgoPnIO38rmjiFTPtMfMOdR65aVIHR+jHK9GnqumIgEGb8hilTDH1RG0QgEQGN3+IdF8zx+LN06dKMG4lo2GSTTdycrV69ehnXowurhkBeBMPXX39tN954Y9X0IMFdzj//fFtllVUKdv9c3RixsP9N4/KW7ShMO1dtXd+269vUNure2O0QrNS8rjWsV9sWLimzv2YuNXY/Pv5lnr3+zWz7berilFWSXenhEzoXjWjQ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uOKZc9Acrlic04AFz6rw7cX9DJEjEwEREAEREAERKH4CEgxF/IwKIRh81iLcdL67qpdbLU5mPpMS7z9yYmfbco2KgFpOFCa9p59UM0E9oF9La7liHXfeQLxgwLXn0mf/snvfmeZcefyk+eoDO7qzD5jY5kswcC+yGd3y2hRjRd8bIohUskdu3Wo5l6KLnp5otw2f6ibE315V2eXo+c9n2jFDK+I/Rly8unNpCmPc+4KnJtob389x/fXGidL0nUxMQcM1ifMbHv5whnH+hTd2Sgbv1m65gHMyOp3/5ER75ZvZsfo5SI1diCG7rxQL+g4jGLgXcQwciPfl2PkxkUJ9/96guQsCxx3LG+Lr2KHjbdhX/2SBIrtWGwmGMENDZURABERABESg4AQkGAr+CJI3oBCCIZc4mATPWVBmq7Spn/IcAn9PVs5/m7LYiRTvg5/L9qSrC3cb4jBarFjXOraoV5CTiBEC46ctttq1a9nKLeu506NTGav8MJu/uMzaNauXdhIOY54Log0xw65CNgaviTOWWqP6tWzlVvVTZm3iFGhiNSiX6lyKbNqja0VABERABERABHJPQIIh90xzVmN1Fww5A6GKREAEREAEREAEREAECkZAgqFg6HVjERABERABERABERABESh+AhIMxf+M1EIREAEREAEREAEREAERKBgBCYaCodeNRUAEREAEREAEREAERKD4CUgwFP8zUgtFQAREQAREQAREQAREoGAEJBgKhl43FgEREAEREAEREAEREIHiJyDBUPzPSC0UAREQAREQAREQAREQgYIRkGAoGHrdWAREQAREQAREQAREQASKn4AEQ/E/I7VQBERABERABERABERABApGQIKhYOh1YxEQAREQAREQAREQAREofgISDMX/jNRCERABERABERABERABESgYAQmGgqHXjUVABERABERABERABESg+AlIMBT/M1ILRUAEREAEREAEREAERKBgBCQYCoZeNxYBERABERABERABERCB4icgwVD8z0gtFAF6EoRDAAAgAElEQVQREAEREAEREAEREIGCEZBgKBh63VgEREAEREAEREAEREAEip+ABEPxPyO1UAREQAREQAREQAREQAQKRkCCoWDodWMREAEREAEREAEREAERKH4CEgzF/4zUQhEQAREQAREQAREQAREoGAEJhoKh141FQAREQAREQAREQAREoPgJSDAU/zNSC0VABERABERABERABESgYAQkGAqGXjcWAREQAREQAREQAREQgeInIMFQ/M9ILRQBERABERABERABERCBghGQYCgYet1YBERABERABERABERABIqfgARD8T8jtVAEREAEREAEREAEREAECkZAgqFg6HVjERABERABERABERABESh+AhIMxf+M1EIREAEREAEREAEREAERKBgBCYaCodeNRUAEREAEREAEREAERKD4CUgwFP8zUgtFQAREQAREQAREQAREoGAEJBgKhl43FgEREAEREAEREAEREIHiJyDBUPzPSC0UAREQAREQAREQAREQgYIRkGAoGHrdWAREQAREQAREQAREQASKn4AEQ/E/I7VQBERABERABERABERABApGQIKhYOh1YxEQAREQAREQAREQAREofgISDMX/jNRCERABERABERABERABESgYAQmGgqHXjUVABERABERABERABESg+AlIMBT/M1ILRUAEREAEREAEREAERKBgBCQYCoZeNxYBERABERABERABERCB4icgwVD8z0gtFAERqIYEysvNxk1ZZLMXlKVsfc8ODa1BvVo2ZfZSmzBjSdKydWqbrdmpUTUkoSaLgAiIgAhUdwISDNX9Car9IiACRUdg9oJltv+N4+yLsfPTtu2983tYt5Ua2G3Dp9pFT09MWn6FBrVt9A2909anAiIgAiIgAiKQawISDLkmqvpEQARKnsA1L02ya16aHIqDBEMoTCokAiIgAiJQQAISDAWEn+mtl5WVO/eFBvVqW4vGdTKtpkZehxvIzPnLbMHiMsemUf3aRdvPkX8utBnzltkmPRpn1cZc1ZNVI3J48YE3j3Pju3enhnbNgSvnsOaqq+qYoePt+c9nWp9VGtmV+3dMeeNkLkn3vTPNHh8xI3atdhiq7vnpTiIgAiIgApUJSDBUoxHx8lez7a43p9qXY+fbkmXlruVtm9a1XddrZifv2NZaNalbjXqT26aOn7bYrh022V77ZrbNnLfMVV67llnPjg3t4C1a2f79Wrr/F4vhqrLblWMMgXPuwPZ29LatM2patvV889sC2+/Gse7eAzdsYRfu1T5pO7a7ZLT9OX3xcu/Xr1vL2jStZ+t3XcEO2rylrd6hYaUyd781za4dNsm9dseRq1i/1VdM2dd/DRllf81c4up7fnDXjLgU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REAESg0BCYZSc6l1oiIgAiIgAiIgAiIgAiIQn4AEQ3xm2kMEREAEREAEREAEREAESg0BCYZSc6l1oiIgAiIgAiIgAiIgAiIQn4AEQ3xm2kMEREAEREAEREAEREAESg0BCYZSc6l1oiIgAiIgAiIgAiIgAiIQn4AEQ3xm2kMEREAEREAEREAEREAESg0BCYZSc6l1oiIgAiIgAiIgAiIgAiIQn4AEQ3xm2kMEREAEREAEREAEREAESg0BCYZSc6l1oiIgAiIgAiIgAiIgAiIQn4AEQ3xm2kMEREAEREAEREAEREAESg0BCYZSc6l1oiKQHQI33XST/fjjj3bllVdat27dsnOQtdTqwoULrWzZsla1atW1dMS1d5jrr7/efv75Z7vmmmts6623XnsHzvKR8vLybMaMGdawYcMsH0nNi4AIiEDpJSDBkMFr/91339k555xjjRo1sg8//NDKlCmTwdbVlAjkJoGTTz7ZvvrqK3v00Udtzz33zM1ORujV7NmzbZdddrFy5cpZ//79rVatWhH2ir7Jm2++abfcckuRHRAotWvXts6dO9sRRxxhO+ywQ/RGY2x54okn2oABA+zJJ5+03XbbLcaeub3pnXfeaY888oidd9557o9MBERABEQg8wQkGDLIlI/V+++/71p88cUXs/bhz2CX1ZQIFCFw991328iRI+3cc891Tmwq21AEA6PU3bt3d4IBx7pu3bqpTj3W71966SW7+uqrrXr16rbRRhsV7Ltq1SqbMGGCLVmyxP3s2GOPtRtuuCFW235jrhvXr02bNnbZZZcVamNDFQxEuJ5++mk79dRTrU+fPmlx004iIAIiIALFE5BgyNAdMn/+fJeOwWghH/4DDzzQ7rvvvgy1rmZEYO0R6NWrl/3www/2zDPPWI8ePVIeeEMRDJzorFmzXGQw02KBtr1g2HXXXe2pp54qxHX16tX2zjvvuHShxYsX24033mjHHHNMSvbhDQYNGmRHH320bbHFFvbWW2+VCsGA4Bo/fry1bNnSvX9lIiACIiACmScgwZAhpi+88IJde+21blT2tddes7lz5zqnq0aNGpGOsHLlSitfvnykbXEuyNtlJDSK8UGNum3ctpcvX24VK1aM0o2MbBPnXNI9IMfAaYzqfHAtuH4VKlSIfMg415tG42xPf7iOya55KoZrWzCk6k9kqIEN497HcY9Bn7k/4qQdFicY/PGfffZZF13YeOON7dNPP43bLYsrGOKeRzrXKtvXIiqkFStWxHpGo7ar7URABESgNBCQYMjQVSaiMHToUJf7/Morr9hjjz2WcpSQtAe2+/33392oIrUPBxxwgJ1//vlFii756L788svuz+jRo50D2a5dOzv++OPdiGLYceHjyCjmq6++6tIdcGa32WYbu+CCC2zLLbcsdNY4mGz33HPPGTcExyJl4pBDDrHTTz/dKleuXGh72rvrrrvs66+/NiIr/H7bbbd1YmmrrbYqlui9995rn3zyie2///6u3iNoOCOHH364Y0E+fOvWrd2vp06dag8//LBzoEgbqVKliovmnH322UWO9+uvv7q0hE033dSlZoSN67Rs2TJ79913C87L/4xrQeEuBbzbbbedIQKLM5yzBx980BWSIpzq1KnjcsMvuugia9y4cZFdp0+fbv/73//c+c+cOdPlrffs2TPp9lHvj6uuusp++uknu+6669x5vffee+7+ID3FG/fmAw88YN9//73j26xZMzv00EPtjDPOsEqVKrnNEArk8U+cONFFydiG4l9SWfhdMksWYaCmB+cXQXnPPfe4+zVOf0466SSbPHmyXXHFFUUiHcOHD3fPCXUGffv2dc573Ps4fD5Lly61gw46yAl3apC8eb7wGzx4sOHUjxkzxgmy7bff3kUF2rZtm/JNEkUw8GwR1eF8/vrrL3fNojwvRx11lOPAdeP68UzyDAfPxackPfHEE+6ZIoL0zz//pDyPOM8fgyW8d0455RR3//C880zyTtlkk03skksuMSIsxRmpVL/99pu7n+EbNM7vP//5j/sRx+L+pCbj9ddfT3ifcg9yvjzT7FuzZk3Hl340b968oOl58+a5VDB48dyTNiYTAREQARFYQ0CCIQN3w4gRI2zfffd1zusbb7zhHLW9997bunTp4hy4REbO7c033+w+UBQ58oHCGZk2bZp16NDBfQz9R4uP7VlnneUcZpxMPqI4qN75O+yww4zCP284Pv/973/daCOOK/3io//LL7844cCxd9xxx4Lt+TA///zzVq1aNdtpp53cz/nQLliwwM2mggDyo9U4GByPCApOObnSOE84pGyD011cQSWCir7Rr4EDBxYSOpwPaRgtWrRwwgvDaUIUIRQYdZhg6T8AACAASURBVCWnftKkSY4VTtUdd9zh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
Download .txt
gitextract_nuchrmnk/

├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── __init__.py
├── talk_like_a_graph/
│   ├── README.md
│   ├── __init__.py
│   ├── graph_generators.py
│   ├── graph_generators_runner.py
│   ├── graph_generators_test.py
│   ├── graph_metrics.py
│   ├── graph_metrics_test.py
│   ├── graph_tasks.py
│   ├── graph_tasks_generator.py
│   ├── graph_tasks_utils.py
│   ├── graph_text_encoders.py
│   ├── graph_text_encoders_test.py
│   └── name_dictionaries.py
└── tutorial/
    ├── KDD-Tutorial-1-Talk-Like-a-Graph.ipynb
    ├── KDD-Tutorial-2-Let-Your-Graph-Do-The-Talking.ipynb
    └── README.md
Download .txt
SYMBOL INDEX (105 symbols across 11 files)

FILE: talk_like_a_graph/graph_generators.py
  function generate_graphs (line 21) | def generate_graphs(
  function remove_graph_data (line 142) | def remove_graph_data(graph: nx.Graph) -> nx.Graph:
  function randomize_directions (line 152) | def randomize_directions(graph: nx.Graph) -> nx.DiGraph:
  function remove_directions (line 166) | def remove_directions(graph: nx.Graph) -> nx.Graph:

FILE: talk_like_a_graph/graph_generators_runner.py
  function write_graphs (line 43) | def write_graphs(graphs: list[nx.Graph], output_dir: str) -> None:
  function main (line 56) | def main(argv: Sequence[str]) -> None:

FILE: talk_like_a_graph/graph_generators_test.py
  class GraphGenerationTest (line 6) | class GraphGenerationTest(absltest.TestCase, parameterized.TestCase):
    method test_number_of_graphs (line 34) | def test_number_of_graphs(self, algorithm, directed, k):
    method test_directions (line 110) | def test_directions(self, algorithm, directed):

FILE: talk_like_a_graph/graph_metrics.py
  function yes_no_accuracy (line 10) | def yes_no_accuracy(

FILE: talk_like_a_graph/graph_metrics_test.py
  class GraphTasksTest (line 5) | class GraphTasksTest(absltest.TestCase):
    method test_yes_no_correct (line 7) | def test_yes_no_correct(self):
    method test_yes_no_ambiguous (line 22) | def test_yes_no_ambiguous(self):
    method test_yes_no_indeterminate (line 37) | def test_yes_no_indeterminate(self):
    method test_yes_no_accuracy_raises_on_ambiguous_target (line 52) | def test_yes_no_accuracy_raises_on_ambiguous_target(self):
    method test_yes_no_accuracy_raises_on_indeterminate_target (line 59) | def test_yes_no_accuracy_raises_on_indeterminate_target(self):

FILE: talk_like_a_graph/graph_tasks.py
  class GraphTask (line 11) | class GraphTask:
    method __init__ (line 14) | def __init__(self):
    method prepare_examples_dict (line 18) | def prepare_examples_dict(
    method create_few_shot_example (line 26) | def create_few_shot_example(
  class CycleCheck (line 32) | class CycleCheck(GraphTask):
    method __init__ (line 35) | def __init__(self):
    method prepare_examples_dict (line 40) | def prepare_examples_dict(
    method create_few_shot_example (line 69) | def create_few_shot_example(
    method choose_few_shot_examples (line 98) | def choose_few_shot_examples(
  class EdgeExistence (line 119) | class EdgeExistence(GraphTask):
    method __init__ (line 122) | def __init__(self):
    method prepare_examples_dict (line 126) | def prepare_examples_dict(
    method create_few_shot_example (line 161) | def create_few_shot_example(
  class NodeCount (line 192) | class NodeCount(GraphTask):
    method __init__ (line 195) | def __init__(self):
    method prepare_examples_dict (line 200) | def prepare_examples_dict(
    method get_nodes_string (line 223) | def get_nodes_string(self, name_dict: dict[int, str], nnodes: int) -> ...
    method create_few_shot_example (line 230) | def create_few_shot_example(
  class NodeDegree (line 247) | class NodeDegree(GraphTask):
    method __init__ (line 250) | def __init__(self):
    method prepare_examples_dict (line 254) | def prepare_examples_dict(
    method get_edge_string (line 282) | def get_edge_string(
    method create_few_shot_example (line 299) | def create_few_shot_example(
  class EdgeCount (line 319) | class EdgeCount(GraphTask):
    method __init__ (line 322) | def __init__(self):
    method prepare_examples_dict (line 327) | def prepare_examples_dict(
    method get_edges_string (line 350) | def get_edges_string(
    method create_few_shot_example (line 360) | def create_few_shot_example(
  class ConnectedNodes (line 376) | class ConnectedNodes(GraphTask):
    method __init__ (line 379) | def __init__(self):
    method prepare_examples_dict (line 383) | def prepare_examples_dict(
    method get_connected_nodes (line 416) | def get_connected_nodes(
    method create_few_shot_example (line 433) | def create_few_shot_example(
  class DisconnectedNodes (line 464) | class DisconnectedNodes(GraphTask):
    method __init__ (line 467) | def __init__(self):
    method prepare_examples_dict (line 471) | def prepare_examples_dict(
    method get_disconnected_nodes (line 508) | def get_disconnected_nodes(
    method create_few_shot_example (line 537) | def create_few_shot_example(
  class Reachability (line 574) | class Reachability(GraphTask):
    method __init__ (line 577) | def __init__(self):
    method prepare_examples_dict (line 581) | def prepare_examples_dict(
    method create_few_shot_example (line 614) | def create_few_shot_example(
  class ShortestPath (line 655) | class ShortestPath(GraphTask):
    method __init__ (line 658) | def __init__(self):
    method prepare_examples_dict (line 662) | def prepare_examples_dict(
    method create_few_shot_example (line 703) | def create_few_shot_example(
  class TriangleCounting (line 751) | class TriangleCounting(GraphTask):
    method __init__ (line 754) | def __init__(self):
    method prepare_examples_dict (line 759) | def prepare_examples_dict(
    method create_few_shot_example (line 786) | def create_few_shot_example(
  class MaximumFlow (line 829) | class MaximumFlow(GraphTask):
    method __init__ (line 832) | def __init__(self):
    method prepare_examples_dict (line 836) | def prepare_examples_dict(
    method create_few_shot_example (line 870) | def create_few_shot_example(
  function has_edge_weights (line 912) | def has_edge_weights(graph):
  function add_edge_weight (line 919) | def add_edge_weight(graph):
  class NodeClassification (line 928) | class NodeClassification(GraphTask):
    method __init__ (line 931) | def __init__(self):
    method prepare_examples_dict (line 943) | def prepare_examples_dict(
    method create_few_shot_example (line 988) | def create_few_shot_example(

FILE: talk_like_a_graph/graph_tasks_generator.py
  function zero_shot (line 35) | def zero_shot(
  function few_shot (line 69) | def few_shot(
  function generate_random_sbm_graph (line 116) | def generate_random_sbm_graph(random_state: np.random.RandomState):
  function main (line 129) | def main(argv: Sequence[str]) -> None:

FILE: talk_like_a_graph/graph_tasks_utils.py
  function laplacian_pos_embedding (line 19) | def laplacian_pos_embedding(graph: nx.Graph, units: int = 4) -> nx.Graph:
  function to_tfgnn (line 33) | def to_tfgnn(graph: nx.Graph, node_ids: list[int]) -> tfgnn.GraphTensor:
  function create_example_feature (line 116) | def create_example_feature(
  function load_graphs (line 179) | def load_graphs(
  function prepare_examples (line 204) | def prepare_examples(
  function create_zero_shot_task (line 247) | def create_zero_shot_task(
  function write_examples (line 267) | def write_examples(examples: list[example_pb2.Example], output_path: str):
  function prepare_few_shots (line 273) | def prepare_few_shots(
  function choose_few_shot_examples (line 291) | def choose_few_shot_examples(
  function create_few_shot_task (line 304) | def create_few_shot_task(

FILE: talk_like_a_graph/graph_text_encoders.py
  function create_node_string (line 8) | def create_node_string(name_dict, nnodes: int) -> str:
  function nx_encoder (line 17) | def nx_encoder(graph: nx.Graph, _: dict[int, str], edge_type="id") -> str:
  function adjacency_encoder (line 44) | def adjacency_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  function friendship_encoder (line 65) | def friendship_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  function coauthorship_encoder (line 80) | def coauthorship_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> ...
  function incident_encoder (line 99) | def incident_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  function social_network_encoder (line 125) | def social_network_encoder(graph: nx.Graph, name_dict: dict[int, str]) -...
  function expert_encoder (line 143) | def expert_encoder(graph: nx.Graph, name_dict: dict[int, str]) -> str:
  function nodes_to_text (line 156) | def nodes_to_text(graph, encoding_type):
  function get_tlag_node_encoder (line 180) | def get_tlag_node_encoder(graph, encoder_name):
  function with_ids (line 224) | def with_ids(graph: nx.Graph, node_encoder: str) -> nx.Graph:
  function encode_graph (line 229) | def encode_graph(

FILE: talk_like_a_graph/graph_text_encoders_test.py
  class GraphTextEncodersTest (line 20) | class GraphTextEncodersTest(absltest.TestCase, parameterized.TestCase):
    method test_encoders (line 65) | def test_encoders(self, encoding_method, expected_result):

FILE: talk_like_a_graph/name_dictionaries.py
  function create_name_dict (line 265) | def create_name_dict(graph, name: str, nnodes: int = 20) -> dict[int, str]:
Condensed preview — 20 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (362K chars).
[
  {
    "path": "CONTRIBUTING.md",
    "chars": 969,
    "preview": "# How to Contribute\n\n## Contributor License Agreement\n\nContributions to this project must be accompanied by a Contributo"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 1223,
    "preview": "# Encoding Graphs and Structured Data in Language Models\n\nThis repository contains code for\n[Talk like a Graph: Encoding"
  },
  {
    "path": "__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "talk_like_a_graph/README.md",
    "chars": 1306,
    "preview": "# Using Large Language Models to Solve Graph Problems\n\nThis repository contains the code to generate graph reasoning pro"
  },
  {
    "path": "talk_like_a_graph/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "talk_like_a_graph/graph_generators.py",
    "chars": 5972,
    "preview": "r\"\"\"Random graph generation.\"\"\"\n\nimport random\n\nimport networkx as nx\nimport numpy as np\n\n\n_NUMBER_OF_NODES_RANGE = {\n  "
  },
  {
    "path": "talk_like_a_graph/graph_generators_runner.py",
    "chars": 2266,
    "preview": "r\"\"\"Random graph generation.\n\nThis code generates random graph using different algorithms.\n\n# Placeholder for Google-int"
  },
  {
    "path": "talk_like_a_graph/graph_generators_test.py",
    "chars": 2817,
    "preview": "from absl.testing import parameterized\nfrom . import graph_generators\nfrom absl.testing import absltest\n\n\nclass GraphGen"
  },
  {
    "path": "talk_like_a_graph/graph_metrics.py",
    "chars": 2263,
    "preview": "\"\"\"Metrics for seqio tasks over graph data.\n\nThis module contains definitions of metric_fns to be used for scoring\ngraph"
  },
  {
    "path": "talk_like_a_graph/graph_metrics_test.py",
    "chars": 2056,
    "preview": "from . import graph_metrics\nfrom absl.testing import absltest\n\n\nclass GraphTasksTest(absltest.TestCase):\n\n  def test_yes"
  },
  {
    "path": "talk_like_a_graph/graph_tasks.py",
    "chars": 33464,
    "preview": "\"\"\"The graph tasks to be tried with LLMs.\"\"\"\n\nimport random\n\nimport networkx as nx\nimport numpy as np\n\nfrom . import gra"
  },
  {
    "path": "talk_like_a_graph/graph_tasks_generator.py",
    "chars": 6385,
    "preview": "r\"\"\"The graph tasks to be tried with LLMs..\n\nThis code loads graphs and creates graph tasks and output them as tf exampl"
  },
  {
    "path": "talk_like_a_graph/graph_tasks_utils.py",
    "chars": 11002,
    "preview": "\"\"\"The graph tasks to be tried with LLMs.\"\"\"\n\nimport os\nimport random\n\nimport networkx as nx\nimport numpy as np\nimport s"
  },
  {
    "path": "talk_like_a_graph/graph_text_encoders.py",
    "chars": 10727,
    "preview": "\"\"\"Library for encoding graphs in text.\"\"\"\n\nimport networkx as nx\n\nfrom . import name_dictionaries\n\n\ndef create_node_str"
  },
  {
    "path": "talk_like_a_graph/graph_text_encoders_test.py",
    "chars": 2448,
    "preview": "\"\"\"Testing for graph_text_encoders.py.\"\"\"\n\nfrom absl.testing import parameterized\nimport networkx as nx\n\nfrom . import g"
  },
  {
    "path": "talk_like_a_graph/name_dictionaries.py",
    "chars": 4404,
    "preview": "\"\"\"Creates a dictionary mapping integers to node names.\"\"\"\n\nimport random\n\n_RANDOM_SEED = 1234\nrandom.seed(_RANDOM_SEED)"
  },
  {
    "path": "tutorial/KDD-Tutorial-1-Talk-Like-a-Graph.ipynb",
    "chars": 198948,
    "preview": "{\n  \"cells\": [\n    {\n      \"cell_type\": \"markdown\",\n      \"metadata\": {\n        \"id\": \"EMeYjNCilDE_\"\n      },\n      \"sou"
  },
  {
    "path": "tutorial/KDD-Tutorial-2-Let-Your-Graph-Do-The-Talking.ipynb",
    "chars": 48987,
    "preview": "{\n  \"nbformat\": 4,\n  \"nbformat_minor\": 0,\n  \"metadata\": {\n    \"colab\": {\n      \"provenance\": [],\n      \"gpuType\": \"V28\"\n"
  },
  {
    "path": "tutorial/README.md",
    "chars": 1907,
    "preview": "# <p align=\"center\"> Tutorial on Graph Reasoning with LLMs (GReaL) </p>\n\n## <p align=\"center\">**KDD'24 Tutorial**</p>\n\n#"
  }
]

About this extraction

This page contains the full source code of the google-research/talk-like-a-graph GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 20 files (340.3 KB), approximately 158.0k tokens, and a symbol index with 105 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!