fix(webber-api): clear ruff, and two things it was pointing at
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 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,9 @@ from pathlib import Path
|
|||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
|
||||||
|
from src.cli.session.loop import AgenticLoop
|
||||||
from src.cli.theme import get_theme
|
from src.cli.theme import get_theme
|
||||||
from src.cli.ui.console import get_console
|
from src.cli.ui.console import get_console
|
||||||
from src.cli.session.loop import AgenticLoop
|
|
||||||
from src.shared.logging import setup_logging
|
from src.shared.logging import setup_logging
|
||||||
|
|
||||||
console = get_console()
|
console = get_console()
|
||||||
|
|||||||
@@ -53,11 +53,17 @@ def main(
|
|||||||
|
|
||||||
|
|
||||||
# Import and register commands
|
# 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
|
# Register subcommands
|
||||||
app.command(name="chat")(chat.chat_command)
|
app.command(name="chat")(chat.chat_command)
|
||||||
app.command(name="explore")(explore.explore_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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ Session management for CLI.
|
|||||||
from src.cli.session.context import SessionState
|
from src.cli.session.context import SessionState
|
||||||
from src.cli.session.loop import AgenticLoop
|
from src.cli.session.loop import AgenticLoop
|
||||||
|
|
||||||
__all__ = ["SessionState", "AgenticLoop"]
|
__all__ = ["AgenticLoop", "SessionState"]
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
Agentic conversation loop for interactive CLI.
|
Agentic conversation loop for interactive CLI.
|
||||||
"""
|
"""
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from src.cli.session.context import SessionState
|
from src.cli.session.context import SessionState
|
||||||
from src.cli.ui.display import format_response
|
from src.cli.ui.display import format_response
|
||||||
from src.domains.agents.base import BaseAgent
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ CLI theme configuration.
|
|||||||
Centralized color and style definitions for the Webber CLI.
|
Centralized color and style definitions for the Webber CLI.
|
||||||
All color choices should be defined here for easy customization.
|
All color choices should be defined here for easy customization.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -36,7 +36,7 @@ class ThemeColors:
|
|||||||
class ThemeConfig:
|
class ThemeConfig:
|
||||||
"""Complete theme configuration."""
|
"""Complete theme configuration."""
|
||||||
|
|
||||||
colors: ThemeColors = ThemeColors()
|
colors: ThemeColors = field(default_factory=ThemeColors)
|
||||||
|
|
||||||
# Spinner style for loading indicators
|
# Spinner style for loading indicators
|
||||||
spinner: str = "dots"
|
spinner: str = "dots"
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
CLI UI components.
|
CLI UI components.
|
||||||
"""
|
"""
|
||||||
from src.cli.ui.console import get_console
|
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"]
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from rich.syntax import Syntax
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from src.cli.theme import get_theme
|
from src.cli.theme import get_theme
|
||||||
from src.cli.ui.console import get_console
|
|
||||||
|
|
||||||
|
|
||||||
def format_response(text: str) -> Markdown | Text:
|
def format_response(text: str) -> Markdown | Text:
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ from src.db.database import Database, get_database, get_session
|
|||||||
from src.db.models import Base
|
from src.db.models import Base
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"Base",
|
||||||
"Database",
|
"Database",
|
||||||
"get_database",
|
"get_database",
|
||||||
"get_session",
|
"get_session",
|
||||||
"Base",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,34 +4,34 @@ Agent implementations.
|
|||||||
All agents inherit from BaseAgent and are registered in the global registry.
|
All agents inherit from BaseAgent and are registered in the global registry.
|
||||||
"""
|
"""
|
||||||
from src.domains.agents.base import (
|
from src.domains.agents.base import (
|
||||||
BaseAgent,
|
|
||||||
AgentContext,
|
AgentContext,
|
||||||
AgentProtocol,
|
AgentProtocol,
|
||||||
register_agent,
|
BaseAgent,
|
||||||
get_agent,
|
get_agent,
|
||||||
list_agents,
|
|
||||||
get_registry,
|
get_registry,
|
||||||
|
list_agents,
|
||||||
|
register_agent,
|
||||||
)
|
)
|
||||||
from src.domains.agents.explore import (
|
from src.domains.agents.explore import (
|
||||||
ExploreAgentImpl,
|
ExploreAgentImpl,
|
||||||
ExploreContext,
|
ExploreContext,
|
||||||
explore_agent,
|
|
||||||
explore,
|
explore,
|
||||||
|
explore_agent,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Base classes
|
|
||||||
"BaseAgent",
|
|
||||||
"AgentContext",
|
"AgentContext",
|
||||||
"AgentProtocol",
|
"AgentProtocol",
|
||||||
# Registry functions
|
# Base classes
|
||||||
"register_agent",
|
"BaseAgent",
|
||||||
"get_agent",
|
|
||||||
"list_agents",
|
|
||||||
"get_registry",
|
|
||||||
# Explore agent
|
# Explore agent
|
||||||
"ExploreAgentImpl",
|
"ExploreAgentImpl",
|
||||||
"ExploreContext",
|
"ExploreContext",
|
||||||
"explore_agent",
|
|
||||||
"explore",
|
"explore",
|
||||||
|
"explore_agent",
|
||||||
|
"get_agent",
|
||||||
|
"get_registry",
|
||||||
|
"list_agents",
|
||||||
|
# Registry functions
|
||||||
|
"register_agent",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ Explore Agent - Fast codebase exploration.
|
|||||||
from src.domains.agents.explore.agent import (
|
from src.domains.agents.explore.agent import (
|
||||||
ExploreAgentImpl,
|
ExploreAgentImpl,
|
||||||
ExploreContext,
|
ExploreContext,
|
||||||
explore_agent,
|
|
||||||
explore,
|
explore,
|
||||||
|
explore_agent,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ExploreAgentImpl",
|
"ExploreAgentImpl",
|
||||||
"ExploreContext",
|
"ExploreContext",
|
||||||
"explore_agent",
|
|
||||||
"explore",
|
"explore",
|
||||||
|
"explore_agent",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ from typing import Any
|
|||||||
from pydantic_ai import Agent
|
from pydantic_ai import Agent
|
||||||
from pydantic_ai.models.openai import OpenAIModel
|
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.domains.agents.explore.prompts import EXPLORE_SYSTEM_PROMPT
|
||||||
from src.ollama.provider import get_ollama_provider
|
from src.ollama.provider import get_ollama_provider
|
||||||
from src.shared.config import get_settings
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ Registers our tool implementations with the PydanticAI agent.
|
|||||||
from pydantic_ai import Agent, RunContext
|
from pydantic_ai import Agent, RunContext
|
||||||
|
|
||||||
from src.domains.agents.base import AgentContext
|
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.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.file.write import WriteFileTool
|
||||||
from src.domains.tools.search.grep import GrepContentTool
|
from src.domains.tools.search.grep import GrepContentTool
|
||||||
from src.domains.tools.search.web import WebSearchTool
|
from src.domains.tools.search.web import WebSearchTool
|
||||||
|
|||||||
@@ -16,15 +16,15 @@ Usage:
|
|||||||
from src.domains.agents.plan.agent import (
|
from src.domains.agents.plan.agent import (
|
||||||
PlanAgentImpl,
|
PlanAgentImpl,
|
||||||
PlanContext,
|
PlanContext,
|
||||||
plan_agent,
|
|
||||||
plan,
|
plan,
|
||||||
|
plan_agent,
|
||||||
plan_stream,
|
plan_stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"PlanAgentImpl",
|
"PlanAgentImpl",
|
||||||
"PlanContext",
|
"PlanContext",
|
||||||
"plan_agent",
|
|
||||||
"plan",
|
"plan",
|
||||||
|
"plan_agent",
|
||||||
"plan_stream",
|
"plan_stream",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ from typing import Any
|
|||||||
from pydantic_ai import Agent
|
from pydantic_ai import Agent
|
||||||
from pydantic_ai.models.openai import OpenAIModel
|
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.domains.agents.plan.prompts import PLAN_SYSTEM_PROMPT
|
||||||
from src.ollama.provider import get_ollama_provider
|
from src.ollama.provider import get_ollama_provider
|
||||||
from src.shared.config import get_settings
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ It cannot modify files - only explore and analyze.
|
|||||||
from pydantic_ai import Agent, RunContext
|
from pydantic_ai import Agent, RunContext
|
||||||
|
|
||||||
from src.domains.agents.base import AgentContext
|
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.glob import GlobFilesTool
|
||||||
|
from src.domains.tools.file.read import ReadFileTool
|
||||||
from src.domains.tools.search.grep import GrepContentTool
|
from src.domains.tools.search.grep import GrepContentTool
|
||||||
from src.domains.tools.shell.bash import BashReadOnlyTool
|
from src.domains.tools.shell.bash import BashReadOnlyTool
|
||||||
|
|
||||||
|
|||||||
@@ -10,23 +10,24 @@ Streaming uses structured events instead of raw text to avoid
|
|||||||
garbled output during tool execution.
|
garbled output during tool execution.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from src.domains.agents.base import get_agent, list_agents
|
|
||||||
|
|
||||||
# Import agents to ensure they're registered
|
# Import agents to ensure they're registered
|
||||||
import src.domains.agents.explore # noqa: F401
|
import src.domains.agents.explore
|
||||||
import src.domains.agents.plan # noqa: F401
|
import src.domains.agents.plan
|
||||||
import src.domains.agents.task # noqa: F401
|
import src.domains.agents.task # noqa: F401
|
||||||
|
from src.domains.agents.base import get_agent, list_agents
|
||||||
from src.domains.agents.schemas import (
|
from src.domains.agents.schemas import (
|
||||||
AgentRunRequest,
|
|
||||||
AgentRunResponse,
|
|
||||||
AgentInfo,
|
AgentInfo,
|
||||||
AgentListResponse,
|
AgentListResponse,
|
||||||
|
AgentRunRequest,
|
||||||
|
AgentRunResponse,
|
||||||
PermissionMode,
|
PermissionMode,
|
||||||
StreamEvent,
|
StreamEvent,
|
||||||
)
|
)
|
||||||
|
from src.shared.logging import get_logger, logged
|
||||||
|
|
||||||
|
|
||||||
def _get_mode(mode_value: str | PermissionMode) -> PermissionMode:
|
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):
|
if isinstance(mode_value, PermissionMode):
|
||||||
return mode_value
|
return mode_value
|
||||||
return PermissionMode(mode_value)
|
return PermissionMode(mode_value)
|
||||||
from src.shared.logging import logged, get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -70,7 +70,13 @@ class ApprovalRuleSet(BaseSchema):
|
|||||||
First matching rule determines the action.
|
First matching rule determines the action.
|
||||||
If no rules match, falls back to default 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
|
default_action: ApprovalAction = ApprovalAction.ask # Default when no rules match
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ Usage:
|
|||||||
from src.domains.agents.task.agent import (
|
from src.domains.agents.task.agent import (
|
||||||
TaskAgentImpl,
|
TaskAgentImpl,
|
||||||
TaskContext,
|
TaskContext,
|
||||||
task_agent,
|
|
||||||
task,
|
task,
|
||||||
|
task_agent,
|
||||||
task_stream,
|
task_stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"TaskAgentImpl",
|
"TaskAgentImpl",
|
||||||
"TaskContext",
|
"TaskContext",
|
||||||
"task_agent",
|
|
||||||
"task",
|
"task",
|
||||||
|
"task_agent",
|
||||||
"task_stream",
|
"task_stream",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Full orchestrator agent that can:
|
|||||||
- Stream structured events instead of raw text
|
- Stream structured events instead of raw text
|
||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
import os
|
import os
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@@ -16,12 +17,12 @@ from typing import Any
|
|||||||
from pydantic_ai import Agent
|
from pydantic_ai import Agent
|
||||||
from pydantic_ai.models.openai import OpenAIModel
|
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.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.ollama.provider import get_ollama_provider
|
||||||
from src.shared.config import get_settings
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -127,8 +128,8 @@ class TaskAgentImpl(BaseAgent):
|
|||||||
def _register_tools(self, agent: Agent[TaskContext, str], mode: PermissionMode) -> None:
|
def _register_tools(self, agent: Agent[TaskContext, str], mode: PermissionMode) -> None:
|
||||||
"""Register tools with the agent based on permission mode."""
|
"""Register tools with the agent based on permission mode."""
|
||||||
from src.domains.agents.task.tools_streaming import (
|
from src.domains.agents.task.tools_streaming import (
|
||||||
register_task_tools_streaming,
|
|
||||||
register_readonly_tools_streaming,
|
register_readonly_tools_streaming,
|
||||||
|
register_task_tools_streaming,
|
||||||
)
|
)
|
||||||
|
|
||||||
if mode == PermissionMode.plan:
|
if mode == PermissionMode.plan:
|
||||||
@@ -293,8 +294,16 @@ User request: {prompt}"""
|
|||||||
message=f"Retrying (attempt {retries + 1})..."
|
message=f"Retrying (attempt {retries + 1})..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run agent in background task so we can yield events
|
# Run agent in background task so we can yield events.
|
||||||
async def run_agent() -> str:
|
#
|
||||||
|
# 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:
|
try:
|
||||||
result = await agent.run(full_prompt, deps=ctx)
|
result = await agent.run(full_prompt, deps=ctx)
|
||||||
return result.output
|
return result.output
|
||||||
@@ -314,7 +323,7 @@ User request: {prompt}"""
|
|||||||
timeout=0.1
|
timeout=0.1
|
||||||
)
|
)
|
||||||
yield event
|
yield event
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
# No events, check if agent is done
|
# No events, check if agent is done
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -346,10 +355,8 @@ User request: {prompt}"""
|
|||||||
# Cancel agent if still running
|
# Cancel agent if still running
|
||||||
if not agent_task.done():
|
if not agent_task.done():
|
||||||
agent_task.cancel()
|
agent_task.cancel()
|
||||||
try:
|
with contextlib.suppress(asyncio.CancelledError):
|
||||||
await agent_task
|
await agent_task
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Yield response in chunks for streaming feel
|
# Yield response in chunks for streaming feel
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ The Task agent has access to tools based on permission mode:
|
|||||||
from pydantic_ai import Agent, RunContext
|
from pydantic_ai import Agent, RunContext
|
||||||
|
|
||||||
from src.domains.agents.base import AgentContext
|
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.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.file.write import WriteFileTool
|
||||||
from src.domains.tools.search.grep import GrepContentTool
|
from src.domains.tools.search.grep import GrepContentTool
|
||||||
from src.domains.tools.search.web import WebSearchTool
|
from src.domains.tools.search.web import WebSearchTool
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from pydantic_ai import Agent, RunContext
|
|||||||
from src.domains.agents.base import AgentContext
|
from src.domains.agents.base import AgentContext
|
||||||
from src.domains.agents.schemas import StreamEvent, StreamEventType
|
from src.domains.agents.schemas import StreamEvent, StreamEventType
|
||||||
from src.domains.agents.task.agent import TaskContext
|
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.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.file.write import WriteFileTool
|
||||||
from src.domains.tools.search.grep import GrepContentTool
|
from src.domains.tools.search.grep import GrepContentTool
|
||||||
from src.domains.tools.search.web import WebSearchTool
|
from src.domains.tools.search.web import WebSearchTool
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ from src.domains.conversations.service import ConversationService
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Conversation",
|
"Conversation",
|
||||||
"Message",
|
|
||||||
"ConversationService",
|
"ConversationService",
|
||||||
|
"Message",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from src.domains.conversations.schemas import (
|
|||||||
)
|
)
|
||||||
from src.domains.conversations.service import ConversationService
|
from src.domains.conversations.service import ConversationService
|
||||||
from src.shared.auth import require_auth
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ async def add_message(
|
|||||||
logger.exception(f"Agent response failed: {e}")
|
logger.exception(f"Agent response failed: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=500,
|
status_code=500,
|
||||||
detail=f"Agent error: {str(e)}"
|
detail=f"Agent error: {e!s}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add assistant message
|
# Add assistant message
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ from uuid import UUID
|
|||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
# === Request Schemas ===
|
# === Request Schemas ===
|
||||||
|
|
||||||
class CreateConversationRequest(BaseModel):
|
class CreateConversationRequest(BaseModel):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Handles CRUD operations, context building, and summarization triggers.
|
|||||||
"""
|
"""
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ main.py only includes this root_router.
|
|||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
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.agents.router import router as agents_router
|
||||||
from src.domains.conversations.router import router as conversations_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.auth.router import router as auth_router
|
||||||
# from src.domains.tools.router import router as tools_router
|
# from src.domains.tools.router import router as tools_router
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ Tool implementations for agent use.
|
|||||||
All tools inherit from BaseTool and return ToolResult.
|
All tools inherit from BaseTool and return ToolResult.
|
||||||
"""
|
"""
|
||||||
from src.domains.tools.base import BaseTool, 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.search import GrepContentTool, WebSearchTool
|
||||||
from src.domains.tools.shell import BashReadOnlyTool, BashTool
|
from src.domains.tools.shell import BashReadOnlyTool, BashTool
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BaseTool",
|
"BaseTool",
|
||||||
"ToolResult",
|
|
||||||
"ReadFileTool",
|
|
||||||
"GlobFilesTool",
|
|
||||||
"EditFileTool",
|
|
||||||
"WriteFileTool",
|
|
||||||
"GrepContentTool",
|
|
||||||
"WebSearchTool",
|
|
||||||
"BashReadOnlyTool",
|
"BashReadOnlyTool",
|
||||||
"BashTool",
|
"BashTool",
|
||||||
|
"EditFileTool",
|
||||||
|
"GlobFilesTool",
|
||||||
|
"GrepContentTool",
|
||||||
|
"ReadFileTool",
|
||||||
|
"ToolResult",
|
||||||
|
"WebSearchTool",
|
||||||
|
"WriteFileTool",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -32,10 +32,7 @@ class ToolResult:
|
|||||||
if not self.success:
|
if not self.success:
|
||||||
return f"ERROR: {self.error}"
|
return f"ERROR: {self.error}"
|
||||||
|
|
||||||
if isinstance(self.data, str):
|
content = self.data if isinstance(self.data, str) else str(self.data)
|
||||||
content = self.data
|
|
||||||
else:
|
|
||||||
content = str(self.data)
|
|
||||||
|
|
||||||
if len(content) > max_length:
|
if len(content) > max_length:
|
||||||
self.truncated = True
|
self.truncated = True
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
File operation tools.
|
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.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.file.write import WriteFileTool
|
||||||
|
|
||||||
__all__ = ["ReadFileTool", "GlobFilesTool", "EditFileTool", "WriteFileTool"]
|
__all__ = ["EditFileTool", "GlobFilesTool", "ReadFileTool", "WriteFileTool"]
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
File editing tool with find-and-replace functionality.
|
File editing tool with find-and-replace functionality.
|
||||||
"""
|
"""
|
||||||
import difflib
|
import difflib
|
||||||
import aiofiles
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -147,15 +148,15 @@ Examples:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Read file content
|
# 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()
|
content = await f.read()
|
||||||
|
|
||||||
# Check if old_string exists
|
# Check if old_string exists
|
||||||
count = content.count(old_string)
|
count = content.count(old_string)
|
||||||
if count == 0:
|
if count == 0:
|
||||||
return self._error(
|
return self._error(
|
||||||
f"old_string not found in file. "
|
"old_string not found in file. "
|
||||||
f"Make sure to match exact whitespace and indentation."
|
"Make sure to match exact whitespace and indentation."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check uniqueness if replace_all is False
|
# Check uniqueness if replace_all is False
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
from src.domains.tools.base import BaseTool, ToolResult
|
||||||
from src.domains.tools.gitignore import filter_gitignored
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
File reading tool with line number formatting and sandboxing.
|
File reading tool with line number formatting and sandboxing.
|
||||||
"""
|
"""
|
||||||
import aiofiles
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -87,7 +88,7 @@ IMPORTANT:
|
|||||||
return self._error(f"Not a file: {file_path}")
|
return self._error(f"Not a file: {file_path}")
|
||||||
|
|
||||||
try:
|
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()
|
content = await f.read()
|
||||||
|
|
||||||
lines = content.splitlines()
|
lines = content.splitlines()
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
File writing tool for creating and overwriting files.
|
File writing tool for creating and overwriting files.
|
||||||
"""
|
"""
|
||||||
import aiofiles
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -90,10 +90,7 @@ class GitignoreFilter:
|
|||||||
|
|
||||||
# Make path relative to root for matching
|
# Make path relative to root for matching
|
||||||
try:
|
try:
|
||||||
if path.is_absolute():
|
rel_path = path.resolve().relative_to(self.root_dir) if path.is_absolute() else path
|
||||||
rel_path = path.resolve().relative_to(self.root_dir)
|
|
||||||
else:
|
|
||||||
rel_path = path
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Path is not under root_dir, don't filter
|
# Path is not under root_dir, don't filter
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Literal
|
|||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
from src.domains.tools.base import BaseTool, ToolResult
|
||||||
from src.domains.tools.gitignore import filter_gitignored
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import httpx
|
|||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
from src.domains.tools.base import BaseTool, ToolResult
|
||||||
from src.shared.config import get_settings
|
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
|
from src.shared.retry import retry_async
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import shlex
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ Examples:
|
|||||||
exit_code=proc.returncode
|
exit_code=proc.returncode
|
||||||
)
|
)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
return self._error(f"Command timed out after {timeout} seconds")
|
return self._error(f"Command timed out after {timeout} seconds")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error executing command: {command}")
|
logger.exception(f"Error executing command: {command}")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import shlex
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from src.domains.tools.base import BaseTool, ToolResult
|
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__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ Examples:
|
|||||||
exit_code=proc.returncode
|
exit_code=proc.returncode
|
||||||
)
|
)
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except TimeoutError:
|
||||||
return self._error(f"Command timed out after {timeout} seconds")
|
return self._error(f"Command timed out after {timeout} seconds")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(f"Error executing command: {command}")
|
logger.exception(f"Error executing command: {command}")
|
||||||
@@ -340,7 +340,7 @@ Examples:
|
|||||||
# Handle git with flags before subcommand (e.g., git -C path status)
|
# Handle git with flags before subcommand (e.g., git -C path status)
|
||||||
if git_subcommand.startswith("-"):
|
if git_subcommand.startswith("-"):
|
||||||
# Find the actual subcommand
|
# Find the actual subcommand
|
||||||
for i, token in enumerate(tokens[2:], 2):
|
for _i, token in enumerate(tokens[2:], 2):
|
||||||
if not token.startswith("-"):
|
if not token.startswith("-"):
|
||||||
git_subcommand = token
|
git_subcommand = token
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -110,8 +110,10 @@ def _sanitize_messages(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
msg_copy = dict(msg)
|
msg_copy = dict(msg)
|
||||||
|
|
||||||
# Fix null content in assistant messages with tool calls
|
# Fix null content in assistant messages with tool calls
|
||||||
if msg_copy.get("role") == "assistant":
|
if (
|
||||||
if msg_copy.get("content") is None and msg_copy.get("tool_calls"):
|
msg_copy.get("role") == "assistant"
|
||||||
|
and msg_copy.get("content") is None and msg_copy.get("tool_calls")
|
||||||
|
):
|
||||||
msg_copy["content"] = ""
|
msg_copy["content"] = ""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Sanitized null content, tool_calls={len(msg_copy['tool_calls'])}"
|
f"Sanitized null content, tool_calls={len(msg_copy['tool_calls'])}"
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ from httpx import ASGITransport, AsyncClient
|
|||||||
|
|
||||||
from src.main import app
|
from src.main import app
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Command Line Options
|
# Command Line Options
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ Tests for agent REST API endpoints.
|
|||||||
Includes integration tests that verify real code paths work correctly
|
Includes integration tests that verify real code paths work correctly
|
||||||
without over-mocking (only LLM calls are mocked).
|
without over-mocking (only LLM calls are mocked).
|
||||||
"""
|
"""
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, patch, MagicMock
|
|
||||||
|
|
||||||
from src.domains.agents.schemas import PermissionMode
|
from src.domains.agents.schemas import PermissionMode
|
||||||
|
|
||||||
@@ -312,9 +313,10 @@ class TestAgentMethodSignatures:
|
|||||||
|
|
||||||
def test_task_agent_run_accepts_mode(self):
|
def test_task_agent_run_accepts_mode(self):
|
||||||
"""Verify TaskAgentImpl.run() accepts mode parameter."""
|
"""Verify TaskAgentImpl.run() accepts mode parameter."""
|
||||||
from src.domains.agents.task.agent import TaskAgentImpl
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
from src.domains.agents.task.agent import TaskAgentImpl
|
||||||
|
|
||||||
sig = inspect.signature(TaskAgentImpl.run)
|
sig = inspect.signature(TaskAgentImpl.run)
|
||||||
params = list(sig.parameters.keys())
|
params = list(sig.parameters.keys())
|
||||||
|
|
||||||
@@ -325,9 +327,10 @@ class TestAgentMethodSignatures:
|
|||||||
|
|
||||||
def test_task_agent_run_stream_accepts_mode(self):
|
def test_task_agent_run_stream_accepts_mode(self):
|
||||||
"""Verify TaskAgentImpl.run_stream() accepts mode parameter."""
|
"""Verify TaskAgentImpl.run_stream() accepts mode parameter."""
|
||||||
from src.domains.agents.task.agent import TaskAgentImpl
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
from src.domains.agents.task.agent import TaskAgentImpl
|
||||||
|
|
||||||
sig = inspect.signature(TaskAgentImpl.run_stream)
|
sig = inspect.signature(TaskAgentImpl.run_stream)
|
||||||
params = list(sig.parameters.keys())
|
params = list(sig.parameters.keys())
|
||||||
|
|
||||||
@@ -337,9 +340,10 @@ class TestAgentMethodSignatures:
|
|||||||
|
|
||||||
def test_trace_span_signature(self):
|
def test_trace_span_signature(self):
|
||||||
"""Verify trace_span only accepts expected parameters."""
|
"""Verify trace_span only accepts expected parameters."""
|
||||||
from src.shared.logging import trace_span
|
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
from src.shared.logging import trace_span
|
||||||
|
|
||||||
sig = inspect.signature(trace_span.__init__)
|
sig = inspect.signature(trace_span.__init__)
|
||||||
params = list(sig.parameters.keys())
|
params = list(sig.parameters.keys())
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ Tests for conversations domain.
|
|||||||
|
|
||||||
Tests conversation CRUD, context building, and API endpoints.
|
Tests conversation CRUD, context building, and API endpoints.
|
||||||
"""
|
"""
|
||||||
import pytest
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from src.domains.conversations.models import Conversation, Message
|
from src.domains.conversations.models import Conversation, Message
|
||||||
from src.domains.conversations.schemas import (
|
from src.domains.conversations.schemas import (
|
||||||
CreateConversationRequest,
|
|
||||||
AddMessageRequest,
|
AddMessageRequest,
|
||||||
ConversationResponse,
|
CreateConversationRequest,
|
||||||
MessageResponse,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -173,9 +172,10 @@ class TestConversationService:
|
|||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_context_prompt_no_history(self):
|
async def test_context_prompt_no_history(self):
|
||||||
"""Test building context prompt with no history."""
|
"""Test building context prompt with no history."""
|
||||||
from src.domains.conversations.service import ConversationService
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from src.domains.conversations.service import ConversationService
|
||||||
|
|
||||||
# Create mock session
|
# Create mock session
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
service = ConversationService(mock_session)
|
service = ConversationService(mock_session)
|
||||||
@@ -190,10 +190,11 @@ class TestConversationService:
|
|||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_context_prompt_with_history(self):
|
async def test_context_prompt_with_history(self):
|
||||||
"""Test building context prompt with message history."""
|
"""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 unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from src.domains.conversations.models import Message
|
||||||
|
from src.domains.conversations.service import ConversationService
|
||||||
|
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
service = ConversationService(mock_session)
|
service = ConversationService(mock_session)
|
||||||
|
|
||||||
@@ -221,10 +222,11 @@ class TestConversationService:
|
|||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_context_prompt_with_summary(self):
|
async def test_context_prompt_with_summary(self):
|
||||||
"""Test building context prompt with summary message."""
|
"""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 unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from src.domains.conversations.models import Message
|
||||||
|
from src.domains.conversations.service import ConversationService
|
||||||
|
|
||||||
mock_session = MagicMock()
|
mock_session = MagicMock()
|
||||||
service = ConversationService(mock_session)
|
service = ConversationService(mock_session)
|
||||||
|
|
||||||
@@ -253,8 +255,8 @@ class TestSummarization:
|
|||||||
|
|
||||||
def test_format_messages_for_summary(self):
|
def test_format_messages_for_summary(self):
|
||||||
"""Test formatting messages for summarization."""
|
"""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.models import Message
|
||||||
|
from src.domains.conversations.summarize import format_messages_for_summary
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
Message(
|
Message(
|
||||||
@@ -276,8 +278,8 @@ class TestSummarization:
|
|||||||
|
|
||||||
def test_format_messages_with_summary(self):
|
def test_format_messages_with_summary(self):
|
||||||
"""Test formatting messages that include a summary."""
|
"""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.models import Message
|
||||||
|
from src.domains.conversations.summarize import format_messages_for_summary
|
||||||
|
|
||||||
messages = [
|
messages = [
|
||||||
Message(
|
Message(
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.domains.tools.gitignore import GitignoreFilter, filter_gitignored
|
|
||||||
from src.domains.tools.file.glob import GlobFilesTool
|
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
|
from src.domains.tools.search.grep import GrepContentTool
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Tests registration, API endpoints, and tool restrictions.
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.domains.agents.base import get_agent, list_agents
|
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:
|
class TestPlanAgentRegistration:
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Tests for retry utilities.
|
Tests for retry utilities.
|
||||||
"""
|
"""
|
||||||
import pytest
|
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
import pytest
|
||||||
|
|
||||||
from src.shared.retry import (
|
from src.shared.retry import (
|
||||||
with_retry,
|
calculate_backoff,
|
||||||
retry_async,
|
|
||||||
is_retryable_exception,
|
is_retryable_exception,
|
||||||
is_retryable_http_status,
|
is_retryable_http_status,
|
||||||
calculate_backoff,
|
retry_async,
|
||||||
|
with_retry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
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.edit import EditFileTool
|
||||||
from src.domains.tools.file.glob import GlobFilesTool
|
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
|
from src.domains.tools.shell.bash_full import BashTool
|
||||||
|
|
||||||
|
|
||||||
@@ -275,5 +275,5 @@ class TestResourceLimits:
|
|||||||
|
|
||||||
assert result.success
|
assert result.success
|
||||||
# Should only return 5 files
|
# 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
|
assert len(lines) <= 5
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ Tests for the Task agent.
|
|||||||
|
|
||||||
Tests registration, API endpoints, tool access, and spawn_agent functionality.
|
Tests registration, API endpoints, tool access, and spawn_agent functionality.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, patch
|
|
||||||
|
|
||||||
from src.domains.agents.base import get_agent, list_agents
|
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:
|
class TestTaskAgentRegistration:
|
||||||
@@ -90,39 +90,28 @@ class TestTaskAgentTools:
|
|||||||
class TestSpawnAgentTool:
|
class TestSpawnAgentTool:
|
||||||
"""Tests for spawn_agent orchestration functionality."""
|
"""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
|
@pytest.mark.anyio
|
||||||
async def test_spawn_explore_agent(self):
|
async def test_spawn_explore_agent(self):
|
||||||
"""Test spawning an explore agent."""
|
"""Spawning an explore agent should delegate to the 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
|
|
||||||
|
|
||||||
# Create a mock context
|
The scaffolding that used to sit here — a MagicMock RunContext, an
|
||||||
ctx = MagicMock(spec=RunContext)
|
AgentContext with a /tmp working dir, and a patch of
|
||||||
ctx.deps = AgentContext(
|
src.domains.agents.base.get_agent — ran and then stopped at the comment
|
||||||
working_dir="/tmp",
|
"For now, verify the explore agent would be called correctly". There was
|
||||||
allowed_paths=["/tmp"],
|
no assertion, so it passed unconditionally.
|
||||||
timeout_seconds=30
|
|
||||||
)
|
|
||||||
|
|
||||||
# Mock the explore agent
|
Removed rather than tidied: ruff flagged its imports as unused, and
|
||||||
with patch("src.domains.agents.base.get_agent") as mock_get_agent:
|
deleting those would have made the test look clean while leaving it
|
||||||
mock_explore = AsyncMock()
|
hollow. git history has the setup for whoever finishes this.
|
||||||
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
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_spawn_unknown_agent_returns_error(self):
|
async def test_spawn_unknown_agent_returns_error(self):
|
||||||
"""Test that spawning unknown agent type returns error."""
|
"""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
|
# We can't easily test the tool directly, but we can verify
|
||||||
# the agent type validation logic
|
# the agent type validation logic
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
Tests for token counting utilities.
|
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:
|
class TestTokenCounting:
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from src.domains.tools.file.read import ReadFileTool
|
|
||||||
from src.domains.tools.file.glob import GlobFilesTool
|
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.search.grep import GrepContentTool
|
||||||
from src.domains.tools.shell.bash import BashReadOnlyTool
|
from src.domains.tools.shell.bash import BashReadOnlyTool
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
Tests for WebSearchTool.
|
Tests for WebSearchTool.
|
||||||
"""
|
"""
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from unittest.mock import AsyncMock, patch, MagicMock
|
|
||||||
|
|
||||||
from src.domains.tools.search.web import WebSearchTool
|
from src.domains.tools.search.web import WebSearchTool
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user