Skip to content

Refactor CLI tool with multi-package architecture#9

Merged
MaryamZi merged 49 commits intowso2:mainfrom
RadCod3:feat/afm-cli
Feb 25, 2026
Merged

Refactor CLI tool with multi-package architecture#9
MaryamZi merged 49 commits intowso2:mainfrom
RadCod3:feat/afm-cli

Conversation

@RadCod3
Copy link
Copy Markdown
Contributor

@RadCod3 RadCod3 commented Feb 13, 2026

Purpose

This PR implements a unified CLI tool for AFM based on a modular, multi-package architecture.
Resolves #8

High-Level Architecture

The Python implementation has been restructured from a monolithic script into a uv workspace that decouples the core AFM engine from specific execution frameworks. This design allows for a lightweight core and pluggable execution backends.

Modular Packages:

  1. afm-core: A lightweight package containing the CLI logic, AFM parser, and Pydantic models. It defines the formal AgentRunner protocol and handles interface implementations (Console, Web, Webhook).
  2. afm-langchain: A concrete implementation of the AgentRunner protocol using LangChain. It handles LLM orchestration and prompt generation.
  3. afm-cli (The Metapackage): A "one-click" convenience package that installs both the core engine and the LangChain adapter, providing an immediate working environment for users.

Pluggable Discovery & Lazy Loading:

  • Autodiscovery: The CLI uses importlib.metadata to scan for installed packages that register under the afm.runner entry point. This allows users to add new frameworks (e.g., afm-llamaindex) simply by installing them.
  • Lazy Loading: Heavy AI libraries (like LangChain) are only imported when an execution command is triggered. This keeps the CLI responsive for tasks like validation or framework listing.

CI/CD & Release Workflows:

To support this multi-package structure, the release process has been completely modularized.

  1. New Specialized Workflows
  • release-python.yml: A dedicated workspace-aware workflow for Python.
    • Supports independent releases for afm-core and afm-langchain.
    • Handles automated PyPI publishing via uv.
    • Now utilizes the shared release-docker.yml for container image management.
  • release-ballerina.yml: A dedicated workflow for the Ballerina implementation.
    • Handles Ballerina-specific builds and tests.
    • Utilizes the shared release-docker.yml for container image management.
  • release-docker.yml (New Reusable): Standardizes the Docker build-push-scan pipeline.
    • Multi-platform builds (linux/amd64, linux/arm64).
    • Integrated vulnerability scanning with Trivy and SARIF upload.
    • Standardized OCI labels and tagging logic.
  • release-finalize.yml (Reusable): Standardizes the finalization of releases (Git tags, release branches, and GitHub Releases) across all implementations.
  1. Standardized Tagging & Branching
  • Git Tags: Follows the format <package-or-implementation>-v<version> (e.g., afm-core-v0.1.0 or ballerina-interpreter-v0.1.0).
  • Release Branches: Automatically created for every release as release-<package-or-implementation>-<version> (e.g., release-afm-core-0.1.0).
  • Docker Image Tags:
    • Version Tag: Releases are tagged with :v<version>.
    • Latest Tag: The :latest tag is only updated during releases from main or dev branches to ensure stable production tags are maintained.
  • Image Naming: Docker images retain implementation-specific names (e.g., afm-langchain-interpreter, afm-ballerina-interpreter) to avoid collisions and provide clarity in the registry.

Summary by CodeRabbit

  • New Features

    • Pluggable, runtime-discoverable execution backends; selectable frameworks in the CLI
    • CLI: framework list, validate, unified run, improved version reporting, and update-notification API
  • Documentation

    • New/readme docs for CLI, Core, and LangChain backend; guides updated for a Python workspace layout and release process
  • Tests

    • Expanded and reorganized test suites and fixtures for CLI, parser, update checks, webhooks, and backends
  • Chores

    • New release and Docker workflows plus workspace packaging and Docker build updates

This refactoring was done to support pluggable adapters in the future to
add support for additional frameworks for AFM.

1. afm-core is a lightweight package containing only the CLI logic, AFM
parser, and interface definitions.
2. afm-langchain is an implementation of the definitions in afm-core
using LangChain.
3. afm-cli is a metapackage that contains no functional code. It has
afm-core and afm-langchain as dependencies.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 13, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Converts the monolithic langchain-interpreter into a uv workspace "python-interpreter" (packages: afm-core, afm-langchain, afm-cli); adds an AgentRunner protocol with discovery/loader; refactors the CLI to use pluggable runners and adds update-checking; reorganizes release workflows into language-specific pipelines; and introduces packaging, docs, tests, and workspace Docker changes.

Changes

