-
Notifications
You must be signed in to change notification settings - Fork 22
test(fixtures): add self-contained openai-default test system YAML #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
|
|
||
| [tools.echo_tool] | ||
| module = "tools.echo_tool" | ||
| callable = "echo_tool" | ||
|
|
||
| [resolvers.shared] | ||
| module = "resolvers.shared" | ||
| class = "SharedResolver" | ||
|
|
||
| [resolvers.greeting_agent] | ||
| module = "resolvers.greeting_agent" | ||
| class = "Resolver" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from __future__ import annotations | ||
|
|
||
| from shared import SharedResolver | ||
|
|
||
|
|
||
| class Resolver(SharedResolver): | ||
| def __init__(self) -> None: | ||
| super().__init__() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| from __future__ import annotations | ||
|
|
||
|
|
||
| class SharedResolver: | ||
| def __init__(self) -> None: | ||
| pass | ||
|
|
||
| def current_date(self, ctx: dict) -> str: | ||
| """Returns the value for {{current_date}}""" | ||
| return "2026-07-19" | ||
|
|
||
| def user_name(self, ctx: dict) -> str: | ||
| """Returns the value for {{user_name}}""" | ||
| return "Tester" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| def echo_tool(input: dict) -> str: | ||
| """Echo back the input text. Used to verify tool wiring end to end. | ||
| """ | ||
| text = input.get("text", "") | ||
| return f"echo: {text}" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Echo Agent — system | ||
|
|
||
| You echo the user's input back to them using the `echo_tool` tool. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Greeting Agent — system | ||
|
|
||
| You handle greetings and small talk. Be friendly, brief, and use the | ||
| `current_date` and `user_name` context when natural. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Root Router — orchestrator | ||
|
|
||
| You are the entry point of the test system. Read the user's request and route it | ||
| to the most appropriate child specialist: | ||
|
|
||
| - `greeting_agent` for greetings and small talk. | ||
| - `echo_agent` for requests that ask to repeat or echo text. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Root Router — system | ||
|
|
||
| You route each incoming request to a single child specialist. Do not answer | ||
| directly; delegate to `greeting_agent` or `echo_agent`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import sys | ||
|
|
||
| import pytest | ||
| from tests.fixtures.utils import FakeEngine, load_test_system | ||
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _cleanup_shared() -> None: | ||
| yield | ||
| sys.modules.pop("shared", None) | ||
|
|
||
|
|
||
| def test_spec_validates_offline() -> None: | ||
| load_test_system() | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_engine_builds_from_fixture() -> None: | ||
| async with FakeEngine(): | ||
| pass | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_orchestrator_routes_to_greeting() -> None: | ||
| async with FakeEngine() as engine: | ||
| result = await engine.run("greeting_agent hello") | ||
| assert "root_router/greeting_agent" in result.visited | ||
| assert result.answer | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_orchestrator_routes_to_echo_and_calls_tool() -> None: | ||
| async with FakeEngine() as engine: | ||
| result = await engine.run("echo_agent echo test") | ||
| assert "root_router/echo_agent" in result.visited | ||
| assert any(record.name == "echo_tool" for record in result.used_tools) | ||
| assert result.answer | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_resolvers_resolve_at_runtime() -> None: | ||
| async with FakeEngine() as engine: | ||
| result = await engine.run("hello") | ||
| assert result.status == "completed" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| system: | ||
| name: "Test Agent System" | ||
|
|
||
|
|
||
| defaults: | ||
| model: | ||
| provider: openai | ||
| name: gpt-4o-mini | ||
| temperature: 0.0 | ||
|
|
||
| execution: | ||
| max_iterations: 10 | ||
| max_tool_calls: 5 | ||
| max_tool_calls_per_agent: 3 | ||
| max_child_agent_calls: 4 | ||
| allow_duplicate_tool_calls: false | ||
|
|
||
| tools: | ||
| echo_tool: | ||
| description: > | ||
| Echo back the input text. Used to verify tool wiring end to end. | ||
|
|
||
| resolvers: | ||
| current_date: | ||
| user_name: | ||
|
|
||
| orchestrators: | ||
| root_router: | ||
| name: "Root Router" | ||
| description: > | ||
| Entry point. Routes the user request to the appropriate specialist. | ||
| model: | ||
| provider: openai | ||
| name: gpt-4o | ||
| temperature: 0.0 | ||
| prompts: | ||
| orchestrator: prompts/root_router/orchestrator.md | ||
| system: prompts/root_router/system.md | ||
|
|
||
| agents: | ||
| greeting_agent: | ||
| name: "greeting" | ||
| description: > | ||
| Handles simple greetings and small talk. | ||
| prompts: | ||
| system: prompts/greeting_agent/system.md | ||
| resolvers: | ||
| - current_date | ||
| - user_name | ||
|
|
||
| echo_agent: | ||
| name: "echo" | ||
| description: > | ||
| Demonstrates tool usage by echoing user input. | ||
| prompts: | ||
| system: prompts/echo_agent/system.md | ||
| tools: | ||
| - echo_tool | ||
| auto: true | ||
|
|
||
| graph: | ||
| root_router: | ||
| greeting_agent: | ||
| echo_agent: | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| from __future__ import annotations | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe lets call this file utils?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure |
||
|
|
||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| from langchain_core.messages import AIMessage | ||
| from langchain_core.messages.tool import ToolCall | ||
|
|
||
| from agent_engine.core.spec import SystemSpec | ||
| from agent_engine.engine.langgraph.engine import LangGraphEngine | ||
| from agent_engine.engine.types import RunResult | ||
|
|
||
|
|
||
| class FakeChatModel: | ||
| def __init__( | ||
| self, | ||
| answer: str = "ok", | ||
| tool_names: list[str] | None = None, | ||
| ) -> None: | ||
| self._answer = answer | ||
| self._tool_names = tool_names or [] | ||
|
|
||
| def bind_tools(self, tools: list[Any]) -> FakeChatModel: | ||
| return FakeChatModel( | ||
| self._answer, | ||
| [t.name for t in tools], | ||
| ) | ||
|
|
||
| async def ainvoke(self, messages: list[Any]) -> AIMessage: | ||
| return self._respond(messages) | ||
|
|
||
| async def astream(self, messages: list[Any]) -> Any: | ||
| yield self._respond(messages) | ||
|
|
||
| def _respond(self, messages: list[Any]) -> AIMessage: | ||
| already_called = any( | ||
| m.__class__.__name__ == "ToolMessage" for m in messages | ||
| ) | ||
| if self._tool_names and not already_called: | ||
| return AIMessage( | ||
| content="", | ||
| tool_calls=[ | ||
| ToolCall( | ||
| name=self._select(messages), | ||
| args={"message": self._user_text(messages)}, | ||
| id="call_1", | ||
| ) | ||
| ], | ||
| ) | ||
| return AIMessage(content=self._answer) | ||
|
|
||
| def _select(self, messages: list[Any]) -> str: | ||
| text = self._user_text(messages).lower() | ||
| for name in self._tool_names: | ||
| if name.lower() in text: | ||
| return name | ||
| return self._tool_names[0] | ||
|
|
||
| @staticmethod | ||
| def _user_text(messages: list[Any]) -> str: | ||
| for m in reversed(messages): | ||
| if m.__class__.__name__ == "HumanMessage": | ||
| return str(m.content) | ||
| return "" | ||
|
|
||
|
|
||
| def fake_model_factory( | ||
| provider: str = "fake", | ||
| name: str = "fake", | ||
| temperature: float | None = None, | ||
| *, | ||
| answer: str = "ok", | ||
| ) -> Any: | ||
| return FakeChatModel(answer=answer) | ||
|
|
||
|
|
||
| FIXTURE_DIR = Path(__file__).resolve().parent | ||
| FIXTURE_SPEC = FIXTURE_DIR / "test_system.yaml" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe lets do it injectable? think about we will added more yamls for tests in your infra
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point . I have made changes ,just need to push |
||
|
|
||
|
|
||
| def load_test_system() -> tuple[SystemSpec, Path]: | ||
| from agent_engine.core.validator import SystemSpecValidator | ||
| from agent_engine.parsers.yaml.parser import YAMLParser | ||
|
|
||
| spec = YAMLParser().parse(str(FIXTURE_SPEC)) | ||
| errors = SystemSpecValidator().validate(spec, FIXTURE_DIR) | ||
| if errors: | ||
| raise AssertionError( | ||
| "Fixture validation failed:\n" + "\n".join(str(e) for e in errors) | ||
| ) | ||
| return spec, FIXTURE_DIR | ||
|
|
||
|
|
||
| class FakeEngine: | ||
| def __init__(self, answer: str = "ok") -> None: | ||
| self._answer = answer | ||
| self._engine: LangGraphEngine | None = None | ||
|
|
||
| async def __aenter__(self) -> FakeEngine: | ||
| spec, base_dir = load_test_system() | ||
| self._engine = LangGraphEngine(base_dir, model_factory=fake_model_factory) | ||
| await self._engine.build(spec) | ||
| return self | ||
|
|
||
| async def __aexit__(self, *args: Any) -> None: | ||
| if self._engine is not None: | ||
| await self._engine.__aexit__(*args) | ||
|
|
||
| async def run(self, message: str) -> RunResult: | ||
| assert self._engine is not None | ||
| return await self._engine.run(message) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about test for all of this configurations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added test_fixture_execution_policy_parsed_from_yaml which loads the spec from YAML and asserts all five fields (max_iterations, max_tool_calls, max_tool_calls_per_agent, max_child_agent_calls, allow_duplicate_tool_calls) match what's in the file. This guards against silent parser/schema changes dropping or misparsing those values. Not testing runtime enforcement here because tests/engine/test_execution_limits.py already covers that with inline specs; this fixture test just guarantees the YAML wiring is correct.