Skip to content

chrislaskey/pyre_core

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

112 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pyre Core

For a fully configured standlone Pyre Application see Pyre App

Core multi-agent LLM library for Pyre.

Pyre orchestrates specialized LLM workflows for software development.

Orchestration layer runs on Jido. Each agent is a reusable Jido Action with a persona that guides its output. The pipeline includes a review loop that iterates until the code reviewer approves.

Installation

Add pyre to your list of dependencies in mix.exs:

def deps do
  [
    {:pyre, git: "https://github.com/chrislaskey/pyre_core", branch: "main"}
  ]
end

Then run the installer to copy persona files and set up the runs directory:

mix deps.get
mix pyre.install

This creates:

  • priv/pyre/personas/ — Editable persona files for each agent
  • priv/pyre/features/.gitkeep — Directory where pipeline artifacts are stored
  • .gitignore entries to exclude run output from version control

Configuration

PubSub

Pyre can emit pubsub events when lifecycle events occur. This is optional if you're only calling pyre on the command line and are not integrating it more deeply within your app.

To enable PubSub events update config/config.exs:

# config/config.exs
config :pyre, :pubsub, MyApp.PubSub

Replace MyApp.PubSub with the PubSub server already started in your application's supervision tree.

Note: This is required if you are using the PyreWeb web interface, since the UI listens for these events.

Allowed Paths

By default, agent file tools (read, write, list directory) are sandboxed to the working directory. If you need access to sibling apps or shared libraries, you can allow additional directories.

Environment variable (comma-separated):

export PYRE_ALLOWED_PATHS="/path/to/apps/other,/path/to/libs/shared"

Application config:

# config/runtime.exs
if paths = System.get_env("PYRE_ALLOWED_PATHS") do
  config :pyre,
    allowed_paths:
      paths
      |> String.split(",", trim: true)
      |> Enum.map(&String.trim/1)
      |> Enum.map(&Path.expand/1)
end

Flow option (programmatic):

Pyre.Flows.FeatureBuild.run("Build a feature",
  project_dir: "apps/tools",
  allowed_paths: ["/path/to/apps/other"]
)

Relative paths are resolved against the working directory (--project-dir), so ../other with --project-dir apps/tools resolves to apps/other. The working directory itself is always included automatically.

Lifecycle hooks

Pyre dispatches lifecycle events (flow start/complete, action start/complete, LLM call complete) to a configurable callback module. Create a module that use Pyre.Config and override the callbacks you need:

defmodule MyApp.Pyre.Config do
  use Pyre.Config

  @impl true
  def after_flow_complete(%Pyre.Events.FlowCompleted{} = event) do
    MyApp.Telemetry.emit(:pyre_flow_complete, %{
      flow: event.flow_module,
      elapsed_ms: event.elapsed_ms
    })
    :ok
  end
end

Then register it in your config:

# config/config.exs
config :pyre, config: MyApp.Pyre.Config

Any callback not overridden returns :ok by default. Exceptions in callbacks are rescued and logged — they never crash the running flow.

GitHub integration

Personal access token (Shipper agent)

To enable the Shipper agent (creates branches and opens GitHub PRs), configure your repository in config/runtime.exs:

# config/runtime.exs
if System.get_env("PYRE_GITHUB_REPO_URL") do
  config :pyre, :github,
    repositories: [
      [
        url: System.get_env("PYRE_GITHUB_REPO_URL"),
        token: System.get_env("PYRE_GITHUB_TOKEN"),
        base_branch: System.get_env("PYRE_GITHUB_BASE_BRANCH", "main")
      ],
      # [
      #   url: System.get_env("ADDITIONAL_GITHUB_REPO_URL"),
      #   token: System.get_env("ADDITIONAL_GITHUB_TOKEN"),
      #   base_branch: System.get_env("ADDITIONAL_GITHUB_BASE_BRANCH", "main")
      # ]
    ]
end

Set the required environment variables:

export PYRE_GITHUB_TOKEN=ghp_...
export PYRE_GITHUB_REPO_URL=https://github.com/myorg/my-app

The Shipper automatically picks up the first configured repository. To target a specific repo at runtime, pass the :github option:

Pyre.Flows.FeatureBuild.run("Build a feature",
  github: %{owner: "acme", repo: "app", token: token, base_branch: "main"}
)
GitHub App (webhook-triggered PR reviews)