Cohort / File(s) Summary
Release workflows
.github/workflows/release-python.yml, .github/workflows/release-finalize.yml, .github/workflows/release-docker.yml, .github/workflows/release-ballerina.yml, .github/workflows/release-common.yml
Adds new Python/Docker/Finalize/Ballerina release workflows and removes the shared release-common.yml. New workflows orchestrate validation, tests, PyPI publish, Docker multi-arch build & Trivy scan, finalize release notes, and bumping logic.
CI workflow
.github/workflows/python-interpreter.yml
Renames workflow context/paths from langchain-interpreterpython-interpreter, updates Docker build context/working-dir references, and adds a formatting check step.
Workspace & Docker context
python-interpreter/pyproject.toml, python-interpreter/Dockerfile, python-interpreter/.dockerignore
Introduces uv workspace config, updates Dockerfile to copy workspace package metadata/sources and use workspace-focused uv install flags; expands .dockerignore to exclude package tests.
afm-core package (core logic & CLI)
python-interpreter/packages/afm-core/pyproject.toml, .../src/afm/runner.py, .../src/afm/cli.py, .../src/afm/update.py, .../src/afm/parser.py, .../src/afm/interfaces/*
Creates afm-core package, adds AgentRunner Protocol, discovery (entry-point) and loader, refactors CLI to support AgentRunner backends (new commands: validate, framework list, version option), implements async update checker/notification, adds parse_afm resolve_env flag, and many license/header adjustments.
afm-langchain backend
python-interpreter/packages/afm-langchain/pyproject.toml, .../src/afm_langchain/backend.py, .../src/...
Adds afm-langchain package implementing LangChainRunner (renamed from Agent), registers afm.runner entry-point, updates imports to afm.*, and adjusts tests and fixtures accordingly.
afm-cli metapackage
python-interpreter/packages/afm-cli/pyproject.toml, python-interpreter/packages/afm-cli/src/...
Adds afm-cli metapackage that depends on afm-core and afm-langchain and exposes the afm console script.
Interfaces changes
.../src/afm/interfaces/web_chat.py, .../console_chat.py, .../webhook.py
Public API type changes: functions/classes now accept AgentRunner instead of Agent; console UI supports update notifications; CORS middleware and cors_origins parameter removed from web app factory.
Parser & variables
.../src/afm/parser.py, .../variables.py
Adds optional env-variable resolution parameter (resolve_env) to parse APIs; many license/header updates.
Tests & fixtures
python-interpreter/packages/afm-core/tests/**, python-interpreter/packages/afm-langchain/tests/**, removed langchain-interpreter/tests/**
Removes legacy langchain-interpreter tests; adds extensive new test suites and fixtures for CLI, parser env resolution, update logic, interfaces, backend, MCP tools, and lifecycles.
Docs & READMEs
README.md, RELEASING.md, python-interpreter/packages/*/README.md
Updates top-level README references to python-interpreter, rewrites RELEASING.md for per-language workflows, and adds package-level READMEs.
Legacy removal
langchain-interpreter/src/..., langchain-interpreter/tests/...
Removes old langchain-interpreter package files and tests in favor of the new workspace layout.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as afm-cli
    participant Core as afm-core
    participant Loader as Runner Discovery
    participant Backend as LangChainRunner
    participant LLM as LLM Provider

    User->>CLI: afm run agent.afm.md --framework langchain
    CLI->>Core: parse_afm_file(agent.afm.md)
    Core-->>CLI: AFMRecord
    CLI->>Loader: load_runner("langchain")
    Loader->>Loader: discover_runners()
    Loader->>Backend: load entry point
    Loader-->>CLI: LangChainRunner class
    CLI->>Backend: instantiate LangChainRunner(afm_record)
    CLI->>Backend: connect / arun(user_input)
    Backend->>LLM: send inference request
    LLM-->>Backend: return response
    Backend-->>CLI: agent response
    CLI->>User: display output
    CLI->>Backend: disconnect()
Loading
sequenceDiagram
    participant GitHub
    participant Release as release-python.yml
    participant Validate as Validate Job
    participant Test as Test Job
    participant Build as Build/Publish
    participant Docker as release-docker.yml
    participant Finalize as release-finalize.yml

    GitHub->>Release: manual or workflow_run trigger
    Release->>Validate: validate branch & tag uniqueness
    Release->>Test: run tests
    Test-->>Release: test results
    Release->>Build: uv build & publish to PyPI
    Build-->>Release: package published
    Release->>Docker: call release-docker.yml (multi-arch build, Trivy scan)
    Docker-->>Release: image pushed & SARIF uploaded
    Release->>Finalize: call release-finalize.yml with tag/version
    Finalize->>GitHub: create release, notes, and push branch/tag
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰
From one burrow sprang a tidy pack,
Runners found, the backends unpack.
CLI hops light, discoveries sing,
Workflows hum and packages spring.
A carrot toast for releases—ping!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes several out-of-scope changes beyond the core objective: removal of test files and conftest from langchain-interpreter, relocation of GitHub Actions workflows (removing release-common.yml), and extensive documentation updates. While these support the overall refactoring, they extend beyond the primary goal of enabling PyPI publishing. Separate out-of-scope changes (test removals, workflow restructuring, documentation) into dedicated PRs, or clearly justify why each change is essential to the PyPI publishing objective.
Docstring Coverage ⚠️ Warning Docstring coverage is 36.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Refactor CLI tool with multi-package architecture' clearly and concisely summarizes the main change—restructuring the CLI from monolithic to a modular multi-package architecture, which is the primary objective of this PR.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering Purpose (with issue link), High-Level Architecture (three modular packages), Pluggable Discovery & Lazy Loading, and CI/CD & Release Workflows. It aligns with the template's expectations for Purpose, Goals, and Approach sections, though some template fields (User stories, Release note, Documentation, etc.) are not explicitly filled.
Linked Issues check ✅ Passed The PR successfully addresses issue #8 by enabling PyPI publishing of the langchain interpreter. The implementation includes afm-langchain as a standalone package with proper entry-point registration, independent release workflows, and automated PyPI publishing via uv, meeting the requirement for independent installation and framework swapping.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@RadCod3 RadCod3 marked this pull request as ready for review February 16, 2026 05:37
@RadCod3 RadCod3 requested a review from MaryamZi as a code owner February 16, 2026 05:37
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python-interpreter/README.md (1)

