Skip to content
Closed
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
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,6 @@ ignore = [
"EM102", # Exception f-strings
"G004", # Logging f-strings
"T201", # print() used for user output
"TRY003", # Raise with inline message strings

# Backwards-compatibility suppressions for existing code
"A001", # Variable shadows built-in
Expand Down Expand Up @@ -253,3 +252,5 @@ ignore = [

[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101", "PLR2004"]
# Vendored third-party code — do not modify to suppress linting issues
"src/seclab_taskflow_agent/mcp_servers/codeql/jsonrpyc/*" = ["TRY003"]
3 changes: 2 additions & 1 deletion src/seclab_taskflow_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from openai import AsyncOpenAI

from .capi import AI_API_ENDPOINT_ENUM, COPILOT_INTEGRATION_ID, get_AI_endpoint, get_AI_token
from .exceptions import TokenEnvVarNotSetError

__all__ = [
"DEFAULT_MODEL",
Expand Down Expand Up @@ -182,7 +183,7 @@ def __init__(
if token:
resolved_token = os.getenv(token, "")
if not resolved_token:
raise RuntimeError(f"Token env var {token!r} is not set")
raise TokenEnvVarNotSetError(token)
else:
resolved_token = get_AI_token()

Expand Down
68 changes: 46 additions & 22 deletions src/seclab_taskflow_agent/available_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,44 @@ class FileTypeException(Exception):
pass


class InvalidToolFormatError(BadToolNameError):
def __init__(self, toolname: str) -> None:
super().__init__(
f'Not a valid toolname: "{toolname}". '
f'Expected format: "packagename.filename"'
)


class ToolDirNotFoundError(BadToolNameError):
def __init__(self, toolname: str, pkg_dir: object) -> None:
super().__init__(f"Cannot load {toolname} because {pkg_dir} is not a valid directory.")


class FiletypeMismatchError(FileTypeException):
def __init__(self, filepath: object, expected: str, got: str) -> None:
super().__init__(f"Error in {filepath}: expected filetype {expected!r}, got {got!r}.")


class UnknownFiletypeError(BadToolNameError):
def __init__(self, filetype: str, toolname: str) -> None:
super().__init__(f"Unknown filetype {filetype!r} in {toolname}")


class ToolValidationError(BadToolNameError):
def __init__(self, toolname: str, exc: Exception) -> None:
super().__init__(f"Validation error loading {toolname}: {exc}")


class ToolLoadError(BadToolNameError):
def __init__(self, toolname: str, exc: Exception) -> None:
super().__init__(f"Cannot load {toolname}: {exc}")


class ToolFileNotFoundError(BadToolNameError):
def __init__(self, toolname: str, filepath: object) -> None:
super().__init__(f"Cannot load {toolname} because {filepath} is not a valid file.")


class AvailableToolType(Enum):
Personality = "personality"
Taskflow = "taskflow"
Expand Down Expand Up @@ -108,18 +146,13 @@ def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel:
# Resolve package and filename from dotted path
components = toolname.rsplit(".", 1)
if len(components) != 2:
raise BadToolNameError(
f'Not a valid toolname: "{toolname}". '
f'Expected format: "packagename.filename"'
)
raise InvalidToolFormatError(toolname)
package, filename = components

try:
pkg_dir = importlib.resources.files(package)
if not pkg_dir.is_dir():
raise BadToolNameError(
f"Cannot load {toolname} because {pkg_dir} is not a valid directory."
)
raise ToolDirNotFoundError(toolname, pkg_dir)
filepath = pkg_dir.joinpath(filename + ".yaml")
with filepath.open() as fh:
raw = yaml.safe_load(fh)
Expand All @@ -128,17 +161,12 @@ def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel:
header = raw.get("seclab-taskflow-agent", {})
filetype = header.get("filetype", "")
if filetype != tooltype.value:
raise FileTypeException(
f"Error in {filepath}: expected filetype {tooltype.value!r}, "
f"got {filetype!r}."
)
raise FiletypeMismatchError(filepath, tooltype.value, filetype)

# Parse into the appropriate Pydantic model
model_cls = DOCUMENT_MODELS.get(filetype)
if model_cls is None:
raise BadToolNameError(
f"Unknown filetype {filetype!r} in {toolname}"
)
raise UnknownFiletypeError(filetype, toolname)

try:
doc = model_cls(**raw)
Expand All @@ -147,9 +175,7 @@ def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel:
for err in exc.errors():
if "Unsupported version" in str(err.get("msg", "")):
raise VersionException(str(err["msg"])) from exc
raise BadToolNameError(
f"Validation error loading {toolname}: {exc}"
) from exc
raise ToolValidationError(toolname, exc) from exc

# Cache and return
if tooltype not in self._cache:
Expand All @@ -158,10 +184,8 @@ def _load(self, tooltype: AvailableToolType, toolname: str) -> DocumentModel:
return doc

except ModuleNotFoundError as exc:
raise BadToolNameError(f"Cannot load {toolname}: {exc}") from exc
raise ToolLoadError(toolname, exc) from exc
except FileNotFoundError:
raise BadToolNameError(
f"Cannot load {toolname} because {filepath} is not a valid file."
)
raise ToolFileNotFoundError(toolname, filepath)
except ValueError as exc:
raise BadToolNameError(f"Cannot load {toolname}: {exc}") from exc
raise ToolLoadError(toolname, exc) from exc
6 changes: 4 additions & 2 deletions src/seclab_taskflow_agent/capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import httpx
from strenum import StrEnum

from .exceptions import AITokenNotFoundError, UnsupportedEndpointError

__all__ = [
"AI_API_ENDPOINT_ENUM",
"COPILOT_INTEGRATION_ID",
Expand Down Expand Up @@ -38,7 +40,7 @@ def to_url(self) -> str:
case AI_API_ENDPOINT_ENUM.AI_API_OPENAI:
return f"https://{self}/v1"
case _:
raise ValueError(f"Unsupported endpoint: {self}")
raise UnsupportedEndpointError(self)


COPILOT_INTEGRATION_ID = "vscode-chat"
Expand All @@ -61,7 +63,7 @@ def get_AI_token() -> str:
token = os.getenv("COPILOT_TOKEN")
if token:
return token
raise RuntimeError("AI_API_TOKEN environment variable is not set.")
raise AITokenNotFoundError()


# assume we are >= python 3.9 for our type hints
Expand Down
3 changes: 2 additions & 1 deletion src/seclab_taskflow_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .available_tools import AvailableTools
from .banner import get_banner
from .capi import get_AI_token, list_tool_call_models
from .exceptions import InvalidGlobalVariableError
from .path_utils import log_file_name

app = typer.Typer(
Expand All @@ -36,7 +37,7 @@
def _parse_global(value: str) -> tuple[str, str]:
"""Parse a ``KEY=VALUE`` string into a (key, value) pair."""
if "=" not in value:
raise typer.BadParameter(f"Invalid global variable format: {value!r}. Expected KEY=VALUE.")
raise InvalidGlobalVariableError(value)
key, _, val = value.partition("=")
return key.strip(), val.strip()

Expand Down
Loading