Skip to content

Generating llms.txt for a repository

This example builds an llms.txt summary of a code repository by composing four ChainOfThought predictors. It demonstrates signatures with List[String] fields and threading predictor outputs through an Either for-comprehension.

Analysis signatures

trait AnalyzeRepository extends Spec:
  def repo_url: InputField[String]
  def file_tree: InputField[String]
  def readme_content: InputField[String]
  def project_purpose: OutputField[String]
  def key_concepts: OutputField[List[String]]
  def architecture_overview: OutputField[String]

trait AnalyzeCodeStructure extends Spec:
  def file_tree: InputField[String]
  def package_files: InputField[String]
  def important_directories: OutputField[List[String]]
  def entry_points: OutputField[List[String]]
  def development_info: OutputField[String]

trait GenerateLLMsTxt extends Spec:
  def project_purpose: InputField[String]
  def key_concepts: InputField[List[String]]
  def architecture_overview: InputField[String]
  def important_directories: InputField[List[String]]
  def entry_points: InputField[List[String]]
  def development_info: InputField[String]
  def usage_examples: InputField[String]
  def llms_txt_content: OutputField[String]

Each Spec declares its inputs and outputs. List[String] outputs such as key_concepts and entry_points map to Scala List[String]. The fields of GenerateLLMsTxt line up with the combined outputs of the two analysis signatures plus a generated usage_examples string.

Composing the predictors

final class RepositoryAnalyzer:
  private val analyzeRepo      = ChainOfThought(Signature.of[AnalyzeRepository])
  private val analyzeStructure = ChainOfThought(Signature.of[AnalyzeCodeStructure])
  private val generateExamples = ChainOfThought(Signature.fromString("repo_info -> usage_examples"))
  private val generateLlmsTxt  = ChainOfThought(Signature.of[GenerateLLMsTxt])

  def forward(
      repoUrl      : String,
      fileTree     : String,
      readmeContent: String,
      packageFiles : String
  )(using RuntimeContext): Either[DspyError, String] =
    for
      repo      <- analyzeRepo((repo_url = repoUrl, file_tree = fileTree, readme_content = readmeContent))
      structure <- analyzeStructure((file_tree = fileTree, package_files = packageFiles))
      examples  <- generateExamples(
                    (repo_info = s"Purpose: ${repo.output.project_purpose}\nConcepts: ${repo.output.key_concepts}")
                  )
      llms <- generateLlmsTxt((
                project_purpose = repo.output.project_purpose,
                key_concepts = repo.output.key_concepts,
                architecture_overview = repo.output.architecture_overview,
                important_directories = structure.output.important_directories,
                entry_points = structure.output.entry_points,
                development_info = structure.output.development_info,
                usage_examples = examples.output.usage_examples
              ))
    yield llms.output.llms_txt_content

RepositoryAnalyzer holds four ChainOfThought fields. Three are built with Signature.of, and generateExamples uses an inline string signature. forward runs them in sequence inside a for-comprehension over Either[DspyError, String], so a failure in any step short-circuits. Each predictor's output feeds the next, and the final step returns the llms_txt_content string.

Running it

OPENAI_API_KEY=sk-... sbt "examples/runMain dspy4s.examples.tutorials.llms_txt_generation.llmsTxtMain"

Notes

Fetching the file tree, README, and package files over the GitHub API is out of scope. That step is plain HTTP I/O, so supply fileTree, readmeContent, and packageFiles to forward however you like.

Full source: LlmsTxtGeneration.scala