60-75: ⚠️ Potential issue | 🟡 Minor

Project structure section in README is stale after the multi-package refactor.

The directory tree shown in lines 60–75 displays a monolithic layout (src/afm/, agent.py, providers.py, etc.) that no longer matches the actual structure. The project now uses a workspace layout with packages/afm-cli/, packages/afm-core/, and packages/afm-langchain/, but the README still shows the old single-package structure. This will mislead contributors about how the project is organized.

Update the tree to reflect the actual workspace structure.

🤖 Fix all issues with AI agents
In @.github/workflows/release-finalize.yml:
- Around line 80-88: The NOTES variable is being assigned a multi-line string
that preserves the YAML run: block indentation, causing leading spaces in
release_notes.md; change the assignment for NOTES (the branches that reference
COMMITS, IMPLEMENTATION, and RERELEASE_NOTE) to build the string using an
unindented heredoc or a method that strips indentation (e.g., assign via
NOTES=$(cat <<'EOF' ... EOF) or use printf to concatenate lines without the
run-block indentation) so the "## Changes in $IMPLEMENTATION" header, the
$COMMITS content, and $RERELEASE_NOTE are written with no extra leading spaces.

In @.github/workflows/release-python.yml:
- Around line 232-248: The multi-line if: block for the finalize job is being
treated as a non-empty string because it includes the ${ { } } delimiters inside
a YAML block; remove the ${ { and } } wrappers and leave the boolean expression
itself in the block (so the if: uses the implicit expression form), e.g. change
the finalize job's if: to the same multiline expression without ${ { } }, and
make the identical fix for the bump-version job so both jobs evaluate the
conditional properly instead of always being truthy.

In `@python-interpreter/packages/afm-cli/pyproject.toml`:
- Around line 13-16: The dependency list in pyproject.toml pins afm-core to
0.1.3 but leaves afm-langchain unpinned; update the dependencies array to pin
afm-langchain to a specific version or compatible range (e.g., match the
released afm-langchain package version) so it will not resolve to an arbitrary
PyPI release—modify the entry for "afm-langchain" in the dependencies block to
the chosen pinned version or range that is known compatible with afm-core.

In `@python-interpreter/packages/afm-core/tests/test_cli.py`:
- Around line 54-58: The test_version test calls meta_version("afm-cli") which
raises PackageNotFoundError when the afm-cli package isn't installed; update
test_version to handle that by trying to get meta_version("afm-cli") inside a
try/except (catching importlib.metadata.PackageNotFoundError) or by
conditionally asserting only if meta_version succeeds; reference the test
function test_version and the meta_version call and mirror the _version_callback
behavior so the test passes when afm-cli is absent.

In `@RELEASING.md`:
- Around line 124-129: Update the workflow filename references in RELEASING.md
so they match the actual workflow file name (release.yml): replace any
occurrences of `release-common.yml` with `release.yml` in the "Workflow Files"
table (the table with header "File | Purpose") and in the "Summary" table (the
block around lines 133-138) so both tables consistently reference `release.yml`
for the ballerina workflow; ensure no other mentions of `release-common.yml`
remain in the document.
- Line 12: The section heading "Langchain Interpreter (Python)" is misleading;
update the heading to "Python Interpreter" to match the renamed directory
`python-interpreter` and the described scope covering `afm-core` and
`afm-langchain`, and ensure any internal references in that section that mention
"Langchain Interpreter" are updated to "Python Interpreter" as well.
🧹 Nitpick comments (18)
.github/workflows/python-interpreter.yml (2)

37-41: Consider running format/lint checks before tests for faster feedback.

Formatting and lint checks are typically faster than test suites. Moving them before pytest would give quicker CI feedback on style violations.

Suggested reordering
      - name: Install dependencies
        run: uv sync --dev

+      - name: Check formatting
+        run: uv run ruff format --check .
+
+      - name: Lint with ruff
+        run: uv run ruff check .
+
       - name: Run tests
         run: uv run pytest

-      - name: Check formatting
-        run: uv run ruff format --check .
-
-      - name: Lint with ruff
-        run: uv run ruff check .

77-78: docker/build-push-action can be updated to v6.

v6 is the current major version of this action.

Suggested update
      - name: Build and push Docker image
-        uses: docker/build-push-action@v5
+        uses: docker/build-push-action@v6
python-interpreter/README.md (1)

1-3: Consider updating the subtitle to reflect the broader scope.

Line 1 was renamed to "AFM Python Interpreter," but Line 3 still describes this as "A LangChain-based reference implementation." Since the refactor introduces pluggable backends (the whole point of the rename), the description could be updated to reflect that LangChain is now one of potentially many backends.

python-interpreter/packages/afm-langchain/src/afm_langchain/tools/mcp.py (1)

