Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ on:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
with:
python-version: "3.12"
cache: "pip"
- name: Install ruff
# Pinned to match the ruff-pre-commit rev in .pre-commit-config.yaml
run: pip install ruff==0.15.8
- name: Ruff lint
run: ruff check src/ tests/
- name: Ruff format check
run: ruff format --check src/ tests/

test:
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -45,7 +61,7 @@ jobs:

build:
runs-on: ubuntu-latest
needs: [test, security]
needs: [lint, test, security]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-python@v6
Expand Down
4 changes: 2 additions & 2 deletions tests/agent/test_agent_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,11 +295,11 @@ def test_routing_mode_caches(self) -> None:
config = AgentConfig(max_iterations=1, routing_only=True, cache=cache)
agent = Agent(tools=[_dummy_tool()], provider=provider, config=config)

result1 = agent.run([Message(role=Role.USER, content="Route me")])
agent.run([Message(role=Role.USER, content="Route me")])
assert provider.call_count == 1

agent.reset()
result2 = agent.run([Message(role=Role.USER, content="Route me")])
agent.run([Message(role=Role.USER, content="Route me")])
assert provider.call_count == 1 # cache hit
assert cache.stats.hits == 1

Expand Down
4 changes: 1 addition & 3 deletions tests/agent/test_agent_observer.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@

from __future__ import annotations

import asyncio
import copy
import json
import logging
from dataclasses import dataclass, field
Expand Down Expand Up @@ -723,7 +721,7 @@ def complete(
return msg, stats

agent, obs = _agent(provider=JSONProvider())
result = agent.run("what is the answer?", response_format=Response)
agent.run("what is the answer?", response_format=Response)
validates = obs.get("structured_validate")
assert len(validates) >= 1
assert validates[0].args["success"] is True
Expand Down
5 changes: 1 addition & 4 deletions tests/agent/test_agent_v013_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,13 @@
ConversationMemory,
FallbackProvider,
Message,
PolicyDecision,
Role,
ToolPolicy,
tool,
)
from selectools.providers.base import Provider, ProviderError
from selectools.providers.base import ProviderError
from selectools.providers.openai_provider import OpenAIProvider
from selectools.trace import AgentTrace
from selectools.types import Message as Msg
from selectools.usage import UsageStats

try:
from pydantic import BaseModel
Expand Down
28 changes: 13 additions & 15 deletions tests/agent/test_astream_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,19 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple, Union
from typing import Any, AsyncGenerator, Dict, List, Tuple, Union
from unittest.mock import MagicMock

import pytest

from selectools.agent.core import Agent, AgentConfig, _RunContext
from selectools.agent.core import Agent, AgentConfig
from selectools.exceptions import GraphExecutionError
from selectools.guardrails import Guardrail, GuardrailAction, GuardrailResult, GuardrailsPipeline
from selectools.guardrails import Guardrail, GuardrailResult, GuardrailsPipeline
from selectools.observer import AgentObserver
from selectools.policy import PolicyDecision, PolicyResult, ToolPolicy
from selectools.providers.base import Provider
from selectools.structured import ResponseFormat
from selectools.tools import Tool, tool
from selectools.trace import AgentTrace
from selectools.types import AgentResult, Message, Role, StreamChunk, ToolCall
from selectools.tools import tool
from selectools.types import AgentResult, Message, Role, ToolCall
from selectools.usage import UsageStats

_DUMMY_USAGE = UsageStats(0, 0, 0, 0.0, "mock", "mock")
Expand Down Expand Up @@ -242,7 +240,7 @@ def check(self, content: str) -> GuardrailResult:
provider=provider,
config=AgentConfig(max_iterations=1, guardrails=pipeline),
)
result = await _collect_astream(agent, "This is bad")
await _collect_astream(agent, "This is bad")
# The guardrail should have rewritten "bad" to "good" in the history
user_msgs = [m for m in agent._history if m.role == Role.USER]
assert any("good" in (m.content or "") for m in user_msgs)
Expand Down Expand Up @@ -288,7 +286,7 @@ async def test_knowledge_memory_context_injected(self) -> None:
provider=provider,
config=AgentConfig(max_iterations=1, knowledge_memory=km),
)
result = await _collect_astream(agent, "Hello")
await _collect_astream(agent, "Hello")
system_msgs = [m for m in agent._history if m.role == Role.SYSTEM]
assert any("User prefers Python" in (m.content or "") for m in system_msgs)

Expand All @@ -308,7 +306,7 @@ async def test_entity_memory_context_injected(self) -> None:
provider=provider,
config=AgentConfig(max_iterations=1, entity_memory=em),
)
result = await _collect_astream(agent, "Hi")
await _collect_astream(agent, "Hi")
system_msgs = [m for m in agent._history if m.role == Role.SYSTEM]
assert any("Alice: engineer" in (m.content or "") for m in system_msgs)

