Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'

- name: Install dependencies
run: |
Expand All @@ -39,7 +39,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'

- name: Install dependencies
run: |
Expand Down
17 changes: 17 additions & 0 deletions Docs/konflux-integration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ Key steps:
| `ab-eval-db-credentials` | Store results in PostgreSQL |
| `minio-credentials` | Upload artifacts to MinIO/S3 |
| `monitoring-slack-webhook` | Send degradation alerts to Slack |
| `a2a-agent-credentials` | Bearer token for JWT-protected A2A agents (`eval-engine=a2a` only) |

### Creating Workload Cluster Credentials

Expand All @@ -310,6 +311,22 @@ stringData:

See `config/konflux/secrets-template.yaml` for the full template.

### Creating A2A Agent Credentials

Only needed when `eval-engine=a2a` and the target agent requires a Bearer
token (e.g. JWT-protected endpoints). If this secret doesn't exist, the
`agent-auth-token-secret` lookup is optional and no `Authorization` header
is sent — existing no-auth agents are unaffected.

```bash
oc create secret generic a2a-agent-credentials \
--from-literal=token="<your-agent-jwt>" \
-n <your-tenant-namespace>
```

If your secret has a different name, pass it via the `agent-auth-token-secret`
pipeline parameter (default: `a2a-agent-credentials`).

## Tekton Bundles

The core tasks are published as Tekton Bundles to Quay.io:
Expand Down
2 changes: 2 additions & 0 deletions Docs/manual_trigger_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ A2A monitoring runs can be triggered three ways (plus manual runs). Choose the r

**Existing agent** (`agent-endpoint`): The pipeline connects to an already-running agent (typically `http://lightspeed-agent.ab-eval-flow.svc:8000`). Do not set `agent-image`/`agent-tag` unless you want a fresh deploy.

**JWT-protected agent** (optional): If the agent requires a Bearer token, create an `a2a-agent-credentials` Secret with a `token` key in the pipeline's namespace (see `Docs/konflux-integration-guide.md#creating-a2a-agent-credentials`). No params need to change — the pipeline picks it up automatically via the `agent-auth-token-secret` param (default: `a2a-agent-credentials`). If the secret doesn't exist, no `Authorization` header is sent, so agents that don't require auth are unaffected.

### 1. Quay Push Webhook

Fires when a new image is pushed to `quay.io/ecosystem-appeng/google-lightspeed-agent` (excluding `sha256:` digest tags and `on-pr-*` PR tags).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ The pipeline is LLM-agnostic. Three modes are supported:
- Container registry (Quay.io) with push credentials
- Harbor fork with OpenShift backend
- LLM access (one of the three modes above)
- Python 3.11+
- Python 3.12+

## Documentation

Expand Down
40 changes: 34 additions & 6 deletions abevalflow/harbor_agents/a2a_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,27 @@
harbor run -p tasks/my-eval \\
--agent-import-path abevalflow.harbor_agents.a2a_adapter:A2AAgent \\
--ak endpoint=https://my-agent.example.com \\
--ak timeout=120
--ak timeout=120 \\
--ak auth_token=<bearer-jwt> \\
--ak verify_ssl=true

