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