From b80d455ed915d2427231523329137370b84df083 Mon Sep 17 00:00:00 2001 From: Vasiliy Radostev Date: Mon, 16 Mar 2026 22:03:33 -0700 Subject: [PATCH 1/2] feat: add AG2 (formerly AutoGen) integration package and sample --- .claude/skills/kagent/SKILL.md | 2 +- CLAUDE.md | 2 +- README.md | 2 +- contrib/cncf/technical-review.md | 2 +- examples/modelconfig-with-tls.yaml | 2 +- python/packages/kagent-ag2/pyproject.toml | 18 +++ .../kagent-ag2/src/kagent/ag2/__init__.py | 7 + .../kagent-ag2/src/kagent/ag2/_a2a.py | 133 ++++++++++++++++++ .../kagent-ag2/src/kagent/ag2/_executor.py | 124 ++++++++++++++++ python/packages/kagent-ag2/tests/__init__.py | 0 .../kagent-ag2/tests/test_executor.py | 42 ++++++ python/samples/ag2/research-report/Dockerfile | 5 + .../ag2/research-report/agent-card.json | 17 +++ python/samples/ag2/research-report/agent.yaml | 21 +++ python/samples/ag2/research-report/main.py | 78 ++++++++++ .../ag2/research-report/pyproject.toml | 9 ++ 16 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 python/packages/kagent-ag2/pyproject.toml create mode 100644 python/packages/kagent-ag2/src/kagent/ag2/__init__.py create mode 100644 python/packages/kagent-ag2/src/kagent/ag2/_a2a.py create mode 100644 python/packages/kagent-ag2/src/kagent/ag2/_executor.py create mode 100644 python/packages/kagent-ag2/tests/__init__.py create mode 100644 python/packages/kagent-ag2/tests/test_executor.py create mode 100644 python/samples/ag2/research-report/Dockerfile create mode 100644 python/samples/ag2/research-report/agent-card.json create mode 100644 python/samples/ag2/research-report/agent.yaml create mode 100644 python/samples/ag2/research-report/main.py create mode 100644 python/samples/ag2/research-report/pyproject.toml diff --git a/.claude/skills/kagent/SKILL.md b/.claude/skills/kagent/SKILL.md index eb42803b6..ce3c34e5d 100644 --- a/.claude/skills/kagent/SKILL.md +++ b/.claude/skills/kagent/SKILL.md @@ -61,7 +61,7 @@ For Helm install, other LLM providers, and provider-specific configuration, see kagent uses Kubernetes CRDs to manage agents, models, and tools: -- **Agent** (`kagent.dev/v1alpha2`) — Defines an AI agent. Two types: **Declarative** (YAML-defined, controller-managed) and **BYO** (custom container image with any framework: Google ADK, OpenAI Agents SDK, LangGraph, CrewAI). +- **Agent** (`kagent.dev/v1alpha2`) — Defines an AI agent. Two types: **Declarative** (YAML-defined, controller-managed) and **BYO** (custom container image with any framework: Google ADK, AG2, OpenAI Agents SDK, LangGraph, CrewAI). - **ModelConfig** (`kagent.dev/v1alpha2`) — Configures LLM provider and model. Agents reference a ModelConfig by name. - **RemoteMCPServer** (`kagent.dev/v1alpha2`) — Connects agents to external MCP tool servers via HTTP. - **MCPServer** (KMCP) — Deploys and manages MCP server pods in the cluster. diff --git a/CLAUDE.md b/CLAUDE.md index e85c54d21..3dea3328a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -68,7 +68,7 @@ kagent/ | Language | Use For | Don't Use For | |----------|---------|---------------| | **Go** | K8s controllers, CLI tools, core APIs, HTTP server, database layer | Agent runtime, LLM integrations, UI | -| **Python** | Agent runtime, ADK, LLM integrations, AI/ML logic | Kubernetes controllers, CLI, infrastructure | +| **Python** | Agent runtime, ADK, AG2, LLM integrations, AI/ML logic | Kubernetes controllers, CLI, infrastructure | | **TypeScript** | Web UI components and API clients only | Backend logic, controllers, agents | **Rule of thumb:** Infrastructure in Go, AI/Agent logic in Python, User interface in TypeScript. diff --git a/README.md b/README.md index 31068ad13..a0da18047 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Kagent has 4 core components: - **Controller**: The controller is a Kubernetes controller that watches the kagent custom resources and creates the necessary resources to run the agents. - **UI**: The UI is a web UI that allows you to manage the agents and tools. -- **Engine**: The engine runs your agents using [ADK](https://google.github.io/adk-docs/). +- **Engine**: The engine runs your agents using [ADK](https://google.github.io/adk-docs/), [AG2](https://docs.ag2.ai) (formerly AutoGen), and other supported frameworks. - **CLI**: The CLI is a command-line tool that allows you to manage the agents and tools. ## Get Involved diff --git a/contrib/cncf/technical-review.md b/contrib/cncf/technical-review.md index 763ff2bd8..85acc84b8 100644 --- a/contrib/cncf/technical-review.md +++ b/contrib/cncf/technical-review.md @@ -40,7 +40,7 @@ The primary use case is enabling AI-powered automation and intelligent operation - Multi-agent coordination for complex operational workflows (via A2A protocol) - Integration with service mesh (Istio), observability (Prometheus/Grafana), and deployment tools (Helm, Argo Rollouts) -- Custom agent development using multiple frameworks (ADK, CrewAI, LangGraph) +- Custom agent development using multiple frameworks (ADK, AG2, CrewAI, LangGraph) **Unsupported Use Cases:** diff --git a/examples/modelconfig-with-tls.yaml b/examples/modelconfig-with-tls.yaml index 7f4905e1d..c4e3dbe43 100644 --- a/examples/modelconfig-with-tls.yaml +++ b/examples/modelconfig-with-tls.yaml @@ -297,7 +297,7 @@ metadata: name: internal-assistant namespace: kagent spec: - # Framework selection (ADK, LangGraph, or CrewAI) + # Framework selection (ADK, AG2, LangGraph, or CrewAI) framework: ADK # Reference to ModelConfig with TLS configuration diff --git a/python/packages/kagent-ag2/pyproject.toml b/python/packages/kagent-ag2/pyproject.toml new file mode 100644 index 000000000..5db001fcf --- /dev/null +++ b/python/packages/kagent-ag2/pyproject.toml @@ -0,0 +1,18 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "kagent-ag2" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "kagent-core>=0.1.0", + "ag2[openai]>=0.11.0", + "a2a-sdk>=0.3.23", + "fastapi>=0.115.0", + "uvicorn>=0.34.0", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/kagent"] diff --git a/python/packages/kagent-ag2/src/kagent/ag2/__init__.py b/python/packages/kagent-ag2/src/kagent/ag2/__init__.py new file mode 100644 index 000000000..b4bbf342f --- /dev/null +++ b/python/packages/kagent-ag2/src/kagent/ag2/__init__.py @@ -0,0 +1,7 @@ +"""AG2 (formerly AutoGen) integration for kagent.""" + +from ._a2a import KAgentApp + +__version__ = "0.1.0" + +__all__ = ["KAgentApp"] diff --git a/python/packages/kagent-ag2/src/kagent/ag2/_a2a.py b/python/packages/kagent-ag2/src/kagent/ag2/_a2a.py new file mode 100644 index 000000000..a0e1b56d6 --- /dev/null +++ b/python/packages/kagent-ag2/src/kagent/ag2/_a2a.py @@ -0,0 +1,133 @@ +"""KAgentApp for AG2 — builds a FastAPI application.""" + +import logging +from collections.abc import Callable + +from a2a.server.request_handling import DefaultRequestHandler +from a2a.server.tasks import InMemoryTaskStore +from a2a.types import AgentCard +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from kagent.core import ( + KAgentConfig, + KAgentRequestContextBuilder, + KAgentTaskStore, + configure_tracing, +) + +from ._executor import AG2AgentExecutor + +try: + from a2a.server.apps import A2AFastAPIApplication +except ImportError: + from a2a.server.apps import A2AStarletteApplication as A2AFastAPIApplication + +logger = logging.getLogger(__name__) + + +class KAgentApp: + """Builds a FastAPI app wrapping an AG2 multi-agent group chat. + + Args: + pattern_factory: Callable returning a fresh AG2 Pattern + per request. + agent_card: A2A AgentCard dict or AgentCard instance. + max_rounds: Max conversation rounds per request. + config: Optional KAgentConfig (reads from env if None). + """ + + def __init__( + self, + pattern_factory: Callable, + agent_card: dict | AgentCard, + max_rounds: int = 20, + config: KAgentConfig | None = None, + ): + self._pattern_factory = pattern_factory + self._max_rounds = max_rounds + self._config = config or KAgentConfig() + + if isinstance(agent_card, dict): + self._agent_card = AgentCard(**agent_card) + else: + self._agent_card = agent_card + + def build(self) -> FastAPI: + """Build and return the FastAPI application.""" + executor = AG2AgentExecutor( + pattern_factory=self._pattern_factory, + max_rounds=self._max_rounds, + ) + + # Use persistent task store if kagent backend is + # available, otherwise in-memory + try: + task_store = KAgentTaskStore( + url=self._config.url, + app_name=self._config.app_name, + ) + except (ConnectionError, OSError, ValueError) as e: + logger.warning( + "kagent backend not available (%s), using " + "in-memory task store", + e, + ) + task_store = InMemoryTaskStore() + + request_handler = DefaultRequestHandler( + agent_executor=executor, + task_store=task_store, + context_builder=KAgentRequestContextBuilder(), + ) + + a2a_app = A2AFastAPIApplication( + agent_card=self._agent_card, + http_handler=request_handler, + ) + + app = FastAPI(title=self._agent_card.name) + + configure_tracing(self._config.name, self._config.namespace, app) + + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], + ) + + @app.get("/health") + async def health(): + return {"status": "ok"} + + a2a_app.mount(app) + return app + + def build_local(self) -> FastAPI: + """Build app with in-memory task store (no kagent + backend required).""" + executor = AG2AgentExecutor( + pattern_factory=self._pattern_factory, + max_rounds=self._max_rounds, + ) + + task_store = InMemoryTaskStore() + request_handler = DefaultRequestHandler( + agent_executor=executor, + task_store=task_store, + ) + + a2a_app = A2AFastAPIApplication( + agent_card=self._agent_card, + http_handler=request_handler, + ) + + app = FastAPI(title=self._agent_card.name) + + @app.get("/health") + async def health(): + return {"status": "ok"} + + a2a_app.mount(app) + return app diff --git a/python/packages/kagent-ag2/src/kagent/ag2/_executor.py b/python/packages/kagent-ag2/src/kagent/ag2/_executor.py new file mode 100644 index 000000000..dcd5b08b5 --- /dev/null +++ b/python/packages/kagent-ag2/src/kagent/ag2/_executor.py @@ -0,0 +1,124 @@ +"""AG2 agent executor for the A2A protocol.""" + +import asyncio +import logging +from collections.abc import Callable + +try: + from typing import override # Python 3.12+ +except ImportError: + from typing_extensions import override + +from a2a.server.agent_execution import AgentExecutor +from a2a.server.events import EventQueue +from a2a.server.request_handling import RequestContext +from a2a.types import ( + DataPart, + Part, + TaskState, + TaskStatus, + TextPart, +) +from a2a.utils import new_agent_text_message + +from autogen.agentchat import initiate_group_chat +from autogen.agentchat.group.patterns.pattern import Pattern + +logger = logging.getLogger(__name__) + + +def _extract_text(context: RequestContext) -> str: + """Extract text content from an A2A request.""" + if context.message and context.message.parts: + for part in context.message.parts: + if isinstance(part, Part) and isinstance( + part.root, TextPart + ): + return part.root.text + if isinstance(part, TextPart): + return part.text + return "" + + +class AG2AgentExecutor(AgentExecutor): + """Wraps an AG2 multi-agent group chat as an A2A executor. + + Args: + pattern_factory: Callable that returns a fresh Pattern + for each request (avoids state leakage between + requests). + max_rounds: Maximum conversation rounds per request. + """ + + def __init__( + self, + pattern_factory: Callable[[], Pattern], + max_rounds: int = 20, + ): + super().__init__() + self._pattern_factory = pattern_factory + self._max_rounds = max_rounds + + @override + async def execute( + self, + context: RequestContext, + event_queue: EventQueue, + ) -> None: + message = _extract_text(context) + if not message: + await event_queue.enqueue_event( + new_agent_text_message( + "Error: No message content received." + ) + ) + return + + # Signal that work has started + await event_queue.enqueue_event( + new_agent_text_message("Processing request...") + ) + + try: + # Run AG2 group chat in a thread (sync -> async) + pattern = self._pattern_factory() + result, ctx, last_agent = await asyncio.to_thread( + initiate_group_chat, + pattern=pattern, + messages=message, + max_rounds=self._max_rounds, + ) + + # Extract final response + final_message = "" + for msg in reversed(result.chat_history): + content = msg.get("content", "") + if content and "TERMINATE" not in content: + final_message = content + break + + if not final_message: + final_message = "Research complete." + + await event_queue.enqueue_event( + new_agent_text_message(final_message) + ) + + except Exception as e: + logger.exception("AG2 group chat failed") + await event_queue.enqueue_event( + new_agent_text_message(f"Error: {e}") + ) + + @override + async def cancel( + self, + context: RequestContext, + event_queue: EventQueue, + ) -> None: + await event_queue.enqueue_event( + new_agent_text_message( + "Cancellation is not supported for AG2 " + "group chat conversations." + ) + ) diff --git a/python/packages/kagent-ag2/tests/__init__.py b/python/packages/kagent-ag2/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/packages/kagent-ag2/tests/test_executor.py b/python/packages/kagent-ag2/tests/test_executor.py new file mode 100644 index 000000000..19d03d8c5 --- /dev/null +++ b/python/packages/kagent-ag2/tests/test_executor.py @@ -0,0 +1,42 @@ +"""Basic tests for AG2AgentExecutor.""" + +import pytest +from unittest.mock import AsyncMock, MagicMock + +from kagent.ag2._executor import AG2AgentExecutor, _extract_text + + +def test_extract_text_empty_parts(): + ctx = MagicMock() + ctx.message.parts = [] + assert _extract_text(ctx) == "" + + +def test_extract_text_no_message(): + ctx = MagicMock() + ctx.message = None + result = _extract_text(ctx) + assert result == "" + + +def test_executor_instantiation(): + factory = MagicMock() + executor = AG2AgentExecutor(pattern_factory=factory, max_rounds=5) + assert executor._max_rounds == 5 + assert executor._pattern_factory is factory + + +@pytest.mark.asyncio +async def test_execute_empty_message(): + factory = MagicMock() + executor = AG2AgentExecutor(pattern_factory=factory, max_rounds=5) + + ctx = MagicMock() + ctx.message.parts = [] + queue = AsyncMock() + + await executor.execute(ctx, queue) + + # Should have sent an error message, not called the factory + factory.assert_not_called() + queue.enqueue_event.assert_called() diff --git a/python/samples/ag2/research-report/Dockerfile b/python/samples/ag2/research-report/Dockerfile new file mode 100644 index 000000000..651d137a7 --- /dev/null +++ b/python/samples/ag2/research-report/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.12-slim +WORKDIR /app +COPY . . +RUN pip install --no-cache-dir . +CMD ["python", "main.py"] diff --git a/python/samples/ag2/research-report/agent-card.json b/python/samples/ag2/research-report/agent-card.json new file mode 100644 index 000000000..dfd8caa88 --- /dev/null +++ b/python/samples/ag2/research-report/agent-card.json @@ -0,0 +1,17 @@ +{ + "name": "AG2 Research Report", + "description": "A multi-agent research team powered by AG2 (formerly AutoGen). Two specialists (researcher + analyst) collaborate to produce structured reports on any topic.", + "url": "http://localhost:8080", + "version": "0.1.0", + "capabilities": { + "streaming": false, + "pushNotifications": false + }, + "skills": [ + { + "id": "research-report", + "name": "Research Report", + "description": "Produce a structured research report on a given topic" + } + ] +} diff --git a/python/samples/ag2/research-report/agent.yaml b/python/samples/ag2/research-report/agent.yaml new file mode 100644 index 000000000..705de5db1 --- /dev/null +++ b/python/samples/ag2/research-report/agent.yaml @@ -0,0 +1,21 @@ +apiVersion: kagent.dev/v1alpha2 +kind: Agent +metadata: + name: ag2-research-report +spec: + type: BYO + description: >- + Multi-agent research team using AG2 (formerly AutoGen). + Two agents collaborate to produce structured reports. + byo: + deployment: + image: ag2-research-report:latest + port: 8080 + env: + - name: MODEL_NAME + value: gpt-4o-mini + - name: OPENAI_API_KEY + valueFrom: + secretKeyRef: + name: openai-credentials + key: api-key diff --git a/python/samples/ag2/research-report/main.py b/python/samples/ag2/research-report/main.py new file mode 100644 index 000000000..fa6b654ea --- /dev/null +++ b/python/samples/ag2/research-report/main.py @@ -0,0 +1,78 @@ +"""AG2 (formerly AutoGen) research report agent for kagent. + +Two agents (researcher + analyst) collaborate via GroupChat to +produce structured reports, exposed as a Kubernetes-native +agent via the A2A protocol. +""" + +import json +import os + +import uvicorn +from autogen import ConversableAgent, LLMConfig +from autogen.agentchat.group.patterns import AutoPattern + +from kagent.ag2 import KAgentApp + +llm_config = LLMConfig( + { + "model": os.getenv("MODEL_NAME", "gpt-4o-mini"), + "api_key": os.environ["OPENAI_API_KEY"], + } +) + + +def create_pattern(): + """Create a fresh AG2 pattern for each request.""" + researcher = ConversableAgent( + name="researcher", + system_message=( + "You are a research specialist. Investigate the " + "given topic thoroughly. Present key facts and " + "data in a structured format. Be concise." + ), + llm_config=llm_config, + ) + + analyst = ConversableAgent( + name="analyst", + system_message=( + "You are an analyst. Review the researcher's " + "findings and produce a structured report with: " + "Summary, Key Findings, Analysis, and " + "Recommendations. Keep it under 500 words. " + "End with TERMINATE when done." + ), + llm_config=llm_config, + ) + + user = ConversableAgent( + name="user", human_input_mode="NEVER" + ) + + return AutoPattern( + initial_agent=researcher, + agents=[researcher, analyst], + user_agent=user, + group_manager_args={"llm_config": llm_config}, + ) + + +def main(): + host = os.getenv("HOST", "0.0.0.0") + port = int(os.getenv("PORT", "8080")) + + with open("agent-card.json") as f: + agent_card = json.load(f) + + app = KAgentApp( + pattern_factory=create_pattern, + agent_card=agent_card, + max_rounds=10, + ) + server = app.build() + uvicorn.run(server, host=host, port=port) + + +if __name__ == "__main__": + main() diff --git a/python/samples/ag2/research-report/pyproject.toml b/python/samples/ag2/research-report/pyproject.toml new file mode 100644 index 000000000..969eb1c70 --- /dev/null +++ b/python/samples/ag2/research-report/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "ag2-research-report" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = ["kagent-ag2"] From c033b82e1b5b1f10fd5274b4c84977cc1f2ad05d Mon Sep 17 00:00:00 2001 From: Vasiliy Radostev Date: Tue, 17 Mar 2026 07:53:50 -0700 Subject: [PATCH 2/2] Removed unused imports --- python/packages/kagent-ag2/src/kagent/ag2/_executor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/packages/kagent-ag2/src/kagent/ag2/_executor.py b/python/packages/kagent-ag2/src/kagent/ag2/_executor.py index dcd5b08b5..31a8deef6 100644 --- a/python/packages/kagent-ag2/src/kagent/ag2/_executor.py +++ b/python/packages/kagent-ag2/src/kagent/ag2/_executor.py @@ -13,10 +13,7 @@ from a2a.server.events import EventQueue from a2a.server.request_handling import RequestContext from a2a.types import ( - DataPart, Part, - TaskState, - TaskStatus, TextPart, ) from a2a.utils import new_agent_text_message