137-137: Unused field _tools on MCPClient.

self._tools is declared but never assigned or read within MCPClient. The tool caching is handled at the MCPManager level instead. Consider removing this dead field.

♻️ Suggested cleanup
     ) -> None:
         """Initialize an MCP client."""
         self.name = name
         self.url = url
         self.authentication = authentication
         self.tool_filter = tool_filter
-        self._tools: list[BaseTool] | None = None
python-interpreter/packages/afm-langchain/src/afm_langchain/providers.py (1)

116-149: _get_api_key fallback logic is reasonable but the else clause could benefit from a comment.

When auth_type is "bearer" with a falsy token or "api-key" with a falsy api_key, the if/elif chain falls through to the else dict-inspection block. This works but is subtle — the model validator on ClientAuthentication should prevent None values, but empty strings could still slip through. A brief inline comment explaining the fallback intent would help future readers.

python-interpreter/packages/afm-core/src/afm/__init__.py (1)

17-17: Hardcoded __version__ is fine but risks drift with pyproject.toml.

Consider using importlib.metadata.version("afm-core") to derive the version from the installed package metadata (single source of truth), especially since the PR mentions automated version bumping in the release workflow.

♻️ Alternative: derive version from package metadata
-__version__ = "0.1.3"
+from importlib.metadata import version
+
+__version__ = version("afm-core")
python-interpreter/packages/afm-core/src/afm/runner.py (1)

109-109: Prefer lazy %-style formatting for logger calls.

Using f-strings in logging calls evaluates the string unconditionally, even when the log level is disabled. Use logger.info("Loaded runner: %s (%s)", ep.name, ep.value) instead.

Suggested fix
-    logger.info(f"Loaded runner: {ep.name} ({ep.value})")
+    logger.info("Loaded runner: %s (%s)", ep.name, ep.value)
python-interpreter/packages/afm-core/src/afm/update.py (2)

102-111: Executable-path heuristic for detecting install context is fragile.

Substring checks like "pipx" in executable or "uv" in executable can produce false positives — e.g., a username containing "uv" (/home/uvalentine/...) or a path with "pipx" in a non-pipx context. Since this only affects the upgrade command suggestion in a non-critical notification, the impact is low, but you could tighten the match.

Slightly more robust alternative
 def _detect_upgrade_command() -> str:
     """Detect the installation context and return the appropriate upgrade command string."""
     executable = sys.executable or ""
+    # Normalize to forward slashes for consistent matching
+    parts = executable.replace("\\", "/").split("/")
 
-    if "pipx" in executable:
+    if "pipx" in parts:
         return f"pipx upgrade {PYPI_PACKAGE}"
-    elif "uv" in executable:
+    elif "uv" in parts:
         return f"uv tool upgrade {PYPI_PACKAGE}"
     else:
         return f"pip install -U {PYPI_PACKAGE}"

155-199: get_update_notification() unconditionally sets notified_version without checking it first.

Unlike notify_if_update_available() (line 244), get_update_notification() doesn't check whether notified_version already matches latest before returning the message and saving state. This means every call to get_update_notification() that finds an update will return the message and overwrite the state, regardless of prior notification.

In the current flow this works because get_update_notification() is called in the console chat path (which replaces the stderr notification), but if both functions are ever called for the same UI flow, the dedup guard in notify_if_update_available() will be pre-empted. Consider adding the same notified_version check for consistency.

python-interpreter/packages/afm-core/pyproject.toml (1)

27-28: Duplicate afm script entry point across afm-core and afm-cli.

Both afm-core (line 28) and afm-cli (line 19 of its pyproject.toml) declare afm = "afm.cli:cli". While they resolve to the same target, having the same console script in two packages can cause pip/uv warnings and makes the ownership ambiguous. Consider keeping the entry point only in afm-cli (the user-facing metapackage) and removing it from afm-core.

python-interpreter/packages/afm-core/src/afm/cli.py (3)

285-294: Narrow the exception handler to PackageNotFoundError.

except Exception is overly broad. The only expected failure here is afm-cli not being installed.

Proposed fix
+from importlib.metadata import PackageNotFoundError, version
-from importlib.metadata import version
 ...
 def _version_callback(ctx: click.Context, _param: click.Parameter, value: bool) -> None:
     if not value or ctx.resilient_parsing:
         return
     core_version = version("afm-core")
     try:
         cli_version = version("afm-cli")
         click.echo(f"afm-cli {cli_version} (afm-core {core_version})")
-    except Exception:
+    except PackageNotFoundError:
         click.echo(f"afm-core {core_version}")
     ctx.exit()

315-331: "Loading:" is printed after parsing has already completed.

In the validate command, click.echo(f"Loading: {file}") at line 330 runs only after parse_afm_file succeeds. Consider moving it before the try block so it prints immediately, or renaming to something like "Validated:" to better reflect the state.

Proposed fix (option A: move before parsing)
 def validate(file: Path) -> None:
+    click.echo(f"Loading: {file}")
     try:
         afm = parse_afm_file(str(file), resolve_env=False)
     except AFMError as e:
         raise click.ClickException(f"Failed to parse AFM file: {e}") from e
     except Exception as e:
         raise click.ClickException(f"Unexpected error parsing AFM file: {e}") from e
 