Pyre also supports a GitHub App integration for @mention-triggered PR reviews. The webhook handling, mention parsing, and job queue live in PyreWeb. The core review workflow (Pyre.RemoteReview) and GitHub API infrastructure (Pyre.GitHub.App) remain in pyre_core. See the PyreWeb README for setup instructions.

LLM Backends

Pyre ships with several built-in LLM backends:

Backend Name Description
Pyre.LLM.ReqLLM req_llm API-based (default) — any major provider via ReqLLM
Pyre.LLM.ClaudeCLI claude_cli Claude CLI subprocess
Pyre.LLM.CursorCLI cursor_cli Cursor CLI subprocess
Pyre.LLM.CodexCLI codex_cli OpenAI Codex CLI subprocess

The default backend is Pyre.LLM.ReqLLM. To switch backends, set the PYRE_LLM_BACKEND environment variable to the backend's name:

export PYRE_LLM_BACKEND=claude_cli
API Keys

When using the req_llm backend, set at least one API key:

export ANTHROPIC_API_KEY=sk-ant-...
export OPENAI_API_KEY=sk-...

Model aliases are configured in config/config.exs:

config :jido_ai,
  model_aliases: %{
    fast: "anthropic:claude-haiku-4-5",
    standard: "anthropic:claude-sonnet-4-20250514",
    advanced: "anthropic:claude-opus-4-20250514"
  }

To use a different provider (e.g., OpenAI), change the model alias strings and set the corresponding API key:

config :jido_ai,
  model_aliases: %{
    fast: "openai:gpt-4o-mini",
    standard: "openai:gpt-4o",
    advanced: "openai:o1"
  }

Usage

Run the feature-building pipeline:

mix pyre.run "Build a products listing page with sorting and filtering"

This runs six agents in sequence:

Feature Request
  -> Product Manager    (requirements & user stories)
  -> Designer           (UI/UX spec with Tailwind layout)
  -> Programmer         (implementation using Phoenix conventions)
  -> Test Writer        (ExUnit tests)
  -> Code Reviewer      (APPROVE or REJECT)
       -> If REJECT: loop Programmer/TestWriter/Reviewer (up to 3 cycles)
  -> Shipper            (git branch, commit, push, open GitHub PR)

Output streams to the terminal token-by-token so you can see each agent working in real time.

Options

Flag Short Description
--fast -f Use the fastest model for all agents
--dry-run -d Print plan without calling LLMs
--verbose -v Print diagnostic information
--no-stream Disable streaming (wait for complete responses)
--project-dir -p Working directory for agents (default: .)
--feature -n Feature name to group related runs
--allowed-paths Comma-separated additional directories agents can access

Artifacts

Each run creates a timestamped directory in priv/pyre/features/<feature>/ containing:

File Agent Content
00_feature.md Original feature request
01_requirements.md Product Manager User stories and acceptance criteria
02_design_spec.md Designer UI/UX specifications
03_implementation_summary.md Programmer Code changes made
04_test_summary.md Test Writer Tests written
05_review_verdict.md Code Reviewer APPROVE or REJECT with feedback
06_shipping_summary.md Shipper Branch name, commit, PR URL

On review rejection cycles, artifacts are versioned (_v2, _v3).

Architecture

Pyre is built on three layers:

Actions — Each agent role is a Jido Action with schema-validated inputs and a run/2 function. Actions are flow-agnostic: the same QAReviewer action can be used in a feature-building flow, a PR review flow, or any other pipeline.

lib/pyre/actions/
  product_manager.ex    # Requirements from feature description
  designer.ex           # UI/UX design spec
  programmer.ex         # Implementation (versioned on review cycles)
  test_writer.ex        # Test coverage (versioned)
  qa_reviewer.ex        # APPROVE/REJECT verdict (reusable across flows)
  shipper.ex            # Git branch, commit, push, and GitHub PR

Flows — Pipeline drivers that compose actions into a specific workflow. Each flow defines its phases and valid transitions:

lib/pyre/flows/
  feature_build.ex      # planning -> designing -> implementing ->
                        #   testing -> reviewing -> shipping -> complete

Plugins — Shared utilities used by all actions:

lib/pyre/plugins/
  persona.ex            # Loads .md persona files, builds LLM messages
  artifact.ex           # Timestamped run directories, versioned files

