Skip to content

Commit 55676f0

Browse files
authored
Merge pull request #10 from RadCod3/feat/langchain-stdio-support
Feat/langchain stdio support
2 parents 0f25b22 + e1706e7 commit 55676f0

File tree

23 files changed

+1296
-234
lines changed

23 files changed

+1296
-234
lines changed

.github/workflows/python-interpreter.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,12 +73,13 @@ jobs:
7373
OWNER_LOWER=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')
7474
echo "image_name=ghcr.io/$OWNER_LOWER/afm-langchain-interpreter" >> $GITHUB_OUTPUT
7575
76-
- name: Build and push Docker image
76+
- name: Build and push full image
7777
uses: docker/build-push-action@v5
7878
with:
7979
context: python-interpreter
8080
push: true
8181
platforms: linux/amd64,linux/arm64
82+
build-args: VARIANT=full
8283
tags: |
8384
${{ steps.meta.outputs.image_name }}:latest
8485
${{ steps.meta.outputs.image_name }}:${{ github.sha }}
@@ -90,3 +91,22 @@ jobs:
9091
annotations: |
9192
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
9293
index:org.opencontainers.image.licenses=Apache-2.0
94+
95+
- name: Build and push slim image
96+
uses: docker/build-push-action@v5
97+
with:
98+
context: python-interpreter
99+
push: true
100+
platforms: linux/amd64,linux/arm64
101+
build-args: VARIANT=slim
102+
tags: |
103+
${{ steps.meta.outputs.image_name }}:slim
104+
${{ steps.meta.outputs.image_name }}:${{ github.sha }}-slim
105+
labels: |
106+
org.opencontainers.image.source=https://github.com/${{ github.repository }}
107+
org.opencontainers.image.revision=${{ github.sha }}
108+
org.opencontainers.image.title=AFM LangChain Interpreter (Slim)
109+
org.opencontainers.image.licenses=Apache-2.0
110+
annotations: |
111+
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
112+
index:org.opencontainers.image.licenses=Apache-2.0

.github/workflows/release-ballerina.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ jobs:
130130
with:
131131
tag: ${{ needs.validate.outputs.tag }}
132132
implementation: ballerina-interpreter
133+
package: ballerina-interpreter
133134
version: ${{ needs.validate.outputs.release_version }}
134135
branch: ${{ inputs.branch }}
135136
is_rerelease: false

.github/workflows/release-docker.yml

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,23 @@ jobs:
5757
# GHCR requires lowercase repository names
5858
OWNER_LOWER=$(echo "$OWNER" | tr '[:upper:]' '[:lower:]')
5959
FULL_IMAGE="ghcr.io/$OWNER_LOWER/$IMAGE_NAME"
60-
TAGS="$FULL_IMAGE:v$VERSION"
61-
if [ "$UPDATE_LATEST" = "true" ]; then
62-
TAGS="$TAGS,$FULL_IMAGE:latest"
63-
fi
64-
echo "TAGS=$TAGS" >> $GITHUB_OUTPUT
60+
TAGS_FULL="$FULL_IMAGE:v$VERSION"
61+
[ "$UPDATE_LATEST" = "true" ] && TAGS_FULL="$TAGS_FULL,$FULL_IMAGE:latest"
62+
echo "TAGS_FULL=$TAGS_FULL" >> $GITHUB_OUTPUT
63+
64+
TAGS_SLIM="$FULL_IMAGE:v$VERSION-slim"
65+
[ "$UPDATE_LATEST" = "true" ] && TAGS_SLIM="$TAGS_SLIM,$FULL_IMAGE:slim"
66+
echo "TAGS_SLIM=$TAGS_SLIM" >> $GITHUB_OUTPUT
6567
echo "FULL_IMAGE=$FULL_IMAGE" >> $GITHUB_OUTPUT
6668
67-
- name: Build and push Docker image
69+
- name: Build and push full image
6870
uses: docker/build-push-action@v5
6971
with:
7072
context: ${{ inputs.context }}
7173
push: true
7274
platforms: linux/amd64,linux/arm64
73-
tags: ${{ steps.docker-tags.outputs.TAGS }}
75+
build-args: VARIANT=full
76+
tags: ${{ steps.docker-tags.outputs.TAGS_FULL }}
7477
labels: |
7578
org.opencontainers.image.source=https://github.com/${{ github.repository }}
7679
org.opencontainers.image.version=${{ inputs.version }}
@@ -81,6 +84,24 @@ jobs:
8184
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
8285
index:org.opencontainers.image.licenses=Apache-2.0
8386
87+
- name: Build and push slim image
88+
uses: docker/build-push-action@v5
89+
with:
90+
context: ${{ inputs.context }}
91+
push: true
92+
platforms: linux/amd64,linux/arm64
93+
build-args: VARIANT=slim
94+
tags: ${{ steps.docker-tags.outputs.TAGS_SLIM }}
95+
labels: |
96+
org.opencontainers.image.source=https://github.com/${{ github.repository }}
97+
org.opencontainers.image.version=${{ inputs.version }}
98+
org.opencontainers.image.revision=${{ github.sha }}
99+
org.opencontainers.image.title=${{ inputs.image_title }} (Slim)
100+
org.opencontainers.image.licenses=Apache-2.0
101+
annotations: |
102+
index:org.opencontainers.image.source=https://github.com/${{ github.repository }}
103+
index:org.opencontainers.image.licenses=Apache-2.0
104+
84105
- name: Scan Docker image for vulnerabilities
85106
uses: aquasecurity/trivy-action@0.34.0
86107
with:

.github/workflows/release-finalize.yml

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ on:
1919
description: 'Branch to release from'
2020
required: true
2121
type: string
22+
package:
23+
description: 'Package name (e.g., afm-core, afm-langchain, ballerina-interpreter)'
24+
required: true
25+
type: string
2226
is_rerelease:
2327
description: 'Whether this is a re-release'
2428
required: false
@@ -41,24 +45,26 @@ jobs:
4145
env:
4246
TAG: ${{ inputs.tag }}
4347
IMPLEMENTATION: ${{ inputs.implementation }}
48+
PACKAGE: ${{ inputs.package }}
4449
VERSION: ${{ inputs.version }}
4550
run: |
4651
git config user.name "github-actions[bot]"
4752
git config user.email "github-actions[bot]@users.noreply.github.com"
48-
git checkout -b "release-${TAG%-v*}-$VERSION"
53+
git checkout -b "release-$PACKAGE-$VERSION"
4954
git tag "$TAG"
50-
git push origin "release-${TAG%-v*}-$VERSION"
55+
git push origin "release-$PACKAGE-$VERSION"
5156
git push origin "$TAG"
5257
5358
- name: Generate release notes
5459
env:
5560
TAG: ${{ inputs.tag }}
5661
IMPLEMENTATION: ${{ inputs.implementation }}
62+
PACKAGE: ${{ inputs.package }}
5763
VERSION: ${{ inputs.version }}
5864
IS_RERELEASE: ${{ inputs.is_rerelease }}
5965
run: |
6066
# Find previous tag for this implementation (exclude current version)
61-
PREV_TAG=$(git tag -l "${TAG%-v*}-v*" --sort=-v:refname | grep -v "^$TAG$" | head -1)
67+
PREV_TAG=$(git tag -l "$PACKAGE-v*" --sort=-v:refname | grep -v "^$TAG$" | head -1)
6268
6369
# Generate notes from commits that touched this implementation
6470
if [ -n "$PREV_TAG" ]; then
@@ -108,9 +114,10 @@ jobs:
108114
env:
109115
GH_TOKEN: ${{ github.token }}
110116
TAG: ${{ inputs.tag }}
117+
PACKAGE: ${{ inputs.package }}
111118
VERSION: ${{ inputs.version }}
112119
run: |
113-
TITLE="${TAG%-v*} v$VERSION"
120+
TITLE="$PACKAGE v$VERSION"
114121
gh release create "$TAG" \
115122
--title "$TITLE" \
116123
--notes-file release_notes.md

.github/workflows/release-python.yml

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
TAG: ${{ steps.version.outputs.TAG }}
8181
run: |
8282
if git rev-parse "$TAG" >/dev/null 2>&1; then
83-
echo "::error::Tag $TAG already exists. Use re-release workflow to overwrite."
83+
echo "::error::Tag $TAG already exists. Delete the existing tag and release branch before re-running, or bump the version in pyproject.toml."
8484
exit 1
8585
fi
8686
@@ -92,7 +92,7 @@ jobs:
9292
run: |
9393
RELEASE_BRANCH="release-$PACKAGE-$VERSION"
9494
if git ls-remote --exit-code --heads origin "$RELEASE_BRANCH" >/dev/null 2>&1; then
95-
echo "::error::Release branch $RELEASE_BRANCH already exists. Use re-release workflow to overwrite."
95+
echo "::error::Release branch $RELEASE_BRANCH already exists. Delete the existing tag and release branch before re-running, or bump the version in pyproject.toml."
9696
exit 1
9797
fi
9898
@@ -150,18 +150,26 @@ jobs:
150150
uv build --package afm-langchain
151151
fi
152152
153-
- name: Publish to PyPI
153+
- name: Publish afm-core to PyPI
154+
if: inputs.package == 'afm-core'
154155
working-directory: python-interpreter
155156
env:
156-
PACKAGE: ${{ inputs.package }}
157-
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
158-
run: |
159-
if [ "$PACKAGE" = "afm-core" ]; then
160-
uv publish dist/afm_core-*
161-
uv publish dist/afm_cli-*
162-
elif [ "$PACKAGE" = "afm-langchain" ]; then
163-
uv publish dist/afm_langchain-*
164-
fi
157+
UV_PUBLISH_TOKEN: ${{ secrets.AFM_CORE_PYPI_API_TOKEN }}
158+
run: uv publish dist/afm_core-*
159+
160+
- name: Publish afm-cli to PyPI
161+
if: inputs.package == 'afm-core'
162+
working-directory: python-interpreter
163+
env:
164+
UV_PUBLISH_TOKEN: ${{ secrets.AFM_CLI_PYPI_API_TOKEN }}
165+
run: uv publish dist/afm_cli-*
166+
167+
- name: Publish afm-langchain to PyPI
168+
if: inputs.package == 'afm-langchain'
169+
working-directory: python-interpreter
170+
env:
171+
UV_PUBLISH_TOKEN: ${{ secrets.AFM_LANGCHAIN_PYPI_API_TOKEN }}
172+
run: uv publish dist/afm_langchain-*
165173

