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>
This commit is contained in:
Etisam Ul Haq
2026-08-11 23:15:57 +05:00
committed by GitHub
parent 74ffba6d4f
commit a15c8e8533
19 changed files with 121 additions and 45 deletions
+7 -1
View File
@@ -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"
+7 -3
View File
@@ -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
+9 -2
View File
@@ -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"
+5 -1
View File
@@ -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,
+18 -11
View File
@@ -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
+1 -1
View File
@@ -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__ = (
+8 -2
View File
@@ -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"
+7 -1
View File
@@ -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
+1 -2
View File
@@ -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,
+13 -7
View File
@@ -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(
@@ -7,7 +7,6 @@ from types import SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "hooks" / "insaits-security-monitor.py"
+14 -2
View File
@@ -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:
+14 -2
View File
@@ -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:
+2 -1
View File
@@ -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
+1 -2
View File
@@ -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
+4 -3
View File
@@ -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"
+10 -1
View File
@@ -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:
-1
View File
@@ -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
-1
View File
@@ -1,4 +1,3 @@
import pytest
from llm.core.types import (
LLMInput,
LLMOutput,