From a15c8e853309818e96fe495d373a7aea47b1298d Mon Sep 17 00:00:00 2001 From: Etisam Ul Haq <123007571+etisamhaq@users.noreply.github.com> Date: Tue, 11 Aug 2026 23:15:57 +0500 Subject: [PATCH] ci: add Python CI job (ruff, mypy, pytest) for the llm-abstraction package (#2364) * ci: add ruff + mypy to the Python CI job and fix pyproject tool config The python-tests job runs pytest but not lint/type checks, and the ruff and mypy configuration in pyproject.toml was silently broken, so neither tool could run at all. - add ruff and mypy steps to the existing python-tests job - fix invalid pyproject keys: [tool.ruff] src-path -> src, [tool.mypy] src_paths -> mypy_path - ignore ruff UP042 (the (str, Enum) mixin is intentional) - resolve ruff findings (unused/unsorted imports) across src and tests - fix mypy errors in tools/executor.py and prompt/builder.py * fix(ci): satisfy Python lint after main refresh --------- Co-authored-by: haelyra <49814733+haelyra@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +++++- pyproject.toml | 10 +++++-- src/llm/__init__.py | 11 ++++++-- src/llm/prompt/__init__.py | 6 +++- src/llm/prompt/builder.py | 29 ++++++++++++-------- src/llm/providers/__init__.py | 2 +- src/llm/providers/ollama.py | 10 +++++-- src/llm/providers/openai.py | 8 +++++- src/llm/providers/resolver.py | 3 +- src/llm/tools/executor.py | 20 +++++++++----- tests/hooks/test_insaits_security_monitor.py | 1 - tests/test_astraflow_provider.py | 16 +++++++++-- tests/test_atlas_provider.py | 16 +++++++++-- tests/test_builder.py | 3 +- tests/test_executor.py | 3 +- tests/test_invariant_runner.py | 7 +++-- tests/test_resolver.py | 11 +++++++- tests/test_selector.py | 1 - tests/test_types.py | 1 - 19 files changed, 121 insertions(+), 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f83256ae..09b11eaf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,7 +172,7 @@ jobs: continue-on-error: false python-tests: - name: Python Tests + name: Python Lint, Type Check & Test runs-on: ubuntu-latest timeout-minutes: 10 @@ -190,6 +190,12 @@ jobs: - name: Install Python dependencies run: python -m pip install --upgrade pip && python -m pip install -e '.[dev]' + - name: Run ruff (lint) + run: python -m ruff check src tests + + - name: Run mypy (type check) + run: python -m mypy src + - name: Run Python tests run: python -m pytest tests/test_*.py -m "not integration" diff --git a/pyproject.toml b/pyproject.toml index d07e88645..2e924826f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,15 +65,19 @@ exclude_lines = [ ] [tool.ruff] -src-path = ["src"] +src = ["src"] target-version = "py311" [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] -ignore = ["E501"] +# E501: line length is handled by the formatter, not enforced here. +# UP042: the (str, Enum) mixin is intentional — enum members must compare +# and serialize as plain strings across providers. StrEnum changes +# str() semantics, so the explicit mixin is kept deliberately. +ignore = ["E501", "UP042"] [tool.mypy] python_version = "3.11" -src_paths = ["src"] +mypy_path = "src" warn_return_any = true warn_unused_ignores = true diff --git a/src/llm/__init__.py b/src/llm/__init__.py index 16eb21067..f1bacc1fe 100644 --- a/src/llm/__init__.py +++ b/src/llm/__init__.py @@ -4,11 +4,18 @@ LLM Abstraction Layer Provider-agnostic interface for multiple LLM backends. """ +from llm.cli.selector import interactive_select from llm.core.interface import LLMProvider -from llm.core.types import LLMInput, LLMOutput, Message, ToolCall, ToolDefinition, ToolResult +from llm.core.types import ( + LLMInput, + LLMOutput, + Message, + ToolCall, + ToolDefinition, + ToolResult, +) from llm.providers import get_provider from llm.tools import ToolExecutor, ToolRegistry -from llm.cli.selector import interactive_select __version__ = "0.1.0" diff --git a/src/llm/prompt/__init__.py b/src/llm/prompt/__init__.py index 418117ea7..7302eb19e 100644 --- a/src/llm/prompt/__init__.py +++ b/src/llm/prompt/__init__.py @@ -1,6 +1,10 @@ """Prompt module for prompt building and normalization.""" -from llm.prompt.builder import PromptBuilder, adapt_messages_for_provider, get_provider_builder +from llm.prompt.builder import ( + PromptBuilder, + adapt_messages_for_provider, + get_provider_builder, +) from llm.prompt.templates import ( TEMPLATES, clear_templates, diff --git a/src/llm/prompt/builder.py b/src/llm/prompt/builder.py index 4f475ce6b..ffa0ed1c6 100644 --- a/src/llm/prompt/builder.py +++ b/src/llm/prompt/builder.py @@ -5,10 +5,7 @@ from __future__ import annotations from dataclasses import dataclass from typing import Any -from llm.core.types import LLMInput, Message, Role, ToolDefinition -from llm.providers.claude import ClaudeProvider -from llm.providers.openai import OpenAIProvider -from llm.providers.ollama import OllamaProvider +from llm.core.types import Message, Role, ToolDefinition @dataclass @@ -36,13 +33,23 @@ class PromptBuilder: raise ValueError("Pass either config or PromptBuilder keyword options, not both") if config is None: - overrides = { - "system_template": system_template, - "user_template": user_template, - "include_tools_in_system": include_tools_in_system, - "tool_format": tool_format, - } - config = PromptConfig(**{key: value for key, value in overrides.items() if value is not None}) + defaults = PromptConfig() + config = PromptConfig( + system_template=( + system_template if system_template is not None else defaults.system_template + ), + user_template=( + user_template if user_template is not None else defaults.user_template + ), + include_tools_in_system=( + include_tools_in_system + if include_tools_in_system is not None + else defaults.include_tools_in_system + ), + tool_format=( + tool_format if tool_format is not None else defaults.tool_format + ), + ) self.config = config diff --git a/src/llm/providers/__init__.py b/src/llm/providers/__init__.py index 3549d1b85..d3536a29f 100644 --- a/src/llm/providers/__init__.py +++ b/src/llm/providers/__init__.py @@ -3,8 +3,8 @@ from llm.providers.astraflow import AstraflowCNProvider, AstraflowProvider from llm.providers.atlas import AtlasProvider from llm.providers.claude import ClaudeProvider -from llm.providers.openai import OpenAIProvider from llm.providers.ollama import OllamaProvider +from llm.providers.openai import OpenAIProvider from llm.providers.resolver import get_provider, register_provider __all__ = ( diff --git a/src/llm/providers/ollama.py b/src/llm/providers/ollama.py index 56ee6eeff..2f83338d0 100644 --- a/src/llm/providers/ollama.py +++ b/src/llm/providers/ollama.py @@ -11,7 +11,13 @@ from llm.core.interface import ( LLMProvider, RateLimitError, ) -from llm.core.types import LLMInput, LLMOutput, Message, ModelInfo, ProviderType, ToolCall +from llm.core.types import ( + LLMInput, + LLMOutput, + ModelInfo, + ProviderType, + ToolCall, +) class OllamaProvider(LLMProvider): @@ -52,8 +58,8 @@ class OllamaProvider(LLMProvider): ] def generate(self, input: LLMInput) -> LLMOutput: - import urllib.request import json + import urllib.request try: url = f"{self.base_url}/api/chat" diff --git a/src/llm/providers/openai.py b/src/llm/providers/openai.py index 7461a8f19..0bf84a33d 100644 --- a/src/llm/providers/openai.py +++ b/src/llm/providers/openai.py @@ -14,7 +14,13 @@ from llm.core.interface import ( LLMProvider, RateLimitError, ) -from llm.core.types import LLMInput, LLMOutput, Message, ModelInfo, ProviderType, ToolCall +from llm.core.types import ( + LLMInput, + LLMOutput, + ModelInfo, + ProviderType, + ToolCall, +) from llm.providers.constants import EMPTY_FILTERED_RESPONSE_ERROR diff --git a/src/llm/providers/resolver.py b/src/llm/providers/resolver.py index f8a5075ef..4156e4fb7 100644 --- a/src/llm/providers/resolver.py +++ b/src/llm/providers/resolver.py @@ -10,9 +10,8 @@ from llm.core.types import ProviderType from llm.providers.astraflow import AstraflowCNProvider, AstraflowProvider from llm.providers.atlas import AtlasProvider from llm.providers.claude import ClaudeProvider -from llm.providers.openai import OpenAIProvider from llm.providers.ollama import OllamaProvider - +from llm.providers.openai import OpenAIProvider _PROVIDER_MAP: dict[ProviderType, type[LLMProvider]] = { ProviderType.ASTRAFLOW: AstraflowProvider, diff --git a/src/llm/tools/executor.py b/src/llm/tools/executor.py index b2aa1a5a3..e4a859b34 100644 --- a/src/llm/tools/executor.py +++ b/src/llm/tools/executor.py @@ -2,12 +2,18 @@ from __future__ import annotations -from abc import ABC, abstractmethod -from typing import Any, Callable - -from llm.core.interface import ToolExecutionError -from llm.core.types import LLMInput, LLMOutput, Message, Role, ToolCall, ToolDefinition, ToolResult +from collections.abc import Callable +from typing import Any +from llm.core.types import ( + LLMInput, + LLMOutput, + Message, + Role, + ToolCall, + ToolDefinition, + ToolResult, +) ToolFunc = Callable[..., Any] @@ -86,7 +92,7 @@ class ReActAgent: tools=tools, ) - output = self.provider.generate(input_copy) + output: LLMOutput = self.provider.generate(input_copy) if not output.has_tool_calls: return output @@ -99,7 +105,7 @@ class ReActAgent: ) ) - results = self.executor.execute_all(output.tool_calls) + results = self.executor.execute_all(output.tool_calls or []) for result in results: messages.append( diff --git a/tests/hooks/test_insaits_security_monitor.py b/tests/hooks/test_insaits_security_monitor.py index 0cf107cc2..5dd41b3ea 100644 --- a/tests/hooks/test_insaits_security_monitor.py +++ b/tests/hooks/test_insaits_security_monitor.py @@ -7,7 +7,6 @@ from types import SimpleNamespace import pytest - ROOT = Path(__file__).resolve().parents[2] SCRIPT = ROOT / "scripts" / "hooks" / "insaits-security-monitor.py" diff --git a/tests/test_astraflow_provider.py b/tests/test_astraflow_provider.py index b70c9bd50..7f154967a 100644 --- a/tests/test_astraflow_provider.py +++ b/tests/test_astraflow_provider.py @@ -1,7 +1,19 @@ from types import SimpleNamespace -from llm.core.types import LLMInput, Message, ProviderType, Role, ToolDefinition, ToolCall -from llm.providers.astraflow import ASTRAFLOW_BASE_URL, ASTRAFLOW_CN_BASE_URL, AstraflowCNProvider, AstraflowProvider +from llm.core.types import ( + LLMInput, + Message, + ProviderType, + Role, + ToolCall, + ToolDefinition, +) +from llm.providers.astraflow import ( + ASTRAFLOW_BASE_URL, + ASTRAFLOW_CN_BASE_URL, + AstraflowCNProvider, + AstraflowProvider, +) def _tool() -> ToolDefinition: diff --git a/tests/test_atlas_provider.py b/tests/test_atlas_provider.py index 404e8f703..479a040b6 100644 --- a/tests/test_atlas_provider.py +++ b/tests/test_atlas_provider.py @@ -1,7 +1,19 @@ from types import SimpleNamespace -from llm.core.types import LLMInput, Message, ProviderType, Role, ToolCall, ToolDefinition -from llm.providers.atlas import ATLAS_BASE_URL, DEFAULT_ATLAS_MAX_TOKENS, DEFAULT_ATLAS_MODEL, AtlasProvider +from llm.core.types import ( + LLMInput, + Message, + ProviderType, + Role, + ToolCall, + ToolDefinition, +) +from llm.providers.atlas import ( + ATLAS_BASE_URL, + DEFAULT_ATLAS_MAX_TOKENS, + DEFAULT_ATLAS_MODEL, + AtlasProvider, +) def _tool() -> ToolDefinition: diff --git a/tests/test_builder.py b/tests/test_builder.py index df2f5da55..439967e91 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,5 +1,6 @@ import pytest -from llm.core.types import LLMInput, Message, Role, ToolDefinition + +from llm.core.types import Message, Role, ToolDefinition from llm.prompt import PromptBuilder, adapt_messages_for_provider from llm.prompt.builder import PromptConfig diff --git a/tests/test_executor.py b/tests/test_executor.py index 07f8fe92b..749c4d1b4 100644 --- a/tests/test_executor.py +++ b/tests/test_executor.py @@ -1,5 +1,4 @@ -import pytest -from llm.core.types import ToolCall, ToolDefinition, ToolResult +from llm.core.types import ToolCall, ToolDefinition from llm.tools import ToolExecutor, ToolRegistry diff --git a/tests/test_invariant_runner.py b/tests/test_invariant_runner.py index a699438a7..699e03e9f 100644 --- a/tests/test_invariant_runner.py +++ b/tests/test_invariant_runner.py @@ -1,14 +1,15 @@ import os import sys -import pytest from pathlib import Path +import pytest + _SKILL_COMPLY_ROOT = Path(__file__).resolve().parent.parent / "skills" / "skill-comply" if str(_SKILL_COMPLY_ROOT) not in sys.path: sys.path.insert(0, str(_SKILL_COMPLY_ROOT)) -from scripts.runner import _setup_sandbox # noqa: E402 -from scripts.scenario_generator import Scenario # noqa: E402 +from scripts.runner import _setup_sandbox # noqa: E402 +from scripts.scenario_generator import Scenario # noqa: E402 _GLOBAL_MARKER = "/tmp/runner_test_pwned_marker" diff --git a/tests/test_resolver.py b/tests/test_resolver.py index e47a72823..c2a65787f 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -1,6 +1,15 @@ import pytest + from llm.core.types import ProviderType -from llm.providers import AstraflowCNProvider, AstraflowProvider, AtlasProvider, ClaudeProvider, OpenAIProvider, OllamaProvider, get_provider +from llm.providers import ( + AstraflowCNProvider, + AstraflowProvider, + AtlasProvider, + ClaudeProvider, + OllamaProvider, + OpenAIProvider, + get_provider, +) class TestGetProvider: diff --git a/tests/test_selector.py b/tests/test_selector.py index af8023bcc..3529d00b9 100644 --- a/tests/test_selector.py +++ b/tests/test_selector.py @@ -7,7 +7,6 @@ from urllib.parse import urlsplit import pytest - SELECTOR_PATH = Path(__file__).parents[1] / "src" / "llm" / "cli" / "selector.py" SPEC = importlib.util.spec_from_file_location("ecc_selector", SELECTOR_PATH) assert SPEC is not None and SPEC.loader is not None diff --git a/tests/test_types.py b/tests/test_types.py index 8399a0bae..a008c96b1 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,4 +1,3 @@ -import pytest from llm.core.types import ( LLMInput, LLMOutput,