diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index e8ef83b..7810db6 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -28,10 +28,17 @@ ProviderConfig, ) from ldai.providers.types import EvalScore, JudgeResponse +from ldai.runners import AgentGraphRunner, AgentRunner +from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry from ldai.tracker import AIGraphTracker __all__ = [ 'LDAIClient', + 'AgentRunner', + 'AgentGraphRunner', + 'AgentResult', + 'AgentGraphResult', + 'ToolRegistry', 'AIAgentConfig', 'AIAgentConfigDefault', 'AIAgentConfigRequest', diff --git a/packages/sdk/server-ai/src/ldai/runners/__init__.py b/packages/sdk/server-ai/src/ldai/runners/__init__.py new file mode 100644 index 0000000..a9a834c --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/__init__.py @@ -0,0 +1,13 @@ +"""Runner ABCs and result types for LaunchDarkly AI SDK.""" + +from ldai.runners.agent_graph_runner import AgentGraphRunner +from ldai.runners.agent_runner import AgentRunner +from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry + +__all__ = [ + 'AgentRunner', + 'AgentGraphRunner', + 'AgentResult', + 'AgentGraphResult', + 'ToolRegistry', +] diff --git a/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py b/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py new file mode 100644 index 0000000..3cfdd80 --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/agent_graph_runner.py @@ -0,0 +1,26 @@ +"""Abstract base class for agent graph runners.""" + +from abc import ABC, abstractmethod +from typing import Any + +from ldai.runners.types import AgentGraphResult + + +class AgentGraphRunner(ABC): + """ + Abstract base class for agent graph runners. + + An AgentGraphRunner encapsulates multi-agent graph execution. + Provider-specific implementations (e.g. OpenAIAgentGraphRunner) are + returned by RunnerFactory.create_agent_graph() and hold all provider + wiring internally. + """ + + @abstractmethod + async def run(self, input: Any) -> AgentGraphResult: + """ + Run the agent graph with the given input. + + :param input: The input to the agent graph (string prompt or structured input) + :return: AgentGraphResult containing the output, raw response, and metrics + """ diff --git a/packages/sdk/server-ai/src/ldai/runners/agent_runner.py b/packages/sdk/server-ai/src/ldai/runners/agent_runner.py new file mode 100644 index 0000000..063198d --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/agent_runner.py @@ -0,0 +1,25 @@ +"""Abstract base class for agent runners.""" + +from abc import ABC, abstractmethod +from typing import Any + +from ldai.runners.types import AgentResult + + +class AgentRunner(ABC): + """ + Abstract base class for single-agent runners. + + An AgentRunner encapsulates the execution of a single AI agent. + Provider-specific implementations (e.g. OpenAIAgentRunner) are returned + by RunnerFactory.create_agent() and hold all provider wiring internally. + """ + + @abstractmethod + async def run(self, input: Any) -> AgentResult: + """ + Run the agent with the given input. + + :param input: The input to the agent (string prompt or structured input) + :return: AgentResult containing the output, raw response, and metrics + """ diff --git a/packages/sdk/server-ai/src/ldai/runners/types.py b/packages/sdk/server-ai/src/ldai/runners/types.py new file mode 100644 index 0000000..f4db9a6 --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/runners/types.py @@ -0,0 +1,32 @@ +"""Result types and type aliases for agent and agent graph runners.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Callable, Dict + +from ldai.providers.types import LDAIMetrics + +# Type alias for a registry of tools available to an agent. +# Keys are tool names; values are the callable implementations. +ToolRegistry = Dict[str, Callable] + + +@dataclass +class AgentResult: + """ + Result from a single-agent run. + """ + output: str + raw: Any + metrics: LDAIMetrics + + +@dataclass +class AgentGraphResult: + """ + Result from an agent graph run. + """ + output: str + raw: Any + metrics: LDAIMetrics diff --git a/packages/sdk/server-ai/tests/test_runner_abcs.py b/packages/sdk/server-ai/tests/test_runner_abcs.py new file mode 100644 index 0000000..1fc03ae --- /dev/null +++ b/packages/sdk/server-ai/tests/test_runner_abcs.py @@ -0,0 +1,100 @@ +import pytest + +from ldai.providers.types import LDAIMetrics +from ldai.runners.agent_graph_runner import AgentGraphRunner +from ldai.runners.agent_runner import AgentRunner +from ldai.runners.types import AgentGraphResult, AgentResult, ToolRegistry + + +# --- Concrete test doubles --- + +class ConcreteAgentRunner(AgentRunner): + async def run(self, input): + return AgentResult( + output=f"agent response to: {input}", + raw={"raw": input}, + metrics=LDAIMetrics(success=True), + ) + + +class ConcreteAgentGraphRunner(AgentGraphRunner): + async def run(self, input): + return AgentGraphResult( + output=f"graph response to: {input}", + raw={"raw": input}, + metrics=LDAIMetrics(success=True), + ) + + +# --- AgentRunner --- + +def test_agent_runner_is_abstract(): + with pytest.raises(TypeError): + AgentRunner() # type: ignore[abstract] + + +@pytest.mark.asyncio +async def test_agent_runner_run_returns_agent_result(): + runner = ConcreteAgentRunner() + result = await runner.run("hello") + assert isinstance(result, AgentResult) + assert result.output == "agent response to: hello" + assert result.raw == {"raw": "hello"} + assert result.metrics.success is True + + +@pytest.mark.asyncio +async def test_agent_result_fields(): + metrics = LDAIMetrics(success=True) + result = AgentResult(output="done", raw={"key": "val"}, metrics=metrics) + assert result.output == "done" + assert result.raw == {"key": "val"} + assert result.metrics is metrics + + +# --- AgentGraphRunner --- + +def test_agent_graph_runner_is_abstract(): + with pytest.raises(TypeError): + AgentGraphRunner() # type: ignore[abstract] + + +@pytest.mark.asyncio +async def test_agent_graph_runner_run_returns_agent_graph_result(): + runner = ConcreteAgentGraphRunner() + result = await runner.run("hello graph") + assert isinstance(result, AgentGraphResult) + assert result.output == "graph response to: hello graph" + assert result.raw == {"raw": "hello graph"} + assert result.metrics.success is True + + +@pytest.mark.asyncio +async def test_agent_graph_result_fields(): + metrics = LDAIMetrics(success=False) + result = AgentGraphResult(output="", raw=None, metrics=metrics) + assert result.output == "" + assert result.raw is None + assert result.metrics.success is False + + +# --- ToolRegistry --- + +def test_tool_registry_is_dict_of_callables(): + tools: ToolRegistry = { + "search": lambda q: f"results for {q}", + "calculator": lambda x: x * 2, + } + assert tools["search"]("python") == "results for python" + assert tools["calculator"](21) == 42 + + +# --- Top-level exports --- + +def test_top_level_exports(): + import ldai + assert hasattr(ldai, 'AgentRunner') + assert hasattr(ldai, 'AgentGraphRunner') + assert hasattr(ldai, 'AgentResult') + assert hasattr(ldai, 'AgentGraphResult') + assert hasattr(ldai, 'ToolRegistry')