166174
docker:
167175
needs: [validate, test]
@@ -189,6 +197,7 @@ jobs:
189197
with:
190198
tag: ${{ needs.validate.outputs.tag }}
191199
implementation: python-interpreter
200+
package: ${{ inputs.package }}
192201
version: ${{ needs.validate.outputs.release_version }}
193202
branch: ${{ inputs.branch }}
194203
is_rerelease: false
@@ -230,6 +239,10 @@ jobs:
230239
uv version --bump patch --package afm-cli --frozen
231240
# Sync the afm-core exact pin in afm-cli's dependencies
232241
NEW_CORE_VERSION=$(uv version --short --package afm-core)
242+
if [[ ! "$NEW_CORE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
243+
echo "::error::Failed to get valid afm-core version, got: '$NEW_CORE_VERSION'"
244+
exit 1
245+
fi
233246
sed -i "s/\"afm-core==.*\"/\"afm-core==$NEW_CORE_VERSION\"/" packages/afm-cli/pyproject.toml
234247
elif [ "$PACKAGE" = "afm-langchain" ]; then
235248
uv version --bump patch --package afm-langchain --frozen

README.md

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ Reference implementations for [Agent-Flavored Markdown (AFM)](https://wso2.githu
77
| Implementation | Language/Framework | Status |
88
|----------------|-------------------|--------|
99
| [ballerina-interpreter](./ballerina-interpreter) | Ballerina | Active |
10-
| [python-interpreter](./python-interpreter) | Python/LangChain | Active |
10+
| [langchain-interpreter](./python-interpreter) | Python/LangChain | Active |
1111

1212
## Repository Structure
1313

1414
```
1515
reference-implementations-afm/
16-
├── ballerina-interpreter/ # Ballerina-based AFM interpreter
17-
├── python-interpreter/ # LangChain-based AFM interpreter
18-
└── .github/workflows/ # CI/CD (path-filtered per implementation)
16+
├── ballerina-interpreter/ # Ballerina-based AFM interpreter
17+
├── python-interpreter/ # LangChain-based AFM interpreter
18+
└── .github/workflows/ # CI/CD (path-filtered per implementation)
1919
```
2020

2121
## Getting Started
@@ -24,13 +24,33 @@ Each implementation has its own README with setup and usage instructions. See th
2424

2525
## Contributing
2626

27-
Contributions are welcome! When adding a new implementation:
27+
Contributions are welcome!
2828

29-
1. Create a new directory: `{language/framework}-{type}/` (e.g., `python-interpreter/`)
29+
### Adding a New Implementation (New Language or Framework)
30+
31+
To add an interpreter in a new language or framework:
32+
33+
1. Create a new directory: `{language/framework}-{type}/` (e.g., `go-interpreter/`)
3034
2. Add a path-filtered workflow in `.github/workflows/`
3135
3. Include a README with setup and usage instructions
3236
4. Follow the [AFM Specification](https://wso2.github.io/agent-flavored-markdown/specification/) for compatibility
3337

38+
### Adding a New Python Execution Backend (Plugin)
39+
40+
The Python interpreter uses a plugin-based architecture. New execution backends (e.g., for LlamaIndex, CrewAI or other agent frameworks) should be contributed as packages inside [`python-interpreter/packages/`](./python-interpreter/packages/).
41+
42+
To add a new Python backend:
43+
44+
1. Create a new package under `python-interpreter/packages/` and add it to the `uv` workspace
45+
2. Implement the `AgentRunner` protocol from [`afm-core`](./python-interpreter/packages/afm-core/)
46+
3. Register your backend via the `afm.runner` entry point in your `pyproject.toml`:
47+
```toml
48+
[project.entry-points."afm.runner"]
49+
your-backend = "your_package.module:YourRunnerClass"
50+
```
51+
4. Use [`afm-langchain`](./python-interpreter/packages/afm-langchain/) as a reference implementation
52+
5. Include a README and tests for your package
53+
3454
## License
3555

3656
Apache License 2.0

python-interpreter/Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,22 @@ RUN --mount=type=cache,target=/root/.cache/uv \
2828
uv sync --frozen --no-editable --package afm-core --package afm-langchain
2929

3030
# Stage 2: Final image
31+
# VARIANT=full installs Node.js, npm (npx), git, and uv/uvx for MCP server support.
32+
# VARIANT=slim ships only Python + .venv.
3133
FROM python:3.13-alpine
34+
ARG VARIANT=full
35+
36+
RUN if [ "$VARIANT" = "full" ]; then \
37+
apk add --no-cache nodejs npm git && \
38+
echo "Full variant: nodejs, npm, git installed"; \
39+
fi
40+
41+
# Install uv and uvx for Python-based MCP server support (full variant only)
42+
COPY --from=ghcr.io/astral-sh/uv:0.10.0 /uv /uvx /uv-bins/
43+
RUN if [ "$VARIANT" = "full" ]; then \
44+
mv /uv-bins/uv /uv-bins/uvx /bin/; \
45+
fi && \
46+
rm -rf /uv-bins
3247

3348
# Set working directory
3449
WORKDIR /app
@@ -39,6 +54,10 @@ COPY --from=builder /app/.venv /app/.venv
3954
# Set environment variables
4055
ENV PATH="/app/.venv/bin:$PATH"
4156
ENV PYTHONUNBUFFERED=1
57+
# Signal to the AFM update checker that it's running inside a container.
58+
# This suppresses package-manager upgrade commands in update notifications
59+
# since users should update by rebuilding or pulling the container image.
60+
ENV AFM_RUNTIME=docker
4261

4362
# Expose default port for web interfaces
4463
EXPOSE 8000

python-interpreter/packages/afm-cli/pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "afm-cli"
3-
version = "0.2.10"
3+
version = "0.3.0"
44
description = "AFM CLI metapackage: installs afm-core and afm-langchain"
55
readme = "README.md"
66
classifiers = [
@@ -11,8 +11,8 @@ license = "Apache-2.0"
1111
requires-python = ">=3.11"
1212
urls = { Repository = "https://github.com/wso2/reference-implementations-afm" }
1313
dependencies = [
14-
"afm-core==0.1.7",
15-
"afm-langchain>=0.1.0",
14+
"afm-core==0.2.0",
15+
"afm-langchain>=0.2.0",
1616
]
1717

1818
[project.scripts]

python-interpreter/packages/afm-core/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "afm-core"
3-
version = "0.1.7"
3+
version = "0.2.0"
44
description = "AFM (Agent-Flavored Markdown) core: parser, CLI, protocols, and interfaces"
55
readme = "README.md"
66
classifiers = [

python-interpreter/packages/afm-core/src/afm/cli.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
)
4040
from .models import (
4141
ConsoleChatInterface,
42+
HttpTransport,
4243
WebChatInterface,
4344
WebhookInterface,
4445
)
@@ -232,7 +233,12 @@ def format_validation_output(afm: AFMRecord) -> str:
232233
lines.append("")
233234
lines.append(" MCP Servers:")
234235
for server in afm.metadata.tools.mcp:
235-
lines.append(f" - {server.name}: {server.transport.url}")
236+
transport = server.transport
237+
if isinstance(transport, HttpTransport):
238+
transport_info = transport.url
239+
else:
240+
transport_info = transport.command
241+
lines.append(f" - {server.name}: {transport_info}")
236242
if server.tool_filter:
237243
if server.tool_filter.allow:
238244
lines.append(f" Allow: {', '.join(server.tool_filter.allow)}")
@@ -499,10 +505,16 @@ def framework_list() -> None:
499505
runners = discover_runners()
500506

501507
if not runners:
508+
from afm.update import _detect_install_command
509+
502510
click.echo("No runner backends found.")
503511
click.echo("")
504-
click.echo("Install a backend package such as 'afm-langchain':")
505-
click.echo(" uv add afm-langchain")
512+
install_cmd = _detect_install_command("afm-langchain")
513+
if install_cmd is None:
514+
click.echo("Use a container image that includes 'afm-langchain'.")
515+
else:
516+
click.echo("Install a backend package such as 'afm-langchain':")
517+
click.echo(f" {install_cmd}")
506518
return
507519

508520
click.echo("Discovered runner backends:")

0 commit comments

Comments
 (0)