Usage in Harbor config YAML:
agents:
- import_path: "abevalflow.harbor_agents.a2a_adapter:A2AAgent"
kwargs:
endpoint: "https://my-agent.example.com"
timeout: 120
auth_token: "<bearer-jwt>" # optional; falls back to AGENT_AUTH_TOKEN env
blocking: true # optional; default True, see A2AAgent.__init__
verify_ssl: true # optional; default False (preserves prior behavior),
# enable when the endpoint has a CA-trusted cert
"""

from __future__ import annotations

import json
import logging
import os
import uuid
from pathlib import Path
from typing import Any
Expand Down Expand Up @@ -72,6 +79,9 @@ def __init__(
context_id: str | None = None,
model_name: str | None = None,
extra_env: dict[str, str] | None = None,
auth_token: str | None = None,
blocking: bool = True,
verify_ssl: bool = False,
**kwargs,
):
"""Initialize the A2A agent adapter.
Expand All @@ -82,14 +92,28 @@ def __init__(
timeout: Request timeout in seconds (default: 120).
context_id: Optional context ID for conversation continuity.
model_name: Optional model name for logging/tracking.
extra_env: Extra environment variables (unused but accepted for compatibility).
auth_token: Optional bearer token for Authorization header (also reads AGENT_AUTH_TOKEN env).
blocking: Whether to request synchronous completion via `configuration.blocking`
in the `message/send` request (default: True). Some A2A servers only
populate `result.artifacts` for the caller when this is set, otherwise the
response may come back before the agent has finished and appear empty.
verify_ssl: Whether to verify TLS certificates when calling the A2A endpoint
(default: False, matching prior behavior). Most internal OpenShift/Kubernetes
Routes use self-signed or cluster-internal certs, so verification is skipped
by default. Set to True when the endpoint has a certificate trusted by the
caller's CA bundle.
**kwargs: Additional arguments passed to BaseAgent.
"""
super().__init__(logs_dir=logs_dir, model_name=model_name, **kwargs)
self.endpoint = endpoint.rstrip("/")
self.timeout = timeout
self.context_id = context_id
self.blocking = blocking
self.verify_ssl = verify_ssl
self._extra_env = extra_env or {}
self._auth_token = (
auth_token or self._extra_env.get("AGENT_AUTH_TOKEN") or os.environ.get("AGENT_AUTH_TOKEN") or ""
)

@staticmethod
def name() -> str:
Expand Down Expand Up @@ -126,11 +150,12 @@ async def run(
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"configuration": {"blocking": self.blocking},
"message": {
"messageId": message_id,
"role": "user",
"parts": [{"text": instruction}],
}
"parts": [{"kind": "text", "text": instruction}],
},
},
"id": request_id,
}
Expand Down Expand Up @@ -164,13 +189,16 @@ async def _send_request(self, payload: dict[str, Any]) -> dict[str, Any]:
The JSON response from the A2A agent.
"""
timeout = aiohttp.ClientTimeout(total=self.timeout)
headers = {"Content-Type": "application/json"}
if self._auth_token:
headers["Authorization"] = f"Bearer {self._auth_token}"

async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(
self.endpoint,
json=payload,
headers={"Content-Type": "application/json"},
ssl=False,
headers=headers,
ssl=self.verify_ssl,
) as response:
response.raise_for_status()
return await response.json()
Expand Down
14 changes: 14 additions & 0 deletions config/konflux/secrets-template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# minio-credentials : Upload artifacts to MinIO/S3
# monitoring-slack-webhook: Send degradation alerts to Slack
# promptfoo-cloud-credentials: Share red-team results to Promptfoo Cloud (optional)
# a2a-agent-credentials : Bearer token for JWT-protected A2A agents (eval-engine=a2a only)
---
# workload-cluster-credentials
# ONLY required when EVAL_MODE=remote (cross-cluster evaluation).
Expand Down Expand Up @@ -55,6 +56,19 @@ type: Opaque
stringData:
token: "<REPLACE_WITH_COMPASS_API_TOKEN>"
---
# a2a-agent-credentials (OPTIONAL)
# Bearer token for JWT-protected A2A agents. Only used when eval-engine=a2a
# and the target agent requires authentication. If this secret doesn't
# exist, no Authorization header is sent (unchanged default behavior).
apiVersion: v1
kind: Secret
metadata:
name: a2a-agent-credentials
namespace: <YOUR-KONFLUX-TENANT-NAMESPACE>
type: Opaque
stringData:
token: "<REPLACE_WITH_A2A_AGENT_JWT>"
---
# promptfoo-cloud-credentials (OPTIONAL)
# API key for Promptfoo Cloud to share red-team results.
# If not configured, red-team runs locally without cloud sharing.
Expand Down
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ <h1>Agentic Eval Flow</h1>
<h2>Make AI artifact evaluation<br><span class="grad">automated, measurable, and observable</span></h2>
<p>Tekton-orchestrated pipeline on OpenShift that evaluates <span class="hero-emphasis">skills</span>, <span class="hero-emphasis">agents</span>, and <span class="hero-emphasis">MCP servers</span> through A/B testing, statistical analysis, and multi-gate certification.</p>
<div class="badges">
<span class="badge">Python 3.11+</span>
<span class="badge">Python 3.12+</span>
<span class="badge">Apache-2.0</span>
<span class="badge">Tekton / OpenShift</span>
<span class="badge">PostgreSQL</span>
Expand Down
31 changes: 31 additions & 0 deletions examples/a2a-agent-eval/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@ harbor run \
--n-attempts 3
```

#### Optional agent kwargs

The A2A adapter accepts a few optional `--ak` flags beyond `endpoint`/`timeout`:

| Kwarg | Default | Description |
|-------|---------|-------------|
| `auth_token` | none | Bearer token sent as `Authorization: Bearer <token>` for JWT-protected agents. Also read from the `AGENT_AUTH_TOKEN` env var if not set. |
| `blocking` | `true` | Requests synchronous completion (`configuration.blocking`) so `result.artifacts` is populated. Set to `false` only if your agent requires async/polling semantics. |
| `verify_ssl` | `false` | TLS certificate verification for the agent endpoint. Defaults to `false` because most internal OpenShift/Kubernetes Routes use self-signed or cluster-internal certs. Set to `true` if your endpoint has a CA-trusted certificate. |

```bash
# Example: JWT-protected agent with a CA-trusted certificate
harbor run \
-p examples/a2a-agent-eval/tasks/lightspeed-qa \
--agent-import-path abevalflow.harbor_agents.a2a_adapter:A2AAgent \
--ak endpoint=$A2A_ENDPOINT \
--ak timeout=120 \
--ak auth_token=$A2A_AUTH_TOKEN \
--ak verify_ssl=true \
-e podman \
--n-attempts 3
```

### Via Agentic Eval Flow Pipeline (Tekton)

```bash
Expand All @@ -35,6 +58,13 @@ tkn pipeline start abevalflow-ci-pipeline \
--param submission-name=lightspeed-qa-eval
```

If the agent requires a Bearer token, create an `a2a-agent-credentials` Secret
(with a `token` key) in the pipeline's namespace beforehand — see
[`Docs/konflux-integration-guide.md`](../../Docs/konflux-integration-guide.md#creating-a2a-agent-credentials).
No extra params are needed unless your secret has a different name (use
`--param agent-auth-token-secret=<name>` in that case). If the secret doesn't
exist, the pipeline runs exactly as before with no `Authorization` header.

## Task Structure

```
Expand Down Expand Up @@ -121,6 +151,7 @@ def grade() -> dict:
| `LLM_JUDGE_MODEL` | Model for LLM-as-judge | `openai/claude-sonnet` |
| `LLM_BASE_URL` | LiteLLM proxy URL | `http://litellm.ab-eval-flow.svc.cluster.local:4000` |
| `A2A_ENDPOINT` | Agent endpoint URL | (required) |
| `AGENT_AUTH_TOKEN` | Bearer token for JWT-protected agents (fallback if `--ak auth_token` isn't set) | none |

## Troubleshooting

Expand Down
33 changes: 27 additions & 6 deletions pipeline/tasks/konflux/evaluate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ spec:
type: string
default: "120"
description: A2A agent request timeout in seconds
- name: agent-auth-token-secret
type: string
default: "a2a-agent-credentials"
description: >-
Name of a Secret (in this task's namespace) with a "token" key holding
a Bearer token for JWT-protected A2A agents. Optional: if the secret
doesn't exist, no Authorization header is sent (unchanged default behavior).
- name: mcp-url
type: string
default: ""
Expand Down Expand Up @@ -155,6 +162,12 @@ spec:
name: llm-credentials
key: api-key
optional: true
- name: AGENT_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: $(params.agent-auth-token-secret)
key: token
optional: true
script: |
#!/usr/bin/env bash
set -euo pipefail
Expand All @@ -165,6 +178,8 @@ spec:
SUBMISSION_NAME="$(params.submission-name)"
SUBMISSION_DIR="$(params.submission-dir)"
AGENT_ENDPOINT="$(params.agent-endpoint)"
# Optional; unset when the secret/key doesn't exist (optional: true above).
AGENT_AUTH_TOKEN="${AGENT_AUTH_TOKEN:-}"
MCP_URL="$(params.mcp-url)"
COMMIT_SHA="$(params.commit-sha)"
PIPELINE_RUN_ID="$(params.pipeline-run-id)"
Expand Down Expand Up @@ -305,14 +320,17 @@ spec:

echo "Task: \$(basename \$TASK_DIR) | Attempts: \$N_ATTEMPTS"

python3 - "\$RESULTS_DIR" "\$TASK_DIR" "\$N_ATTEMPTS" "$AGENT_ENDPOINT" "$(params.agent-timeout)" "$(params.llm-api-base)" "openai/$(params.llm-model)" <<'GENCFG'
python3 - "\$RESULTS_DIR" "\$TASK_DIR" "\$N_ATTEMPTS" "$AGENT_ENDPOINT" "$(params.agent-timeout)" "$(params.llm-api-base)" "openai/$(params.llm-model)" "$AGENT_AUTH_TOKEN" <<'GENCFG'
import sys, yaml
results_dir, task_dir, n_attempts, endpoint, timeout, llm_api_base, llm_model = sys.argv[1:8]
results_dir, task_dir, n_attempts, endpoint, timeout, llm_api_base, llm_model, auth_token = sys.argv[1:9]
agent_kwargs = {"endpoint": endpoint, "timeout": int(timeout)}
if auth_token:
agent_kwargs["auth_token"] = auth_token
config = {
"job_name": "a2a-eval",
"jobs_dir": results_dir,
"n_attempts": int(n_attempts),
"agents": [{"import_path": "abevalflow.harbor_agents.a2a_adapter:A2AAgent", "kwargs": {"endpoint": endpoint, "timeout": int(timeout)}}],
"agents": [{"import_path": "abevalflow.harbor_agents.a2a_adapter:A2AAgent", "kwargs": agent_kwargs}],
"tasks": [{"path": task_dir}],
"environment": {"type": "local"},
"verifier": {"env": {"LLM_JUDGE_MODEL": llm_model, "LLM_API_BASE": llm_api_base, "OPENAI_API_KEY": "sk-dummy"}}
Expand Down Expand Up @@ -532,14 +550,17 @@ spec:
LLM_API_BASE="$(params.llm-api-base)"
LLM_MODEL="openai/$(params.llm-model)"

python3 - "$CONFIG_FILE" "$TASK_DIR" "$RESULTS_DIR" "$N_ATTEMPTS" "$AGENT_ENDPOINT" "$(params.agent-timeout)" "$LLM_API_BASE" "$LLM_MODEL" <<'GENCFG'
python3 - "$CONFIG_FILE" "$TASK_DIR" "$RESULTS_DIR" "$N_ATTEMPTS" "$AGENT_ENDPOINT" "$(params.agent-timeout)" "$LLM_API_BASE" "$LLM_MODEL" "$AGENT_AUTH_TOKEN" <<'GENCFG'
import sys, yaml
config_file, task_dir, results_dir, n_attempts, endpoint, timeout, llm_api_base, llm_model = sys.argv[1:9]
config_file, task_dir, results_dir, n_attempts, endpoint, timeout, llm_api_base, llm_model, auth_token = sys.argv[1:10]
agent_kwargs = {"endpoint": endpoint, "timeout": int(timeout)}
if auth_token:
agent_kwargs["auth_token"] = auth_token
config = {
"job_name": "a2a-eval",
"jobs_dir": results_dir,
"n_attempts": int(n_attempts),
"agents": [{"import_path": "abevalflow.harbor_agents.a2a_adapter:A2AAgent", "kwargs": {"endpoint": endpoint, "timeout": int(timeout)}}],
"agents": [{"import_path": "abevalflow.harbor_agents.a2a_adapter:A2AAgent", "kwargs": agent_kwargs}],
"tasks": [{"path": task_dir}],
"environment": {"type": "local"},
"verifier": {"env": {"LLM_JUDGE_MODEL": llm_model, "LLM_API_BASE": llm_api_base, "OPENAI_API_KEY": "sk-dummy"}}
Expand Down
Loading
Loading