Expand All @@ -328,7 +326,7 @@ async def test_knowledge_graph_context_injected(self) -> None:
provider=provider,
config=AgentConfig(max_iterations=1, knowledge_graph=kg),
)
result = await _collect_astream(agent, "Tell me about Alice")
await _collect_astream(agent, "Tell me about Alice")
system_msgs = [m for m in agent._history if m.role == Role.SYSTEM]
assert any("Alice -> works_at -> Acme" in (m.content or "") for m in system_msgs)

Expand Down Expand Up @@ -501,7 +499,7 @@ async def test_analytics_recorded(self) -> None:
provider=provider,
config=AgentConfig(max_iterations=2, enable_analytics=True),
)
result = await _collect_astream(agent, "Greet Bob")
await _collect_astream(agent, "Greet Bob")
assert agent.analytics is not None
metrics = agent.analytics.get_metrics("greet_tool")
assert metrics is not None
Expand Down Expand Up @@ -895,7 +893,7 @@ async def test_tool_usage_dict_populated(self) -> None:
provider=provider,
config=AgentConfig(max_iterations=2),
)
result = await _collect_astream(agent, "Greet Alice")
await _collect_astream(agent, "Greet Alice")
assert "greet_tool" in agent.usage.tool_usage
assert agent.usage.tool_usage["greet_tool"] >= 1

Expand Down Expand Up @@ -1184,7 +1182,7 @@ def suspicious_tool() -> str:
provider=provider,
config=AgentConfig(max_iterations=2, screen_tool_output=True),
)
result = agent.run("Use both tools")
agent.run("Use both tools")
# Screening should have caught the injection pattern in the tool result
# The tool result in history should be modified by screening
tool_msgs = [
Expand All @@ -1209,7 +1207,7 @@ def suspicious_tool_async() -> str:
provider=provider,
config=AgentConfig(max_iterations=2, screen_tool_output=True),
)
result = await agent.arun("Use both tools")
await agent.arun("Use both tools")
tool_msgs = [
m
for m in agent._history
Expand Down
7 changes: 3 additions & 4 deletions tests/agent/test_batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@

from __future__ import annotations

from typing import Any, Dict, List, Optional, Tuple
from unittest.mock import MagicMock
from typing import Any, List, Tuple

import pytest

from selectools.agent.core import Agent, AgentConfig
from selectools.providers.base import Provider, ProviderError
from selectools.tools import tool
from selectools.types import AgentResult, Message, Role, ToolCall
from selectools.types import AgentResult, Message, Role
from selectools.usage import UsageStats

_DUMMY_USAGE = UsageStats(0, 0, 0, 0.0, "mock", "mock")
Expand Down Expand Up @@ -183,6 +182,6 @@ def test_partial_failure_returns_all_results(self) -> None:
assert len(results) == 3

contents = [r.content for r in results]
error_results = [c for c in contents if "error" in c.lower() or "Error" in c]
[c for c in contents if "error" in c.lower() or "Error" in c]
success_results = [c for c in contents if "ok" in c.lower()]
assert len(success_results) >= 1
4 changes: 2 additions & 2 deletions tests/agent/test_parallel_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ async def test_partial_failure_async(self) -> None:
provider=provider,
config=AgentConfig(max_iterations=3, parallel_tool_execution=True),
)
result = await agent.arun([Message(role=Role.USER, content="go")])
await agent.arun([Message(role=Role.USER, content="go")])

tool_results = [m for m in agent._history if m.role == Role.TOOL]
assert len(tool_results) == 3
Expand All @@ -334,7 +334,7 @@ def test_unknown_tool_in_parallel_batch(self) -> None:
provider=provider,
config=AgentConfig(max_iterations=3, parallel_tool_execution=True),
)
result = agent.run([Message(role=Role.USER, content="go")])
agent.run([Message(role=Role.USER, content="go")])

tool_results = [m for m in agent._history if m.role == Role.TOOL]
assert len(tool_results) == 2
Expand Down
20 changes: 5 additions & 15 deletions tests/agent/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import asyncio
import base64
import copy
import json
import threading
import time
Expand All @@ -22,7 +21,7 @@

from selectools.agent.core import Agent, AgentConfig
from selectools.observer import AgentObserver
from selectools.policy import PolicyDecision, ToolPolicy
from selectools.policy import ToolPolicy
from selectools.providers.base import Provider, ProviderError
from selectools.providers.fallback import FallbackProvider
from selectools.providers.stubs import LocalProvider
Expand Down Expand Up @@ -291,7 +290,7 @@ async def test_routing_only_fires_iteration_end_async(self) -> None:
observers=[observer],
),
)
result = await agent.arun("route me")
await agent.arun("route me")
assert len(observer.events.get("on_iteration_end", [])) == 1


Expand Down Expand Up @@ -1130,7 +1129,6 @@ def tool_b() -> str:

# Second call returns no tool calls, ending the loop
call_no = {"n": 0}
original_complete = provider.complete

def complete_once(**kw):
call_no["n"] += 1
Expand Down Expand Up @@ -1230,9 +1228,6 @@ class TestCoherenceCheckTraceStepType:

