diff --git a/python/Makefile b/python/Makefile index fc49fb4d33..10a7d033d4 100644 --- a/python/Makefile +++ b/python/Makefile @@ -19,7 +19,7 @@ lint: .PHONY: test test: update generate-test-certs - uv run pytest ./packages/**/tests/ + uv run pytest --import-mode=importlib ./packages/**/tests/ ./samples/langgraph/currency/tests/ ./samples/openai/basic_agent/tests/ ./samples/langgraph/hitl-tools/tests/ ./samples/langgraph/kebab/tests/ ./samples/adk/basic/tests/ ./samples/crewai/research-crew/tests/ ./samples/crewai/poem_flow/tests/ .PHONY: build build: update format diff --git a/python/README.md b/python/README.md index 2aee1635a4..0a17a1618c 100644 --- a/python/README.md +++ b/python/README.md @@ -6,20 +6,12 @@ ## Python -First, set up a virtual environment: -```bash -uv venv .venv -``` - -We use uv to manage dependencies as well as the python version. +The workspace uses `uv` to manage its Python version, dependencies, and local +`.venv`. From this directory, install the configured Python version and sync the +workspace: ```bash uv python install -``` - -Once we have python installed, we can download the dependencies: - -```bash uv sync --all-extras ``` @@ -67,3 +59,42 @@ In this example, the HTTP listener uses port `8080`. Substrate readiness uses a separate listener on port `8081`. A2A gRPC binds to `[::]` on `runtime.port`, which the controller sets to `80`. The same variable selects the Go ADK's shared HTTP/gRPC port. + +## API v2 Inventory + +The Python workspace contains these packages: + +| Package | Responsibility | +| --- | --- | +| `agentsts-adk` | AgentSTS integration points for ADK | +| `agentsts-core` | OAuth 2.0 token exchange client | +| `kagent-adk` | ADK A2A runtime integration | +| `kagent-core` | Shared Python runtime support | +| `kagent-crewai` | CrewAI A2A runtime integration | +| `kagent-langgraph` | LangGraph A2A runtime integration | +| `kagent-openai` | OpenAI Agents SDK A2A runtime integration | +| `kagent-proto` | Generated protobuf and gRPC contracts | +| `kagent-skills` | Skills discovery and loading | + +The retained samples and their installed entry points are: + +| Sample | Command | +| --- | --- | +| `adk/basic` | `kagent-adk run basic --working-dir /app --host 0.0.0.0` | +| `crewai/poem_flow` | `poem-flow` | +| `crewai/research-crew` | `research-crew` | +| `langgraph/currency` | `currency` | +| `langgraph/hitl-tools` | `hitl-tools` | +| `langgraph/kebab` | `kebab` | +| `openai/basic_agent` | `basic-openai-agent` | + +`make test` verifies package tests plus ASGI construction and `GET /health` for +every listed sample. These checks do not establish container startup, live A2A +requests, deployment, external-model execution, or durable restart behavior. + +## Removed REST Session Client + +The test suite rejects the removed Kagent-owned Python REST session modules. +Production A2A task history is persisted through the controller gRPC API by +`KAgentTaskStore`. Framework-level conversation state, such as an OpenAI Agents +SDK session or a LangGraph checkpointer, remains the framework's responsibility. diff --git a/python/packages/kagent-adk/tests/unittests/test_local_session_store.py b/python/packages/kagent-adk/tests/unittests/test_local_session_store.py index 95e4c6084f..642b10ab6a 100644 --- a/python/packages/kagent-adk/tests/unittests/test_local_session_store.py +++ b/python/packages/kagent-adk/tests/unittests/test_local_session_store.py @@ -1,5 +1,4 @@ -"""Tests for durable-dir session storage: AgentConfig.session_db_url selects the local -DatabaseSessionService instead of controller-backed session storage.""" +"""Tests for durable-dir and in-memory ADK session storage.""" from a2a.types import AgentCard from google.protobuf.json_format import ParseDict @@ -29,7 +28,7 @@ def make_kagent_app(agent_config: AgentConfig | None = None) -> KAgentApp: return KAgentApp( root_agent_factory=lambda: None, agent_card=card, - kagent_api_url="http://kagent-controller:8083", + kagent_api_url="http://localhost:8083", app_name=APP_NAME, config=Config(agent=agent_config), ) @@ -57,7 +56,7 @@ def __init__(self, db_url): assert constructed == {"db_url": "sqlite+aiosqlite:////data/sessions.db"} -def test_no_url_selects_kagent_session_service(monkeypatch): +def test_no_url_keeps_in_memory_sessions(monkeypatch): def boom(*args, **kwargs): raise AssertionError("DatabaseSessionService must not be constructed without a session DB URL") diff --git a/python/packages/kagent-core/tests/test_removed_session_apis.py b/python/packages/kagent-core/tests/test_removed_session_apis.py new file mode 100644 index 0000000000..4dba76ba8b --- /dev/null +++ b/python/packages/kagent-core/tests/test_removed_session_apis.py @@ -0,0 +1,22 @@ +"""Guard against removed Kagent-owned Python session APIs.""" + +from pathlib import Path + +import pytest + +PYTHON_ROOT = Path(__file__).parents[3] +SOURCE_SUFFIXES = {".py", ".md", ".json", ".yaml", ".yml"} +REMOVED_API_MARKERS = ("KAgent" + "Session", "kagent.openai._session" + "_service") + + +@pytest.mark.parametrize("marker", REMOVED_API_MARKERS) +def test_python_sources_do_not_reference_removed_kagent_session_apis(marker: str): + matches = [] + for directory in (PYTHON_ROOT / "packages", PYTHON_ROOT / "samples"): + for path in directory.rglob("*"): + if path.suffix not in SOURCE_SUFFIXES or ".venv" in path.parts: + continue + if marker in path.read_text(encoding="utf-8"): + matches.append(path.relative_to(PYTHON_ROOT)) + + assert not matches, f"Removed Kagent session API {marker!r} found in: {matches}" diff --git a/python/packages/kagent-crewai/README.md b/python/packages/kagent-crewai/README.md index c0537f5f59..e603b03fc4 100644 --- a/python/packages/kagent-crewai/README.md +++ b/python/packages/kagent-crewai/README.md @@ -1,14 +1,12 @@ # KAgent CrewAI Integration -This package provides CrewAI integration for KAgent with A2A (Agent-to-Agent) server support and session-aware memory storage. +This package provides CrewAI integration for KAgent with A2A (Agent-to-Agent) server support. ## Features - **A2A Server Integration**: Compatible with KAgent's Agent-to-Agent protocol - **Event Streaming**: Real-time streaming of crew execution events - **FastAPI Integration**: Ready-to-deploy web server for agent execution -- **Session-aware Memory**: Store and retrieve agent memories scoped by session ID -- **Flow State Persistence**: Save and restore CrewAI Flow states to KAgent backend ## Quick Start @@ -54,19 +52,15 @@ research_task: This is equivalent of `crew.kickoff(inputs={"input": "your input text"})` when triggering agents manually. -### Session-aware Memory +### Memory and Flow State #### CrewAI Crews -Session scoped memory is implemented using the `LongTermMemory` interface in CrewAI. If you wish to share memories between agents, you must interact with them in the same session to share long term memory so they can search and access the previous conversation history (because agent ID is volatile, we must use session ID). You can enable this by setting `memory=True` when creating your CrewAI crew. Note that this memory is also scoped by user ID so different users will not see each other's memories. - -Our KAgent backend is designed to handle long term memory saving and retrieval with the identical logic as `LTMSQLiteStorage` which is used by default for `LongTermMemory` in CrewAI, with the addition of session and user scoping. It will search the LTM items based on the task description and return the most relevant items (sorted and limited). - -> Note that when you set `memory=True`, you are responsible to ensure that short term and entity memory are configured properly (e.g. with `OPENAI_API_KEY` or set your own providers). The KAgent CrewAI integration only handles long term memory. +`KAgentApp` does not configure or persist CrewAI memory. Configure CrewAI memory and its backing storage explicitly when your application needs it. #### CrewAI Flows -In flow mode, we implement memory similar to checkpointing in LangGraph so that the flow state is persisted to the KAgent backend after each method finishes execution. We consider each session to be a single flow execution, so you can reuse state within the same session by enabling `@persist()` for flow or methods. We do not manage `LongTermMemory` for crews inside a flow since flow is designed to be very customizable. You are responsible for implementing your own memory management for all the crew you use in the flow. +`KAgentApp` creates a Flow instance for each A2A request. It does not persist Flow state or restore it for later requests. Configure persistence in your Flow application when needed. ### Tracing @@ -79,9 +73,9 @@ The package mirrors the structure of `kagent-adk` and `kagent-langgraph` but use - **CrewAIAgentExecutor**: Executes CrewAI workflows within A2A protocol - **KAgentApp**: FastAPI application builder with A2A integration - **Event Converters**: Translates CrewAI events into A2A events for streaming. -- **Task history**: The public A2A gateway persists client-visible task and event history. +- **KAgentTaskStore**: Persists public A2A task history through the controller gRPC API. -For local development, configure the HTTP endpoint used by protocol traffic: +For local development, provide the runtime configuration described below. ```bash # Load process settings from /config/config.json (see Configuration). @@ -89,7 +83,9 @@ For local development, configure the HTTP endpoint used by protocol traffic: ## Deployment -The uses the same deployment approach as other KAgent A2A applications (ADK / LangGraph). You can refer to `samples/crewai/` for examples. +Use the `samples/crewai/` applications as deployment examples. This package does +not configure CrewAI memory or Flow persistence; applications that need either +must provide their own backing storage. ### Runtime configuration file diff --git a/python/packages/kagent-langgraph/README.md b/python/packages/kagent-langgraph/README.md index 5df912e979..304200a66b 100644 --- a/python/packages/kagent-langgraph/README.md +++ b/python/packages/kagent-langgraph/README.md @@ -4,21 +4,22 @@ This package provides LangGraph integration for KAgent with A2A (Agent-to-Agent) ## Features -- **A2A Server Integration**: Compatible with KAgent's Agent-to-Agent protocol -- **Event Streaming**: Real-time streaming of graph execution events -- **FastAPI Integration**: Ready-to-deploy web server for agent execution +- **A2A Server Integration**: Serves LangGraph workflows over A2A +- **Event Streaming**: Streams graph execution events +- **FastAPI Integration**: Builds a deployable FastAPI application ## Quick Start ```python -from kagent.core import AsyncControllerClient, AsyncFileTokenProvider, KAgentConfig -from kagent.langgraph import KAgentApp import os import sqlite3 +from typing import Annotated, Sequence, TypedDict + +from kagent.core import KAgentConfig +from kagent.langgraph import KAgentApp +from langchain_core.messages import BaseMessage from langgraph.checkpoint.sqlite import SqliteSaver from langgraph.graph import StateGraph -from langchain_core.messages import BaseMessage -from typing import TypedDict, Annotated, Sequence class State(TypedDict): @@ -26,13 +27,6 @@ class State(TypedDict): config = KAgentConfig.from_file() -controller_client = AsyncControllerClient( - config.grpc_url, - agent_name=config.app_name, - token_provider=AsyncFileTokenProvider(), -) - -# Define and compile your graph builder = StateGraph(State) # Add nodes and edges... checkpointer = SqliteSaver( @@ -43,32 +37,37 @@ checkpointer = SqliteSaver( ) graph = builder.compile(checkpointer=checkpointer) -# Create KAgent app app = KAgentApp( graph=graph, agent_card={ "name": "my-langgraph-agent", "description": "A LangGraph agent with KAgent integration", "version": "0.1.0", + "supportedInterfaces": [{"url": "http://localhost:8080", "protocolBinding": "JSONRPC"}], "capabilities": {"streaming": True}, - "defaultInputModes": ["text"], - "defaultOutputModes": ["text"], + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"], }, config=config, - controller_client=controller_client, ) -# Build FastAPI application fastapi_app = app.build() ``` -## Architecture +## State and Task Storage + +The LangGraph checkpointer owns graph conversation state. A SQLite checkpointer stores checkpoints in a local file; persistence across pod replacement requires placing that file on durable storage or selecting another durable LangGraph checkpointer. -The package mirrors the structure of `kagent-adk` but uses LangGraph instead of Google's ADK: +`KAgentApp` persists public A2A task history through `KAgentTaskStore` and the +controller gRPC API. This task history is separate from LangGraph conversation +state; configuring one does not make the other durable. + +## Architecture -- **LangGraphAgentExecutor**: Executes LangGraph workflows within A2A protocol -- **KAgentApp**: FastAPI application builder with A2A integration -- **Task Management**: Automatic A2A task persistence through one shared authenticated gRPC channel +- **LangGraphAgentExecutor**: Executes LangGraph workflows over A2A +- **KAgentApp**: Builds the FastAPI A2A application +- **LangGraph checkpointer**: Stores graph conversation state when configured +- **KAgentTaskStore**: Persists public A2A task history through controller gRPC ## Configuration diff --git a/python/packages/kagent-openai/README.md b/python/packages/kagent-openai/README.md index 282197dc07..08f154e3de 100644 --- a/python/packages/kagent-openai/README.md +++ b/python/packages/kagent-openai/README.md @@ -1,38 +1,46 @@ # KAgent OpenAI Agents SDK Integration -OpenAI Agents SDK integration for KAgent with A2A (Agent-to-Agent) protocol support, session management, and optional skills integration. +OpenAI Agents SDK integration for KAgent with A2A (Agent-to-Agent) protocol support and optional skills integration. --- ## Quick Start +Provide the runtime configuration at `/config/config.json`: + +```json +{"runtime":{"control_plane_url":"http://localhost:8083","gateway_url":"http://localhost:8083","name":"my-openai-agent","namespace":"default","log_level":"info"}} +``` + +Then build the A2A application: + ```python -from kagent.openai import KAgentApp from agents.agent import Agent +from kagent.core import KAgentConfig +from kagent.openai import KAgentApp -# Create your OpenAI agent agent = Agent( name="Assistant", instructions="You are a helpful assistant.", - tools=[my_tool], # Optional + tools=[], ) -# Create KAgent app +agent_card = { + "name": "my-openai-agent", + "description": "My OpenAI agent", + "version": "0.1.0", + "supportedInterfaces": [{"url": "http://localhost:8080", "protocolBinding": "JSONRPC"}], + "capabilities": {"streaming": True}, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"], +} + app = KAgentApp( agent=agent, - agent_card={ - "name": "my-openai-agent", - "description": "My OpenAI agent", - "version": "0.1.0", - "capabilities": {"streaming": True}, - "defaultInputModes": ["text"], - "defaultOutputModes": ["text"], - }, - kagent_url="http://localhost:8083", - app_name="my-agent", + agent_card=agent_card, + config=KAgentConfig.from_file(), ) -# Run fastapi_app = app.build() # uvicorn run_me:fastapi_app ``` @@ -61,37 +69,30 @@ See [skills README](../../kagent-skills/README.md) for skill format and structur --- -## Session Management +## Task and Conversation State -Sessions persist conversation history in KAgent backend: +`build()` persists public A2A task history through `KAgentTaskStore` and the +controller gRPC API. It does not configure an OpenAI Agents SDK session, so +framework-level conversation state is not persisted automatically. -```python -from agents.agent import Agent -from agents.run import Runner -from kagent.openai._session_service import KAgentSession -import httpx - -client = httpx.AsyncClient(base_url="http://localhost:8083") -session = KAgentSession( - session_id="conversation_123", - client=client, - app_name="my-agent", -) - -agent = Agent(name="Assistant", instructions="Be helpful") -result = await Runner.run(agent, "Hello!", session=session) -``` +Applications that need durable framework-level conversation state must configure +an OpenAI Agents SDK session explicitly. There is no Kagent REST session client +in this package. --- ## Local Development -Test without KAgent backend using in-memory mode: +`build_local()` creates the same kind of in-memory A2A application without connecting to a KAgent backend. `KAgentConfig` still requires its runtime configuration: ```python -app = KAgentApp(agent=agent, agent_card=agent_card, kagent_url="http://localhost:8083", app_name="test-agent") +app = KAgentApp( + agent=agent, + agent_card=agent_card, + config=KAgentConfig.from_file(), +) -fastapi_app = app.build_local() # In-memory, no persistence +fastapi_app = app.build_local() ``` --- @@ -112,19 +113,21 @@ CMD ["uvicorn", "agent:fastapi_app", "--host", "0.0.0.0", "--port", "8000"] Load `/config/config.json` with `KAgentConfig.from_file()` to connect to kagent. --- - ## Architecture -| Component | Purpose | -| ----------------------- | -------------------------------------------- | -| **KAgentApp** | FastAPI application builder with A2A support | -| **KAgentSession** | Session persistence via KAgent REST API | -| **OpenAIAgentExecutor** | Executes agents with event streaming | +| Component | Purpose | +| --- | --- | +| **KAgentApp** | FastAPI application builder with A2A support | +| **OpenAIAgentExecutor** | Executes agents with event streaming | +| **KAgentTaskStore** | Persists public A2A task history through controller gRPC | +| **InMemoryTaskStore** | Tracks local-development tasks for the lifetime of the process | --- ## Environment Variables +- `OPENAI_API_KEY` - OpenAI API key +- `OPENAI_API_BASE` - Optional OpenAI-compatible API base URL --- @@ -133,15 +136,14 @@ Load `/config/config.json` with `KAgentConfig.from_file()` to connect to kagent. See `samples/openai/` for complete examples: - `basic_agent/` - Simple agent with custom tools -- More examples coming soon --- ## See Also -- [OpenAI Agents SDK Docs](https://github.com/openai/agents) +- [OpenAI Agents SDK Docs](https://github.com/openai/openai-agents-python) - [KAgent Skills](../../kagent-skills/README.md) -- [A2A Protocol](https://docs.kagent.ai/a2a) +- [A2A Protocol](https://a2a-protocol.org/) --- diff --git a/python/samples/adk/basic/Dockerfile b/python/samples/adk/basic/Dockerfile index 37ca0c275d..136988b8af 100644 --- a/python/samples/adk/basic/Dockerfile +++ b/python/samples/adk/basic/Dockerfile @@ -4,13 +4,8 @@ ARG VERSION=latest FROM $DOCKER_REGISTRY/kagent-dev/kagent/kagent-adk:$VERSION WORKDIR /app +ENV PYTHONPATH=/app -COPY basic/ basic/ -COPY pyproject.toml pyproject.toml -COPY README.md README.md -COPY .python-version .python-version -COPY uv.lock uv.lock +COPY samples/adk/basic/basic/ basic/ -RUN uv sync --locked --refresh - -CMD ["basic"] \ No newline at end of file +CMD ["basic", "--working-dir", "/app"] \ No newline at end of file diff --git a/python/samples/adk/basic/tests/test_cli.py b/python/samples/adk/basic/tests/test_cli.py new file mode 100644 index 0000000000..02f62190f1 --- /dev/null +++ b/python/samples/adk/basic/tests/test_cli.py @@ -0,0 +1,36 @@ +"""Regression test for the basic ADK sample runtime command.""" + +from pathlib import Path + +from fastapi.testclient import TestClient +from kagent.adk import cli +from kagent.core import KAgentConfig + + +def test_run_loads_basic_sample_and_passes_app_to_uvicorn(monkeypatch): + monkeypatch.setattr( + cli, + "KAgentConfig", + lambda runtime: KAgentConfig( + api_url="http://localhost:8083", + gateway_url="http://localhost:8083", + name="basic", + namespace="default", + runtime=runtime, + ), + ) + monkeypatch.chdir(Path(__file__).parent.parent) + monkeypatch.syspath_prepend(str(Path.cwd())) + captured = {} + monkeypatch.setattr(cli.uvicorn, "run", lambda app, **kwargs: captured.update(app=app, kwargs=kwargs)) + + cli.run("basic", working_dir=".", host="0.0.0.0", local=True) + + assert TestClient(captured["app"]).get("/health").text == "OK" + assert captured["kwargs"] == { + "host": "0.0.0.0", + "port": 8080, + "workers": 1, + "log_level": "info", + "log_config": None, + } diff --git a/python/samples/conftest.py b/python/samples/conftest.py new file mode 100644 index 0000000000..6d2d12df12 --- /dev/null +++ b/python/samples/conftest.py @@ -0,0 +1,25 @@ +"""Shared fixtures for Python sample tests.""" + +from collections.abc import Callable + +import pytest +from kagent.core import KAgentConfig + + +@pytest.fixture +def configure_runtime(monkeypatch: pytest.MonkeyPatch) -> Callable[[str], None]: + """Make runtime-file loading return a valid standalone test configuration.""" + + def configure(name: str) -> None: + def load(cls, path="/config/config.json"): + del path + return cls( + api_url="http://localhost:8083", + gateway_url="http://localhost:8083", + name=name, + namespace="default", + ) + + monkeypatch.setattr(KAgentConfig, "from_file", classmethod(load)) + + return configure diff --git a/python/samples/crewai/poem_flow/Dockerfile b/python/samples/crewai/poem_flow/Dockerfile index 64406faeac..d6e91d5a8f 100644 --- a/python/samples/crewai/poem_flow/Dockerfile +++ b/python/samples/crewai/poem_flow/Dockerfile @@ -46,4 +46,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Run the application -CMD ["python", "samples/crewai/poem_flow/src/poem_flow/main.py"] +CMD ["uv", "run", "--package", "poem_flow", "poem-flow"] diff --git a/python/samples/crewai/poem_flow/README.md b/python/samples/crewai/poem_flow/README.md index 1ee83af7cb..fc39f35be4 100644 --- a/python/samples/crewai/poem_flow/README.md +++ b/python/samples/crewai/poem_flow/README.md @@ -4,7 +4,7 @@ This sample demonstrates how to use the `kagent-crewai` toolkit to run a CrewAI This example is generated directly from the `crewai create flow poem_flow` command. -If you wish to use the memory persistence integration with KAgent, edit `poem_flow.py` and set `@persist()` on the flow or methods you want to persist. +`KAgentApp` creates a Flow instance for each A2A request. It does not persist or restore Flow state. Configure Flow persistence in the application when needed. ## Quick Start @@ -27,7 +27,8 @@ If you wish to use the memory persistence integration with KAgent, edit `poem_fl ``` 3. Run the image through a BYO `Harness` and matching `AgentTemplate`; see the - current examples and E2E fixtures for the resource shape. + current examples and E2E fixtures for the resource shape. The sample does + not configure Flow persistence. When interacting with the agent, you do not need to provide any input because the design of the flow does not take in user input for its tasks. diff --git a/python/samples/crewai/poem_flow/pyproject.toml b/python/samples/crewai/poem_flow/pyproject.toml index 518905ed23..663e6526a5 100644 --- a/python/samples/crewai/poem_flow/pyproject.toml +++ b/python/samples/crewai/poem_flow/pyproject.toml @@ -8,6 +8,9 @@ dependencies = [ "kagent-crewai", ] +[project.scripts] +poem-flow = "poem_flow.main:main" + [build-system] requires = ["setuptools>=84.0.0", "wheel>=0.48.0"] build-backend = "setuptools.build_meta" diff --git a/python/samples/crewai/poem_flow/src/poem_flow/main.py b/python/samples/crewai/poem_flow/src/poem_flow/main.py index 225309f5eb..9149e50609 100644 --- a/python/samples/crewai/poem_flow/src/poem_flow/main.py +++ b/python/samples/crewai/poem_flow/src/poem_flow/main.py @@ -22,8 +22,8 @@ class PoemState(BaseModel): poem: str = "" -# The persist decorator will persist all the flow method states to KAgent backend -# Alternatively, you can persist only certain methods by adding @persist to those methods +# CrewAI's persist decorator manages Flow state; KAgentApp does not persist it. +# Alternatively, persist only selected methods by adding @persist to those methods. @persist(verbose=True) class PoemFlow(Flow[PoemState]): @start() @@ -75,14 +75,17 @@ def plot(): # To integrate with Kagent, just replace the kickoff above with the KAgentApp code below -def main(): - """Main entry point to run the KAgent CrewAI server.""" +def build_app(): + """Build the poem flow ASGI application.""" with open(os.path.join(os.path.dirname(__file__), "agent-card.json"), "r") as f: agent_card = json.load(f) - app = KAgentApp(crew=PoemFlow(), agent_card=agent_card) + return KAgentApp(crew=PoemFlow(), agent_card=agent_card).build() + - server = app.build() +def main(): + """Main entry point to run the KAgent CrewAI server.""" + server = build_app() port = int(os.getenv("PORT", "8080")) host = os.getenv("HOST", "0.0.0.0") diff --git a/python/samples/crewai/poem_flow/tests/test_poem_flow_main.py b/python/samples/crewai/poem_flow/tests/test_poem_flow_main.py new file mode 100644 index 0000000000..879d2cacc6 --- /dev/null +++ b/python/samples/crewai/poem_flow/tests/test_poem_flow_main.py @@ -0,0 +1,18 @@ +"""Regression tests for the poem flow console entry point.""" + +import importlib + +from fastapi.testclient import TestClient + + +def test_main_passes_healthy_app_to_uvicorn(monkeypatch, configure_runtime): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + configure_runtime("poem-flow") + module = importlib.import_module("poem_flow.main") + captured = {} + monkeypatch.setattr(module.uvicorn, "run", lambda app, **kwargs: captured.update(app=app, kwargs=kwargs)) + + module.main() + + assert TestClient(captured["app"]).get("/health").text == "OK" + assert captured["kwargs"] == {"host": "0.0.0.0", "port": 8080, "log_level": "info"} diff --git a/python/samples/crewai/research-crew/Dockerfile b/python/samples/crewai/research-crew/Dockerfile index 271eead58f..6dcffb20aa 100644 --- a/python/samples/crewai/research-crew/Dockerfile +++ b/python/samples/crewai/research-crew/Dockerfile @@ -46,4 +46,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Run the application -CMD ["python", "samples/crewai/research-crew/src/research_crew/main.py"] +CMD ["uv", "run", "--package", "research-crew", "research-crew"] diff --git a/python/samples/crewai/research-crew/README.md b/python/samples/crewai/research-crew/README.md index 774c17d1c0..885b7873c2 100644 --- a/python/samples/crewai/research-crew/README.md +++ b/python/samples/crewai/research-crew/README.md @@ -4,7 +4,7 @@ This sample demonstrates how to use the `kagent-crewai` toolkit to run a CrewAI It follows the standard CrewAI project structure and developer experience, allowing you to define your agents and tasks in Python. -If you wish to use the memory persistence integration with KAgent, edit `crew.py` and set `memory=True` when creating the crew. +`KAgentApp` does not configure or persist CrewAI memory. Configure CrewAI memory and its backing storage explicitly when needed. ## Features @@ -43,7 +43,8 @@ If you wish to use the memory persistence integration with KAgent, edit `crew.py ``` 4. Run the image through a BYO `Harness` and matching `AgentTemplate`; see the - current examples and E2E fixtures for the resource shape. + current examples and E2E fixtures for the resource shape. The sample does + not configure CrewAI memory persistence. ## Local Development diff --git a/python/samples/crewai/research-crew/pyproject.toml b/python/samples/crewai/research-crew/pyproject.toml index 3ebbe4d7c4..d5ad218abb 100644 --- a/python/samples/crewai/research-crew/pyproject.toml +++ b/python/samples/crewai/research-crew/pyproject.toml @@ -8,6 +8,9 @@ dependencies = [ "kagent-crewai", ] +[project.scripts] +research-crew = "research_crew.main:main" + [build-system] requires = ["setuptools>=84.0.0", "wheel>=0.48.0"] build-backend = "setuptools.build_meta" diff --git a/python/samples/crewai/research-crew/src/research_crew/agent-card.json b/python/samples/crewai/research-crew/src/research_crew/agent-card.json index f89bbe6af7..6ced77eec1 100644 --- a/python/samples/crewai/research-crew/src/research_crew/agent-card.json +++ b/python/samples/crewai/research-crew/src/research_crew/agent-card.json @@ -12,7 +12,7 @@ "id": "research-crew", "name": "Research Crew", "description": "Can conduct research and analysis by providing a topic as input", - "example": "{\"input\": \"Latest advancements in AI\"}", + "examples": ["{\"input\": \"Latest advancements in AI\"}"], "tags": ["research", "analysis"] } ] diff --git a/python/samples/crewai/research-crew/src/research_crew/main.py b/python/samples/crewai/research-crew/src/research_crew/main.py index 27b906f437..0200fe22b1 100644 --- a/python/samples/crewai/research-crew/src/research_crew/main.py +++ b/python/samples/crewai/research-crew/src/research_crew/main.py @@ -13,17 +13,20 @@ logger = logging.getLogger(__name__) -def main(): - """Main entry point to run the KAgent CrewAI server.""" +def build_app(): + """Build the research crew ASGI application.""" # 1. Load the agent card or define it inline with open(os.path.join(os.path.dirname(__file__), "agent-card.json"), "r") as f: agent_card = json.load(f) # 2. Load the Crew, then create the kagent app - app = KAgentApp(crew=ResearchCrew().crew(), agent_card=agent_card) + return KAgentApp(crew=ResearchCrew().crew(), agent_card=agent_card).build() + + +def main(): + """Main entry point to run the KAgent CrewAI server.""" + server = build_app() - # 3. Build the FastAPI app and run the server - server = app.build() port = int(os.getenv("PORT", "8080")) host = os.getenv("HOST", "0.0.0.0") logger.info(f"Starting server on {host}:{port}") diff --git a/python/samples/crewai/research-crew/tests/test_research_crew_main.py b/python/samples/crewai/research-crew/tests/test_research_crew_main.py new file mode 100644 index 0000000000..9559a7f910 --- /dev/null +++ b/python/samples/crewai/research-crew/tests/test_research_crew_main.py @@ -0,0 +1,18 @@ +"""Regression tests for the research crew console entry point.""" + +import importlib + +from fastapi.testclient import TestClient + + +def test_main_passes_healthy_app_to_uvicorn(monkeypatch, configure_runtime): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + configure_runtime("research-crew") + module = importlib.import_module("research_crew.main") + captured = {} + monkeypatch.setattr(module.uvicorn, "run", lambda app, **kwargs: captured.update(app=app, kwargs=kwargs)) + + module.main() + + assert TestClient(captured["app"]).get("/health").text == "OK" + assert captured["kwargs"] == {"host": "0.0.0.0", "port": 8080, "log_level": "info"} diff --git a/python/samples/langgraph/currency/Dockerfile b/python/samples/langgraph/currency/Dockerfile index 27b98160ec..65e923ee9e 100644 --- a/python/samples/langgraph/currency/Dockerfile +++ b/python/samples/langgraph/currency/Dockerfile @@ -44,5 +44,5 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -# Run the application -CMD ["uv", "run", "samples/langgraph/currency/currency/cli.py"] +# Run the installed console entry point so package-relative imports resolve. +CMD ["uv", "run", "--package", "currency", "currency"] diff --git a/python/samples/langgraph/currency/README.md b/python/samples/langgraph/currency/README.md index c070316cf7..21b72e5f72 100644 --- a/python/samples/langgraph/currency/README.md +++ b/python/samples/langgraph/currency/README.md @@ -1,16 +1,15 @@ # Currency LangGraph Agent -This is a currency LangGraph agent that demonstrates KAgent integration with session persistence via REST API. +This sample serves a currency-conversion LangGraph agent over A2A. LangGraph conversation state is checkpointed to a local SQLite file. ## Features -- Currency conversion agent using OpenAI -- LangGraph state management with KAgent checkpointer -- A2A protocol compatibility -- Session persistence via KAgent REST API +- Currency conversion using OpenAI and the Frankfurter API +- LangGraph state checkpointing with `SqliteSaver` +- A2A protocol support - Streaming responses -## Quick Start +## Deployment 1. Build the agent image: @@ -64,22 +63,25 @@ uv run currency uv run currency test ``` +The startup regression test invokes `main()` with the server runner mocked and +checks `GET /health`. It does not cover port binding, container startup, +OpenAI-backed execution, A2A requests, or checkpoint reuse after restart. + ## Architecture This agent demonstrates: -- **StateGraph**: Simple conversation flow with one node -- **SqliteSaver**: Stores conversation state in a local SQLite file -- **A2A Integration**: Compatible with KAgent's agent-to-agent protocol -- **Streaming**: Real-time response streaming via A2A events +- **StateGraph**: A ReAct conversation graph with a currency tool +- **SqliteSaver**: Stores LangGraph conversation state in a local SQLite file +- **A2A Integration**: Serves the graph through KAgent's A2A application wrapper +- **Streaming**: Emits graph execution updates as A2A events -The agent stores conversation history in `KAGENT_CHECKPOINT_DB` (default: `/tmp/currency-checkpoints.sqlite`). Mount a persistent volume and point the variable there for persistence across pod replacement. +The agent stores LangGraph checkpoints in `KAGENT_CHECKPOINT_DB` (default: `/tmp/currency-checkpoints.sqlite`). Mount a persistent volume and point the variable there to retain checkpoints across pod replacement. A2A task tracking inside the agent process remains in memory. ## Configuration -The agent can be configured via environment variables: - - `OPENAI_API_KEY`: Required for OpenAI API access +- `KAGENT_CHECKPOINT_DB`: SQLite checkpoint path (default: `/tmp/currency-checkpoints.sqlite`) - `PORT`: Server port (default: 8080) - `HOST`: Server host (default: 0.0.0.0) diff --git a/python/samples/langgraph/currency/currency/cli.py b/python/samples/langgraph/currency/currency/cli.py index c1709eb91c..1bab36c8b9 100644 --- a/python/samples/langgraph/currency/currency/cli.py +++ b/python/samples/langgraph/currency/currency/cli.py @@ -5,36 +5,41 @@ import os import uvicorn -from agent import graph from kagent.core import KAgentConfig from kagent.langgraph import KAgentApp +from .agent import graph + # Configure logging logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) -def main(): - """Main entry point for the CLI.""" - # from script directory +def build_app(): + """Build the currency agent ASGI application.""" with open(os.path.join(os.path.dirname(__file__), "agent-card.json"), "r") as f: agent_card = json.load(f) config = KAgentConfig.from_file() - app = KAgentApp( + return KAgentApp( graph=graph, agent_card=agent_card, config=config, tracing=True, - ) + ).build() + + +def main(): + """Main entry point for the CLI.""" + app = build_app() port = int(os.getenv("PORT", "8080")) host = os.getenv("HOST", "0.0.0.0") logger.info(f"Starting server on {host}:{port}") uvicorn.run( - app.build(), + app, host=host, port=port, log_level="info", diff --git a/python/samples/langgraph/currency/tests/test_cli.py b/python/samples/langgraph/currency/tests/test_cli.py new file mode 100644 index 0000000000..2b5f4d6c64 --- /dev/null +++ b/python/samples/langgraph/currency/tests/test_cli.py @@ -0,0 +1,29 @@ +"""Regression tests for the currency console entry point.""" + +import importlib + +from fastapi.testclient import TestClient + + +def test_main_passes_healthy_app_to_uvicorn(monkeypatch, tmp_path, configure_runtime): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + configure_runtime("currency") + checkpoint_db = tmp_path / "currency-checkpoints.sqlite" + monkeypatch.setenv("KAGENT_CHECKPOINT_DB", str(checkpoint_db)) + + cli = importlib.import_module("currency.cli") + captured = {} + + def run(app, **kwargs): + captured["app"] = app + captured["kwargs"] = kwargs + + monkeypatch.setattr(cli.uvicorn, "run", run) + cli.main() + + response = TestClient(captured["app"]).get("/health") + + assert response.status_code == 200 + assert response.text == "OK" + assert checkpoint_db.exists() + assert captured["kwargs"] == {"host": "0.0.0.0", "port": 8080, "log_level": "info"} diff --git a/python/samples/langgraph/hitl-tools/Dockerfile b/python/samples/langgraph/hitl-tools/Dockerfile index d783b11f46..a204e3be71 100644 --- a/python/samples/langgraph/hitl-tools/Dockerfile +++ b/python/samples/langgraph/hitl-tools/Dockerfile @@ -44,5 +44,5 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -# Run the application -CMD ["uv", "run", "samples/langgraph/hitl-tools/hitl_tools/cli.py"] +# Run the installed console entry point so package-relative imports resolve. +CMD ["uv", "run", "--package", "hitl-tools", "hitl-tools"] diff --git a/python/samples/langgraph/hitl-tools/README.md b/python/samples/langgraph/hitl-tools/README.md new file mode 100644 index 0000000000..df344fc1b3 --- /dev/null +++ b/python/samples/langgraph/hitl-tools/README.md @@ -0,0 +1,24 @@ +# HITL Tools LangGraph Agent + +This sample serves a LangGraph workflow with human-in-the-loop tool approval +through `kagent-langgraph`'s A2A application wrapper. + +## Local Startup + +From `python/`, provide `/config/config.json`, set the OpenAI API key, and run +the installed entry point: + +```bash +export OPENAI_API_KEY=your-api-key +uv run --package hitl-tools hitl-tools +``` + +The runtime file must define `control_plane_url`, `gateway_url`, `name`, and +`namespace`. The wrapper uses the control-plane endpoint for public A2A task +history. `GET /health` is available on port 8080. + +## Validation + +The repository validates package importability, ASGI application construction, +and the health endpoint. It does not validate container startup, live A2A +requests, OpenAI-backed execution, deployment, or durable checkpoint behavior. diff --git a/python/samples/langgraph/hitl-tools/hitl_tools/cli.py b/python/samples/langgraph/hitl-tools/hitl_tools/cli.py index a9efa05325..5f4e7572ab 100644 --- a/python/samples/langgraph/hitl-tools/hitl_tools/cli.py +++ b/python/samples/langgraph/hitl-tools/hitl_tools/cli.py @@ -5,34 +5,40 @@ import os import uvicorn -from agent import graph from kagent.core import KAgentConfig from kagent.langgraph import KAgentApp +from .agent import graph + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) -def main(): - """Main entry point for the CLI.""" +def build_app(): + """Build the HITL tools ASGI application.""" with open(os.path.join(os.path.dirname(__file__), "agent-card.json"), "r") as f: agent_card = json.load(f) config = KAgentConfig.from_file() - app = KAgentApp( + return KAgentApp( graph=graph, agent_card=agent_card, config=config, tracing=True, - ) + ).build() + + +def main(): + """Main entry point for the CLI.""" + app = build_app() port = int(os.getenv("PORT", "8080")) host = os.getenv("HOST", "0.0.0.0") logger.info(f"Starting server on {host}:{port}") uvicorn.run( - app.build(), + app, host=host, port=port, log_level="info", diff --git a/python/samples/langgraph/hitl-tools/tests/test_hitl_tools_cli.py b/python/samples/langgraph/hitl-tools/tests/test_hitl_tools_cli.py new file mode 100644 index 0000000000..03a9b2cea9 --- /dev/null +++ b/python/samples/langgraph/hitl-tools/tests/test_hitl_tools_cli.py @@ -0,0 +1,19 @@ +"""Regression tests for the HITL tools console entry point.""" + +import importlib + +from fastapi.testclient import TestClient + + +def test_main_passes_healthy_app_to_uvicorn(monkeypatch, configure_runtime): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + configure_runtime("hitl-tools") + + cli = importlib.import_module("hitl_tools.cli") + captured = {} + monkeypatch.setattr(cli.uvicorn, "run", lambda app, **kwargs: captured.update(app=app, kwargs=kwargs)) + + cli.main() + + assert TestClient(captured["app"]).get("/health").text == "OK" + assert captured["kwargs"] == {"host": "0.0.0.0", "port": 8080, "log_level": "info"} diff --git a/python/samples/langgraph/kebab/Dockerfile b/python/samples/langgraph/kebab/Dockerfile index 5eecc34f62..49e268b87a 100644 --- a/python/samples/langgraph/kebab/Dockerfile +++ b/python/samples/langgraph/kebab/Dockerfile @@ -41,4 +41,5 @@ EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -CMD ["uv", "run", "samples/langgraph/kebab/kebab/cli.py"] +# Run the installed console entry point so package-relative imports resolve. +CMD ["uv", "run", "--package", "kebab", "kebab"] diff --git a/python/samples/langgraph/kebab/README.md b/python/samples/langgraph/kebab/README.md new file mode 100644 index 0000000000..cc2637189d --- /dev/null +++ b/python/samples/langgraph/kebab/README.md @@ -0,0 +1,22 @@ +# Kebab LangGraph Agent + +This sample serves a minimal LangGraph workflow through `kagent-langgraph`'s A2A +application wrapper. + +## Local Startup + +From `python/`, provide `/config/config.json` and run the installed entry point: + +```bash +uv run --package kebab kebab +``` + +The runtime file must define `control_plane_url`, `gateway_url`, `name`, and +`namespace`. The wrapper uses the control-plane endpoint for public A2A task +history. `GET /health` is available on port 8080. + +## Validation + +The repository validates package importability, ASGI application construction, +and the health endpoint. It does not validate container startup, live A2A +requests, deployment, or durable checkpoint behavior. diff --git a/python/samples/langgraph/kebab/kebab/cli.py b/python/samples/langgraph/kebab/kebab/cli.py index 4aa9f952be..53f6c79fd4 100644 --- a/python/samples/langgraph/kebab/kebab/cli.py +++ b/python/samples/langgraph/kebab/kebab/cli.py @@ -5,33 +5,40 @@ import os import uvicorn -from agent import graph from kagent.core import KAgentConfig from kagent.langgraph import KAgentApp +from .agent import graph + logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) -def main(): +def build_app(): + """Build the kebab ASGI application.""" with open(os.path.join(os.path.dirname(__file__), "agent-card.json"), "r") as f: agent_card = json.load(f) config = KAgentConfig.from_file() - app = KAgentApp( + return KAgentApp( graph=graph, agent_card=agent_card, config=config, tracing=False, - ) + ).build() + + +def main(): + """Run the kebab agent server.""" + app = build_app() port = int(os.getenv("PORT", "8080")) host = os.getenv("HOST", "0.0.0.0") logger.info("Starting server on %s:%s", host, port) uvicorn.run( - app.build(), + app, host=host, port=port, log_level="info", diff --git a/python/samples/langgraph/kebab/tests/test_kebab_cli.py b/python/samples/langgraph/kebab/tests/test_kebab_cli.py new file mode 100644 index 0000000000..1e8a452617 --- /dev/null +++ b/python/samples/langgraph/kebab/tests/test_kebab_cli.py @@ -0,0 +1,19 @@ +"""Regression tests for the kebab console entry point.""" + +import importlib + +from fastapi.testclient import TestClient + + +def test_main_passes_healthy_app_to_uvicorn(monkeypatch, configure_runtime): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + configure_runtime("kebab") + + cli = importlib.import_module("kebab.cli") + captured = {} + monkeypatch.setattr(cli.uvicorn, "run", lambda app, **kwargs: captured.update(app=app, kwargs=kwargs)) + + cli.main() + + assert TestClient(captured["app"]).get("/health").text == "OK" + assert captured["kwargs"] == {"host": "0.0.0.0", "port": 8080, "log_level": "info"} diff --git a/python/samples/openai/basic_agent/Dockerfile b/python/samples/openai/basic_agent/Dockerfile index 4f9216593b..3ed621aee4 100644 --- a/python/samples/openai/basic_agent/Dockerfile +++ b/python/samples/openai/basic_agent/Dockerfile @@ -29,6 +29,6 @@ ENV PATH="/app/.venv/bin:$PATH" HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 -# Run the agent -CMD ["python", "samples/openai/basic_agent/basic_agent/agent.py"] +# Run the installed console entry point so package imports resolve. +CMD ["uv", "run", "--package", "basic-openai-agent", "basic-openai-agent"] diff --git a/python/samples/openai/basic_agent/README.md b/python/samples/openai/basic_agent/README.md index e69de29bb2..aee3b089e6 100644 --- a/python/samples/openai/basic_agent/README.md +++ b/python/samples/openai/basic_agent/README.md @@ -0,0 +1,24 @@ +# Basic OpenAI Agent + +This sample serves an OpenAI Agents SDK agent through `kagent-openai`'s A2A +application wrapper. + +## Local Startup + +From `python/`, provide `/config/config.json`, set the OpenAI API key, and run +the installed entry point: + +```bash +export OPENAI_API_KEY=your-api-key +uv run --package basic-openai-agent basic-openai-agent +``` + +The runtime file must define `control_plane_url`, `gateway_url`, `name`, and +`namespace`. The wrapper uses the control-plane endpoint for public A2A task +history. `GET /health` is available on port 8080. + +## Validation + +The repository validates package importability, ASGI application construction, +and the health endpoint. It does not validate container startup, live A2A +requests, OpenAI-backed execution, deployment, or durable conversation state. diff --git a/python/samples/openai/basic_agent/basic_agent/agent.py b/python/samples/openai/basic_agent/basic_agent/agent.py index f5d907edf7..470037c13b 100644 --- a/python/samples/openai/basic_agent/basic_agent/agent.py +++ b/python/samples/openai/basic_agent/basic_agent/agent.py @@ -99,25 +99,29 @@ def get_weather(location: str) -> str: AgentCard(), ) -config = KAgentConfig.from_file() -# Create KAgent app -app = KAgentApp( - agent=agent, - agent_card=agent_card, - config=config, -) +def build_app(): + """Build the basic OpenAI agent ASGI application.""" + return KAgentApp( + agent=agent, + agent_card=agent_card, + config=KAgentConfig.from_file(), + ).build() -# Build the FastAPI application -fastapi_app = app.build() +app = build_app() -if __name__ == "__main__": +def main(): + """Run the basic OpenAI agent server.""" import uvicorn logging.basicConfig(level=logging.INFO) logger.info("Starting Basic OpenAI Agent...") logger.info("Server will be available at http://0.0.0.0:8080") - uvicorn.run(fastapi_app, host="0.0.0.0", port=8080) + uvicorn.run(app, host="0.0.0.0", port=8080) + + +if __name__ == "__main__": + main() diff --git a/python/samples/openai/basic_agent/pyproject.toml b/python/samples/openai/basic_agent/pyproject.toml index 177754224c..bcc06db0fd 100644 --- a/python/samples/openai/basic_agent/pyproject.toml +++ b/python/samples/openai/basic_agent/pyproject.toml @@ -10,6 +10,9 @@ dependencies = [ "uvicorn>=0.54.0", ] +[project.scripts] +basic-openai-agent = "basic_agent.agent:main" + [tool.uv.sources] kagent-openai = { workspace = true } diff --git a/python/samples/openai/basic_agent/tests/test_agent.py b/python/samples/openai/basic_agent/tests/test_agent.py new file mode 100644 index 0000000000..8afa2dcc43 --- /dev/null +++ b/python/samples/openai/basic_agent/tests/test_agent.py @@ -0,0 +1,26 @@ +"""Regression tests for the basic OpenAI agent console entry point.""" + +import importlib + +from fastapi.testclient import TestClient + + +def test_main_passes_healthy_app_to_uvicorn(monkeypatch, configure_runtime): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + configure_runtime("basic-openai-agent") + + agent = importlib.import_module("basic_agent.agent") + captured = {} + + def run(app, **kwargs): + captured["app"] = app + captured["kwargs"] = kwargs + + monkeypatch.setattr("uvicorn.run", run) + agent.main() + + response = TestClient(captured["app"]).get("/health") + + assert response.status_code == 200 + assert response.text == "OK" + assert captured["kwargs"] == {"host": "0.0.0.0", "port": 8080}