-    click.echo(f"Loading: {file}")
     click.echo(format_validation_output(afm))

448-455: Runner constructor convention (runner_cls(afm)) is not enforced by the AgentRunner protocol.

The protocol defines runtime methods and properties but not __init__. If a third-party runner expects a different constructor signature, this will fail at runtime. Consider documenting this convention (e.g., in the AgentRunner protocol docstring or a factory method) so backend authors know they must accept a single AFMRecord positional argument.

python-interpreter/packages/afm-langchain/src/afm_langchain/backend.py (2)

250-253: self.tools property re-evaluated on every tool call iteration.

Inside the agent loop, self.tools (line 253) calls _get_all_tools() which concatenates two lists on each access. Consider caching the tool list before the loop (similar to all_tools on line 91) to avoid redundant list allocations per iteration.

♻️ Suggested improvement
+            available_tools = self.tools
             # Main agent loop to handle tool calls
             while iterations < max_iterations:
                 # ...
                 for tool_call in response.tool_calls:
                     tool_name = tool_call["name"]
-                    tool = next((t for t in self.tools if t.name == tool_name), None)
+                    tool = next((t for t in available_tools if t.name == tool_name), None)

297-304: Exception handling: consider a dedicated except AgentError clause.

The current pattern catches AgentError inside the generic except Exception and re-raises it via isinstance check. A separate except AgentError: raise clause before the generic handler would be cleaner and more explicit.

♻️ Suggested simplification
         except InputValidationError:
             raise
         except OutputValidationError:
             raise
+        except AgentError:
+            raise
         except Exception as e:
-            if isinstance(e, AgentError):
-                raise
             raise AgentError(f"Agent execution failed: {e}") from e
.github/workflows/release-finalize.yml (2)

63-70: Release notes for Python packages will include commits from all packages.

The path filter -- "$IMPLEMENTATION/" resolves to python-interpreter/ for both afm-core and afm-langchain releases. This means the release notes for an afm-core release will also include commits that only touched afm-langchain files, and vice versa.

Consider using a more specific path filter based on the tag or a new input to narrow the commit scope per package.


40-51: No error handling if the branch or tag push fails.

If git push origin on Line 50 or 51 fails (e.g., due to a race condition or permission issue), the workflow continues to the next step, potentially creating a GitHub Release pointing to a non-existent tag. Consider adding set -euo pipefail at the start of the script to fail fast on any command error.

📝 Proposed fix
        run: |
+         set -euo pipefail
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
.github/workflows/release.yml (1)

232-242: Missing fallback when no version file is found in bump-version.

If neither Ballerina.toml nor pyproject.toml exists (e.g., directory structure changes between validate and bump), VERSION_FILE will be unset and the sed command on Line 239 will fail with an unclear error. While validate checks this earlier, adding an else clause here would make failures easier to diagnose.

📝 Proposed fix
          if [ -f "$IMPLEMENTATION/Ballerina.toml" ]; then
            VERSION_FILE="$IMPLEMENTATION/Ballerina.toml"
          elif [ -f "$IMPLEMENTATION/pyproject.toml" ]; then
            VERSION_FILE="$IMPLEMENTATION/pyproject.toml"
+         else
+           echo "::error::No version file found for $IMPLEMENTATION"
+           exit 1
          fi

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
python-interpreter/README.md (1)

43-49: Consider updating Docker image name to reflect generic architecture.

The Docker image is still named afm-langchain-interpreter, which implies a LangChain-specific implementation. Since the project now supports pluggable backends, consider using a more generic name like afm-python-interpreter or afm-interpreter to align with the new architecture.

If the Docker image specifically bundles the LangChain backend by default, the current naming may be intentional and acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python-interpreter/README.md` around lines 43 - 49, The README uses the
Docker image name afm-langchain-interpreter which implies a LangChain-only
build; update the docker tag and run references to a more generic name (e.g.,
afm-python-interpreter or afm-interpreter) by changing the build command (docker
build -t afm-langchain-interpreter .) and the run command invocation
(afm-langchain-interpreter afm /app/agent.afm.md) to the chosen generic name,
and if the image truly bundles LangChain by default, keep the current name but
add a brief note explaining that it includes the LangChain backend.
python-interpreter/packages/afm-core/tests/test_cli.py (2)

237-240: Missing exit_code assertion.

runner.invoke result is not checked for success. If the CLI exits with an error after setting up uvicorn config but before completing, this test would still pass. Consider adding assert result.exit_code == 0 to catch unexpected failures.

Proposed fix
-        runner.invoke(cli, ["run", str(sample_agent_path), "--port", "9000"])
+        result = runner.invoke(cli, ["run", str(sample_agent_path), "--port", "9000"])
+        assert result.exit_code == 0
 
         # Should have called uvicorn.run
         assert mock_uvicorn.run.called or mock_uvicorn.Config.called
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python-interpreter/packages/afm-core/tests/test_cli.py` around lines 237 -
240, The test calls runner.invoke(cli, ["run", str(sample_agent_path), "--port",
"9000"]) but doesn't check the invocation result; capture the return value
(e.g., result = runner.invoke(...)) and assert result.exit_code == 0 to ensure
the CLI completed successfully. Keep the existing uvicorn assertions
(mock_uvicorn.run.called or mock_uvicorn.Config.called) but add the exit_code
check immediately after the invoke to catch failures from cli/run before or
after uvicorn setup.