def _make_coherence_provider(self, incoherent: bool):
"""Return a provider that fails coherence checks when incoherent=True."""
from unittest.mock import patch

from selectools.coherence import CoherenceResult

class _CoherenceProvider(Provider):
name = "coherence"
Expand Down Expand Up @@ -1661,7 +1656,7 @@ def complete(self, **kwargs: Any) -> Tuple[Message, UsageStats]:
"selectools.agent._memory_manager.estimate_run_tokens",
return_value=high_fill,
):
result = agent.run("current user message")
agent.run("current user message")

# The current user message must have been seen by the provider.
assert any("current user message" in msg for msg in call_log), (
Expand Down Expand Up @@ -1755,7 +1750,7 @@ def complete(self, **kwargs: Any) -> Tuple[Message, UsageStats]:
)

# Must complete within a reasonable timeout — not deadlock.
result = agent.run("run all tools")
agent.run("run all tools")
assert call_count["n"] == 10, f"All 10 tool calls must execute, got {call_count['n']}"


Expand Down Expand Up @@ -1887,7 +1882,6 @@ class _FailingGuardrail:
"""Guardrail that always raises so _prepare_run fails after prompt mutation."""

def check(self, text: str) -> Any:
from selectools.guardrails.base import GuardrailResult

raise RuntimeError("guardrail boom")

Expand Down Expand Up @@ -2968,7 +2962,7 @@ def test_bug15_summary_helper_caps_at_max_chars():


def test_bug15_summary_helper_empty_existing():
from selectools.agent._memory_manager import _MAX_SUMMARY_CHARS, _append_summary
from selectools.agent._memory_manager import _append_summary

assert _append_summary(None, "first summary") == "first summary"
assert _append_summary("", "first summary") == "first summary"
Expand Down Expand Up @@ -3175,7 +3169,6 @@ def worker(thread_id: int) -> None:

def test_bug17_agent_trace_has_lock():
"""Verify the lock attribute exists and is a threading.Lock."""
import threading

from selectools.trace import AgentTrace

Expand Down Expand Up @@ -3383,7 +3376,6 @@ def test_bug16_build_cancelled_result_calls_extraction():


def test_bug22_optional_without_default_is_not_required():
from typing import Optional

from selectools.tools import tool as _bug22_tool

Expand All @@ -3400,7 +3392,6 @@ def search(query: str, filter: Optional[str]) -> str:

def test_bug22_optional_with_default_still_not_required():
"""Regression guard: Optional[T] with a default value remains optional."""
from typing import Optional

from selectools.tools import tool as _bug22_tool

Expand Down Expand Up @@ -3968,7 +3959,6 @@ def f(stuff: list) -> str:

def test_bug29_optional_list_str_still_preserves_items():
"""`Optional[list[str]]` must still emit `items: {type: string}`."""
from typing import Optional

from selectools.tools import tool

Expand Down
2 changes: 1 addition & 1 deletion tests/benchmarks/bench_overhead.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from selectools import Agent, AgentConfig, AgentGraph, tool
from selectools.orchestration.state import STATE_KEY_LAST_OUTPUT, GraphState
from selectools.pipeline import Pipeline, Step, parallel, step
from selectools.pipeline import Pipeline, Step, step
from selectools.providers.stubs import LocalProvider


Expand Down
2 changes: 1 addition & 1 deletion tests/benchmarks/bench_vs_langchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def lg_router(state):
# Task 3: Pipeline composition (selectools only — no LCEL comparison)
# =========================================================

from selectools.pipeline import Pipeline, Step, step
from selectools.pipeline import step

@step
def upper(x: str) -> str:
Expand Down
8 changes: 4 additions & 4 deletions tests/core/test_framework.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys
import types
from pathlib import Path
from typing import Any, Callable, Coroutine, Dict, Generator, List, Optional
from typing import Any, Callable, Coroutine, Generator, List, Optional

import pytest

Expand Down Expand Up @@ -229,7 +229,7 @@ def test_conversation_memory_with_agent() -> None:
assert memory.get_history()[1].role == Role.ASSISTANT

# Second turn - memory should persist
response2 = agent.run([Message(role=Role.USER, content="Hi again")])
agent.run([Message(role=Role.USER, content="Hi again")])
assert len(memory) == 4 # 2 previous + 2 new
history = memory.get_history()
assert history[0].content == "Hello"
Expand Down Expand Up @@ -698,11 +698,11 @@ def simple_tool(x: int) -> str:
agent = Agent(tools=[tool], provider=LocalProvider(), memory=memory)

# First turn
response1 = await agent.arun([Message(role=Role.USER, content="Hello 1")])
await agent.arun([Message(role=Role.USER, content="Hello 1")])
assert len(memory) >= 1

# Second turn
response2 = await agent.arun([Message(role=Role.USER, content="Hello 2")])
await agent.arun([Message(role=Role.USER, content="Hello 2")])
assert len(memory) >= 2


Expand Down
Loading