From eb3467d06a60f0930e7e6ec6a853687f710d02ec Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 11 Aug 2026 15:05:02 +0200 Subject: [PATCH] fix(webber-api): clear ruff, and two things it was pointing at MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 97 findings to zero. Most were mechanical — 52 unsorted import blocks, 10 unsorted __all__, assorted pyupgrade and simplify hints. Two were not, and both were visible only because the lint made me look. `webber version` did not exist. src/cli/commands/version.py defines show_version(), main.py imported it, and the registration line was never written — the CLI exposed chat and explore only. The import carried `# noqa: F401`, which is what kept the omission quiet: someone marked the symptom as intentional instead of asking why it was unused. show_version is not redundant with the --version flag; it prints the resolved Ollama URL, model and debug state, which is the form worth having when something is misconfigured. Registered, and the suppression dropped because the import is now genuinely used. test_spawn_explore_agent asserted nothing. It built a mock RunContext, patched get_agent, and stopped at the comment "For now, verify the explore agent would be called correctly". It had been counted as a passing test. An AST sweep of all 238 test functions found it was the only one, which is worth knowing — the problem was contained, not systemic. It is now skipped with a reason, so it reports as unfinished rather than as passing. Reducing it rather than deleting its imports was the point: tidying the imports would have made a hollow test look clean. Two findings were false positives, and both are recorded rather than silently worked around: B023 flagged run_agent closing over full_prompt and ctx. Traced: agent_task is awaited at line 326 before `continue` reaches the next iteration, so neither name can be rebound while the closure is pending, and the exception path cancels and awaits too. Not a bug. Bound as defaults anyway, because that stays true if the await ever moves. I had called it a live bug before tracing it, which is the mistake Rule 5 exists for. RUF012 flagged `rules: list[ApprovalRule] = []` on ApprovalRuleSet. Its suggested fix — annotate ClassVar — would remove the field from the model. ApprovalRuleSet is a pydantic model and pydantic deep-copies defaults per instance; verified by constructing two and confirming their lists are distinct objects. Suppressed with that evidence in the comment. Ruff cannot see the pydantic base because BaseSchema is a local subclass of BaseModel. Also moved a stray `from src.shared.logging import ...` that had drifted below a function definition, and merged a nested if in the ollama provider. 215 passed, 23 skipped, unchanged except for the new skip. `webber version` exercised end to end. mypy is NOT addressed here and the gate still fails on it — 55 errors in 14 files, 35 of them no-any-return from pydantic_ai's untyped returns. That was hidden behind ruff, because the gate stops at the first failing stage. Co-Authored-By: Claude --- webber-api/src/cli/commands/chat.py | 2 +- webber-api/src/cli/main.py | 8 +++- webber-api/src/cli/session/__init__.py | 2 +- webber-api/src/cli/session/loop.py | 3 +- webber-api/src/cli/theme.py | 4 +- webber-api/src/cli/ui/__init__.py | 4 +- webber-api/src/cli/ui/display.py | 1 - webber-api/src/db/__init__.py | 2 +- webber-api/src/domains/agents/__init__.py | 24 +++++------ .../src/domains/agents/explore/__init__.py | 4 +- .../src/domains/agents/explore/agent.py | 4 +- .../src/domains/agents/explore/tools.py | 4 +- .../src/domains/agents/plan/__init__.py | 4 +- webber-api/src/domains/agents/plan/agent.py | 4 +- webber-api/src/domains/agents/plan/tools.py | 2 +- webber-api/src/domains/agents/router.py | 14 +++--- webber-api/src/domains/agents/schemas.py | 8 +++- .../src/domains/agents/task/__init__.py | 4 +- webber-api/src/domains/agents/task/agent.py | 27 +++++++----- webber-api/src/domains/agents/task/tools.py | 4 +- .../domains/agents/task/tools_streaming.py | 4 +- .../src/domains/conversations/__init__.py | 2 +- .../src/domains/conversations/router.py | 4 +- .../src/domains/conversations/schemas.py | 1 - .../src/domains/conversations/service.py | 2 +- webber-api/src/domains/router.py | 2 +- webber-api/src/domains/tools/__init__.py | 16 +++---- webber-api/src/domains/tools/base.py | 5 +-- webber-api/src/domains/tools/file/__init__.py | 6 +-- webber-api/src/domains/tools/file/edit.py | 11 ++--- webber-api/src/domains/tools/file/glob.py | 2 +- webber-api/src/domains/tools/file/read.py | 7 +-- webber-api/src/domains/tools/file/write.py | 5 ++- webber-api/src/domains/tools/gitignore.py | 5 +-- webber-api/src/domains/tools/search/grep.py | 2 +- webber-api/src/domains/tools/search/web.py | 2 +- webber-api/src/domains/tools/shell/bash.py | 4 +- .../src/domains/tools/shell/bash_full.py | 6 +-- webber-api/src/ollama/provider.py | 6 ++- webber-api/tests/conftest.py | 1 - webber-api/tests/test_agents_api.py | 12 ++++-- webber-api/tests/test_conversations.py | 24 ++++++----- webber-api/tests/test_gitignore.py | 2 +- webber-api/tests/test_plan_agent.py | 2 +- webber-api/tests/test_retry.py | 9 ++-- webber-api/tests/test_security.py | 6 +-- webber-api/tests/test_task_agent.py | 43 +++++++------------ webber-api/tests/test_tokens.py | 3 +- webber-api/tests/test_tools.py | 2 +- webber-api/tests/test_web_search.py | 3 +- 50 files changed, 168 insertions(+), 160 deletions(-) diff --git a/webber-api/src/cli/commands/chat.py b/webber-api/src/cli/commands/chat.py index bb06bad..0d67167 100644 --- a/webber-api/src/cli/commands/chat.py +++ b/webber-api/src/cli/commands/chat.py @@ -6,9 +6,9 @@ from pathlib import Path import typer +from src.cli.session.loop import AgenticLoop from src.cli.theme import get_theme from src.cli.ui.console import get_console -from src.cli.session.loop import AgenticLoop from src.shared.logging import setup_logging console = get_console() diff --git a/webber-api/src/cli/main.py b/webber-api/src/cli/main.py index b910935..bfd21a0 100644 --- a/webber-api/src/cli/main.py +++ b/webber-api/src/cli/main.py @@ -53,11 +53,17 @@ def main( # Import and register commands -from src.cli.commands import chat, explore, version # noqa: E402, F401 +from src.cli.commands import chat, explore, version # noqa: E402 # Register subcommands app.command(name="chat")(chat.chat_command) app.command(name="explore")(explore.explore_command) +# version was imported and never registered, so `webber version` did not exist. +# The --version flag above is the terse form; show_version prints the panel with +# the resolved Ollama URL, model and debug state, which is the one worth having +# when something is misconfigured. The F401 suppression on the import was what +# kept the omission quiet. +app.command(name="version")(version.show_version) if __name__ == "__main__": diff --git a/webber-api/src/cli/session/__init__.py b/webber-api/src/cli/session/__init__.py index 14fb096..13060e2 100644 --- a/webber-api/src/cli/session/__init__.py +++ b/webber-api/src/cli/session/__init__.py @@ -4,4 +4,4 @@ Session management for CLI. from src.cli.session.context import SessionState from src.cli.session.loop import AgenticLoop -__all__ = ["SessionState", "AgenticLoop"] +__all__ = ["AgenticLoop", "SessionState"] diff --git a/webber-api/src/cli/session/loop.py b/webber-api/src/cli/session/loop.py index e2d745c..d70c716 100644 --- a/webber-api/src/cli/session/loop.py +++ b/webber-api/src/cli/session/loop.py @@ -1,14 +1,13 @@ """ Agentic conversation loop for interactive CLI. """ -from typing import Any from rich.console import Console from src.cli.session.context import SessionState from src.cli.ui.display import format_response from src.domains.agents.base import BaseAgent -from src.shared.logging import logged, trace_span, get_logger +from src.shared.logging import get_logger, logged, trace_span logger = get_logger(__name__) diff --git a/webber-api/src/cli/theme.py b/webber-api/src/cli/theme.py index 470be46..d713aa8 100644 --- a/webber-api/src/cli/theme.py +++ b/webber-api/src/cli/theme.py @@ -4,7 +4,7 @@ CLI theme configuration. Centralized color and style definitions for the Webber CLI. All color choices should be defined here for easy customization. """ -from dataclasses import dataclass +from dataclasses import dataclass, field @dataclass(frozen=True) @@ -36,7 +36,7 @@ class ThemeColors: class ThemeConfig: """Complete theme configuration.""" - colors: ThemeColors = ThemeColors() + colors: ThemeColors = field(default_factory=ThemeColors) # Spinner style for loading indicators spinner: str = "dots" diff --git a/webber-api/src/cli/ui/__init__.py b/webber-api/src/cli/ui/__init__.py index 7eeee51..fd36f0b 100644 --- a/webber-api/src/cli/ui/__init__.py +++ b/webber-api/src/cli/ui/__init__.py @@ -2,6 +2,6 @@ CLI UI components. """ from src.cli.ui.console import get_console -from src.cli.ui.display import format_response, format_code +from src.cli.ui.display import format_code, format_response -__all__ = ["get_console", "format_response", "format_code"] +__all__ = ["format_code", "format_response", "get_console"] diff --git a/webber-api/src/cli/ui/display.py b/webber-api/src/cli/ui/display.py index 3a7e07b..1b7c9f9 100644 --- a/webber-api/src/cli/ui/display.py +++ b/webber-api/src/cli/ui/display.py @@ -8,7 +8,6 @@ from rich.syntax import Syntax from rich.text import Text from src.cli.theme import get_theme -from src.cli.ui.console import get_console def format_response(text: str) -> Markdown | Text: diff --git a/webber-api/src/db/__init__.py b/webber-api/src/db/__init__.py index f8bc104..d9322eb 100644 --- a/webber-api/src/db/__init__.py +++ b/webber-api/src/db/__init__.py @@ -7,8 +7,8 @@ from src.db.database import Database, get_database, get_session from src.db.models import Base __all__ = [ + "Base", "Database", "get_database", "get_session", - "Base", ] diff --git a/webber-api/src/domains/agents/__init__.py b/webber-api/src/domains/agents/__init__.py index 9aa954d..af8c622 100644 --- a/webber-api/src/domains/agents/__init__.py +++ b/webber-api/src/domains/agents/__init__.py @@ -4,34 +4,34 @@ Agent implementations. All agents inherit from BaseAgent and are registered in the global registry. """ from src.domains.agents.base import ( - BaseAgent, AgentContext, AgentProtocol, - register_agent, + BaseAgent, get_agent, - list_agents, get_registry, + list_agents, + register_agent, ) from src.domains.agents.explore import ( ExploreAgentImpl, ExploreContext, - explore_agent, explore, + explore_agent, ) __all__ = [ - # Base classes - "BaseAgent", "AgentContext", "AgentProtocol", - # Registry functions - "register_agent", - "get_agent", - "list_agents", - "get_registry", + # Base classes + "BaseAgent", # Explore agent "ExploreAgentImpl", "ExploreContext", - "explore_agent", "explore", + "explore_agent", + "get_agent", + "get_registry", + "list_agents", + # Registry functions + "register_agent", ] diff --git a/webber-api/src/domains/agents/explore/__init__.py b/webber-api/src/domains/agents/explore/__init__.py index c8085c5..894e9f6 100644 --- a/webber-api/src/domains/agents/explore/__init__.py +++ b/webber-api/src/domains/agents/explore/__init__.py @@ -4,13 +4,13 @@ Explore Agent - Fast codebase exploration. from src.domains.agents.explore.agent import ( ExploreAgentImpl, ExploreContext, - explore_agent, explore, + explore_agent, ) __all__ = [ "ExploreAgentImpl", "ExploreContext", - "explore_agent", "explore", + "explore_agent", ] diff --git a/webber-api/src/domains/agents/explore/agent.py b/webber-api/src/domains/agents/explore/agent.py index 9d9ddea..991fc57 100644 --- a/webber-api/src/domains/agents/explore/agent.py +++ b/webber-api/src/domains/agents/explore/agent.py @@ -12,11 +12,11 @@ from typing import Any from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel -from src.domains.agents.base import BaseAgent, AgentContext, register_agent +from src.domains.agents.base import AgentContext, BaseAgent, register_agent from src.domains.agents.explore.prompts import EXPLORE_SYSTEM_PROMPT from src.ollama.provider import get_ollama_provider from src.shared.config import get_settings -from src.shared.logging import logged, get_logger, trace_span +from src.shared.logging import get_logger, logged, trace_span logger = get_logger(__name__) diff --git a/webber-api/src/domains/agents/explore/tools.py b/webber-api/src/domains/agents/explore/tools.py index a5c840b..6a206c7 100644 --- a/webber-api/src/domains/agents/explore/tools.py +++ b/webber-api/src/domains/agents/explore/tools.py @@ -6,9 +6,9 @@ Registers our tool implementations with the PydanticAI agent. from pydantic_ai import Agent, RunContext from src.domains.agents.base import AgentContext -from src.domains.tools.file.read import ReadFileTool -from src.domains.tools.file.glob import GlobFilesTool from src.domains.tools.file.edit import EditFileTool +from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.file.read import ReadFileTool from src.domains.tools.file.write import WriteFileTool from src.domains.tools.search.grep import GrepContentTool from src.domains.tools.search.web import WebSearchTool diff --git a/webber-api/src/domains/agents/plan/__init__.py b/webber-api/src/domains/agents/plan/__init__.py index ab4bf36..069cc23 100644 --- a/webber-api/src/domains/agents/plan/__init__.py +++ b/webber-api/src/domains/agents/plan/__init__.py @@ -16,15 +16,15 @@ Usage: from src.domains.agents.plan.agent import ( PlanAgentImpl, PlanContext, - plan_agent, plan, + plan_agent, plan_stream, ) __all__ = [ "PlanAgentImpl", "PlanContext", - "plan_agent", "plan", + "plan_agent", "plan_stream", ] diff --git a/webber-api/src/domains/agents/plan/agent.py b/webber-api/src/domains/agents/plan/agent.py index 6eab2fc..a3d7249 100644 --- a/webber-api/src/domains/agents/plan/agent.py +++ b/webber-api/src/domains/agents/plan/agent.py @@ -12,11 +12,11 @@ from typing import Any from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel -from src.domains.agents.base import BaseAgent, AgentContext, register_agent +from src.domains.agents.base import AgentContext, BaseAgent, register_agent from src.domains.agents.plan.prompts import PLAN_SYSTEM_PROMPT from src.ollama.provider import get_ollama_provider from src.shared.config import get_settings -from src.shared.logging import logged, get_logger, trace_span +from src.shared.logging import get_logger, logged, trace_span logger = get_logger(__name__) diff --git a/webber-api/src/domains/agents/plan/tools.py b/webber-api/src/domains/agents/plan/tools.py index b1ede1c..e5fa074 100644 --- a/webber-api/src/domains/agents/plan/tools.py +++ b/webber-api/src/domains/agents/plan/tools.py @@ -7,8 +7,8 @@ It cannot modify files - only explore and analyze. from pydantic_ai import Agent, RunContext from src.domains.agents.base import AgentContext -from src.domains.tools.file.read import ReadFileTool from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.file.read import ReadFileTool from src.domains.tools.search.grep import GrepContentTool from src.domains.tools.shell.bash import BashReadOnlyTool diff --git a/webber-api/src/domains/agents/router.py b/webber-api/src/domains/agents/router.py index a580860..3072a19 100644 --- a/webber-api/src/domains/agents/router.py +++ b/webber-api/src/domains/agents/router.py @@ -10,23 +10,24 @@ Streaming uses structured events instead of raw text to avoid garbled output during tool execution. """ import json + from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse -from src.domains.agents.base import get_agent, list_agents - # Import agents to ensure they're registered -import src.domains.agents.explore # noqa: F401 -import src.domains.agents.plan # noqa: F401 +import src.domains.agents.explore +import src.domains.agents.plan import src.domains.agents.task # noqa: F401 +from src.domains.agents.base import get_agent, list_agents from src.domains.agents.schemas import ( - AgentRunRequest, - AgentRunResponse, AgentInfo, AgentListResponse, + AgentRunRequest, + AgentRunResponse, PermissionMode, StreamEvent, ) +from src.shared.logging import get_logger, logged def _get_mode(mode_value: str | PermissionMode) -> PermissionMode: @@ -34,7 +35,6 @@ def _get_mode(mode_value: str | PermissionMode) -> PermissionMode: if isinstance(mode_value, PermissionMode): return mode_value return PermissionMode(mode_value) -from src.shared.logging import logged, get_logger logger = get_logger(__name__) diff --git a/webber-api/src/domains/agents/schemas.py b/webber-api/src/domains/agents/schemas.py index cd50200..d5916d9 100644 --- a/webber-api/src/domains/agents/schemas.py +++ b/webber-api/src/domains/agents/schemas.py @@ -70,7 +70,13 @@ class ApprovalRuleSet(BaseSchema): First matching rule determines the action. If no rules match, falls back to default action. """ - rules: list[ApprovalRule] = [] + # Suppression justified: this is a pydantic model, not a plain class. Pydantic + # deep-copies field defaults per instance — verified: two ApprovalRuleSet() + # instances have `rules` lists that are not the same object, and appending + # to one leaves the other empty. RUF012's suggested fix, annotating this + # ClassVar, would remove the field from the model altogether. Ruff cannot + # see the pydantic base because BaseSchema is a local subclass of BaseModel. + rules: list[ApprovalRule] = [] # noqa: RUF012 default_action: ApprovalAction = ApprovalAction.ask # Default when no rules match diff --git a/webber-api/src/domains/agents/task/__init__.py b/webber-api/src/domains/agents/task/__init__.py index dff4666..a09318d 100644 --- a/webber-api/src/domains/agents/task/__init__.py +++ b/webber-api/src/domains/agents/task/__init__.py @@ -19,15 +19,15 @@ Usage: from src.domains.agents.task.agent import ( TaskAgentImpl, TaskContext, - task_agent, task, + task_agent, task_stream, ) __all__ = [ "TaskAgentImpl", "TaskContext", - "task_agent", "task", + "task_agent", "task_stream", ] diff --git a/webber-api/src/domains/agents/task/agent.py b/webber-api/src/domains/agents/task/agent.py index 84c2e40..d234b8e 100644 --- a/webber-api/src/domains/agents/task/agent.py +++ b/webber-api/src/domains/agents/task/agent.py @@ -8,6 +8,7 @@ Full orchestrator agent that can: - Stream structured events instead of raw text """ import asyncio +import contextlib import os from collections.abc import AsyncIterator from dataclasses import dataclass, field @@ -16,12 +17,12 @@ from typing import Any from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIModel -from src.domains.agents.base import BaseAgent, AgentContext, register_agent +from src.domains.agents.base import AgentContext, BaseAgent, register_agent from src.domains.agents.schemas import PermissionMode, StreamEvent, StreamEventType -from src.domains.agents.task.prompts import TASK_SYSTEM_PROMPT, TASK_PLAN_MODE_PROMPT +from src.domains.agents.task.prompts import TASK_PLAN_MODE_PROMPT, TASK_SYSTEM_PROMPT from src.ollama.provider import get_ollama_provider from src.shared.config import get_settings -from src.shared.logging import logged, get_logger, trace_span +from src.shared.logging import get_logger, logged, trace_span logger = get_logger(__name__) @@ -127,8 +128,8 @@ class TaskAgentImpl(BaseAgent): def _register_tools(self, agent: Agent[TaskContext, str], mode: PermissionMode) -> None: """Register tools with the agent based on permission mode.""" from src.domains.agents.task.tools_streaming import ( - register_task_tools_streaming, register_readonly_tools_streaming, + register_task_tools_streaming, ) if mode == PermissionMode.plan: @@ -293,8 +294,16 @@ User request: {prompt}""" message=f"Retrying (attempt {retries + 1})..." ) - # Run agent in background task so we can yield events - async def run_agent() -> str: + # Run agent in background task so we can yield events. + # + # full_prompt and ctx are bound as defaults rather than closed + # over. Today the closure is safe either way — the task is + # awaited below before `continue` reaches the next iteration, so + # neither name can be rebound while it is pending. Binding them + # keeps that true if the await ever moves, which is the failure + # B023 is warning about and the kind that surfaces as one agent + # silently running another's prompt. + async def run_agent(full_prompt: str = full_prompt, ctx: TaskContext = ctx) -> str: try: result = await agent.run(full_prompt, deps=ctx) return result.output @@ -314,7 +323,7 @@ User request: {prompt}""" timeout=0.1 ) yield event - except asyncio.TimeoutError: + except TimeoutError: # No events, check if agent is done continue @@ -346,10 +355,8 @@ User request: {prompt}""" # Cancel agent if still running if not agent_task.done(): agent_task.cancel() - try: + with contextlib.suppress(asyncio.CancelledError): await agent_task - except asyncio.CancelledError: - pass return # Yield response in chunks for streaming feel diff --git a/webber-api/src/domains/agents/task/tools.py b/webber-api/src/domains/agents/task/tools.py index 71b769f..516dc36 100644 --- a/webber-api/src/domains/agents/task/tools.py +++ b/webber-api/src/domains/agents/task/tools.py @@ -8,9 +8,9 @@ The Task agent has access to tools based on permission mode: from pydantic_ai import Agent, RunContext from src.domains.agents.base import AgentContext -from src.domains.tools.file.read import ReadFileTool -from src.domains.tools.file.glob import GlobFilesTool from src.domains.tools.file.edit import EditFileTool +from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.file.read import ReadFileTool from src.domains.tools.file.write import WriteFileTool from src.domains.tools.search.grep import GrepContentTool from src.domains.tools.search.web import WebSearchTool diff --git a/webber-api/src/domains/agents/task/tools_streaming.py b/webber-api/src/domains/agents/task/tools_streaming.py index 5bbd8d0..a60d86b 100644 --- a/webber-api/src/domains/agents/task/tools_streaming.py +++ b/webber-api/src/domains/agents/task/tools_streaming.py @@ -9,9 +9,9 @@ from pydantic_ai import Agent, RunContext from src.domains.agents.base import AgentContext from src.domains.agents.schemas import StreamEvent, StreamEventType from src.domains.agents.task.agent import TaskContext -from src.domains.tools.file.read import ReadFileTool -from src.domains.tools.file.glob import GlobFilesTool from src.domains.tools.file.edit import EditFileTool +from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.file.read import ReadFileTool from src.domains.tools.file.write import WriteFileTool from src.domains.tools.search.grep import GrepContentTool from src.domains.tools.search.web import WebSearchTool diff --git a/webber-api/src/domains/conversations/__init__.py b/webber-api/src/domains/conversations/__init__.py index 977910b..af5ec03 100644 --- a/webber-api/src/domains/conversations/__init__.py +++ b/webber-api/src/domains/conversations/__init__.py @@ -11,6 +11,6 @@ from src.domains.conversations.service import ConversationService __all__ = [ "Conversation", - "Message", "ConversationService", + "Message", ] diff --git a/webber-api/src/domains/conversations/router.py b/webber-api/src/domains/conversations/router.py index 2882db4..ee139d0 100644 --- a/webber-api/src/domains/conversations/router.py +++ b/webber-api/src/domains/conversations/router.py @@ -20,7 +20,7 @@ from src.domains.conversations.schemas import ( ) from src.domains.conversations.service import ConversationService from src.shared.auth import require_auth -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) @@ -170,7 +170,7 @@ async def add_message( logger.exception(f"Agent response failed: {e}") raise HTTPException( status_code=500, - detail=f"Agent error: {str(e)}" + detail=f"Agent error: {e!s}" ) # Add assistant message diff --git a/webber-api/src/domains/conversations/schemas.py b/webber-api/src/domains/conversations/schemas.py index 1197bff..3decd18 100644 --- a/webber-api/src/domains/conversations/schemas.py +++ b/webber-api/src/domains/conversations/schemas.py @@ -6,7 +6,6 @@ from uuid import UUID from pydantic import BaseModel, Field - # === Request Schemas === class CreateConversationRequest(BaseModel): diff --git a/webber-api/src/domains/conversations/service.py b/webber-api/src/domains/conversations/service.py index 7c56bfc..a0b18d2 100644 --- a/webber-api/src/domains/conversations/service.py +++ b/webber-api/src/domains/conversations/service.py @@ -5,7 +5,7 @@ Handles CRUD operations, context building, and summarization triggers. """ from uuid import UUID -from sqlalchemy import select, func +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload diff --git a/webber-api/src/domains/router.py b/webber-api/src/domains/router.py index 9bed016..90b1fb2 100644 --- a/webber-api/src/domains/router.py +++ b/webber-api/src/domains/router.py @@ -6,9 +6,9 @@ main.py only includes this root_router. """ from fastapi import APIRouter -from src.domains.health.router import router as health_router from src.domains.agents.router import router as agents_router from src.domains.conversations.router import router as conversations_router +from src.domains.health.router import router as health_router # from src.domains.auth.router import router as auth_router # from src.domains.tools.router import router as tools_router diff --git a/webber-api/src/domains/tools/__init__.py b/webber-api/src/domains/tools/__init__.py index e1bf1bb..8fcae64 100644 --- a/webber-api/src/domains/tools/__init__.py +++ b/webber-api/src/domains/tools/__init__.py @@ -4,19 +4,19 @@ Tool implementations for agent use. All tools inherit from BaseTool and return ToolResult. """ from src.domains.tools.base import BaseTool, ToolResult -from src.domains.tools.file import ReadFileTool, GlobFilesTool, EditFileTool, WriteFileTool +from src.domains.tools.file import EditFileTool, GlobFilesTool, ReadFileTool, WriteFileTool from src.domains.tools.search import GrepContentTool, WebSearchTool from src.domains.tools.shell import BashReadOnlyTool, BashTool __all__ = [ "BaseTool", - "ToolResult", - "ReadFileTool", - "GlobFilesTool", - "EditFileTool", - "WriteFileTool", - "GrepContentTool", - "WebSearchTool", "BashReadOnlyTool", "BashTool", + "EditFileTool", + "GlobFilesTool", + "GrepContentTool", + "ReadFileTool", + "ToolResult", + "WebSearchTool", + "WriteFileTool", ] diff --git a/webber-api/src/domains/tools/base.py b/webber-api/src/domains/tools/base.py index 4577d76..0e513a2 100644 --- a/webber-api/src/domains/tools/base.py +++ b/webber-api/src/domains/tools/base.py @@ -32,10 +32,7 @@ class ToolResult: if not self.success: return f"ERROR: {self.error}" - if isinstance(self.data, str): - content = self.data - else: - content = str(self.data) + content = self.data if isinstance(self.data, str) else str(self.data) if len(content) > max_length: self.truncated = True diff --git a/webber-api/src/domains/tools/file/__init__.py b/webber-api/src/domains/tools/file/__init__.py index fdeec41..0634d8d 100644 --- a/webber-api/src/domains/tools/file/__init__.py +++ b/webber-api/src/domains/tools/file/__init__.py @@ -1,9 +1,9 @@ """ File operation tools. """ -from src.domains.tools.file.read import ReadFileTool -from src.domains.tools.file.glob import GlobFilesTool from src.domains.tools.file.edit import EditFileTool +from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.file.read import ReadFileTool from src.domains.tools.file.write import WriteFileTool -__all__ = ["ReadFileTool", "GlobFilesTool", "EditFileTool", "WriteFileTool"] +__all__ = ["EditFileTool", "GlobFilesTool", "ReadFileTool", "WriteFileTool"] diff --git a/webber-api/src/domains/tools/file/edit.py b/webber-api/src/domains/tools/file/edit.py index 176d2fb..ca5a11c 100644 --- a/webber-api/src/domains/tools/file/edit.py +++ b/webber-api/src/domains/tools/file/edit.py @@ -2,11 +2,12 @@ File editing tool with find-and-replace functionality. """ import difflib -import aiofiles from pathlib import Path +import aiofiles + from src.domains.tools.base import BaseTool, ToolResult -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) @@ -147,15 +148,15 @@ Examples: try: # Read file content - async with aiofiles.open(path, 'r', encoding='utf-8', errors='replace') as f: + async with aiofiles.open(path, encoding='utf-8', errors='replace') as f: content = await f.read() # Check if old_string exists count = content.count(old_string) if count == 0: return self._error( - f"old_string not found in file. " - f"Make sure to match exact whitespace and indentation." + "old_string not found in file. " + "Make sure to match exact whitespace and indentation." ) # Check uniqueness if replace_all is False diff --git a/webber-api/src/domains/tools/file/glob.py b/webber-api/src/domains/tools/file/glob.py index 8f53cb9..09ad46a 100644 --- a/webber-api/src/domains/tools/file/glob.py +++ b/webber-api/src/domains/tools/file/glob.py @@ -6,7 +6,7 @@ from pathlib import Path from src.domains.tools.base import BaseTool, ToolResult from src.domains.tools.gitignore import filter_gitignored -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) diff --git a/webber-api/src/domains/tools/file/read.py b/webber-api/src/domains/tools/file/read.py index 80c401f..55247bd 100644 --- a/webber-api/src/domains/tools/file/read.py +++ b/webber-api/src/domains/tools/file/read.py @@ -1,11 +1,12 @@ """ File reading tool with line number formatting and sandboxing. """ -import aiofiles from pathlib import Path +import aiofiles + from src.domains.tools.base import BaseTool, ToolResult -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) @@ -87,7 +88,7 @@ IMPORTANT: return self._error(f"Not a file: {file_path}") try: - async with aiofiles.open(path, 'r', encoding='utf-8', errors='replace') as f: + async with aiofiles.open(path, encoding='utf-8', errors='replace') as f: content = await f.read() lines = content.splitlines() diff --git a/webber-api/src/domains/tools/file/write.py b/webber-api/src/domains/tools/file/write.py index 2cba895..bae773d 100644 --- a/webber-api/src/domains/tools/file/write.py +++ b/webber-api/src/domains/tools/file/write.py @@ -1,11 +1,12 @@ """ File writing tool for creating and overwriting files. """ -import aiofiles from pathlib import Path +import aiofiles + from src.domains.tools.base import BaseTool, ToolResult -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) diff --git a/webber-api/src/domains/tools/gitignore.py b/webber-api/src/domains/tools/gitignore.py index 05b7b38..b21a588 100644 --- a/webber-api/src/domains/tools/gitignore.py +++ b/webber-api/src/domains/tools/gitignore.py @@ -90,10 +90,7 @@ class GitignoreFilter: # Make path relative to root for matching try: - if path.is_absolute(): - rel_path = path.resolve().relative_to(self.root_dir) - else: - rel_path = path + rel_path = path.resolve().relative_to(self.root_dir) if path.is_absolute() else path except ValueError: # Path is not under root_dir, don't filter return False diff --git a/webber-api/src/domains/tools/search/grep.py b/webber-api/src/domains/tools/search/grep.py index 82e0786..c45494f 100644 --- a/webber-api/src/domains/tools/search/grep.py +++ b/webber-api/src/domains/tools/search/grep.py @@ -7,7 +7,7 @@ from typing import Literal from src.domains.tools.base import BaseTool, ToolResult from src.domains.tools.gitignore import filter_gitignored -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) diff --git a/webber-api/src/domains/tools/search/web.py b/webber-api/src/domains/tools/search/web.py index 9274c09..753b176 100644 --- a/webber-api/src/domains/tools/search/web.py +++ b/webber-api/src/domains/tools/search/web.py @@ -8,7 +8,7 @@ import httpx from src.domains.tools.base import BaseTool, ToolResult from src.shared.config import get_settings -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged from src.shared.retry import retry_async logger = get_logger(__name__) diff --git a/webber-api/src/domains/tools/shell/bash.py b/webber-api/src/domains/tools/shell/bash.py index 4651ad5..ac73325 100644 --- a/webber-api/src/domains/tools/shell/bash.py +++ b/webber-api/src/domains/tools/shell/bash.py @@ -8,7 +8,7 @@ import shlex from pathlib import Path from src.domains.tools.base import BaseTool, ToolResult -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) @@ -190,7 +190,7 @@ Examples: exit_code=proc.returncode ) - except asyncio.TimeoutError: + except TimeoutError: return self._error(f"Command timed out after {timeout} seconds") except Exception as e: logger.exception(f"Error executing command: {command}") diff --git a/webber-api/src/domains/tools/shell/bash_full.py b/webber-api/src/domains/tools/shell/bash_full.py index 5686b1c..d3b7186 100644 --- a/webber-api/src/domains/tools/shell/bash_full.py +++ b/webber-api/src/domains/tools/shell/bash_full.py @@ -8,7 +8,7 @@ import shlex from pathlib import Path from src.domains.tools.base import BaseTool, ToolResult -from src.shared.logging import logged, get_logger +from src.shared.logging import get_logger, logged logger = get_logger(__name__) @@ -228,7 +228,7 @@ Examples: exit_code=proc.returncode ) - except asyncio.TimeoutError: + except TimeoutError: return self._error(f"Command timed out after {timeout} seconds") except Exception as e: logger.exception(f"Error executing command: {command}") @@ -340,7 +340,7 @@ Examples: # Handle git with flags before subcommand (e.g., git -C path status) if git_subcommand.startswith("-"): # Find the actual subcommand - for i, token in enumerate(tokens[2:], 2): + for _i, token in enumerate(tokens[2:], 2): if not token.startswith("-"): git_subcommand = token break diff --git a/webber-api/src/ollama/provider.py b/webber-api/src/ollama/provider.py index e588bb4..4e86277 100644 --- a/webber-api/src/ollama/provider.py +++ b/webber-api/src/ollama/provider.py @@ -110,8 +110,10 @@ def _sanitize_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: msg_copy = dict(msg) # Fix null content in assistant messages with tool calls - if msg_copy.get("role") == "assistant": - if msg_copy.get("content") is None and msg_copy.get("tool_calls"): + if ( + msg_copy.get("role") == "assistant" + and msg_copy.get("content") is None and msg_copy.get("tool_calls") + ): msg_copy["content"] = "" logger.debug( f"Sanitized null content, tool_calls={len(msg_copy['tool_calls'])}" diff --git a/webber-api/tests/conftest.py b/webber-api/tests/conftest.py index 9f226c6..5f27489 100644 --- a/webber-api/tests/conftest.py +++ b/webber-api/tests/conftest.py @@ -21,7 +21,6 @@ from httpx import ASGITransport, AsyncClient from src.main import app - # ============================================================================= # Command Line Options # ============================================================================= diff --git a/webber-api/tests/test_agents_api.py b/webber-api/tests/test_agents_api.py index bd9e9e2..71efbaa 100644 --- a/webber-api/tests/test_agents_api.py +++ b/webber-api/tests/test_agents_api.py @@ -4,8 +4,9 @@ Tests for agent REST API endpoints. Includes integration tests that verify real code paths work correctly without over-mocking (only LLM calls are mocked). """ +from unittest.mock import AsyncMock, MagicMock, patch + import pytest -from unittest.mock import AsyncMock, patch, MagicMock from src.domains.agents.schemas import PermissionMode @@ -312,9 +313,10 @@ class TestAgentMethodSignatures: def test_task_agent_run_accepts_mode(self): """Verify TaskAgentImpl.run() accepts mode parameter.""" - from src.domains.agents.task.agent import TaskAgentImpl import inspect + from src.domains.agents.task.agent import TaskAgentImpl + sig = inspect.signature(TaskAgentImpl.run) params = list(sig.parameters.keys()) @@ -325,9 +327,10 @@ class TestAgentMethodSignatures: def test_task_agent_run_stream_accepts_mode(self): """Verify TaskAgentImpl.run_stream() accepts mode parameter.""" - from src.domains.agents.task.agent import TaskAgentImpl import inspect + from src.domains.agents.task.agent import TaskAgentImpl + sig = inspect.signature(TaskAgentImpl.run_stream) params = list(sig.parameters.keys()) @@ -337,9 +340,10 @@ class TestAgentMethodSignatures: def test_trace_span_signature(self): """Verify trace_span only accepts expected parameters.""" - from src.shared.logging import trace_span import inspect + from src.shared.logging import trace_span + sig = inspect.signature(trace_span.__init__) params = list(sig.parameters.keys()) diff --git a/webber-api/tests/test_conversations.py b/webber-api/tests/test_conversations.py index 845e034..19c0a1d 100644 --- a/webber-api/tests/test_conversations.py +++ b/webber-api/tests/test_conversations.py @@ -3,15 +3,14 @@ Tests for conversations domain. Tests conversation CRUD, context building, and API endpoints. """ -import pytest from uuid import uuid4 +import pytest + from src.domains.conversations.models import Conversation, Message from src.domains.conversations.schemas import ( - CreateConversationRequest, AddMessageRequest, - ConversationResponse, - MessageResponse, + CreateConversationRequest, ) @@ -173,9 +172,10 @@ class TestConversationService: @pytest.mark.anyio async def test_context_prompt_no_history(self): """Test building context prompt with no history.""" - from src.domains.conversations.service import ConversationService from unittest.mock import MagicMock + from src.domains.conversations.service import ConversationService + # Create mock session mock_session = MagicMock() service = ConversationService(mock_session) @@ -190,10 +190,11 @@ class TestConversationService: @pytest.mark.anyio async def test_context_prompt_with_history(self): """Test building context prompt with message history.""" - from src.domains.conversations.service import ConversationService - from src.domains.conversations.models import Message from unittest.mock import MagicMock + from src.domains.conversations.models import Message + from src.domains.conversations.service import ConversationService + mock_session = MagicMock() service = ConversationService(mock_session) @@ -221,10 +222,11 @@ class TestConversationService: @pytest.mark.anyio async def test_context_prompt_with_summary(self): """Test building context prompt with summary message.""" - from src.domains.conversations.service import ConversationService - from src.domains.conversations.models import Message from unittest.mock import MagicMock + from src.domains.conversations.models import Message + from src.domains.conversations.service import ConversationService + mock_session = MagicMock() service = ConversationService(mock_session) @@ -253,8 +255,8 @@ class TestSummarization: def test_format_messages_for_summary(self): """Test formatting messages for summarization.""" - from src.domains.conversations.summarize import format_messages_for_summary from src.domains.conversations.models import Message + from src.domains.conversations.summarize import format_messages_for_summary messages = [ Message( @@ -276,8 +278,8 @@ class TestSummarization: def test_format_messages_with_summary(self): """Test formatting messages that include a summary.""" - from src.domains.conversations.summarize import format_messages_for_summary from src.domains.conversations.models import Message + from src.domains.conversations.summarize import format_messages_for_summary messages = [ Message( diff --git a/webber-api/tests/test_gitignore.py b/webber-api/tests/test_gitignore.py index d02eef9..3dd5590 100644 --- a/webber-api/tests/test_gitignore.py +++ b/webber-api/tests/test_gitignore.py @@ -6,8 +6,8 @@ from pathlib import Path import pytest -from src.domains.tools.gitignore import GitignoreFilter, filter_gitignored from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.gitignore import GitignoreFilter, filter_gitignored from src.domains.tools.search.grep import GrepContentTool diff --git a/webber-api/tests/test_plan_agent.py b/webber-api/tests/test_plan_agent.py index a39cbd0..8e1c0f1 100644 --- a/webber-api/tests/test_plan_agent.py +++ b/webber-api/tests/test_plan_agent.py @@ -6,7 +6,7 @@ Tests registration, API endpoints, and tool restrictions. import pytest from src.domains.agents.base import get_agent, list_agents -from src.domains.agents.plan import plan_agent, PlanAgentImpl +from src.domains.agents.plan import PlanAgentImpl, plan_agent class TestPlanAgentRegistration: diff --git a/webber-api/tests/test_retry.py b/webber-api/tests/test_retry.py index e73ec43..9a2c91c 100644 --- a/webber-api/tests/test_retry.py +++ b/webber-api/tests/test_retry.py @@ -1,17 +1,16 @@ """ Tests for retry utilities. """ -import pytest -from unittest.mock import AsyncMock, patch import httpx +import pytest from src.shared.retry import ( - with_retry, - retry_async, + calculate_backoff, is_retryable_exception, is_retryable_http_status, - calculate_backoff, + retry_async, + with_retry, ) diff --git a/webber-api/tests/test_security.py b/webber-api/tests/test_security.py index fd9b7bc..b96b154 100644 --- a/webber-api/tests/test_security.py +++ b/webber-api/tests/test_security.py @@ -6,10 +6,10 @@ from pathlib import Path import pytest -from src.domains.tools.file.read import ReadFileTool -from src.domains.tools.file.write import WriteFileTool from src.domains.tools.file.edit import EditFileTool from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.file.read import ReadFileTool +from src.domains.tools.file.write import WriteFileTool from src.domains.tools.shell.bash_full import BashTool @@ -275,5 +275,5 @@ class TestResourceLimits: assert result.success # Should only return 5 files - lines = [l for l in result.data.strip().split("\n") if l] + lines = [line for line in result.data.strip().split("\n") if line] assert len(lines) <= 5 diff --git a/webber-api/tests/test_task_agent.py b/webber-api/tests/test_task_agent.py index 5ed18dd..11840a8 100644 --- a/webber-api/tests/test_task_agent.py +++ b/webber-api/tests/test_task_agent.py @@ -3,11 +3,11 @@ Tests for the Task agent. Tests registration, API endpoints, tool access, and spawn_agent functionality. """ + import pytest -from unittest.mock import AsyncMock, patch from src.domains.agents.base import get_agent, list_agents -from src.domains.agents.task import task_agent, TaskAgentImpl +from src.domains.agents.task import TaskAgentImpl, task_agent class TestTaskAgentRegistration: @@ -90,39 +90,28 @@ class TestTaskAgentTools: class TestSpawnAgentTool: """Tests for spawn_agent orchestration functionality.""" + @pytest.mark.skip( + reason="never finished — the body built a mock context and then asserted " + "nothing, so it counted as a passing test while verifying nothing" + ) @pytest.mark.anyio async def test_spawn_explore_agent(self): - """Test spawning an explore agent.""" - from src.domains.agents.task.tools import register_task_tools - from src.domains.agents.base import AgentContext - from pydantic_ai import Agent, RunContext - from unittest.mock import MagicMock + """Spawning an explore agent should delegate to the explore agent. - # Create a mock context - ctx = MagicMock(spec=RunContext) - ctx.deps = AgentContext( - working_dir="/tmp", - allowed_paths=["/tmp"], - timeout_seconds=30 - ) + The scaffolding that used to sit here — a MagicMock RunContext, an + AgentContext with a /tmp working dir, and a patch of + src.domains.agents.base.get_agent — ran and then stopped at the comment + "For now, verify the explore agent would be called correctly". There was + no assertion, so it passed unconditionally. - # Mock the explore agent - with patch("src.domains.agents.base.get_agent") as mock_get_agent: - mock_explore = AsyncMock() - mock_explore.run = AsyncMock(return_value="Found 5 Python files") - mock_get_agent.return_value = mock_explore - - # Import and call spawn_agent directly - from src.domains.agents.task import tools - # We need to test the actual tool function - # For now, verify the explore agent would be called correctly + Removed rather than tidied: ruff flagged its imports as unused, and + deleting those would have made the test look clean while leaving it + hollow. git history has the setup for whoever finishes this. + """ @pytest.mark.anyio async def test_spawn_unknown_agent_returns_error(self): """Test that spawning unknown agent type returns error.""" - from src.domains.agents.base import AgentContext - from unittest.mock import MagicMock - from pydantic_ai import RunContext # We can't easily test the tool directly, but we can verify # the agent type validation logic diff --git a/webber-api/tests/test_tokens.py b/webber-api/tests/test_tokens.py index 514b4ca..8425ad2 100644 --- a/webber-api/tests/test_tokens.py +++ b/webber-api/tests/test_tokens.py @@ -1,9 +1,8 @@ """ Tests for token counting utilities. """ -import pytest -from src.shared.tokens import count_tokens, count_message_tokens, estimate_tokens +from src.shared.tokens import count_message_tokens, count_tokens, estimate_tokens class TestTokenCounting: diff --git a/webber-api/tests/test_tools.py b/webber-api/tests/test_tools.py index b26b159..9fcb0be 100644 --- a/webber-api/tests/test_tools.py +++ b/webber-api/tests/test_tools.py @@ -6,8 +6,8 @@ from pathlib import Path import pytest -from src.domains.tools.file.read import ReadFileTool from src.domains.tools.file.glob import GlobFilesTool +from src.domains.tools.file.read import ReadFileTool from src.domains.tools.search.grep import GrepContentTool from src.domains.tools.shell.bash import BashReadOnlyTool diff --git a/webber-api/tests/test_web_search.py b/webber-api/tests/test_web_search.py index 38f8a67..ca909f6 100644 --- a/webber-api/tests/test_web_search.py +++ b/webber-api/tests/test_web_search.py @@ -1,8 +1,9 @@ """ Tests for WebSearchTool. """ +from unittest.mock import AsyncMock, MagicMock, patch + import pytest -from unittest.mock import AsyncMock, patch, MagicMock from src.domains.tools.search.web import WebSearchTool