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 218f96bb10..998a8ce7ea 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 ``` @@ -29,4 +21,44 @@ The python code in this project uses the UV workspaces to manage the dependencie The package directory contains various sub-packages which comprise the kagent engine. Each framework which kagent supports has its own package. -In addition there is a top-level kagent package which contains the main entry point for the engine. In the future we may want to have separate entrypoints for each framework to reduce the number of dependencies we have to install. \ No newline at end of file +In addition there is a top-level kagent package which contains the main entry point for the engine. In the future we may want to have separate entrypoints for each framework to reduce the number of dependencies we have to install. + +## 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. + +## Legacy Session References + +The test suite rejects the removed Kagent-owned REST session APIs. Remaining uses +of "session" are framework-local: OpenAI SDK session factories, ADK's in-memory +or SQLite `DatabaseSessionService`, ADK remote-agent isolation, and temporary +skills working directories. They do not identify or call a Kagent-owned REST +session API. \ No newline at end of file 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 f092e974cf..b6e42c0c0c 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, agent_config=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..921c51c865 --- /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", "_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 0f34c61f5d..be9445f2a8 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 @@ -49,19 +47,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 @@ -74,9 +68,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. +- **Task store**: Tracks A2A tasks in memory for the lifetime of the application process. -For local development, configure the HTTP endpoint used by protocol traffic: +`KAgentConfig` requires its configuration values, but this wrapper does not use `KAGENT_API_URL` or `KAGENT_GATEWAY_URL` for outbound connections. For local development, configure the required values: ```bash export KAGENT_API_URL=http://localhost:8083 @@ -87,4 +81,4 @@ export KAGENT_NAMESPACE=default ## 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 provide a Kagent-backed memory or Flow-persistence service. diff --git a/python/packages/kagent-langgraph/README.md b/python/packages/kagent-langgraph/README.md index 98d26f448e..df8c37b468 100644 --- a/python/packages/kagent-langgraph/README.md +++ b/python/packages/kagent-langgraph/README.md @@ -4,71 +4,26 @@ 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 +## State and Task Storage -```python -from kagent.core import AsyncControllerClient, AsyncFileTokenProvider, KAgentConfig -from kagent.langgraph import KAgentApp -import os -import sqlite3 -from langgraph.checkpoint.sqlite import SqliteSaver -from langgraph.graph import StateGraph -from langchain_core.messages import BaseMessage -from typing import TypedDict, Annotated, Sequence +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. -class State(TypedDict): - messages: Annotated[Sequence[BaseMessage], "The conversation history"] - -config = KAgentConfig() -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(sqlite3.connect( - os.getenv("KAGENT_CHECKPOINT_DB", "/tmp/langgraph-checkpoints.sqlite"), - check_same_thread=False, -)) -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", - "capabilities": {"streaming": True}, - "defaultInputModes": ["text"], - "defaultOutputModes": ["text"] - }, - config=config, - controller_client=controller_client, -) - -# Build FastAPI application -fastapi_app = app.build() -``` +`KAgentApp` uses an in-memory A2A task store inside the agent process. That task state does not survive a process restart. This package does not configure a gateway connection; a deployment may place a gateway in front of the application and have that gateway own durable task history. ## Architecture -The package mirrors the structure of `kagent-adk` but uses LangGraph instead of Google's ADK: - -- **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 +- **InMemoryTaskStore**: Tracks A2A tasks for the lifetime of the process ## Configuration -Set both endpoints when running locally. A2A and MCP use `KAGENT_GATEWAY_URL`, while control-plane calls use `KAGENT_API_URL`. +`KAgentConfig` currently requires both endpoint values and the agent identity. This wrapper uses the identity and tracing configuration, but does not use these URLs for outbound control-plane or gateway calls: ```bash export KAGENT_API_URL=http://localhost:8083 @@ -79,4 +34,4 @@ export KAGENT_NAMESPACE=default ## Deployment -Use the same deployment pattern as kagent-adk samples with Docker and Kubernetes. +This package has no documented end-to-end deployment path yet. Sample documentation identifies the validation available for each sample. diff --git a/python/packages/kagent-openai/README.md b/python/packages/kagent-openai/README.md index 43e9b942d0..28af250b49 100644 --- a/python/packages/kagent-openai/README.md +++ b/python/packages/kagent-openai/README.md @@ -1,38 +1,52 @@ # 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 +Set the required configuration first: + +```bash +export KAGENT_API_URL=http://localhost:8083 +export KAGENT_GATEWAY_URL=http://localhost:8083 +export KAGENT_NAME=my-openai-agent +export KAGENT_NAMESPACE=default +export OPENAI_API_KEY=your-api-key +``` + +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(), ) -# Run fastapi_app = app.build() # uvicorn run_me:fastapi_app ``` @@ -61,77 +75,48 @@ 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: - -```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", -) +`KAgentApp` uses an in-memory A2A task store and does not configure an OpenAI Agents SDK session. Task state held by the agent process is lost when that process restarts. This package does not configure a gateway connection; a deployment may place a gateway in front of the application and have that gateway own durable task history. -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 it explicitly rather than relying on a KAgent REST session service. --- ## 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 configuration values: ```python app = KAgentApp( agent=agent, agent_card=agent_card, - kagent_url="http://localhost:8083", - app_name="test-agent" + config=KAgentConfig(), ) -fastapi_app = app.build_local() # In-memory, no persistence -``` - ---- - -## Deployment - -Standard Docker deployment: - -```dockerfile -FROM python:3.13-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install -r requirements.txt -COPY agent.py . -CMD ["uvicorn", "agent:fastapi_app", "--host", "0.0.0.0", "--port", "8000"] +fastapi_app = app.build_local() ``` -Set `KAGENT_API_URL` and `KAGENT_GATEWAY_URL` 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 | +| **InMemoryTaskStore** | Tracks A2A tasks for the lifetime of the process | --- ## Environment Variables -- `KAGENT_API_URL` - KAgent control-plane API URL -- `KAGENT_GATEWAY_URL` - KAgent A2A and MCP gateway URL +- `KAGENT_API_URL` - Required by `KAgentConfig`; not used for outbound control-plane calls by this wrapper +- `KAGENT_GATEWAY_URL` - Required by `KAgentConfig`; not used for outbound gateway calls by this wrapper +- `KAGENT_NAME` - Agent name +- `KAGENT_NAMESPACE` - Agent namespace +- `OPENAI_API_KEY` - OpenAI API key +- `OPENAI_API_BASE` - Optional OpenAI-compatible API base URL - `LOG_LEVEL` - Logging level (default: INFO) --- @@ -141,15 +126,14 @@ Set `KAGENT_API_URL` and `KAGENT_GATEWAY_URL` 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..e6be71f9f5 --- /dev/null +++ b/python/samples/adk/basic/tests/test_cli.py @@ -0,0 +1,20 @@ +"""Regression test for the basic ADK sample runtime command.""" + +from pathlib import Path + +from kagent.adk import cli + + +def test_run_loads_basic_sample_and_passes_app_to_uvicorn(monkeypatch): + monkeypatch.setenv("KAGENT_API_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_GATEWAY_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_NAME", "basic") + monkeypatch.setenv("KAGENT_NAMESPACE", "default") + 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 captured["kwargs"] == {"host": "0.0.0.0", "port": 8080, "workers": 1, "log_level": "info"} 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 9e127656f9..bd74d69da9 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 - API v2 examples and E2E fixtures for the current resource shape. + API v2 examples and E2E fixtures for the current resource shape. The sample + does not configure Kagent-backed task history or 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. @@ -70,7 +71,7 @@ When interacting with the agent, you do not need to provide any input because th The agent can be configured via environment variables: - `GEMINI_API_KEY`: Required for LLM access -- `KAGENT_API_URL`: Required. KAgent control-plane API URL (for local development, `http://localhost:8083`) -- `KAGENT_GATEWAY_URL`: Required. KAgent A2A and MCP gateway URL (for local development, `http://localhost:8083`) +- `KAGENT_API_URL`: Required by `KAgentConfig`; not used by this wrapper for outbound control-plane calls +- `KAGENT_GATEWAY_URL`: Required by `KAgentConfig`; not used by this wrapper for outbound gateway calls - `PORT`: Server port (default: 8080) - `HOST`: Server host (default: 0.0.0.0) diff --git a/python/samples/crewai/poem_flow/pyproject.toml b/python/samples/crewai/poem_flow/pyproject.toml index b2f6ca1107..2e462cb0d6 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>=83.0.0", "wheel>=0.47.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..4865aa42f1 --- /dev/null +++ b/python/samples/crewai/poem_flow/tests/test_poem_flow_main.py @@ -0,0 +1,21 @@ +"""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): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + monkeypatch.setenv("KAGENT_API_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_GATEWAY_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_NAME", "poem-flow") + monkeypatch.setenv("KAGENT_NAMESPACE", "default") + 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 6a3f24b5de..7d8b287f5c 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 - API v2 examples and E2E fixtures for the current resource shape. + API v2 examples and E2E fixtures for the current resource shape. The sample + does not configure Kagent-backed task history or CrewAI memory persistence. ## Local Development @@ -86,7 +87,7 @@ The agent can be configured via environment variables: - `OPENAI_API_KEY`: Required for LLM access - `SERPER_API_KEY`: Required for web search functionality -- `KAGENT_API_URL`: Required KAgent control-plane API URL (typically `http://localhost:8083`) -- `KAGENT_GATEWAY_URL`: Required KAgent A2A and MCP gateway URL (typically `http://localhost:8083`) +- `KAGENT_API_URL`: Required by `KAgentConfig`; not used by this wrapper for outbound control-plane calls +- `KAGENT_GATEWAY_URL`: Required by `KAgentConfig`; not used by this wrapper for outbound gateway calls - `PORT`: Server port (default: 8080) - `HOST`: Server host (default: 0.0.0.0) diff --git a/python/samples/crewai/research-crew/pyproject.toml b/python/samples/crewai/research-crew/pyproject.toml index c79a4bc25c..cc08528cda 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>=83.0.0", "wheel>=0.47.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..9ecdaddb14 --- /dev/null +++ b/python/samples/crewai/research-crew/tests/test_research_crew_main.py @@ -0,0 +1,21 @@ +"""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): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + monkeypatch.setenv("KAGENT_API_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_GATEWAY_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_NAME", "research-crew") + monkeypatch.setenv("KAGENT_NAMESPACE", "default") + 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 26a8670e2e..44e6a5d529 100644 --- a/python/samples/langgraph/currency/README.md +++ b/python/samples/langgraph/currency/README.md @@ -1,88 +1,37 @@ # 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: - -Run the basic-langchain-sample target from the top-level Python directory. - -```bash -make basic-langchain-sample -``` - -2. Push to local registry (if using one): - -```bash -docker push localhost:5001/langgraph-currency:latest -``` - -3. Create a secret with the OpenAI API key: - -```bash -kubectl create secret generic kagent-openai -n kagent \ - --from-literal=OPENAI_API_KEY=$OPENAI_API_KEY \ - --dry-run=client -o yaml | kubectl apply -f - -``` - -4. Run the image through a BYO `Harness` and matching `AgentTemplate`; see the - API v2 examples and E2E fixtures for the current resource shape. - -## Local Development - -1. Install dependencies: - -```bash -uv sync -``` - -2. Set environment variables: - -```bash -export OPENAI_API_KEY=your_api_key_here -export KAGENT_API_URL=http://localhost:8083 -export KAGENT_GATEWAY_URL=http://localhost:8083 -``` - -3. Run the agent server: - -```bash -uv run currency -``` - -4. Test the agent: - -```bash -uv run currency test -``` +The repository validates the packaged module import, ASGI application construction, and `GET /health` response. It does not validate the `main()` process lifecycle, port binding, container startup, OpenAI-backed execution, A2A requests, or checkpoint reuse after restart. This sample therefore has no documented end-to-end runtime command yet. ## 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_API_URL`: Required. KAgent control-plane API URL; locally, `http://localhost:8083` -- `KAGENT_GATEWAY_URL`: Required. KAgent A2A and MCP gateway URL; locally, `http://localhost:8083` +- `KAGENT_API_URL`: Required by `KAgentConfig`; not used for outbound control-plane calls by this wrapper +- `KAGENT_GATEWAY_URL`: Required by `KAgentConfig`; not used for outbound gateway calls by this wrapper +- `KAGENT_NAME`: Required agent name +- `KAGENT_NAMESPACE`: Required agent namespace +- `KAGENT_CHECKPOINT_DB`: SQLite checkpoint path (default: `/tmp/currency-checkpoints.sqlite`) - `PORT`: Server port (default: 8080) - `HOST`: Server host (default: 0.0.0.0) @@ -107,5 +56,6 @@ High-level options for tracing this sample: - If you create custom tools, decorate them with the LangSmith SDK's `@traceable` decorator; this sample shows it for the exchange-rate tool. References: -- LangSmith SDK: `https://github.com/langchain-ai/langsmith-sdk` -- Trace with OpenTelemetry: `https://docs.langchain.com/langsmith/trace-with-opentelemetry` + +- LangSmith SDK: https://github.com/langchain-ai/langsmith-sdk +- Trace with OpenTelemetry: https://docs.langchain.com/langsmith/trace-with-opentelemetry diff --git a/python/samples/langgraph/currency/currency/cli.py b/python/samples/langgraph/currency/currency/cli.py index 973d875a48..1de8bea687 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() - 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..0b08270363 --- /dev/null +++ b/python/samples/langgraph/currency/tests/test_cli.py @@ -0,0 +1,32 @@ +"""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): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + monkeypatch.setenv("KAGENT_API_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_GATEWAY_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_NAME", "currency") + monkeypatch.setenv("KAGENT_NAMESPACE", "default") + 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..cfd4784c2d --- /dev/null +++ b/python/samples/langgraph/hitl-tools/README.md @@ -0,0 +1,27 @@ +# 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/`, set the configuration required by `KAgentConfig` and the OpenAI +API key, then run the installed entry point: + +```bash +export KAGENT_API_URL=http://localhost:8083 +export KAGENT_GATEWAY_URL=http://localhost:8083 +export KAGENT_NAME=hitl-tools +export KAGENT_NAMESPACE=default +export OPENAI_API_KEY=your-api-key +uv run --package hitl-tools hitl-tools +``` + +The wrapper requires the Kagent configuration values but does not use the API or +gateway URLs for outbound connections. `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 7f9dee1424..bb68760822 100644 --- a/python/samples/langgraph/hitl-tools/hitl_tools/cli.py +++ b/python/samples/langgraph/hitl-tools/hitl_tools/cli.py @@ -5,34 +5,39 @@ 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() - app = KAgentApp( + return KAgentApp( graph=graph, agent_card=agent_card, - config=config, + config=KAgentConfig(), 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..b024899b88 --- /dev/null +++ b/python/samples/langgraph/hitl-tools/tests/test_hitl_tools_cli.py @@ -0,0 +1,22 @@ +"""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): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + monkeypatch.setenv("KAGENT_API_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_GATEWAY_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_NAME", "hitl-tools") + monkeypatch.setenv("KAGENT_NAMESPACE", "default") + + 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..1d50df90aa --- /dev/null +++ b/python/samples/langgraph/kebab/README.md @@ -0,0 +1,26 @@ +# Kebab LangGraph Agent + +This sample serves a minimal LangGraph workflow through `kagent-langgraph`'s A2A +application wrapper. + +## Local Startup + +From `python/`, set the configuration required by `KAgentConfig` and run the +installed entry point: + +```bash +export KAGENT_API_URL=http://localhost:8083 +export KAGENT_GATEWAY_URL=http://localhost:8083 +export KAGENT_NAME=kebab +export KAGENT_NAMESPACE=default +uv run --package kebab kebab +``` + +The wrapper requires the Kagent configuration values but does not use the API or +gateway URLs for outbound connections. `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 6b719c54c7..d323c2228b 100644 --- a/python/samples/langgraph/kebab/kebab/cli.py +++ b/python/samples/langgraph/kebab/kebab/cli.py @@ -5,33 +5,39 @@ 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() - app = KAgentApp( + return KAgentApp( graph=graph, agent_card=agent_card, - config=config, + config=KAgentConfig(), 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..7adfd6a142 --- /dev/null +++ b/python/samples/langgraph/kebab/tests/test_kebab_cli.py @@ -0,0 +1,22 @@ +"""Regression tests for the kebab console entry point.""" + +import importlib + +from fastapi.testclient import TestClient + + +def test_main_passes_healthy_app_to_uvicorn(monkeypatch): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + monkeypatch.setenv("KAGENT_API_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_GATEWAY_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_NAME", "kebab") + monkeypatch.setenv("KAGENT_NAMESPACE", "default") + + 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..6789c1d9fb 100644 --- a/python/samples/openai/basic_agent/README.md +++ b/python/samples/openai/basic_agent/README.md @@ -0,0 +1,27 @@ +# Basic OpenAI Agent + +This sample serves an OpenAI Agents SDK agent through `kagent-openai`'s A2A +application wrapper. + +## Local Startup + +From `python/`, set the configuration required by `KAgentConfig` and the OpenAI +API key, then run the installed entry point: + +```bash +export KAGENT_API_URL=http://localhost:8083 +export KAGENT_GATEWAY_URL=http://localhost:8083 +export KAGENT_NAME=basic-openai-agent +export KAGENT_NAMESPACE=default +export OPENAI_API_KEY=your-api-key +uv run --package basic-openai-agent basic-openai-agent +``` + +The wrapper requires the Kagent configuration values but does not use the API or +gateway URLs for outbound connections. `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 07f006bc44..b157994457 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() -# 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(), + ).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 4fbd63fea9..763b219a2e 100644 --- a/python/samples/openai/basic_agent/pyproject.toml +++ b/python/samples/openai/basic_agent/pyproject.toml @@ -10,6 +10,9 @@ dependencies = [ "uvicorn>=0.51.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..4bec3d7485 --- /dev/null +++ b/python/samples/openai/basic_agent/tests/test_agent.py @@ -0,0 +1,29 @@ +"""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): + monkeypatch.setenv("OPENAI_API_KEY", "fake") + monkeypatch.setenv("KAGENT_API_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_GATEWAY_URL", "http://localhost:8083") + monkeypatch.setenv("KAGENT_NAME", "basic-openai-agent") + monkeypatch.setenv("KAGENT_NAMESPACE", "default") + + 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}