165-169: Unused sample_minimal_path fixture parameter in three tests.

test_requires_at_least_one_interface, test_creates_app_with_webhook, and test_creates_app_with_both_interfaces all declare sample_minimal_path as a parameter but never reference it. This unnecessarily triggers fixture setup and is misleading to readers.

Proposed fix (example for one method; apply similarly to the other two)
-    def test_requires_at_least_one_interface(self, sample_minimal_path: Path):
+    def test_requires_at_least_one_interface(self):
         agent = _make_mock_agent()
 
         with pytest.raises(ValueError, match="At least one HTTP interface"):
             create_unified_app(agent)

Also applies to: 189-203, 205-219

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@python-interpreter/packages/afm-core/tests/test_cli.py` around lines 165 -
169, The three test functions test_requires_at_least_one_interface,
test_creates_app_with_webhook, and test_creates_app_with_both_interfaces declare
the fixture parameter sample_minimal_path but never use it; remove the unused
parameter from each test signature (i.e., delete ", sample_minimal_path: Path"
or "sample_minimal_path" from the def lines) so the fixture is not unnecessarily
invoked and the test intent is clearer; verify no other references to
sample_minimal_path exist inside those functions and run tests to confirm no
further changes are needed.
.github/workflows/release-ballerina.yml (1)

91-112: fetch-depth: 0 is unnecessary for the test job.

The test job only needs to build and run tests — it doesn't inspect git history. Using the default shallow clone (fetch-depth: 1) would be faster.

📝 Proposed fix
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: ${{ inputs.branch }}
-         fetch-depth: 0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-ballerina.yml around lines 91 - 112, The workflow
checkout step in the "test" job sets fetch-depth: 0 which forces a full clone;
remove the fetch-depth key (or set it to 1) in the "Checkout repository" step
that uses actions/checkout@v4 so the test job uses the default shallow clone and
runs faster; update the checkout step under the test job accordingly (refer to
the "test" job and the "Checkout repository" step that uses actions/checkout@v4
and the fetch-depth field).
.github/workflows/release-docker.yml (1)

84-98: Trivy scan runs post-push — vulnerable images are briefly available on GHCR.

The vulnerability scan (line 84) executes after the image is pushed (line 67). If the scan fails, the image has already been published. This is a common pattern and may be acceptable, but if the goal is to gate publication on scan results, consider building without pushing first, scanning the local image, then pushing only on success.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-docker.yml around lines 84 - 98, The Trivy scan
("Scan Docker image for vulnerabilities") runs after the image push step, so a
failed scan still leaves a published image; change the workflow to build the
image without pushing (use the existing build step or docker/build-push-action
with push: false and tag matching steps.docker-tags.outputs.FULL_IMAGE), run the
Trivy scan against that local tag (image-ref pointing to the local image), and
only run the push step (the current "Push Docker image" step) conditionally on
the scan succeeding (use job/step outputs or job status to gate the push); keep
the "Upload Trivy scan results" step but run it regardless for visibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/release-ballerina.yml:
- Around line 158-162: The current sed command replaces every line matching
^version = "..." in VERSION_FILE, corrupting dependency versions; change the
replacement to only affect the first match by using the sed range for the first
occurrence, e.g. replace the sed invocation in the block that touches
VERSION_FILE with: sed -i '0,/^version = ".*"/s//version = "'"$NEXT_VERSION"'"'
"$VERSION_FILE" (i.e. update the sed command that currently reads sed -i
"s/^version = \".*\"/version = \"$NEXT_VERSION\"/" to use the 0,/pattern/ range
so only the first ^version = ... line is updated).

In @.github/workflows/release-docker.yml:
- Around line 49-65: The workflow currently sets UPDATE_LATEST when
inputs.branch == 'main' || inputs.branch == 'dev', causing dev builds to be
tagged as :latest; change the condition to only set UPDATE_LATEST for main
(remove the dev branch) so the run block that appends ,$FULL_IMAGE:latest only
executes for main; specifically update the env key UPDATE_LATEST (or the branch
check used in the run block) so it uses inputs.branch == 'main' and leave the
TAGS/FULL_IMAGE/TAGS append logic unchanged.
- Around line 84-98: Update the GitHub Actions step versions to pinned, current
releases: replace the aquasecurity action reference
aquasecurity/trivy-action@0.33.1 with aquasecurity/trivy-action@0.34.0, and
replace the upload action github/codeql-action/upload-sarif@v4 with a fixed
release tag such as github/codeql-action/upload-sarif@v4.31.10 (or another
chosen exact v4.x.y), ensuring the steps that use image-ref, format, output and
sarif_file remain unchanged.

---

Duplicate comments:
In @.github/workflows/release-finalize.yml:
- Around line 72-105: The heredoc blocks using "cat <<-EOF > release_notes.md"
are indented with spaces so the dash-only variant only strips tabs and leaves
leading spaces in the generated release_notes.md; update each "cat <<-EOF >
release_notes.md" block so the heredoc content is dedented to column 0 (move the
lines starting with "## Changes in $IMPLEMENTATION", "$COMMITS", and the "No
changes..." text to flush-left) or alternatively replace the indented "cat
<<-EOF" usage with an unindented "cat <<'EOF' > release_notes.md" / dedented
heredoc closing token so the output has no extra leading spaces; ensure you
update all occurrences that reference $IMPLEMENTATION and $COMMITS accordingly.

In `@python-interpreter/packages/afm-core/tests/test_cli.py`:
- Around line 54-61: Test already updated to handle PackageNotFoundError for
afm-cli in test_version: keep the try/except around the assert
meta_version("afm-cli") in result.output and ensure the fallback assertion
asserting f"afm-core {meta_version('afm-core')}" remains in the except block;
verify the test function name test_version and calls to runner.invoke(cli,
["--version"]) and meta_version are unchanged so the behavior and fallback are
preserved.

---

Nitpick comments:
In @.github/workflows/release-ballerina.yml:
- Around line 91-112: The workflow checkout step in the "test" job sets
fetch-depth: 0 which forces a full clone; remove the fetch-depth key (or set it
to 1) in the "Checkout repository" step that uses actions/checkout@v4 so the
test job uses the default shallow clone and runs faster; update the checkout
step under the test job accordingly (refer to the "test" job and the "Checkout
repository" step that uses actions/checkout@v4 and the fetch-depth field).

In @.github/workflows/release-docker.yml:
- Around line 84-98: The Trivy scan ("Scan Docker image for vulnerabilities")
runs after the image push step, so a failed scan still leaves a published image;
change the workflow to build the image without pushing (use the existing build
step or docker/build-push-action with push: false and tag matching
steps.docker-tags.outputs.FULL_IMAGE), run the Trivy scan against that local tag
(image-ref pointing to the local image), and only run the push step (the current
"Push Docker image" step) conditionally on the scan succeeding (use job/step
outputs or job status to gate the push); keep the "Upload Trivy scan results"
step but run it regardless for visibility.

In `@python-interpreter/packages/afm-core/tests/test_cli.py`:
- Around line 237-240: The test calls runner.invoke(cli, ["run",
str(sample_agent_path), "--port", "9000"]) but doesn't check the invocation
result; capture the return value (e.g., result = runner.invoke(...)) and assert
result.exit_code == 0 to ensure the CLI completed successfully. Keep the
existing uvicorn assertions (mock_uvicorn.run.called or
mock_uvicorn.Config.called) but add the exit_code check immediately after the
invoke to catch failures from cli/run before or after uvicorn setup.
- Around line 165-169: The three test functions
test_requires_at_least_one_interface, test_creates_app_with_webhook, and
test_creates_app_with_both_interfaces declare the fixture parameter
sample_minimal_path but never use it; remove the unused parameter from each test
signature (i.e., delete ", sample_minimal_path: Path" or "sample_minimal_path"
from the def lines) so the fixture is not unnecessarily invoked and the test
intent is clearer; verify no other references to sample_minimal_path exist
inside those functions and run tests to confirm no further changes are needed.

In `@python-interpreter/README.md`:
- Around line 43-49: The README uses the Docker image name
afm-langchain-interpreter which implies a LangChain-only build; update the
docker tag and run references to a more generic name (e.g.,
afm-python-interpreter or afm-interpreter) by changing the build command (docker
build -t afm-langchain-interpreter .) and the run command invocation
(afm-langchain-interpreter afm /app/agent.afm.md) to the chosen generic name,
and if the image truly bundles LangChain by default, keep the current name but
add a brief note explaining that it includes the LangChain backend.

Comment on lines +158 to +162

VERSION_FILE="ballerina-interpreter/Ballerina.toml"
sed -i "s/^version = \".*\"/version = \"$NEXT_VERSION\"/" "$VERSION_FILE"
git add "$VERSION_FILE"
git commit -m "Bump $IMPLEMENTATION version to $NEXT_VERSION"
git commit -m "Bump ballerina-interpreter version to $NEXT_VERSION"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find Ballerina.toml files in the repository
fd "Ballerina.toml" --type f

Repository: wso2/reference-implementations-afm

Length of output: 114


🏁 Script executed:

# Read the Ballerina.toml file and check for lines matching ^version =
cat ballerina-interpreter/Ballerina.toml

Repository: wso2/reference-implementations-afm

Length of output: 1777


sed replaces all version entries in Ballerina.toml, corrupting dependency versions.

Line 160's sed -i "s/^version = \".*\"/version = \"$NEXT_VERSION\"/" replaces every line matching ^version = "...". The Ballerina.toml file has 10 such lines: the package version (line 3) plus 9 dependency versions (lines 10, 15, 20, 25, 30, 35, 40, 45, 50). This would set all dependency versions to $NEXT_VERSION, breaking the build. The read step (line 53) correctly uses head -1 to grab only the first match, but the write step lacks this constraint.

📝 Proposed fix — limit replacement to the first match
-          sed -i "s/^version = \".*\"/version = \"$NEXT_VERSION\"/" "$VERSION_FILE"
+          sed -i '0,/^version = ".*"/{s/^version = ".*"/version = "'"$NEXT_VERSION"'"/}' "$VERSION_FILE"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/release-ballerina.yml around lines 158 - 162, The current
sed command replaces every line matching ^version = "..." in VERSION_FILE,
corrupting dependency versions; change the replacement to only affect the first
match by using the sed range for the first occurrence, e.g. replace the sed
invocation in the block that touches VERSION_FILE with: sed -i '0,/^version =
".*"/s//version = "'"$NEXT_VERSION"'"' "$VERSION_FILE" (i.e. update the sed
command that currently reads sed -i "s/^version = \".*\"/version =
\"$NEXT_VERSION\"/" to use the 0,/pattern/ range so only the first ^version =
... line is updated).

- Handle scenarios wehre only afm-core is installed
- Notify every time instead of one time as other applications do
Previously no matter the installation method used, the helper text would
suggest uv add afm-langchain. With this change similar to update
notifications we will see a dynamic msg.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive architectural refactor of the Python AFM interpreter, transitioning from a monolithic structure to a modular multi-package workspace with pluggable execution backends.

Changes:

  • Restructures code into 3 packages: afm-core (parser, CLI, protocols), afm-langchain (LangChain backend), and afm-cli (metapackage)
  • Introduces AgentRunner protocol for pluggable backends with entry-point-based discovery
  • Adds comprehensive update notification system with package manager detection
  • Refactors CI/CD to support independent package releases with dedicated workflows for Python and Ballerina

Reviewed changes

Copilot reviewed 61 out of 72 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
python-interpreter/pyproject.toml Workspace configuration with uv and pytest settings
python-interpreter/packages/afm-core/pyproject.toml Core package configuration (v0.1.7) with CLI and interface dependencies
python-interpreter/packages/afm-langchain/pyproject.toml LangChain backend (v0.1.4) with runner entry point registration
python-interpreter/packages/afm-cli/pyproject.toml Metapackage (v0.2.10) that bundles core and langchain
python-interpreter/packages/afm-core/src/afm/runner.py AgentRunner protocol and pluggable backend discovery system
python-interpreter/packages/afm-core/src/afm/update.py Update checker with PyPI integration and install command detection
python-interpreter/packages/afm-core/src/afm/cli.py Refactored CLI with validate, run, and framework commands
python-interpreter/packages/afm-core/src/afm/parser.py Added resolve_env parameter for validation without env vars
python-interpreter/packages/afm-langchain/src/afm_langchain/backend.py Renamed Agent class to LangChainRunner
.github/workflows/release-python.yml New Python release workflow supporting per-package releases
.github/workflows/release-docker.yml Reusable Docker build/push/scan workflow
.github/workflows/release-finalize.yml Reusable finalization workflow for tags and releases
python-interpreter/Dockerfile Updated multi-stage build for workspace packages
Multiple test files Updated imports, mocks, and fixtures for new architecture

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@MaryamZi MaryamZi requested a review from Copilot February 24, 2026 05:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 61 out of 72 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@RadCod3 RadCod3 requested a review from Copilot February 24, 2026 06:06
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 61 out of 72 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

python-interpreter/packages/afm-core/src/afm/update.py:1

  • The magic number 0x00000008 lacks clarity. Consider defining a named constant like DETACHED_PROCESS = 0x00000008 to improve code readability and maintainability.
    python-interpreter/packages/afm-core/tests/test_update.py:1
  • In test file line references, this appears to test the actual implementation. The test for Windows-specific creationflags should verify the flag is set correctly. Consider adding an explicit test case that verifies the DETACHED_PROCESS flag (0x00000008) is used on Windows platforms.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

README.md Outdated
Contributions are welcome! When adding a new implementation:

1. Create a new directory: `{language/framework}-{type}/` (e.g., `langchain-interpreter/`)
1. Create a new directory: `{language/framework}-{type}/` (e.g., `python-interpreter/`)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and the rest still says python?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 4b9b515

Copy link
Copy Markdown
Contributor Author

@RadCod3 RadCod3 Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following our offline discussion, refactored the README files to reduce confusion b68cbb3

README.md Outdated
├── ballerina-interpreter/ # Ballerina-based AFM interpreter
├── langchain-interpreter/ # LangChain-based AFM interpreter
└── .github/workflows/ # CI/CD (path-filtered per implementation)
├── python-interpreter/ # Python-based AFM interpreters (plugin-based)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have langchain-interpreter also here (one level down)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6fa87ef

README.md Outdated
Comment on lines +24 to +25
> [!NOTE]
> Python-based implementations share a common runtime and CLI ([`afm-core`](./python-interpreter/packages/afm-core/)) that make it easy to switch between backends. LangChain is currently the supported backend; support for additional frameworks is planned.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would redo this to match the user's PoV as discussed offline.

> [!NOTE]
> Python-based implementations are made available via a single package/CLI tool ... facilitating easily switching between backends. LangChain is currently the supported backend; support for additional frameworks is planned.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 6fa87ef

RadCod3 and others added 2 commits February 25, 2026 10:44
Co-authored-by: Maryam Ziyad <maryamziyadm@gmail.com>
@MaryamZi MaryamZi closed this Feb 25, 2026
@MaryamZi MaryamZi reopened this Feb 25, 2026
@MaryamZi MaryamZi merged commit 815af5e into wso2:main Feb 25, 2026
6 checks passed
@RadCod3 RadCod3 deleted the feat/afm-cli branch February 27, 2026 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Work on PyPi publishing of langchain interpreter

3 participants