Customization

Persona files

Edit the persona files in priv/pyre/personas/ to customize agent behavior for your project. Each file is a Markdown document used as the system prompt. The installer will not overwrite files that already exist, so your changes are preserved across updates.

Adding a new flow

Create a new module under lib/pyre/flows/ that reuses existing actions:

defmodule Pyre.Flows.PRReview do
  alias Pyre.Actions.QAReviewer

  def run(pr_diff, opts \\ []) do
    context = %{llm: Keyword.get(opts, :llm, Pyre.LLM), streaming: true}

    with {:ok, result} <- QAReviewer.run(%{
           feature_description: "Review this PR",
           requirements: pr_diff,
           design: "",
           implementation: pr_diff,
           tests: "",
           run_dir: "/tmp/review",
           review_cycle: 1
         }, context) do
      {:ok, result.verdict}
    end
  end
end

Adding a new action

defmodule Pyre.Actions.SecurityReviewer do
  use Jido.Action,
    name: "security_reviewer",
    schema: [
      code: [type: :string, required: true],
      run_dir: [type: :string, required: true]
    ]

  def run(params, context) do
    model = Pyre.Actions.Helpers.resolve_model(:advanced, context)
    {:ok, sys} = Pyre.Plugins.Persona.system_message(:security_reviewer)
    user = Pyre.Plugins.Persona.user_message("Security review", params.code, params.run_dir, "security.md")

    case Pyre.Actions.Helpers.call_llm(context, model, [sys, user]) do
      {:ok, text} -> {:ok, %{review: text}}
      error -> error
    end
  end
end

Custom LLM backends

You can define your own LLM backend by implementing the Pyre.LLM behaviour. Use use Pyre.LLM to get the behaviour and a default manages_tool_loop?/0 returning false:

defmodule MyApp.LLM.Ollama do
  use Pyre.LLM

  @impl true
  def generate(model, messages, opts \\ []) do
    # Call your LLM provider
    {:ok, "response text"}
  end

  @impl true
  def stream(model, messages, opts \\ []) do
    output_fn = Keyword.get(opts, :output_fn, &IO.write/1)
    # Stream tokens via output_fn, return full text
    {:ok, "response text"}
  end

  @impl true
  def chat(model, messages, tools, opts \\ []) do
    # Handle tool-use conversations
    {:ok, "response text"}
  end
end

Then register it in your Pyre.Config module:

defmodule MyApp.Pyre.Config do
  use Pyre.Config

  @impl true
  def list_llm_backends do
    Pyre.Config.included_llm_backends() ++ [
      %{module: MyApp.LLM.Ollama, name: "ollama",
        label: "Ollama", description: "Local models via Ollama"}
    ]
  end
end

This makes your backend available via PYRE_LLM_BACKEND=ollama and visible in any UI that calls Pyre.Config.list_llm_backends/0.

To set it as the default regardless of env vars, also override get_llm_backend/1:

@impl true
def get_llm_backend(_arg), do: MyApp.LLM.Ollama

For CLI-style backends that manage their own tool-calling loop (like Claude CLI or Codex CLI), override manages_tool_loop?/0 to return true. This tells Pyre to call chat/4 directly instead of routing through the agentic loop.

Generators

Pyre includes Igniter-based generators that agents use during the pipeline:

  • mix pyre.gen.context — Generates a context module with CRUD functions
  • mix pyre.gen.live — Generates LiveView pages with index/show views
  • mix pyre.gen.modal — Adds a modal component to a LiveView
  • mix pyre.gen.filter — Adds a filter function to an existing context

Testing

Actions and flows are testable without LLM calls using the mock:

# Test a single action
Process.put(:mock_llm_response, "APPROVE\n\nLooks great!")
{:ok, result} = Pyre.Actions.QAReviewer.run(params, %{llm: Pyre.LLM.Mock, streaming: false})
assert result.verdict == :approve

# Test a full flow with sequenced responses
Process.put(:mock_llm_responses, [
  "Requirements...", "Design...", "Implementation...", "Tests...", "APPROVE\n\nDone."
])
{:ok, state} = Pyre.Flows.FeatureBuild.run("Build a feature", llm: Pyre.LLM.Mock, streaming: false)
assert state.phase == :complete

About

Core module for Pyre, A software development framework

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages