diff --git a/packages/sdk/server-ai/src/ldai/__init__.py b/packages/sdk/server-ai/src/ldai/__init__.py index e8ef83b4..944a0cb8 100644 --- a/packages/sdk/server-ai/src/ldai/__init__.py +++ b/packages/sdk/server-ai/src/ldai/__init__.py @@ -27,11 +27,23 @@ ModelConfig, ProviderConfig, ) +from ldai.providers import ( + AgentGraphResult, + AgentGraphRunner, + AgentResult, + AgentRunner, + ToolRegistry, +) from ldai.providers.types import EvalScore, JudgeResponse from ldai.tracker import AIGraphTracker __all__ = [ 'LDAIClient', + 'AgentRunner', + 'AgentGraphRunner', + 'AgentResult', + 'AgentGraphResult', + 'ToolRegistry', 'AIAgentConfig', 'AIAgentConfigDefault', 'AIAgentConfigRequest', diff --git a/packages/sdk/server-ai/src/ldai/providers/__init__.py b/packages/sdk/server-ai/src/ldai/providers/__init__.py index db1c8836..0148698b 100644 --- a/packages/sdk/server-ai/src/ldai/providers/__init__.py +++ b/packages/sdk/server-ai/src/ldai/providers/__init__.py @@ -1,9 +1,31 @@ +from ldai.providers.agent_graph_runner import AgentGraphRunner +from ldai.providers.agent_runner import AgentRunner from ldai.providers.ai_provider import AIProvider from ldai.providers.model_runner import ModelRunner from ldai.providers.runner_factory import RunnerFactory +from ldai.providers.types import ( + AgentGraphResult, + AgentResult, + EvalScore, + JudgeResponse, + LDAIMetrics, + ModelResponse, + StructuredResponse, + ToolRegistry, +) __all__ = [ 'AIProvider', + 'AgentGraphResult', + 'AgentGraphRunner', + 'AgentResult', + 'AgentRunner', + 'EvalScore', + 'JudgeResponse', + 'LDAIMetrics', + 'ModelResponse', 'ModelRunner', 'RunnerFactory', + 'StructuredResponse', + 'ToolRegistry', ] diff --git a/packages/sdk/server-ai/src/ldai/providers/agent_graph_runner.py b/packages/sdk/server-ai/src/ldai/providers/agent_graph_runner.py new file mode 100644 index 00000000..a7bdefee --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/providers/agent_graph_runner.py @@ -0,0 +1,23 @@ +from typing import Any, Protocol, runtime_checkable + +from ldai.providers.types import AgentGraphResult + + +@runtime_checkable +class AgentGraphRunner(Protocol): + """ + Runtime capability interface for multi-agent graph execution. + + An AgentGraphRunner is a focused, configured object returned by + AIProvider.create_agent_graph(). It holds all provider wiring internally — + the caller just passes input. + """ + + 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/providers/agent_runner.py b/packages/sdk/server-ai/src/ldai/providers/agent_runner.py new file mode 100644 index 00000000..d0bcc883 --- /dev/null +++ b/packages/sdk/server-ai/src/ldai/providers/agent_runner.py @@ -0,0 +1,23 @@ +from typing import Any, Protocol, runtime_checkable + +from ldai.providers.types import AgentResult + + +@runtime_checkable +class AgentRunner(Protocol): + """ + Runtime capability interface for single-agent execution. + + An AgentRunner is a focused, configured object returned by + AIProvider.create_agent(). It holds all provider wiring internally — + the caller just passes input. + """ + + 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/providers/model_runner.py b/packages/sdk/server-ai/src/ldai/providers/model_runner.py index 79309f33..5f00887c 100644 --- a/packages/sdk/server-ai/src/ldai/providers/model_runner.py +++ b/packages/sdk/server-ai/src/ldai/providers/model_runner.py @@ -1,20 +1,19 @@ -from abc import ABC, abstractmethod -from typing import Any, Dict, List +from typing import Any, Dict, List, Protocol, runtime_checkable from ldai.models import LDMessage from ldai.providers.types import ModelResponse, StructuredResponse -class ModelRunner(ABC): +@runtime_checkable +class ModelRunner(Protocol): """ Runtime capability interface for model invocation. A ModelRunner is a focused, configured object returned by - AIConnector.create_model(). It knows exactly which model to call + AIProvider.create_model(). It knows exactly which model to call and with what parameters — the caller just passes messages. """ - @abstractmethod async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse: """ Invoke the model with an array of messages. @@ -22,8 +21,8 @@ async def invoke_model(self, messages: List[LDMessage]) -> ModelResponse: :param messages: Array of LDMessage objects representing the conversation :return: ModelResponse containing the model's response and metrics """ + ... - @abstractmethod async def invoke_structured_model( self, messages: List[LDMessage], @@ -36,3 +35,4 @@ async def invoke_structured_model( :param response_structure: Dictionary defining the JSON schema for output structure :return: StructuredResponse containing the structured data """ + ... diff --git a/packages/sdk/server-ai/src/ldai/providers/types.py b/packages/sdk/server-ai/src/ldai/providers/types.py index 0a07151f..bb87350e 100644 --- a/packages/sdk/server-ai/src/ldai/providers/types.py +++ b/packages/sdk/server-ai/src/ldai/providers/types.py @@ -3,11 +3,15 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, List, Optional +from typing import Any, Callable, Dict, List, Optional from ldai.models import LDMessage from ldai.tracker import TokenUsage +# 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 LDAIMetrics: @@ -94,3 +98,23 @@ def to_dict(self) -> Dict[str, Any]: if self.error is not None: result['error'] = self.error return result + + +@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 00000000..d5136fd0 --- /dev/null +++ b/packages/sdk/server-ai/tests/test_runner_abcs.py @@ -0,0 +1,108 @@ +import pytest + +from ldai.providers import AgentGraphResult, AgentGraphRunner, AgentResult, AgentRunner, ToolRegistry +from ldai.providers.types import LDAIMetrics + + +# --- Concrete test doubles --- + +class ConcreteAgentRunner: + async def run(self, input): + return AgentResult( + output=f"agent response to: {input}", + raw={"raw": input}, + metrics=LDAIMetrics(success=True), + ) + + +class ConcreteAgentGraphRunner: + async def run(self, input): + return AgentGraphResult( + output=f"graph response to: {input}", + raw={"raw": input}, + metrics=LDAIMetrics(success=True), + ) + + +class MissingRunMethod: + pass + + +# --- AgentRunner --- + +def test_agent_runner_structural_check_passes(): + assert isinstance(ConcreteAgentRunner(), AgentRunner) + + +def test_agent_runner_structural_check_fails_when_run_missing(): + assert not isinstance(MissingRunMethod(), AgentRunner) + + +@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_structural_check_passes(): + assert isinstance(ConcreteAgentGraphRunner(), AgentGraphRunner) + + +def test_agent_graph_runner_structural_check_fails_when_run_missing(): + assert not isinstance(MissingRunMethod(), AgentGraphRunner) + + +@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')