diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b79bd63..8678300 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,5 +17,7 @@ jobs: cache-dependency-glob: "**/pyproject.toml" - name: Install dependencies run: uv sync --dev - - name: Run ruff - run: uv run ruff check . + - name: Run ruff linter + run: uv run ruff check --output-format=github . + - name: Run ruff format + run: uv run ruff format --check . diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py index 576b149..1a3feaa 100644 --- a/EvoScientist/EvoScientist.py +++ b/EvoScientist/EvoScientist.py @@ -23,10 +23,10 @@ import os from datetime import datetime from pathlib import Path -from .config import get_effective_config, apply_config_to_env -from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt from . import paths as _paths_mod +from .config import apply_config_to_env, get_effective_config from .paths import set_active_workspace, set_workspace_root +from .prompts import RESEARCHER_INSTRUCTIONS, get_system_prompt # Suppress noisy warnings from deepagents skill loader (non-string frontmatter fields, etc.) logging.getLogger("deepagents.middleware.skills").setLevel(logging.ERROR) @@ -153,8 +153,8 @@ def _build_prompt_refs() -> dict: def _build_base_kwargs(base_backend, base_middleware): """Build agent kwargs *without* MCP (fast, no subprocess spawning).""" + from .tools import skill_manager, tavily_search, think_tool from .utils import load_subagents - from .tools import tavily_search, think_tool, skill_manager tool_registry = {"think_tool": think_tool} if os.environ.get("TAVILY_API_KEY"): @@ -185,8 +185,8 @@ def load_mcp_and_build_kwargs(base_backend, base_middleware): Re-connects to MCP servers only when the effective MCP config changes. Falls back to base kwargs if no MCP configured. """ + from .tools import skill_manager, tavily_search, think_tool from .utils import load_subagents - from .tools import tavily_search, think_tool, skill_manager mcp_by_agent = _load_mcp_tools_cached() if not mcp_by_agent: @@ -237,7 +237,8 @@ def load_mcp_and_build_kwargs(base_backend, base_middleware): def _get_default_backend(): """Build the default composite backend from current paths.""" - from deepagents.backends import FilesystemBackend, CompositeBackend + from deepagents.backends import CompositeBackend, FilesystemBackend + from .backends import CustomSandboxBackend, MergedReadOnlyBackend workspace_dir = str(_paths_mod.WORKSPACE_ROOT) @@ -269,7 +270,7 @@ def _get_default_backend(): def _get_default_middleware(): """Build the default middleware list.""" - from .middleware import create_memory_middleware, ToolErrorHandlerMiddleware + from .middleware import ToolErrorHandlerMiddleware, create_memory_middleware cfg = _ensure_config() memory_dir = str(_paths_mod.MEMORY_DIR) @@ -336,15 +337,18 @@ def create_cli_agent(workspace_dir: str | None = None, checkpointer=None, config import os as _os from deepagents import create_deep_agent - from deepagents.backends import FilesystemBackend, CompositeBackend - from .backends import CustomSandboxBackend, MergedReadOnlyBackend - from .middleware import create_memory_middleware, ToolErrorHandlerMiddleware + from deepagents.backends import CompositeBackend, FilesystemBackend + from . import paths as _paths + from .backends import CustomSandboxBackend, MergedReadOnlyBackend + from .middleware import ToolErrorHandlerMiddleware, create_memory_middleware cfg = _ensure_config(config) if checkpointer is None: - from langgraph.checkpoint.memory import InMemorySaver # type: ignore[import-untyped] + from langgraph.checkpoint.memory import ( + InMemorySaver, # type: ignore[import-untyped] + ) checkpointer = InMemorySaver() diff --git a/EvoScientist/__init__.py b/EvoScientist/__init__.py index 2a5af27..afc4c08 100644 --- a/EvoScientist/__init__.py +++ b/EvoScientist/__init__.py @@ -9,7 +9,6 @@ from __future__ import annotations from importlib import import_module - _EXPORTS: dict[str, tuple[str, str]] = { # Agent graph (lazy to avoid expensive initialization at import time) "EvoScientist_agent": (".EvoScientist", "EvoScientist_agent"), diff --git a/EvoScientist/ccproxy_manager.py b/EvoScientist/ccproxy_manager.py index 42cd37e..1c1ed1e 100644 --- a/EvoScientist/ccproxy_manager.py +++ b/EvoScientist/ccproxy_manager.py @@ -56,8 +56,8 @@ def _is_editable_install() -> bool: project root can shadow the real ``dist-info`` in site-packages. """ try: - import json import importlib.metadata as _meta + import json for dist in _meta.distributions(): name = dist.metadata.get("Name", "") diff --git a/EvoScientist/channels/__init__.py b/EvoScientist/channels/__init__.py index 4bfc959..ce35e10 100644 --- a/EvoScientist/channels/__init__.py +++ b/EvoScientist/channels/__init__.py @@ -4,19 +4,19 @@ This module provides an extensible interface for different messaging channels (iMessage, Telegram, Discord, Slack, WeChat, DingTalk, Feishu, Email, QQ, Signal) to communicate with the EvoScientist agent. """ -from .base import Channel, RawIncoming, IncomingMessage, OutgoingMessage, chunk_text -from .bus import MessageBus, InboundMessage, OutboundMessage +from .base import Channel, IncomingMessage, OutgoingMessage, RawIncoming, chunk_text +from .bus import InboundMessage, MessageBus, OutboundMessage from .capabilities import ChannelCapabilities from .channel_manager import ( ChannelManager, - register_channel, - create_channel, available_channels, + create_channel, + register_channel, ) from .consumer import InboundConsumer from .formatter import UnifiedFormatter from .middleware import TypingManager -from .plugin import ChannelPlugin, ChannelMeta, ReloadPolicy +from .plugin import ChannelMeta, ChannelPlugin, ReloadPolicy from .standalone import run_standalone # Backward compat: ChannelServer is now Channel itself diff --git a/EvoScientist/channels/base.py b/EvoScientist/channels/base.py index 2614297..f7be93e 100644 --- a/EvoScientist/channels/base.py +++ b/EvoScientist/channels/base.py @@ -4,23 +4,23 @@ This module defines the Channel interface that all messaging channels (iMessage, WeChat, etc.) must implement. """ -from abc import ABC, abstractmethod import asyncio import logging import re +from abc import ABC, abstractmethod from collections import OrderedDict -from collections.abc import Awaitable, Callable as CallableABC +from collections.abc import AsyncIterator, Awaitable, Callable +from collections.abc import Callable as CallableABC from dataclasses import dataclass, field from datetime import datetime from pathlib import Path -from typing import Any, AsyncIterator, Callable +from typing import Any from ..paths import MEDIA_DIR - from .bus.events import InboundMessage, OutboundMessage from .capabilities import ChannelCapabilities from .formatter import UnifiedFormatter -from .plugin import ChannelPlugin, ChannelMeta +from .plugin import ChannelMeta, ChannelPlugin _logger = logging.getLogger(__name__) @@ -55,7 +55,7 @@ def chunk_text(text: str, limit: int) -> list[str]: # Effective limit is reduced if we need to add fences # We reserve ~20 chars for fences (```lang\n and \n```) effective_limit = limit - (20 if in_code_block else 0) - + if len(remaining) <= effective_limit: segment = remaining best = len(remaining) @@ -69,13 +69,13 @@ def chunk_text(text: str, limit: int) -> list[str]: pos = segment.rfind("\n\n") if pos > 0: best = pos - + # Line if best == -1: pos = segment.rfind("\n") if pos > 0: best = pos - + # Word if best == -1: pos = segment.rfind(" ") @@ -91,12 +91,12 @@ def chunk_text(text: str, limit: int) -> list[str]: best = effective_limit chunk_raw = remaining[:best].rstrip() - + # Track state transitions within this raw segment starts_in_code = in_code_block current_lang = code_block_lang - - # We use a simple count of ``` to toggle state. + + # We use a simple count of ``` to toggle state. # Note: This handles both opening and closing fences. fences = list(re.finditer(r"```(\w*)", chunk_raw)) for f in fences: @@ -106,17 +106,17 @@ def chunk_text(text: str, limit: int) -> list[str]: else: in_code_block = False code_block_lang = "" - + ends_in_code = in_code_block # Build the final chunk with necessary fences prefix = f"```{current_lang}\n" if starts_in_code else "" suffix = "\n```" if ends_in_code else "" - + final_chunk = prefix + chunk_raw + suffix if final_chunk.strip(): chunks.append(final_chunk) - + remaining = remaining[best:].lstrip("\n") return chunks @@ -326,7 +326,7 @@ class Channel(ChannelPlugin, ABC): self._message_was_mentioned: dict[str, bool] = {} # Retry configuration (auto-resolved from channel name) - from .retry import RetryConfig, DEFAULT_RETRY, RETRY_PRESETS + from .retry import DEFAULT_RETRY, RETRY_PRESETS, RetryConfig self._retry_config: RetryConfig = RETRY_PRESETS.get(self.name, DEFAULT_RETRY) @@ -349,11 +349,11 @@ class Channel(ChannelPlugin, ABC): 5. MentionGatingMiddleware — filter by mention policy """ from .middleware import ( - DedupMiddleware, AllowListMiddleware, - PairingMiddleware, + DedupMiddleware, GroupHistoryMiddleware, MentionGatingMiddleware, + PairingMiddleware, ) middlewares = [] @@ -454,7 +454,7 @@ class Channel(ChannelPlugin, ABC): try: msg = await asyncio.wait_for(self._queue.get(), timeout=1.0) yield msg - except asyncio.TimeoutError: + except TimeoutError: continue def _acquire_send_lock(self, chat_id: str) -> asyncio.Lock: diff --git a/EvoScientist/channels/bus/__init__.py b/EvoScientist/channels/bus/__init__.py index 3510967..d2ee6e3 100644 --- a/EvoScientist/channels/bus/__init__.py +++ b/EvoScientist/channels/bus/__init__.py @@ -3,4 +3,4 @@ from .events import InboundMessage, OutboundMessage from .message_bus import MessageBus -__all__ = ["MessageBus", "InboundMessage", "OutboundMessage"] +__all__ = ["InboundMessage", "MessageBus", "OutboundMessage"] diff --git a/EvoScientist/channels/bus/message_bus.py b/EvoScientist/channels/bus/message_bus.py index ad9cce3..4e54320 100644 --- a/EvoScientist/channels/bus/message_bus.py +++ b/EvoScientist/channels/bus/message_bus.py @@ -10,7 +10,7 @@ Deduplication is handled at the Channel level (single dedup point). import asyncio import logging -from typing import Callable, Awaitable +from collections.abc import Awaitable, Callable from .events import InboundMessage, OutboundMessage @@ -72,7 +72,7 @@ class MessageBus: self.outbound.get(), timeout=1.0, ) - except asyncio.TimeoutError: + except TimeoutError: continue subscribers = self._outbound_subscribers.get(msg.channel, []) if not subscribers: diff --git a/EvoScientist/channels/channel_manager.py b/EvoScientist/channels/channel_manager.py index 9f781b7..b8c0f3b 100644 --- a/EvoScientist/channels/channel_manager.py +++ b/EvoScientist/channels/channel_manager.py @@ -16,10 +16,11 @@ import json import logging import pkgutil import time +from collections.abc import Callable from dataclasses import dataclass, field from datetime import datetime from pathlib import Path -from typing import Any, Callable +from typing import Any from .base import Channel, OutboundMessage from .bus import MessageBus @@ -352,7 +353,7 @@ class _HealthServer: self._process_request(reader, writer), timeout=self._CONNECTION_TIMEOUT, ) - except (asyncio.TimeoutError, ConnectionError, OSError): + except (TimeoutError, ConnectionError, OSError): pass finally: try: @@ -448,7 +449,7 @@ def _parse_csv(value: str) -> set[str] | None: if not value or not value.strip(): return None items = {s.strip() for s in value.split(",") if s.strip()} - return items if items else None + return items or None def register_channel(name: str, factory: ChannelFactory) -> None: @@ -596,7 +597,7 @@ class ChannelManager: self._shared_webhook_server: SharedWebhookServer | None = None @classmethod - def from_config(cls, config, bus: MessageBus | None = None) -> "ChannelManager": + def from_config(cls, config, bus: MessageBus | None = None) -> ChannelManager: """Create a ChannelManager from application config. Parses ``config.channel_enabled`` (comma-separated channel types), @@ -820,7 +821,7 @@ class ChannelManager: self.bus.consume_outbound(), timeout=1.0, ) - except asyncio.TimeoutError: + except TimeoutError: continue except asyncio.CancelledError: break diff --git a/EvoScientist/channels/consumer.py b/EvoScientist/channels/consumer.py index 56a7b8a..4b350a5 100644 --- a/EvoScientist/channels/consumer.py +++ b/EvoScientist/channels/consumer.py @@ -13,8 +13,9 @@ import asyncio import logging import uuid from collections import OrderedDict +from collections.abc import AsyncIterator, Callable from dataclasses import dataclass -from typing import Any, AsyncIterator, Callable, TypeVar +from typing import Any, TypeVar from .base import Channel from .bus import MessageBus @@ -307,7 +308,7 @@ class InboundConsumer: self.bus.consume_inbound(), timeout=1.0, ) - except asyncio.TimeoutError: + except TimeoutError: continue except asyncio.CancelledError: break @@ -577,7 +578,7 @@ class InboundConsumer: pending.event.wait(), timeout=_HITL_APPROVAL_TIMEOUT, ) - except asyncio.TimeoutError: + except TimeoutError: # Auto-approve on timeout pending.decision = "approve" finally: @@ -606,7 +607,7 @@ class InboundConsumer: ) # continue to next HITL round - except asyncio.TimeoutError: + except TimeoutError: self._metrics.total_timeouts += 1 logger.error( f"Inference timeout ({self._inference_timeout}s idle) " @@ -678,7 +679,7 @@ class InboundConsumer: self._pending_ask_user_replies[session_key] = pending try: await asyncio.wait_for(pending.event.wait(), timeout=timeout) - except asyncio.TimeoutError: + except TimeoutError: pass finally: self._pending_ask_user_replies.pop(session_key, None) diff --git a/EvoScientist/channels/dingtalk/__init__.py b/EvoScientist/channels/dingtalk/__init__.py index 2dd7aab..6a9f0a6 100644 --- a/EvoScientist/channels/dingtalk/__init__.py +++ b/EvoScientist/channels/dingtalk/__init__.py @@ -9,15 +9,15 @@ Usage in config: dingtalk_client_secret = "your_app_secret" """ +from ..channel_manager import _parse_csv, register_channel from .channel import DingTalkChannel, DingTalkConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["DingTalkChannel", "DingTalkConfig"] def create_from_config(config) -> DingTalkChannel: allowed = _parse_csv(config.dingtalk_allowed_senders) - proxy = config.dingtalk_proxy if config.dingtalk_proxy else None + proxy = config.dingtalk_proxy or None return DingTalkChannel( DingTalkConfig( client_id=config.dingtalk_client_id, diff --git a/EvoScientist/channels/dingtalk/channel.py b/EvoScientist/channels/dingtalk/channel.py index 9efb47a..3ddd5f0 100644 --- a/EvoScientist/channels/dingtalk/channel.py +++ b/EvoScientist/channels/dingtalk/channel.py @@ -3,15 +3,15 @@ import asyncio import json import logging -from urllib.parse import quote_plus from dataclasses import dataclass from datetime import datetime from pathlib import Path +from urllib.parse import quote_plus -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import DINGTALK as DINGTALK_CAPS -from ..mixins import WebSocketMixin, TokenMixin from ..config import BaseChannelConfig +from ..mixins import TokenMixin, WebSocketMixin logger = logging.getLogger(__name__) diff --git a/EvoScientist/channels/dingtalk/serve.py b/EvoScientist/channels/dingtalk/serve.py index 8fb4abb..09e3817 100644 --- a/EvoScientist/channels/dingtalk/serve.py +++ b/EvoScientist/channels/dingtalk/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from .channel import DingTalkChannel, DingTalkConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import DingTalkChannel, DingTalkConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/discord/__init__.py b/EvoScientist/channels/discord/__init__.py index 94d84e2..8d79e72 100644 --- a/EvoScientist/channels/discord/__init__.py +++ b/EvoScientist/channels/discord/__init__.py @@ -1,5 +1,5 @@ +from ..channel_manager import _parse_csv, register_channel from .channel import DiscordChannel, DiscordConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["DiscordChannel", "DiscordConfig"] @@ -7,7 +7,7 @@ __all__ = ["DiscordChannel", "DiscordConfig"] def create_from_config(config) -> DiscordChannel: allowed = _parse_csv(config.discord_allowed_senders) channels = _parse_csv(config.discord_allowed_channels) - proxy = config.discord_proxy if config.discord_proxy else None + proxy = config.discord_proxy or None return DiscordChannel( DiscordConfig( bot_token=config.discord_bot_token, diff --git a/EvoScientist/channels/discord/channel.py b/EvoScientist/channels/discord/channel.py index 7dc54bd..af3c987 100644 --- a/EvoScientist/channels/discord/channel.py +++ b/EvoScientist/channels/discord/channel.py @@ -6,7 +6,7 @@ import os from dataclasses import dataclass from datetime import datetime -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import DISCORD as DISCORD_CAPS from ..config import BaseChannelConfig @@ -97,7 +97,7 @@ class DiscordChannel(Channel): try: await asyncio.wait_for(self._ready.wait(), timeout=60) - except asyncio.TimeoutError: + except TimeoutError: raise ChannelError( "Discord bot failed to connect within 60s. " "Check network/proxy connectivity to gateway.discord.gg" diff --git a/EvoScientist/channels/discord/serve.py b/EvoScientist/channels/discord/serve.py index 9e8f000..93d0817 100644 --- a/EvoScientist/channels/discord/serve.py +++ b/EvoScientist/channels/discord/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from .channel import DiscordChannel, DiscordConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import DiscordChannel, DiscordConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/email/__init__.py b/EvoScientist/channels/email/__init__.py index f9e690c..fff3e2b 100644 --- a/EvoScientist/channels/email/__init__.py +++ b/EvoScientist/channels/email/__init__.py @@ -9,8 +9,8 @@ Usage in config: ... """ +from ..channel_manager import _parse_csv, register_channel from .channel import EmailChannel, EmailConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["EmailChannel", "EmailConfig"] diff --git a/EvoScientist/channels/email/channel.py b/EvoScientist/channels/email/channel.py index 879c705..378166b 100644 --- a/EvoScientist/channels/email/channel.py +++ b/EvoScientist/channels/email/channel.py @@ -21,10 +21,10 @@ from email.mime.text import MIMEText from email.utils import parseaddr from pathlib import Path -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import EMAIL as EMAIL_CAPS -from ..mixins import PollingMixin from ..config import BaseChannelConfig +from ..mixins import PollingMixin logger = logging.getLogger(__name__) @@ -37,9 +37,9 @@ def _decode_hdr(raw: str) -> str: def _strip_html(text: str) -> str: - text = re.sub(r"", "\n", text, flags=re.I) - text = re.sub(r"]*>", "\n", text, flags=re.I) - text = re.sub(r"

", "\n", text, flags=re.I) + text = re.sub(r"", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"]*>", "\n", text, flags=re.IGNORECASE) + text = re.sub(r"

", "\n", text, flags=re.IGNORECASE) text = re.sub(r"<[^>]+>", "", text) return html.unescape(text).strip() diff --git a/EvoScientist/channels/email/serve.py b/EvoScientist/channels/email/serve.py index c014dca..4f6e82a 100644 --- a/EvoScientist/channels/email/serve.py +++ b/EvoScientist/channels/email/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from .channel import EmailChannel, EmailConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import EmailChannel, EmailConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/feishu/__init__.py b/EvoScientist/channels/feishu/__init__.py index 057fbd5..5b8c553 100644 --- a/EvoScientist/channels/feishu/__init__.py +++ b/EvoScientist/channels/feishu/__init__.py @@ -1,12 +1,12 @@ +from ..channel_manager import _parse_csv, register_channel from .channel import FeishuChannel, FeishuConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["FeishuChannel", "FeishuConfig"] def create_from_config(config) -> FeishuChannel: allowed = _parse_csv(config.feishu_allowed_senders) - proxy = config.feishu_proxy if config.feishu_proxy else None + proxy = config.feishu_proxy or None return FeishuChannel( FeishuConfig( app_id=config.feishu_app_id, diff --git a/EvoScientist/channels/feishu/channel.py b/EvoScientist/channels/feishu/channel.py index 739e405..cd4c7a7 100644 --- a/EvoScientist/channels/feishu/channel.py +++ b/EvoScientist/channels/feishu/channel.py @@ -23,18 +23,18 @@ import hashlib import json import logging import re -from typing import Any, TYPE_CHECKING from dataclasses import dataclass from datetime import datetime from pathlib import Path +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from aiohttp import web -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import FEISHU as FEISHU_CAPS -from ..mixins import WebhookMixin, TokenMixin from ..config import BaseChannelConfig +from ..mixins import TokenMixin, WebhookMixin logger = logging.getLogger(__name__) @@ -294,8 +294,8 @@ class FeishuChannel(Channel, WebhookMixin, TokenMixin): async def start(self) -> None: try: - from aiohttp import web # noqa: F401 import httpx # noqa: F401 + from aiohttp import web # noqa: F401 except ImportError: raise ChannelError( "aiohttp or httpx not installed. " @@ -611,7 +611,7 @@ class FeishuChannel(Channel, WebhookMixin, TokenMixin): # ── Webhook event handler ───────────────────────────────────── - async def _handle_event(self, request) -> "web.Response": + async def _handle_event(self, request) -> web.Response: """Handle POST /webhook/event from Feishu.""" from aiohttp import web diff --git a/EvoScientist/channels/feishu/serve.py b/EvoScientist/channels/feishu/serve.py index c9d0709..945e90e 100644 --- a/EvoScientist/channels/feishu/serve.py +++ b/EvoScientist/channels/feishu/serve.py @@ -20,9 +20,9 @@ Examples: import argparse import logging -from .channel import FeishuChannel, FeishuConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import FeishuChannel, FeishuConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/formatter.py b/EvoScientist/channels/formatter.py index 12c911e..210a9bb 100644 --- a/EvoScientist/channels/formatter.py +++ b/EvoScientist/channels/formatter.py @@ -12,8 +12,7 @@ Channels no longer need per-file format functions — they just declare from __future__ import annotations import re -from typing import Callable - +from collections.abc import Callable # ═════════════════════════════════════════════════════════════════════ # Markdown conversion engine (formerly markdown_utils.py) @@ -290,6 +289,6 @@ class UnifiedFormatter: return convert_markdown(text, **self._profile) @classmethod - def for_channel(cls, format_type: str) -> "UnifiedFormatter": + def for_channel(cls, format_type: str) -> UnifiedFormatter: """Factory: create a formatter for the given format type.""" return cls(format_type) diff --git a/EvoScientist/channels/imessage/__init__.py b/EvoScientist/channels/imessage/__init__.py index 412fcc2..aa7becd 100644 --- a/EvoScientist/channels/imessage/__init__.py +++ b/EvoScientist/channels/imessage/__init__.py @@ -9,28 +9,28 @@ Requirements: - Messages.app logged into iCloud """ +from ..channel_manager import _parse_csv, register_channel from .channel_rpc import IMessageChannelRpc as IMessageChannel from .channel_rpc import IMessageConfig -from .probe import probe_imessage, ProbeResult +from .probe import ProbeResult, probe_imessage from .targets import ( - parse_target, - normalize_handle, - normalize_e164, - IMessageTarget, IMessageService, + IMessageTarget, + normalize_e164, + normalize_handle, + parse_target, ) -from ..channel_manager import register_channel, _parse_csv __all__ = [ "IMessageChannel", "IMessageConfig", - "probe_imessage", - "ProbeResult", - "parse_target", - "normalize_handle", - "normalize_e164", - "IMessageTarget", "IMessageService", + "IMessageTarget", + "ProbeResult", + "normalize_e164", + "normalize_handle", + "parse_target", + "probe_imessage", ] diff --git a/EvoScientist/channels/imessage/channel_rpc.py b/EvoScientist/channels/imessage/channel_rpc.py index d7c94f1..1dd47cf 100644 --- a/EvoScientist/channels/imessage/channel_rpc.py +++ b/EvoScientist/channels/imessage/channel_rpc.py @@ -10,15 +10,15 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..config import BaseChannelConfig from .rpc_client import ImsgRpcClient, RpcNotification from .targets import ( - normalize_handle, - parse_target, - ChatIdTarget, ChatGuidTarget, ChatIdentifierTarget, + ChatIdTarget, + normalize_handle, + parse_target, ) logger = logging.getLogger(__name__) diff --git a/EvoScientist/channels/imessage/rpc_client.py b/EvoScientist/channels/imessage/rpc_client.py index a6c95b8..75fe9bb 100644 --- a/EvoScientist/channels/imessage/rpc_client.py +++ b/EvoScientist/channels/imessage/rpc_client.py @@ -7,8 +7,9 @@ similar to OpenClaw's approach. import asyncio import json import logging +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable +from typing import Any logger = logging.getLogger(__name__) @@ -96,7 +97,7 @@ class ImsgRpcClient: try: self._process.terminate() await asyncio.wait_for(self._process.wait(), timeout=2.0) - except asyncio.TimeoutError: + except TimeoutError: self._process.kill() await self._process.wait() @@ -150,7 +151,7 @@ class ImsgRpcClient: try: return await asyncio.wait_for(future, timeout=timeout) - except asyncio.TimeoutError: + except TimeoutError: self._pending.pop(request_id, None) raise Exception(f"RPC request timeout: {method}") @@ -209,7 +210,7 @@ class ImsgRpcClient: if future is None: return - if "error" in data and data["error"]: + if data.get("error"): error = data["error"] msg = error.get("message", "RPC error") future.set_exception(Exception(msg)) diff --git a/EvoScientist/channels/imessage/serve.py b/EvoScientist/channels/imessage/serve.py index c8038f3..d74a559 100644 --- a/EvoScientist/channels/imessage/serve.py +++ b/EvoScientist/channels/imessage/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from . import IMessageChannel, IMessageConfig from ..bus import MessageBus from ..standalone import run_standalone +from . import IMessageChannel, IMessageConfig logger = logging.getLogger(__name__) diff --git a/EvoScientist/channels/middleware.py b/EvoScientist/channels/middleware.py index 2cd9d60..e6c02a6 100644 --- a/EvoScientist/channels/middleware.py +++ b/EvoScientist/channels/middleware.py @@ -16,12 +16,12 @@ import logging import random import time from collections import OrderedDict, deque -from collections.abc import Awaitable +from collections.abc import Awaitable, Callable from dataclasses import dataclass -from typing import Any, Callable +from typing import Any -from .bus.events import InboundMessage, OutboundMessage from .base import RawIncoming +from .bus.events import InboundMessage, OutboundMessage _logger = logging.getLogger(__name__) @@ -543,8 +543,8 @@ class FormattingMiddleware(OutboundMiddlewareBase): """ def __init__(self, capabilities: Any) -> None: - from .formatter import UnifiedFormatter from .capabilities import ChannelCapabilities + from .formatter import UnifiedFormatter caps: ChannelCapabilities = capabilities self._formatter = UnifiedFormatter.for_channel(caps.format_type) diff --git a/EvoScientist/channels/mixins.py b/EvoScientist/channels/mixins.py index f810c4b..d7eeb0c 100644 --- a/EvoScientist/channels/mixins.py +++ b/EvoScientist/channels/mixins.py @@ -18,7 +18,6 @@ import logging import time from typing import Any - logger = logging.getLogger(__name__) @@ -192,6 +191,7 @@ class WebSocketMixin: async def _ws_loop(self) -> None: """Main WebSocket loop with auto-reconnect.""" import os + import aiohttp while getattr(self, "_running", False): diff --git a/EvoScientist/channels/plugin.py b/EvoScientist/channels/plugin.py index 51099f1..752583e 100644 --- a/EvoScientist/channels/plugin.py +++ b/EvoScientist/channels/plugin.py @@ -15,7 +15,6 @@ from typing import Any, Protocol, runtime_checkable from .capabilities import ChannelCapabilities - # ── Channel metadata ───────────────────────────────────────────────── diff --git a/EvoScientist/channels/qq/__init__.py b/EvoScientist/channels/qq/__init__.py index ad27179..7f560c2 100644 --- a/EvoScientist/channels/qq/__init__.py +++ b/EvoScientist/channels/qq/__init__.py @@ -8,8 +8,8 @@ Usage in config: qq_app_secret = "your_app_secret" """ +from ..channel_manager import _parse_csv, register_channel from .channel import QQChannel, QQConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["QQChannel", "QQConfig"] diff --git a/EvoScientist/channels/qq/channel.py b/EvoScientist/channels/qq/channel.py index 42f7bb7..28eee01 100644 --- a/EvoScientist/channels/qq/channel.py +++ b/EvoScientist/channels/qq/channel.py @@ -6,7 +6,7 @@ from collections import deque from dataclasses import dataclass from datetime import datetime -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import QQ as QQ_CAPS from ..config import BaseChannelConfig @@ -64,7 +64,7 @@ class QQChannel(Channel): def __init__(self, config: QQConfig): super().__init__(config) - self._client: "botpy.Client | None" = None + self._client: botpy.Client | None = None self._bot_task: asyncio.Task | None = None self._processed_ids: deque = deque(maxlen=1000) self._msg_seq: dict[str, int] = {} # msg_id -> next seq number diff --git a/EvoScientist/channels/qq/serve.py b/EvoScientist/channels/qq/serve.py index 4e34552..96bab76 100644 --- a/EvoScientist/channels/qq/serve.py +++ b/EvoScientist/channels/qq/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from .channel import QQChannel, QQConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import QQChannel, QQConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/retry.py b/EvoScientist/channels/retry.py index 1dd50b4..61f7a03 100644 --- a/EvoScientist/channels/retry.py +++ b/EvoScientist/channels/retry.py @@ -105,7 +105,7 @@ async def retry_async( await asyncio.sleep(delay) # Should never reach here, but satisfy the type checker. - assert last_exc is not None # noqa: S101 + assert last_exc is not None raise last_exc diff --git a/EvoScientist/channels/signal/__init__.py b/EvoScientist/channels/signal/__init__.py index 2b8c658..ecc05fb 100644 --- a/EvoScientist/channels/signal/__init__.py +++ b/EvoScientist/channels/signal/__init__.py @@ -7,8 +7,8 @@ Usage in config: signal_phone_number = "+1234567890" """ +from ..channel_manager import _parse_csv, register_channel from .channel import SignalChannel, SignalConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["SignalChannel", "SignalConfig"] diff --git a/EvoScientist/channels/signal/channel.py b/EvoScientist/channels/signal/channel.py index 9b8b2f6..b7e5437 100644 --- a/EvoScientist/channels/signal/channel.py +++ b/EvoScientist/channels/signal/channel.py @@ -10,7 +10,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Any -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import SIGNAL as SIGNAL_CAPS from ..config import BaseChannelConfig @@ -102,7 +102,7 @@ class SignalChannel(Channel): await writer.wait_closed() logger.info("signal-cli daemon already running") return - except (ConnectionRefusedError, asyncio.TimeoutError, OSError): + except (TimeoutError, ConnectionRefusedError, OSError): pass # Start daemon @@ -437,7 +437,7 @@ class SignalChannel(Channel): try: return await asyncio.wait_for(fut, timeout=timeout) - except asyncio.TimeoutError: + except TimeoutError: self._pending_rpcs.pop(rpc_id, None) logger.warning(f"Signal RPC '{method}' timed out after {timeout}s") return None diff --git a/EvoScientist/channels/signal/serve.py b/EvoScientist/channels/signal/serve.py index 1d04964..fb6252b 100644 --- a/EvoScientist/channels/signal/serve.py +++ b/EvoScientist/channels/signal/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from .channel import SignalChannel, SignalConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import SignalChannel, SignalConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/slack/__init__.py b/EvoScientist/channels/slack/__init__.py index b458f85..0520371 100644 --- a/EvoScientist/channels/slack/__init__.py +++ b/EvoScientist/channels/slack/__init__.py @@ -1,5 +1,5 @@ +from ..channel_manager import _parse_csv, register_channel from .channel import SlackChannel, SlackConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["SlackChannel", "SlackConfig"] @@ -7,7 +7,7 @@ __all__ = ["SlackChannel", "SlackConfig"] def create_from_config(config) -> SlackChannel: allowed = _parse_csv(config.slack_allowed_senders) channels = _parse_csv(config.slack_allowed_channels) - proxy = config.slack_proxy if config.slack_proxy else None + proxy = config.slack_proxy or None return SlackChannel( SlackConfig( bot_token=config.slack_bot_token, diff --git a/EvoScientist/channels/slack/channel.py b/EvoScientist/channels/slack/channel.py index a94b3d9..e7881d0 100644 --- a/EvoScientist/channels/slack/channel.py +++ b/EvoScientist/channels/slack/channel.py @@ -5,7 +5,7 @@ import logging from dataclasses import dataclass from datetime import datetime -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import SLACK as SLACK_CAPS from ..config import BaseChannelConfig @@ -43,10 +43,10 @@ class SlackChannel(Channel): ) try: - from slack_sdk.web.async_client import AsyncWebClient from slack_sdk.socket_mode.aiohttp import SocketModeClient from slack_sdk.socket_mode.request import SocketModeRequest from slack_sdk.socket_mode.response import SocketModeResponse + from slack_sdk.web.async_client import AsyncWebClient except ImportError: raise ChannelError( "slack-sdk or aiohttp not installed. " @@ -65,7 +65,7 @@ class SlackChannel(Channel): timeout=15, ) self._bot_user_id = auth["user_id"] - except asyncio.TimeoutError: + except TimeoutError: raise ChannelError( "Slack auth_test timed out — check network and bot token" ) @@ -110,7 +110,7 @@ class SlackChannel(Channel): self._socket_client.connect(), timeout=30, ) - except asyncio.TimeoutError: + except TimeoutError: raise ChannelError( "Slack Socket Mode connection timed out — " "check app token (must start with xapp-) and " diff --git a/EvoScientist/channels/slack/serve.py b/EvoScientist/channels/slack/serve.py index 8bcae33..d994547 100644 --- a/EvoScientist/channels/slack/serve.py +++ b/EvoScientist/channels/slack/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from .channel import SlackChannel, SlackConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import SlackChannel, SlackConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/standalone.py b/EvoScientist/channels/standalone.py index 7290158..8690f3d 100644 --- a/EvoScientist/channels/standalone.py +++ b/EvoScientist/channels/standalone.py @@ -36,7 +36,7 @@ async def standalone_outbound_dispatcher( bus.consume_outbound(), timeout=1.0, ) - except asyncio.TimeoutError: + except TimeoutError: continue except asyncio.CancelledError: break diff --git a/EvoScientist/channels/telegram/__init__.py b/EvoScientist/channels/telegram/__init__.py index a116361..82277b5 100644 --- a/EvoScientist/channels/telegram/__init__.py +++ b/EvoScientist/channels/telegram/__init__.py @@ -1,12 +1,12 @@ +from ..channel_manager import _parse_csv, register_channel from .channel import TelegramChannel, TelegramConfig -from ..channel_manager import register_channel, _parse_csv __all__ = ["TelegramChannel", "TelegramConfig"] def create_from_config(config) -> TelegramChannel: allowed = _parse_csv(config.telegram_allowed_senders) - proxy = config.telegram_proxy if config.telegram_proxy else None + proxy = config.telegram_proxy or None return TelegramChannel( TelegramConfig( bot_token=config.telegram_bot_token, diff --git a/EvoScientist/channels/telegram/channel.py b/EvoScientist/channels/telegram/channel.py index 20aedf3..e1e8515 100644 --- a/EvoScientist/channels/telegram/channel.py +++ b/EvoScientist/channels/telegram/channel.py @@ -6,12 +6,12 @@ from datetime import datetime from pathlib import Path from ..base import ( - Channel, - RawIncoming, - ChannelError, + AUDIO_EXTS, IMAGE_EXTS, VIDEO_EXTS, - AUDIO_EXTS, + Channel, + ChannelError, + RawIncoming, ) from ..capabilities import TELEGRAM as TELEGRAM_CAPS from ..config import BaseChannelConfig diff --git a/EvoScientist/channels/telegram/serve.py b/EvoScientist/channels/telegram/serve.py index 074e69b..52585cc 100644 --- a/EvoScientist/channels/telegram/serve.py +++ b/EvoScientist/channels/telegram/serve.py @@ -19,9 +19,9 @@ Examples: import argparse import logging -from .channel import TelegramChannel, TelegramConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import TelegramChannel, TelegramConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/wechat/__init__.py b/EvoScientist/channels/wechat/__init__.py index 539261e..9534812 100644 --- a/EvoScientist/channels/wechat/__init__.py +++ b/EvoScientist/channels/wechat/__init__.py @@ -29,16 +29,16 @@ Usage in config: wechat_webhook_port = 9001 """ -from .channel import WeChatChannel, WeComConfig, WeChatMPConfig -from ..channel_manager import register_channel, _parse_csv +from ..channel_manager import _parse_csv, register_channel +from .channel import WeChatChannel, WeChatMPConfig, WeComConfig -__all__ = ["WeChatChannel", "WeComConfig", "WeChatMPConfig"] +__all__ = ["WeChatChannel", "WeChatMPConfig", "WeComConfig"] def create_from_config(config) -> WeChatChannel: backend = config.wechat_backend or "wecom" allowed = _parse_csv(config.wechat_allowed_senders) - proxy = config.wechat_proxy if config.wechat_proxy else None + proxy = config.wechat_proxy or None port = int(config.wechat_webhook_port or 9001) if backend == "wechatmp": diff --git a/EvoScientist/channels/wechat/channel.py b/EvoScientist/channels/wechat/channel.py index e560e84..63e4963 100644 --- a/EvoScientist/channels/wechat/channel.py +++ b/EvoScientist/channels/wechat/channel.py @@ -27,15 +27,15 @@ import time from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: from aiohttp import web -from ..mixins import WebhookMixin, TokenMixin -from ..base import Channel, RawIncoming, ChannelError +from ..base import Channel, ChannelError, RawIncoming from ..capabilities import WECHAT as WECHAT_CAPS from ..config import BaseChannelConfig +from ..mixins import TokenMixin, WebhookMixin logger = logging.getLogger(__name__) @@ -139,8 +139,8 @@ class WeChatChannel(Channel, WebhookMixin, TokenMixin): async def start(self) -> None: try: + import httpx from aiohttp import web - import httpx # noqa: F401 except ImportError: raise ChannelError( "aiohttp or httpx not installed. " @@ -274,7 +274,7 @@ class WeChatChannel(Channel, WebhookMixin, TokenMixin): # ── Signature verification (GET callback) ───────────────────── - async def _handle_verify(self, request) -> "web.Response": + async def _handle_verify(self, request) -> web.Response: """Handle GET /wechat/callback for URL verification. WeChat/WeCom sends: msg_signature, timestamp, nonce, echostr @@ -322,9 +322,10 @@ class WeChatChannel(Channel, WebhookMixin, TokenMixin): # ── Inbound message handling (POST callback) ────────────────── - async def _handle_message(self, request) -> "web.Response": + async def _handle_message(self, request) -> web.Response: """Handle POST /wechat/callback for incoming messages.""" from aiohttp import web + from .crypto import parse_xml try: diff --git a/EvoScientist/channels/wechat/serve.py b/EvoScientist/channels/wechat/serve.py index 1a518bf..1521b8c 100644 --- a/EvoScientist/channels/wechat/serve.py +++ b/EvoScientist/channels/wechat/serve.py @@ -30,9 +30,9 @@ Options: import argparse import logging -from .channel import WeChatChannel, WeComConfig, WeChatMPConfig from ..bus import MessageBus from ..standalone import run_standalone +from .channel import WeChatChannel, WeChatMPConfig, WeComConfig logging.basicConfig( level=logging.DEBUG, diff --git a/EvoScientist/channels/wechat/verify_server.py b/EvoScientist/channels/wechat/verify_server.py index 8e19c10..f8e8572 100644 --- a/EvoScientist/channels/wechat/verify_server.py +++ b/EvoScientist/channels/wechat/verify_server.py @@ -89,7 +89,7 @@ class VerifyServer: try: await asyncio.wait_for(self._verified.wait(), timeout=timeout) return True - except asyncio.TimeoutError: + except TimeoutError: return False @property diff --git a/EvoScientist/cli/__init__.py b/EvoScientist/cli/__init__.py index c7b651a..9a355db 100644 --- a/EvoScientist/cli/__init__.py +++ b/EvoScientist/cli/__init__.py @@ -2,27 +2,26 @@ # Backward-compat re-exports (tests import these from EvoScientist.cli) from ..stream.state import ( # noqa: F401 - SubAgentState, StreamState, - _parse_todo_items, + SubAgentState, _build_todo_stats, + _parse_todo_items, ) -from .channel import _channels_is_running, _channels_stop # noqa: F401 -from .agent import _deduplicate_run_name # noqa: F401 - -from ._app import app # noqa: F401 from . import commands # noqa: F401 — registers @app.command decorators +from ._app import app +from ._constants import WELCOME_SLOGANS # noqa: F401 +from .agent import _deduplicate_run_name # noqa: F401 +from .channel import _channels_is_running, _channels_stop # noqa: F401 # UI runtime re-exports (merged from former tui/ package) from .tui_runtime import ( # noqa: F401 DEFAULT_UI_BACKEND, SUPPORTED_UI_BACKENDS, + get_backend, normalize_ui_backend, resolve_ui_backend, - get_backend, run_streaming, ) -from ._constants import WELCOME_SLOGANS # noqa: F401 def main(): diff --git a/EvoScientist/cli/_constants.py b/EvoScientist/cli/_constants.py index e8d9b44..8c507a5 100644 --- a/EvoScientist/cli/_constants.py +++ b/EvoScientist/cli/_constants.py @@ -1,6 +1,6 @@ """Shared constants and utilities for CLI and TUI modules.""" -from datetime import datetime, timezone +from datetime import UTC, datetime from ..sessions import AGENT_NAME @@ -35,7 +35,7 @@ def build_metadata(workspace_dir: str | None, model: str | None) -> dict: """Build metadata dict for LangGraph checkpoint persistence.""" return { "agent_name": AGENT_NAME, - "updated_at": datetime.now(timezone.utc).isoformat(), + "updated_at": datetime.now(UTC).isoformat(), "workspace_dir": workspace_dir or "", "model": model or "", } diff --git a/EvoScientist/cli/channel.py b/EvoScientist/cli/channel.py index 7fcfdeb..59289eb 100644 --- a/EvoScientist/cli/channel.py +++ b/EvoScientist/cli/channel.py @@ -12,11 +12,11 @@ for the main thread to set a response via ``_set_channel_response()``. import asyncio import logging import queue -import time import threading +import time import uuid from dataclasses import dataclass -from typing import Any, Optional +from typing import Any from rich.panel import Panel from rich.table import Table @@ -263,11 +263,11 @@ def channel_hitl_prompt( Returns approval decisions list on approve/auto, or None on reject/timeout. """ + from ..channels.bus.events import OutboundMessage from ..channels.consumer import ( _format_approval_prompt, _parse_approval_reply, ) - from ..channels.bus.events import OutboundMessage # Check session auto-approve (set by a previous "3" reply) session_key = f"{msg.channel_type}:{msg.chat_id}" @@ -333,11 +333,11 @@ def channel_hitl_prompt( # Module-level channel state (bus mode) # --------------------------------------------------------------------------- -_manager: Optional[Any] = None # ChannelManager -_bus_loop: Optional[asyncio.AbstractEventLoop] = None -_bus_thread: Optional[threading.Thread] = None +_manager: Any | None = None # ChannelManager +_bus_loop: asyncio.AbstractEventLoop | None = None +_bus_thread: threading.Thread | None = None _cli_agent: Any = None # shared agent reference (same as CLI) -_cli_thread_id: Optional[str] = None # shared thread_id (same conversation) +_cli_thread_id: str | None = None # shared thread_id (same conversation) def _channels_is_running(channel_type: str | None = None) -> bool: @@ -498,7 +498,7 @@ async def _bus_inbound_consumer(bus, manager) -> None: while True: try: msg = await asyncio.wait_for(bus.consume_inbound(), timeout=1.0) - except asyncio.TimeoutError: + except TimeoutError: continue except asyncio.CancelledError: break diff --git a/EvoScientist/cli/commands.py b/EvoScientist/cli/commands.py index 913b506..520c9a4 100644 --- a/EvoScientist/cli/commands.py +++ b/EvoScientist/cli/commands.py @@ -7,20 +7,19 @@ import re from datetime import datetime from importlib.metadata import version as _pkg_version from pathlib import Path -from typing import Any, Optional +from typing import Any import typer # type: ignore[import-untyped] +from rich.markup import escape from rich.table import Table -from rich.markup import escape - -from ..stream.display import console from ..paths import ensure_dirs, set_workspace_root -from ._app import app, config_app, mcp_app, channel_app +from ..stream.display import console +from ._app import app, channel_app, config_app, mcp_app from ._constants import build_metadata from .agent import ( - _deduplicate_run_name, _create_session_workspace, + _deduplicate_run_name, _load_agent, _shorten_path, ) @@ -33,16 +32,15 @@ from .channel import ( channel_ask_user_prompt, channel_hitl_prompt, ) -from .tui_runtime import run_streaming +from .interactive import cmd_interactive, cmd_run from .mcp_ui import ( - _mcp_list_servers, _mcp_add_server_from_kwargs, _mcp_edit_server_fields, + _mcp_list_servers, _mcp_remove_server, _show_mcp_config, ) -from .interactive import cmd_interactive, cmd_run - +from .tui_runtime import run_streaming # ============================================================================= # Onboard command @@ -119,15 +117,15 @@ class CompactResult: """ __slots__ = ( - "status", "message", "messages_compacted", "messages_kept", - "tokens_before", + "pct_decrease", + "status", "tokens_after", + "tokens_before", "tokens_summarized", "tokens_summary", - "pct_decrease", ) def __init__( @@ -243,13 +241,14 @@ async def compact_conversation(agent: Any, thread_id: str | None) -> CompactResu "noop", "Nothing to compact — no messages in conversation." ) - from ..EvoScientist import _ensure_chat_model, _get_default_backend from deepagents.middleware.summarization import ( SummarizationEvent, SummarizationMiddleware, compute_summarization_defaults, ) + from ..EvoScientist import _ensure_chat_model, _get_default_backend + try: model = _ensure_chat_model() except Exception as exc: @@ -476,7 +475,7 @@ def serve( no_thinking: bool = typer.Option( False, "--no-thinking", help="Disable thinking relay to channels" ), - workdir: Optional[str] = typer.Option( + workdir: str | None = typer.Option( None, "--workdir", help="Override workspace directory" ), auto_approve: bool = typer.Option( @@ -499,7 +498,7 @@ def serve( nest_asyncio.apply() - from ..config import get_effective_config, apply_config_to_env + from ..config import apply_config_to_env, get_effective_config cli_overrides = {} if auto_approve: @@ -594,7 +593,7 @@ def config_callback(ctx: typer.Context): @config_app.command("list") def config_list(): """List all configuration values""" - from ..config import list_config, get_config_path + from ..config import get_config_path, list_config config_data = list_config() @@ -658,7 +657,7 @@ def config_reset( yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"), ): """Reset configuration to defaults""" - from ..config import reset_config, get_config_path + from ..config import get_config_path, reset_config config_path = get_config_path() @@ -707,7 +706,7 @@ def mcp_list(): @mcp_app.command("config") def mcp_config( - name: Optional[str] = typer.Argument(None, help="Server name (omit to show all)"), + name: str | None = typer.Argument(None, help="Server name (omit to show all)"), ): """Show detailed configuration for MCP servers @@ -730,28 +729,26 @@ def mcp_config( def mcp_add( name: str = typer.Argument(..., help="Server name"), target: str = typer.Argument(..., help="Command (stdio) or URL (http/sse)"), - args: Optional[list[str]] = typer.Argument( - None, help="Extra args for stdio command" - ), - transport: Optional[str] = typer.Option( + args: list[str] | None = typer.Argument(None, help="Extra args for stdio command"), + transport: str | None = typer.Option( None, "--transport", "-T", help="Transport type (default: auto-detect)" ), - tools: Optional[str] = typer.Option( + tools: str | None = typer.Option( None, "--tools", "-t", help="Comma-separated tool allowlist (supports wildcards: *_exa, read_*)", ), - expose_to: Optional[str] = typer.Option( + expose_to: str | None = typer.Option( None, "--expose-to", "-e", help="Comma-separated target agents" ), - header: Optional[list[str]] = typer.Option( + header: list[str] | None = typer.Option( None, "--header", "-H", help="HTTP header as Key:Value (repeatable)" ), - env: Optional[list[str]] = typer.Option( + env: list[str] | None = typer.Option( None, "--env", help="Env var as KEY=VALUE for stdio (repeatable)" ), - env_ref: Optional[list[str]] = typer.Option( + env_ref: list[str] | None = typer.Option( None, "--env-ref", help="Env var name as ${NAME} runtime ref (repeatable)" ), ): @@ -804,31 +801,27 @@ def mcp_add( @mcp_app.command("edit") def mcp_edit( name: str = typer.Argument(..., help="Server name to edit"), - transport: Optional[str] = typer.Option( + transport: str | None = typer.Option( None, "--transport", help="New transport type" ), - command: Optional[str] = typer.Option( - None, "--command", help="New command (stdio)" - ), - url: Optional[str] = typer.Option( - None, "--url", help="New URL (http/sse/websocket)" - ), - tools: Optional[str] = typer.Option( + command: str | None = typer.Option(None, "--command", help="New command (stdio)"), + url: str | None = typer.Option(None, "--url", help="New URL (http/sse/websocket)"), + tools: str | None = typer.Option( None, "--tools", "-t", help="Comma-separated tool allowlist, supports wildcards ('none' to clear)", ), - expose_to: Optional[str] = typer.Option( + expose_to: str | None = typer.Option( None, "--expose-to", "-e", help="Comma-separated target agents ('none' to clear)", ), - header: Optional[list[str]] = typer.Option( + header: list[str] | None = typer.Option( None, "--header", "-H", help="HTTP header as Key:Value (repeatable)" ), - env: Optional[list[str]] = typer.Option( + env: list[str] | None = typer.Option( None, "--env", help="Env var as KEY=VALUE for stdio (repeatable)" ), ): @@ -897,7 +890,7 @@ def _version_callback(value: bool): @app.callback(invoke_without_command=True) def _main_callback( ctx: typer.Context, - version: Optional[bool] = typer.Option( + version: bool | None = typer.Option( None, "-V", "--version", @@ -905,25 +898,25 @@ def _main_callback( is_eager=True, help="Show version and exit.", ), - mode: Optional[str] = typer.Option( + mode: str | None = typer.Option( None, "-m", "--mode", help="Workspace mode: 'daemon' (persistent, default) or 'run' (isolated per-session)", ), - name: Optional[str] = typer.Option( + name: str | None = typer.Option( None, "-n", "--name", help="Name for this run (used as directory name instead of timestamp; requires --mode run)", ), - prompt: Optional[str] = typer.Option( + prompt: str | None = typer.Option( None, "-p", "--prompt", help="Query to execute (single-shot mode)" ), - thread_id: Optional[str] = typer.Option( + thread_id: str | None = typer.Option( None, "--thread-id", help="Thread ID for conversation persistence" ), - workdir: Optional[str] = typer.Option( + workdir: str | None = typer.Option( None, "--workdir", help="Override workspace directory for this session" ), use_cwd: bool = typer.Option( @@ -942,12 +935,12 @@ def _main_callback( "--ask-user", help="Enable agent to ask clarifying questions about your research preferences", ), - auth_mode: Optional[str] = typer.Option( + auth_mode: str | None = typer.Option( None, "--auth-mode", help="Auth mode for Anthropic/OpenAI: api_key (default) or oauth (ccproxy).", ), - ui: Optional[str] = typer.Option( + ui: str | None = typer.Option( None, "--ui", help="UI backend: tui (default) or cli.", @@ -959,7 +952,7 @@ def _main_callback( return # Load and apply configuration - from ..config import get_effective_config, apply_config_to_env + from ..config import apply_config_to_env, get_effective_config # Build CLI overrides dict cli_overrides = {} @@ -1101,7 +1094,8 @@ def _main_callback( if prompt: # Single-shot mode: wrap in persistent checkpointer import asyncio - from ..sessions import get_checkpointer, generate_thread_id + + from ..sessions import generate_thread_id, get_checkpointer async def _single_shot(): async with get_checkpointer() as checkpointer: diff --git a/EvoScientist/cli/interactive.py b/EvoScientist/cli/interactive.py index bea650e..98e2b02 100644 --- a/EvoScientist/cli/interactive.py +++ b/EvoScientist/cli/interactive.py @@ -9,49 +9,55 @@ from typing import Any import typer # type: ignore[import-untyped] from prompt_toolkit import PromptSession # type: ignore[import-untyped] -from prompt_toolkit.completion import Completer, Completion # type: ignore[import-untyped] -from prompt_toolkit.history import FileHistory # type: ignore[import-untyped] -from prompt_toolkit.auto_suggest import AutoSuggestFromHistory # type: ignore[import-untyped] +from prompt_toolkit.auto_suggest import ( + AutoSuggestFromHistory, # type: ignore[import-untyped] +) +from prompt_toolkit.completion import ( # type: ignore[import-untyped] + Completer, + Completion, +) from prompt_toolkit.formatted_text import HTML # type: ignore[import-untyped] +from prompt_toolkit.history import FileHistory # type: ignore[import-untyped] from prompt_toolkit.shortcuts import CompleteStyle # type: ignore[import-untyped] from prompt_toolkit.styles import Style as PtStyle # type: ignore[import-untyped] from rich.markup import escape from rich.table import Table from rich.text import Text +import EvoScientist.cli.channel as _ch_mod + from ..sessions import ( + _format_relative_time, + delete_thread, + find_similar_threads, generate_thread_id, get_checkpointer, + get_thread_messages, + get_thread_metadata, list_threads, thread_exists, - find_similar_threads, - delete_thread, - get_thread_metadata, - get_thread_messages, - _format_relative_time, ) from ..stream.display import console -from .tui_runtime import run_streaming, resolve_ui_backend -from .tui_interactive import run_textual_interactive -from ._constants import WELCOME_SLOGANS, LOGO_LINES, LOGO_GRADIENT, build_metadata -from .agent import _shorten_path, _create_session_workspace, _load_agent +from ._constants import LOGO_GRADIENT, LOGO_LINES, WELCOME_SLOGANS, build_metadata +from .agent import _create_session_workspace, _load_agent, _shorten_path from .channel import ( ChannelMessage, + _auto_start_channel, _channels_is_running, _cmd_channel, _cmd_channel_stop, - _auto_start_channel, _message_queue, _set_channel_response, ) -import EvoScientist.cli.channel as _ch_mod from .mcp_ui import _cmd_mcp from .skills_cmd import ( - _cmd_list_skills, _cmd_install_skill, - _cmd_uninstall_skill, _cmd_install_skills, + _cmd_list_skills, + _cmd_uninstall_skill, ) +from .tui_interactive import run_textual_interactive +from .tui_runtime import resolve_ui_backend, run_streaming _channel_logger = logging.getLogger(__name__) diff --git a/EvoScientist/cli/mcp_install_cmd.py b/EvoScientist/cli/mcp_install_cmd.py index 7044af4..3d4e060 100644 --- a/EvoScientist/cli/mcp_install_cmd.py +++ b/EvoScientist/cli/mcp_install_cmd.py @@ -23,7 +23,6 @@ from ..mcp.registry import ( ) from ..stream.display import console - _PICKER_STYLE = PtStyle.from_dict( { "questionmark": "#888888", @@ -82,9 +81,7 @@ def _browse_and_select( if pre_filter_tag: pre_filter_tag = pre_filter_tag.lower() - filtered = [ - e for e in servers if pre_filter_tag in [t.lower() for t in e.tags] - ] + filtered = [e for e in servers if pre_filter_tag in [t.lower() for t in e.tags]] if not filtered: console.print( f"[yellow]No servers found with tag: {pre_filter_tag}[/yellow]" diff --git a/EvoScientist/cli/mcp_ui.py b/EvoScientist/cli/mcp_ui.py index af43f01..5bd08b9 100644 --- a/EvoScientist/cli/mcp_ui.py +++ b/EvoScientist/cli/mcp_ui.py @@ -186,6 +186,7 @@ def _show_mcp_config(name: str = "", *, show_blank_line: bool = True) -> str: def _cmd_mcp_add(args_str: str) -> None: """Handle ``/mcp add ...``.""" import shlex + from ..mcp import parse_mcp_add_args if not args_str.strip(): @@ -230,6 +231,7 @@ def _cmd_mcp_add(args_str: str) -> None: def _cmd_mcp_edit(args_str: str) -> None: """Handle ``/mcp edit --field value ...``.""" import shlex + from ..mcp import parse_mcp_edit_args if not args_str.strip(): diff --git a/EvoScientist/cli/skills_cmd.py b/EvoScientist/cli/skills_cmd.py index a2d4b50..e977fec 100644 --- a/EvoScientist/cli/skills_cmd.py +++ b/EvoScientist/cli/skills_cmd.py @@ -8,8 +8,8 @@ from .agent import _shorten_path def _cmd_list_skills() -> None: """List all available skills (user and system).""" - from ..tools.skills_manager import list_skills from ..paths import USER_SKILLS_DIR + from ..tools.skills_manager import list_skills skills = list_skills(include_system=True) diff --git a/EvoScientist/cli/tui_backends.py b/EvoScientist/cli/tui_backends.py index e5c4d9b..750b60a 100644 --- a/EvoScientist/cli/tui_backends.py +++ b/EvoScientist/cli/tui_backends.py @@ -2,8 +2,9 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Any, Callable, Protocol +from typing import Any, Protocol from ..stream.display import _run_streaming diff --git a/EvoScientist/cli/tui_interactive.py b/EvoScientist/cli/tui_interactive.py index d3466ff..f807e2c 100644 --- a/EvoScientist/cli/tui_interactive.py +++ b/EvoScientist/cli/tui_interactive.py @@ -10,21 +10,17 @@ import asyncio import logging import queue import random -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from rich.console import Group from rich.text import Text import EvoScientist.cli.channel as _ch_mod -from .channel import ( - ChannelMessage, - _channels_is_running, - _channels_running_list, - _channels_stop, - _auto_start_channel, - _message_queue, - _set_channel_response, -) + +from ..commands import CommandContext +from ..commands import manager as cmd_manager +from ..config.settings import get_config_dir from ..sessions import ( find_similar_threads, generate_thread_id, @@ -33,14 +29,20 @@ from ..sessions import ( get_thread_metadata, thread_exists, ) -from ..config.settings import get_config_dir from ..stream.events import stream_agent_events -from ..stream.state import StreamState, _INTERNAL_TOOLS +from ..stream.state import _INTERNAL_TOOLS, StreamState +from ._constants import LOGO_GRADIENT, LOGO_LINES, WELCOME_SLOGANS, build_metadata +from .channel import ( + ChannelMessage, + _auto_start_channel, + _channels_is_running, + _channels_running_list, + _channels_stop, + _message_queue, + _set_channel_response, +) from .history_suggester import HistorySuggester -from ..commands import manager as cmd_manager, CommandContext -from ._constants import LOGO_LINES, LOGO_GRADIENT, WELCOME_SLOGANS, build_metadata - _channel_logger = logging.getLogger(__name__) @@ -179,16 +181,16 @@ def run_textual_interactive( from .clipboard import copy_selection_to_clipboard, get_clipboard_text from .widgets import ( - LoadingWidget, - ThinkingWidget, - SummarizationWidget, AssistantMessage, - ToolCallWidget, + LoadingWidget, SubAgentWidget, - TodoWidget, - UserMessage, + SummarizationWidget, SystemMessage, + ThinkingWidget, + TodoWidget, + ToolCallWidget, UsageWidget, + UserMessage, ) except Exception as e: # pragma: no cover - runtime fallback path raise RuntimeError( @@ -394,11 +396,11 @@ def run_textual_interactive( _ch_mod._cli_thread_id = self._conversation_tid self._render_welcome() self._render_status() - self.append_system( - f"New session: {self._conversation_tid}", style="green" - ) + self.append_system(f"New session: {self._conversation_tid}", style="green") - async def handle_session_resume(self, thread_id: str, workspace_dir: str | None = None) -> None: + async def handle_session_resume( + self, thread_id: str, workspace_dir: str | None = None + ) -> None: if workspace_dir: self._workspace_dir = workspace_dir @@ -520,7 +522,7 @@ def run_textual_interactive( self._approval_future = asyncio.get_event_loop().create_future() try: return await asyncio.wait_for(self._approval_future, timeout=300) - except (asyncio.TimeoutError, asyncio.CancelledError): + except (TimeoutError, asyncio.CancelledError): return None finally: self._approval_future = None @@ -542,7 +544,7 @@ def run_textual_interactive( try: result = await asyncio.wait_for(self._ask_user_future, timeout=300) - except (asyncio.TimeoutError, asyncio.CancelledError): + except (TimeoutError, asyncio.CancelledError): ask_w.action_cancel() return {"status": "cancelled"} finally: @@ -564,7 +566,7 @@ def run_textual_interactive( self._picker_future = asyncio.get_event_loop().create_future() try: return await asyncio.wait_for(self._picker_future, timeout=120) - except (asyncio.TimeoutError, asyncio.CancelledError): + except (TimeoutError, asyncio.CancelledError): return None finally: self._picker_future = None @@ -592,7 +594,7 @@ def run_textual_interactive( self._browser_future = asyncio.get_event_loop().create_future() try: return await asyncio.wait_for(self._browser_future, timeout=300) - except (asyncio.TimeoutError, asyncio.CancelledError): + except (TimeoutError, asyncio.CancelledError): return None finally: self._browser_future = None @@ -619,7 +621,7 @@ def run_textual_interactive( self._mcp_browser_future = asyncio.get_event_loop().create_future() try: return await asyncio.wait_for(self._mcp_browser_future, timeout=300) - except (asyncio.TimeoutError, asyncio.CancelledError): + except (TimeoutError, asyncio.CancelledError): return None finally: self._mcp_browser_future = None @@ -1100,7 +1102,9 @@ def run_textual_interactive( except Exception: pass _prompt.disabled = False - from langgraph.types import Command # type: ignore[import-untyped] + from langgraph.types import ( + Command, # type: ignore[import-untyped] + ) _stream_input = Command(resume=result) _hitl_resuming = True @@ -1112,7 +1116,9 @@ def run_textual_interactive( # HITL: check session auto-approve first if self._hitl_auto_approve: - from langgraph.types import Command # type: ignore[import-untyped] + from langgraph.types import ( + Command, # type: ignore[import-untyped] + ) _stream_input = Command( resume={ @@ -1135,7 +1141,9 @@ def run_textual_interactive( action_reqs, ) if decisions is not None: - from langgraph.types import Command # type: ignore[import-untyped] + from langgraph.types import ( + Command, # type: ignore[import-untyped] + ) _stream_input = Command( resume={"decisions": decisions} @@ -1168,7 +1176,9 @@ def run_textual_interactive( if decided_event and decided_event.decisions is not None: if decided_event.auto_approve_session: self._hitl_auto_approve = True - from langgraph.types import Command # type: ignore[import-untyped] + from langgraph.types import ( + Command, # type: ignore[import-untyped] + ) _stream_input = Command( resume={"decisions": decided_event.decisions} @@ -1437,7 +1447,7 @@ def run_textual_interactive( return _ch_mod.channel_ask_user_prompt(ask_user_data, msg) from ..commands.channel_ui import ChannelCommandUI - + # Handle slash commands from channel if msg.content.strip().startswith("/"): ctx = CommandContext( @@ -1457,7 +1467,9 @@ def run_textual_interactive( f"[{msg.channel_type}: Executed command from {msg.sender}]", style="dim", ) - _set_channel_response(msg.msg_id, f"Command executed: {msg.content}") + _set_channel_response( + msg.msg_id, f"Command executed: {msg.content}" + ) self._busy = False self._render_status() prompt_widget.disabled = False @@ -1915,6 +1927,7 @@ def run_textual_interactive( ) -> None: """Check tool calls for media files and forward to channel.""" import os + from ..paths import resolve_virtual_path arg_key = "path" if tool_name == "write_file" else "file_path" diff --git a/EvoScientist/cli/tui_runtime.py b/EvoScientist/cli/tui_runtime.py index 9772fe7..c3a8a67 100644 --- a/EvoScientist/cli/tui_runtime.py +++ b/EvoScientist/cli/tui_runtime.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from ..stream.display import console from .tui_backends import RichStreamingBackend, StreamingTUIBackend diff --git a/EvoScientist/cli/widgets/__init__.py b/EvoScientist/cli/widgets/__init__.py index a0e3f7f..9206e47 100644 --- a/EvoScientist/cli/widgets/__init__.py +++ b/EvoScientist/cli/widgets/__init__.py @@ -1,31 +1,31 @@ """TUI widgets for EvoScientist Textual interface.""" -from .loading_widget import LoadingWidget -from .thinking_widget import ThinkingWidget -from .summarization_widget import SummarizationWidget -from .assistant_message import AssistantMessage -from .tool_call_widget import ToolCallWidget -from .subagent_widget import SubAgentWidget -from .todo_widget import TodoWidget -from .user_message import UserMessage -from .system_message import SystemMessage -from .usage_widget import UsageWidget from .approval_widget import ApprovalWidget from .ask_user_widget import AskUserWidget +from .assistant_message import AssistantMessage +from .loading_widget import LoadingWidget +from .subagent_widget import SubAgentWidget +from .summarization_widget import SummarizationWidget +from .system_message import SystemMessage +from .thinking_widget import ThinkingWidget from .thread_selector import ThreadPickerWidget +from .todo_widget import TodoWidget +from .tool_call_widget import ToolCallWidget +from .usage_widget import UsageWidget +from .user_message import UserMessage __all__ = [ - "LoadingWidget", - "ThinkingWidget", - "SummarizationWidget", - "AssistantMessage", - "ToolCallWidget", - "SubAgentWidget", - "TodoWidget", - "UserMessage", - "SystemMessage", - "UsageWidget", "ApprovalWidget", "AskUserWidget", + "AssistantMessage", + "LoadingWidget", + "SubAgentWidget", + "SummarizationWidget", + "SystemMessage", + "ThinkingWidget", "ThreadPickerWidget", + "TodoWidget", + "ToolCallWidget", + "UsageWidget", + "UserMessage", ] diff --git a/EvoScientist/cli/widgets/ask_user_widget.py b/EvoScientist/cli/widgets/ask_user_widget.py index 54ecd88..d70df61 100644 --- a/EvoScientist/cli/widgets/ask_user_widget.py +++ b/EvoScientist/cli/widgets/ask_user_widget.py @@ -137,9 +137,7 @@ class AskUserWidget(Widget): if total == 1: title = ">>> Quick check-in from EvoScientist <<<" else: - title = ">>> Question 1/{} — Quick check-in from EvoScientist <<<".format( - total - ) + title = f">>> Question 1/{total} — Quick check-in from EvoScientist <<<" self._title_w = Static(title, classes="ask-title") yield self._title_w diff --git a/EvoScientist/cli/widgets/mcp_browser.py b/EvoScientist/cli/widgets/mcp_browser.py index 19e4410..dbea943 100644 --- a/EvoScientist/cli/widgets/mcp_browser.py +++ b/EvoScientist/cli/widgets/mcp_browser.py @@ -13,7 +13,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, ClassVar from rich.text import Text - from textual.binding import Binding, BindingType from textual.containers import Container from textual.message import Message @@ -163,7 +162,9 @@ class MCPBrowserWidget(Widget): if self._title_widget: self._title_widget.update("Filter by tag:") if self._help_widget: - self._help_widget.update("\u2191/\u2193 navigate \u00b7 Enter select \u00b7 Esc cancel") + self._help_widget.update( + "\u2191/\u2193 navigate \u00b7 Enter select \u00b7 Esc cancel" + ) for i, widget in enumerate(self._row_widgets): if i < len(self._tag_items): @@ -303,9 +304,7 @@ class MCPBrowserWidget(Widget): self._server_items = list(self._servers) else: self._server_items = [ - s - for s in self._servers - if tag in [t.lower() for t in s.tags] + s for s in self._servers if tag in [t.lower() for t in s.tags] ] self._phase = 2 self._selected = 0 diff --git a/EvoScientist/cli/widgets/skill_browser.py b/EvoScientist/cli/widgets/skill_browser.py index 4d3f539..1f59a14 100644 --- a/EvoScientist/cli/widgets/skill_browser.py +++ b/EvoScientist/cli/widgets/skill_browser.py @@ -13,7 +13,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, ClassVar from rich.text import Text - from textual.binding import Binding, BindingType from textual.containers import Container from textual.message import Message diff --git a/EvoScientist/cli/widgets/subagent_widget.py b/EvoScientist/cli/widgets/subagent_widget.py index 0a83898..393085e 100644 --- a/EvoScientist/cli/widgets/subagent_widget.py +++ b/EvoScientist/cli/widgets/subagent_widget.py @@ -3,7 +3,6 @@ from __future__ import annotations from rich.text import Text - from textual.containers import Vertical from textual.widgets import Static diff --git a/EvoScientist/cli/widgets/summarization_widget.py b/EvoScientist/cli/widgets/summarization_widget.py index c5045c6..03cf97c 100644 --- a/EvoScientist/cli/widgets/summarization_widget.py +++ b/EvoScientist/cli/widgets/summarization_widget.py @@ -14,7 +14,6 @@ from __future__ import annotations from rich.panel import Panel from rich.text import Text - from textual.events import Click from textual.widgets import Static diff --git a/EvoScientist/cli/widgets/system_message.py b/EvoScientist/cli/widgets/system_message.py index 368e4dc..08e9f8e 100644 --- a/EvoScientist/cli/widgets/system_message.py +++ b/EvoScientist/cli/widgets/system_message.py @@ -3,7 +3,6 @@ from __future__ import annotations from rich.text import Text - from textual.widgets import Static diff --git a/EvoScientist/cli/widgets/thinking_widget.py b/EvoScientist/cli/widgets/thinking_widget.py index 19aad88..9873f32 100644 --- a/EvoScientist/cli/widgets/thinking_widget.py +++ b/EvoScientist/cli/widgets/thinking_widget.py @@ -9,7 +9,6 @@ from __future__ import annotations from rich.panel import Panel from rich.text import Text - from textual.events import Click from textual.widgets import Static diff --git a/EvoScientist/cli/widgets/thread_selector.py b/EvoScientist/cli/widgets/thread_selector.py index a2756d0..91be915 100644 --- a/EvoScientist/cli/widgets/thread_selector.py +++ b/EvoScientist/cli/widgets/thread_selector.py @@ -10,7 +10,6 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any, ClassVar from rich.text import Text - from textual.binding import Binding, BindingType from textual.containers import Container from textual.message import Message diff --git a/EvoScientist/cli/widgets/todo_widget.py b/EvoScientist/cli/widgets/todo_widget.py index 503db51..df59067 100644 --- a/EvoScientist/cli/widgets/todo_widget.py +++ b/EvoScientist/cli/widgets/todo_widget.py @@ -8,7 +8,6 @@ from __future__ import annotations from rich.panel import Panel from rich.text import Text - from textual.widgets import Static diff --git a/EvoScientist/cli/widgets/tool_call_widget.py b/EvoScientist/cli/widgets/tool_call_widget.py index 24f1dcc..fa3b062 100644 --- a/EvoScientist/cli/widgets/tool_call_widget.py +++ b/EvoScientist/cli/widgets/tool_call_widget.py @@ -3,7 +3,6 @@ from __future__ import annotations from rich.text import Text - from textual.containers import Vertical from textual.events import Click from textual.widgets import Static diff --git a/EvoScientist/cli/widgets/usage_widget.py b/EvoScientist/cli/widgets/usage_widget.py index 1f301a4..52ff82d 100644 --- a/EvoScientist/cli/widgets/usage_widget.py +++ b/EvoScientist/cli/widgets/usage_widget.py @@ -3,7 +3,6 @@ from __future__ import annotations from rich.text import Text - from textual.widgets import Static diff --git a/EvoScientist/cli/widgets/user_message.py b/EvoScientist/cli/widgets/user_message.py index 21e3809..319cfe3 100644 --- a/EvoScientist/cli/widgets/user_message.py +++ b/EvoScientist/cli/widgets/user_message.py @@ -3,7 +3,6 @@ from __future__ import annotations from rich.text import Text - from textual.widgets import Static diff --git a/EvoScientist/commands/__init__.py b/EvoScientist/commands/__init__.py index 5494f4a..3b9eb24 100644 --- a/EvoScientist/commands/__init__.py +++ b/EvoScientist/commands/__init__.py @@ -7,11 +7,11 @@ from .manager import CommandManager, manager __all__ = [ "Argument", + "ChannelCommandUI", "Command", "CommandContext", - "CommandUI", - "manager", "CommandManager", - "ChannelCommandUI", + "CommandUI", "implementation", + "manager", ] diff --git a/EvoScientist/commands/base.py b/EvoScientist/commands/base.py index 43c5518..9937e78 100644 --- a/EvoScientist/commands/base.py +++ b/EvoScientist/commands/base.py @@ -2,7 +2,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Protocol, Type, runtime_checkable +from typing import Any, Protocol, runtime_checkable @dataclass @@ -10,7 +10,7 @@ class Argument: """Definition of a command argument.""" name: str - type: Type + type: type description: str required: bool = True @@ -61,9 +61,9 @@ class Command(ABC): """Base class for all EvoScientist slash commands.""" name: str - alias: list[str] = [] + alias: list[str] description: str - arguments: list[Argument] = [] + arguments: list[Argument] @abstractmethod async def execute(self, ctx: CommandContext, args: list[str]) -> None: diff --git a/EvoScientist/commands/implementation/__init__.py b/EvoScientist/commands/implementation/__init__.py index 577f0f4..087d1bd 100644 --- a/EvoScientist/commands/implementation/__init__.py +++ b/EvoScientist/commands/implementation/__init__.py @@ -2,4 +2,4 @@ from __future__ import annotations from . import channel, general, mcp, session, skills -__all__ = ["general", "session", "skills", "mcp", "channel"] +__all__ = ["channel", "general", "mcp", "session", "skills"] diff --git a/EvoScientist/commands/implementation/mcp.py b/EvoScientist/commands/implementation/mcp.py index acc1f3f..de55986 100644 --- a/EvoScientist/commands/implementation/mcp.py +++ b/EvoScientist/commands/implementation/mcp.py @@ -31,6 +31,7 @@ class MCPCommand(Command): await self._mcp_remove(ctx, subargs[0] if subargs else "") elif subcmd == "install": from .mcp_install import InstallMCPCommand + await InstallMCPCommand().execute(ctx, subargs) else: ctx.ui.append_system("MCP commands:", style="bold") @@ -48,7 +49,9 @@ class MCPCommand(Command): " /mcp edit ... Edit an existing server", style="dim" ) ctx.ui.append_system(" /mcp remove ... Remove a server", style="dim") - ctx.ui.append_system(" /mcp install ... Browse and install servers", style="dim") + ctx.ui.append_system( + " /mcp install ... Browse and install servers", style="dim" + ) async def _mcp_list(self, ctx: CommandContext) -> None: from ...mcp import load_mcp_config diff --git a/EvoScientist/commands/implementation/mcp_install.py b/EvoScientist/commands/implementation/mcp_install.py index d189da3..b8760f7 100644 --- a/EvoScientist/commands/implementation/mcp_install.py +++ b/EvoScientist/commands/implementation/mcp_install.py @@ -55,9 +55,7 @@ class InstallMCPCommand(Command): f"{match.name} is already configured.", style="yellow" ) return - if install_mcp_server( - match, print_fn=ctx.ui.append_system - ): + if install_mcp_server(match, print_fn=ctx.ui.append_system): ctx.ui.append_system(f"Configured: {match.name}", style="green") ctx.ui.append_system("Reload with /new to apply.", style="dim") else: @@ -92,9 +90,7 @@ class InstallMCPCommand(Command): ctx.ui.append_system("No servers selected.", style="dim") return - count = install_mcp_servers( - selected_entries, print_fn=ctx.ui.append_system - ) + count = install_mcp_servers(selected_entries, print_fn=ctx.ui.append_system) if count: ctx.ui.append_system( f"{count} server(s) configured. Reload with /new to apply.", diff --git a/EvoScientist/commands/manager.py b/EvoScientist/commands/manager.py index 10b2e0e..ae33623 100644 --- a/EvoScientist/commands/manager.py +++ b/EvoScientist/commands/manager.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging import shlex -from typing import Dict, List, Tuple from .base import Command, CommandContext @@ -13,7 +12,7 @@ class CommandManager: """Manages slash command registration and execution.""" def __init__(self) -> None: - self._commands: Dict[str, Command] = {} + self._commands: dict[str, Command] = {} def register(self, command: Command) -> None: """Register a command and its aliases.""" @@ -28,7 +27,7 @@ class CommandManager: """Lookup a command by name.""" return self._commands.get(name.lower()) - def list_commands(self) -> List[Tuple[str, str]]: + def list_commands(self) -> list[tuple[str, str]]: """List all registered command names and descriptions.""" seen = set() results = [] @@ -38,7 +37,7 @@ class CommandManager: seen.add(cmd) return results - def get_all_commands(self) -> List[Command]: + def get_all_commands(self) -> list[Command]: """Return all registered command instances.""" seen = set() results = [] diff --git a/EvoScientist/config/__init__.py b/EvoScientist/config/__init__.py index f0ce6b8..faf11b7 100644 --- a/EvoScientist/config/__init__.py +++ b/EvoScientist/config/__init__.py @@ -9,17 +9,17 @@ The onboard module is loaded lazily because it pulls in heavy dependencies """ from .settings import ( + EvoScientistConfig, + apply_config_to_env, get_config_dir, get_config_path, - EvoScientistConfig, - load_config, - save_config, - reset_config, get_config_value, - set_config_value, - list_config, get_effective_config, - apply_config_to_env, + list_config, + load_config, + reset_config, + save_config, + set_config_value, ) __all__ = [ diff --git a/EvoScientist/config/onboard.py b/EvoScientist/config/onboard.py index 3f0f018..dc170e8 100644 --- a/EvoScientist/config/onboard.py +++ b/EvoScientist/config/onboard.py @@ -14,19 +14,19 @@ from pathlib import Path import questionary from prompt_toolkit.formatted_text import FormattedText from prompt_toolkit.styles import Style -from prompt_toolkit.validation import Validator, ValidationError +from prompt_toolkit.validation import ValidationError, Validator from questionary import Choice from rich.console import Console from rich.panel import Panel from rich.text import Text +from ..llm import get_models_for_provider from .settings import ( EvoScientistConfig, + get_config_path, load_config, save_config, - get_config_path, ) -from ..llm import get_models_for_provider console = Console() @@ -779,7 +779,7 @@ def _prompt_and_validate_api_key( valid, msg = validate_fn(key_to_validate) if valid: console.print(f"\r [green]\u2713 {msg}[/green] ") - return new_key if new_key else None + return new_key or None else: console.print(f"\r [red]\u2717 {msg}[/red] ") if not new_key: @@ -795,7 +795,7 @@ def _prompt_and_validate_api_key( raise KeyboardInterrupt() return new_key if save_anyway else None - return new_key if new_key else None + return new_key or None def _prompt_ccproxy_port(config: EvoScientistConfig) -> None: @@ -822,7 +822,7 @@ def _prompt_ccproxy_port(config: EvoScientistConfig) -> None: ccproxy_port = current_port console.print(f" [dim]Using default port: {ccproxy_port}[/dim]") - setattr(config, "ccproxy_port", ccproxy_port) + config.ccproxy_port = ccproxy_port console.print( f" [green]✓ ccproxy will run on http://127.0.0.1:{ccproxy_port}[/green]" ) @@ -864,7 +864,7 @@ def _step_anthropic_auth_mode(config: EvoScientistConfig) -> str: Returns: Selected auth mode: "api_key", "oauth", or "auto". """ - from ..ccproxy_manager import is_ccproxy_available, check_ccproxy_auth + from ..ccproxy_manager import check_ccproxy_auth, is_ccproxy_available ccproxy_available = is_ccproxy_available() @@ -960,7 +960,7 @@ def _step_openai_auth_mode(config: EvoScientistConfig) -> str: Returns: Selected auth mode: "api_key" or "oauth". """ - from ..ccproxy_manager import is_ccproxy_available, check_ccproxy_auth + from ..ccproxy_manager import check_ccproxy_auth, is_ccproxy_available ccproxy_available = is_ccproxy_available() @@ -1085,7 +1085,7 @@ def _step_base_url(config: EvoScientistConfig, current_value: str | None = None) """ current = current_value if current_value is not None else "" hint = f"Current: {current}" if current else "" - default = current if current else "" + default = current or "" url = questionary.text( f"Base URL{' (' + hint + ', Enter to keep)' if hint else ''}:", @@ -1111,7 +1111,7 @@ def _step_ollama_base_url(config: EvoScientistConfig) -> tuple[str, list[str]]: Tuple of (base_url, detected_model_names). """ current = config.ollama_base_url or os.environ.get("OLLAMA_BASE_URL", "") - default = current if current else "http://localhost:11434" + default = current or "http://localhost:11434" url = questionary.text( f"Ollama base URL (Enter for {default}):", diff --git a/EvoScientist/config/settings.py b/EvoScientist/config/settings.py index f73a824..e8ed427 100644 --- a/EvoScientist/config/settings.py +++ b/EvoScientist/config/settings.py @@ -8,13 +8,12 @@ with the following priority (highest to lowest): from __future__ import annotations import os -from dataclasses import dataclass, asdict, fields -from dotenv import find_dotenv, load_dotenv +from dataclasses import asdict, dataclass, fields from pathlib import Path from typing import Any, Literal import yaml - +from dotenv import find_dotenv, load_dotenv # ============================================================================= # Configuration paths diff --git a/EvoScientist/llm/__init__.py b/EvoScientist/llm/__init__.py index 09d27a5..7b08dc0 100644 --- a/EvoScientist/llm/__init__.py +++ b/EvoScientist/llm/__init__.py @@ -5,19 +5,19 @@ with support for multiple providers. """ from .models import ( - MODELS, DEFAULT_MODEL, + MODELS, get_chat_model, + get_model_info, get_models_for_provider, list_models, - get_model_info, ) __all__ = [ - "MODELS", "DEFAULT_MODEL", + "MODELS", "get_chat_model", + "get_model_info", "get_models_for_provider", "list_models", - "get_model_info", ] diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py index ebd7be2..edc58ab 100644 --- a/EvoScientist/llm/models.py +++ b/EvoScientist/llm/models.py @@ -25,6 +25,7 @@ from langchain.chat_models import init_chat_model def _patch_anthropic_proxy_compat() -> None: try: import types as _types + from langchain_anthropic.chat_models import ChatAnthropic as _CA _orig = _CA._make_message_chunk_from_anthropic_event diff --git a/EvoScientist/mcp/__init__.py b/EvoScientist/mcp/__init__.py index 73fd482..c709330 100644 --- a/EvoScientist/mcp/__init__.py +++ b/EvoScientist/mcp/__init__.py @@ -4,17 +4,17 @@ See mcp/README.md for usage details. """ from .client import ( - load_mcp_config, - load_mcp_tools, - aload_mcp_tools, + VALID_TRANSPORTS, add_mcp_server, - edit_mcp_server, - remove_mcp_server, - parse_mcp_add_args, - parse_mcp_edit_args, + aload_mcp_tools, build_mcp_add_kwargs, build_mcp_edit_fields, - VALID_TRANSPORTS, + edit_mcp_server, + load_mcp_config, + load_mcp_tools, + parse_mcp_add_args, + parse_mcp_edit_args, + remove_mcp_server, ) from .registry import ( MCPServerEntry, @@ -27,14 +27,9 @@ from .registry import ( ) __all__ = [ - "load_mcp_config", - "load_mcp_tools", - "aload_mcp_tools", + "VALID_TRANSPORTS", "add_mcp_server", - "edit_mcp_server", - "remove_mcp_server", - "parse_mcp_add_args", - "parse_mcp_edit_args", + "aload_mcp_tools", "build_mcp_add_kwargs", "build_mcp_edit_fields", "VALID_TRANSPORTS", diff --git a/EvoScientist/mcp/client.py b/EvoScientist/mcp/client.py index 9104d0a..5680d07 100644 --- a/EvoScientist/mcp/client.py +++ b/EvoScientist/mcp/client.py @@ -18,7 +18,6 @@ from typing import Any import yaml - logger = logging.getLogger(__name__) # ============================================================================= diff --git a/EvoScientist/mcp/registry.py b/EvoScientist/mcp/registry.py index b17ff5b..a4acb58 100644 --- a/EvoScientist/mcp/registry.py +++ b/EvoScientist/mcp/registry.py @@ -17,9 +17,9 @@ import subprocess import sys import tempfile import time +from collections.abc import Callable from dataclasses import dataclass, field from pathlib import Path -from typing import Callable import yaml @@ -83,9 +83,7 @@ def install_pip_package(package: str) -> bool: for cmd in commands: try: - result = subprocess.run( - cmd, capture_output=True, text=True, timeout=120 - ) + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) if result.returncode == 0: import importlib @@ -160,7 +158,9 @@ def _scan_mcp_dir(mcp_root: Path) -> list[MCPServerEntry]: try: entries.append(parse_marketplace_yaml(yaml_file)) except Exception as exc: - logger.warning("Failed to parse marketplace MCP %s: %s", yaml_file.name, exc) + logger.warning( + "Failed to parse marketplace MCP %s: %s", yaml_file.name, exc + ) return entries @@ -242,9 +242,7 @@ def install_mcp_server( if entry.pip_package: print_fn(f" Installing {entry.pip_package}...", "dim") if not install_pip_package(entry.pip_package): - print_fn( - f" Failed: {pip_install_hint()} {entry.pip_package}", "red" - ) + print_fn(f" Failed: {pip_install_hint()} {entry.pip_package}", "red") return False # Add to mcp.yaml diff --git a/EvoScientist/middleware/memory.py b/EvoScientist/middleware/memory.py index ca40abd..1a8725f 100644 --- a/EvoScientist/middleware/memory.py +++ b/EvoScientist/middleware/memory.py @@ -31,9 +31,7 @@ import logging import re from collections.abc import Awaitable, Callable from contextvars import ContextVar -from typing import TYPE_CHECKING, Any, Annotated, NotRequired, cast - -from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Annotated, Any, NotRequired, cast from langchain.agents.middleware.types import ( AgentMiddleware, @@ -46,10 +44,11 @@ from langchain.tools import ToolRuntime from langchain_core.messages import AnyMessage, HumanMessage, filter_messages from langchain_core.runnables.config import RunnableConfig from langgraph.runtime import Runtime +from pydantic import BaseModel, Field if TYPE_CHECKING: - from langchain.chat_models import BaseChatModel from deepagents.backends.protocol import BACKEND_TYPES, BackendProtocol + from langchain.chat_models import BaseChatModel logger = logging.getLogger(__name__) @@ -235,7 +234,7 @@ def _get_thread_id(runtime: Runtime) -> str: thread_id = config.get("configurable", {}).get("thread_id") if thread_id is not None: return str(thread_id) - except Exception: # noqa: BLE001 + except Exception: logger.debug("Failed to resolve thread_id from runtime config") return "default" @@ -532,7 +531,7 @@ class EvoMemoryMiddleware(AgentMiddleware): and responses[0].error is None ): return responses[0].content.decode("utf-8") - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("Failed to read memory at %s: %s", self._memory_path, e) return "" @@ -545,7 +544,7 @@ class EvoMemoryMiddleware(AgentMiddleware): and responses[0].error is None ): return responses[0].content.decode("utf-8") - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("Failed to read memory at %s: %s", self._memory_path, e) return "" @@ -560,7 +559,7 @@ class EvoMemoryMiddleware(AgentMiddleware): result = backend.write(self._memory_path, new_content) if result and result.error: logger.warning("Failed to write memory: %s", result.error) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.warning("Exception writing memory: %s", e) async def _awrite_memory( @@ -575,7 +574,7 @@ class EvoMemoryMiddleware(AgentMiddleware): result = await backend.awrite(self._memory_path, new_content) if result and result.error: logger.warning("Failed to write memory: %s", result.error) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.warning("Exception writing memory: %s", e) # -- threshold check ----------------------------------------------------- @@ -650,7 +649,7 @@ class EvoMemoryMiddleware(AgentMiddleware): # field cleared) over bind() which only adds invocation kwargs. try: return model.model_copy(update=updates) - except Exception: # noqa: BLE001 + except Exception: # Fallback for non-Pydantic or unusual model classes return model.bind(**{k: v for k, v in updates.items() if v is not None}) @@ -667,7 +666,7 @@ class EvoMemoryMiddleware(AgentMiddleware): ) result = structured_model.invoke(prompt) return result.model_dump(exclude_none=True) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.warning("Memory extraction failed: %s", e) return {} @@ -684,7 +683,7 @@ class EvoMemoryMiddleware(AgentMiddleware): ) result = await structured_model.ainvoke(prompt) return result.model_dump(exclude_none=True) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.warning("Memory extraction failed: %s", e) return {} @@ -705,7 +704,7 @@ class EvoMemoryMiddleware(AgentMiddleware): backend = self._get_backend(state, request.runtime) memory_content = self._read_memory(backend) _CURRENT_MEMORY.set(memory_content) - except Exception as e: # noqa: BLE001 + except Exception as e: logger.debug("Failed to load memory during modify_request: %s", e) # Use placeholder when memory file doesn't exist yet if not memory_content: @@ -823,6 +822,7 @@ def create_memory_middleware( Configured EvoMemoryMiddleware instance. """ from deepagents.backends import FilesystemBackend + from ..paths import MEMORY_DIR as _DEFAULT_MEMORY_DIR if memory_dir is None: diff --git a/EvoScientist/sessions.py b/EvoScientist/sessions.py index bb953f6..1bfe9d0 100644 --- a/EvoScientist/sessions.py +++ b/EvoScientist/sessions.py @@ -9,7 +9,7 @@ Adapted from upstream ``deepagents_cli/sessions.py``. import uuid from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path import aiosqlite @@ -137,8 +137,8 @@ def _format_relative_time(iso_ts: str | None) -> str: try: dt = datetime.fromisoformat(iso_ts) if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - now = datetime.now(timezone.utc) + dt = dt.replace(tzinfo=UTC) + now = datetime.now(UTC) delta = now - dt seconds = int(delta.total_seconds()) if seconds < 60: diff --git a/EvoScientist/stream/__init__.py b/EvoScientist/stream/__init__.py index ec33528..292a1ce 100644 --- a/EvoScientist/stream/__init__.py +++ b/EvoScientist/stream/__init__.py @@ -11,33 +11,33 @@ Provides: - Display functions: Rich rendering for streaming and final output """ -from .emitter import StreamEventEmitter, StreamEvent -from .tracker import ToolCallTracker, ToolCallInfo -from .formatter import ToolResultFormatter, ContentType, FormattedResult +from .diff_format import build_edit_diff, format_diff_rich +from .display import ( + _astream_to_console, + console, + create_streaming_display, + display_final_results, + format_tool_result_compact, + formatter, +) +from .emitter import StreamEvent, StreamEventEmitter +from .events import stream_agent_events +from .formatter import ContentType, FormattedResult, ToolResultFormatter +from .state import StreamState, SubAgentState, _build_todo_stats, _parse_todo_items +from .tracker import ToolCallInfo, ToolCallTracker from .utils import ( - SUCCESS_PREFIX, FAILURE_PREFIX, - ToolStatus, + SUCCESS_PREFIX, DisplayLimits, + ToolStatus, + count_lines, + format_tool_compact, + format_tree_output, + get_status_symbol, has_args, is_success, truncate, - format_tool_compact, - format_tree_output, - count_lines, truncate_with_line_hint, - get_status_symbol, -) -from .state import SubAgentState, StreamState, _parse_todo_items, _build_todo_stats -from .events import stream_agent_events -from .diff_format import build_edit_diff, format_diff_rich -from .display import ( - console, - formatter, - format_tool_result_compact, - create_streaming_display, - display_final_results, - _astream_to_console, ) __all__ = [ diff --git a/EvoScientist/stream/diff_format.py b/EvoScientist/stream/diff_format.py index 63984fa..1af44f4 100644 --- a/EvoScientist/stream/diff_format.py +++ b/EvoScientist/stream/diff_format.py @@ -14,7 +14,6 @@ import os import re import sys - # --------------------------------------------------------------------------- # Charset detection (simplified from upstream config.py) # --------------------------------------------------------------------------- diff --git a/EvoScientist/stream/display.py b/EvoScientist/stream/display.py index 61a187d..581ee98 100644 --- a/EvoScientist/stream/display.py +++ b/EvoScientist/stream/display.py @@ -9,7 +9,8 @@ import asyncio import logging import os import sys -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from rich.console import Console, Group # type: ignore[import-untyped] from rich.live import Live # type: ignore[import-untyped] @@ -19,17 +20,17 @@ from rich.spinner import Spinner # type: ignore[import-untyped] from rich.text import Text # type: ignore[import-untyped] from ..paths import resolve_virtual_path +from .diff_format import build_edit_diff +from .events import stream_agent_events from .formatter import ToolResultFormatter from .state import ( + _INTERNAL_TOOLS, StreamState, SubAgentState, _build_todo_stats, _parse_todo_items, - _INTERNAL_TOOLS, ) from .utils import DisplayLimits, ToolStatus, format_tool_compact, is_success -from .diff_format import build_edit_diff -from .events import stream_agent_events # --------------------------------------------------------------------------- # Shared globals diff --git a/EvoScientist/stream/emitter.py b/EvoScientist/stream/emitter.py index de09ebe..3e10a3e 100644 --- a/EvoScientist/stream/emitter.py +++ b/EvoScientist/stream/emitter.py @@ -5,7 +5,7 @@ All events contain a type and associated data dict. """ from dataclasses import dataclass -from typing import Any, Dict +from typing import Any @dataclass @@ -13,7 +13,7 @@ class StreamEvent: """Unified stream event.""" type: str - data: Dict[str, Any] + data: dict[str, Any] class StreamEventEmitter: @@ -32,7 +32,7 @@ class StreamEventEmitter: return StreamEvent("text", {"type": "text", "content": content}) @staticmethod - def tool_call(name: str, args: Dict[str, Any], tool_id: str = "") -> StreamEvent: + def tool_call(name: str, args: dict[str, Any], tool_id: str = "") -> StreamEvent: """Tool call event.""" return StreamEvent( "tool_call", @@ -66,7 +66,7 @@ class StreamEventEmitter: @staticmethod def subagent_tool_call( - subagent: str, name: str, args: Dict[str, Any], tool_id: str = "" + subagent: str, name: str, args: dict[str, Any], tool_id: str = "" ) -> StreamEvent: """Tool call from inside a sub-agent.""" return StreamEvent( diff --git a/EvoScientist/stream/events.py b/EvoScientist/stream/events.py index de43cbb..bf0960a 100644 --- a/EvoScientist/stream/events.py +++ b/EvoScientist/stream/events.py @@ -8,9 +8,13 @@ import asyncio import base64 import mimetypes import os -from typing import Any, AsyncIterator +from collections.abc import AsyncIterator +from typing import Any -from langchain_core.messages import AIMessage, AIMessageChunk # type: ignore[import-untyped] +from langchain_core.messages import ( # type: ignore[import-untyped] + AIMessage, + AIMessageChunk, +) from .emitter import StreamEventEmitter from .tracker import ToolCallTracker diff --git a/EvoScientist/stream/formatter.py b/EvoScientist/stream/formatter.py index 930b721..83e3867 100644 --- a/EvoScientist/stream/formatter.py +++ b/EvoScientist/stream/formatter.py @@ -7,15 +7,16 @@ Detects content type (success/error/json/markdown/text) and formats accordingly. import json from dataclasses import dataclass from enum import Enum -from typing import Any, List +from typing import Any +from rich.markdown import Markdown from rich.markup import escape from rich.panel import Panel from rich.syntax import Syntax from rich.text import Text -from rich.markdown import Markdown -from .utils import SUCCESS_PREFIX, FAILURE_PREFIX, is_success as _is_success, truncate +from .utils import FAILURE_PREFIX, SUCCESS_PREFIX, truncate +from .utils import is_success as _is_success class ContentType(Enum): @@ -33,7 +34,7 @@ class FormattedResult: """Formatted result container.""" content_type: ContentType - elements: List[Any] # Rich renderable elements + elements: list[Any] # Rich renderable elements success: bool = True @@ -125,7 +126,7 @@ class ToolResultFormatter: md_patterns = ["```", "**", "##", "- **"] return content.startswith("#") or any(p in content for p in md_patterns) - def _format_success(self, name: str, content: str, max_length: int) -> List[Any]: + def _format_success(self, name: str, content: str, max_length: int) -> list[Any]: display = truncate(content, max_length) return [ Panel( @@ -135,7 +136,7 @@ class ToolResultFormatter: ) ] - def _format_error(self, name: str, content: str, max_length: int) -> List[Any]: + def _format_error(self, name: str, content: str, max_length: int) -> list[Any]: display = truncate(content, max_length) return [ Panel( @@ -145,7 +146,7 @@ class ToolResultFormatter: ) ] - def _format_json(self, name: str, content: str, max_length: int) -> List[Any]: + def _format_json(self, name: str, content: str, max_length: int) -> list[Any]: json_content = content if content.startswith(SUCCESS_PREFIX): json_content = self._extract_body(content) @@ -161,7 +162,7 @@ class ToolResultFormatter: except (json.JSONDecodeError, ValueError): return self._format_text(name, content, max_length) - def _format_markdown(self, name: str, content: str, max_length: int) -> List[Any]: + def _format_markdown(self, name: str, content: str, max_length: int) -> list[Any]: display = truncate(content, max_length) return [ Panel( @@ -171,7 +172,7 @@ class ToolResultFormatter: ) ] - def _format_text(self, name: str, content: str, max_length: int) -> List[Any]: + def _format_text(self, name: str, content: str, max_length: int) -> list[Any]: display = truncate(content, max_length) return [ Text(f"{name}:", style="cyan bold"), diff --git a/EvoScientist/stream/tracker.py b/EvoScientist/stream/tracker.py index f1c2435..821613c 100644 --- a/EvoScientist/stream/tracker.py +++ b/EvoScientist/stream/tracker.py @@ -6,7 +6,6 @@ Handles tool_use blocks where arguments arrive in fragments via input_json_delta import json from dataclasses import dataclass, field -from typing import Dict, Optional @dataclass @@ -15,7 +14,7 @@ class ToolCallInfo: id: str name: str - args: Dict = field(default_factory=dict) + args: dict = field(default_factory=dict) emitted: bool = False args_complete: bool = False _json_buffer: str = "" @@ -35,14 +34,14 @@ class ToolCallTracker: """ def __init__(self): - self._calls: Dict[str, ToolCallInfo] = {} - self._last_tool_id: Optional[str] = None + self._calls: dict[str, ToolCallInfo] = {} + self._last_tool_id: str | None = None def update( self, tool_id: str, - name: Optional[str] = None, - args: Optional[Dict] = None, + name: str | None = None, + args: dict | None = None, args_complete: bool = False, ) -> None: """Update tool call info (accumulative).""" @@ -96,7 +95,7 @@ class ToolCallTracker: if tool_id in self._calls: self._calls[tool_id].emitted = True - def get(self, tool_id: str) -> Optional[ToolCallInfo]: + def get(self, tool_id: str) -> ToolCallInfo | None: """Get tool call info by ID.""" return self._calls.get(tool_id) diff --git a/EvoScientist/stream/utils.py b/EvoScientist/stream/utils.py index 0b3be48..b11711a 100644 --- a/EvoScientist/stream/utils.py +++ b/EvoScientist/stream/utils.py @@ -6,9 +6,8 @@ adapted for deepagents tool names. """ import sys -from pathlib import PurePath from enum import Enum - +from pathlib import PurePath # === Status marker constants === SUCCESS_PREFIX = "[OK]" diff --git a/EvoScientist/tools/__init__.py b/EvoScientist/tools/__init__.py index b11de60..5fe21d4 100644 --- a/EvoScientist/tools/__init__.py +++ b/EvoScientist/tools/__init__.py @@ -4,13 +4,13 @@ External imports like ``from EvoScientist.tools import tavily_search`` continue to work unchanged thanks to these re-exports. """ -from .search import tavily_search, fetch_webpage_content -from .think import think_tool +from .search import fetch_webpage_content, tavily_search from .skill_manager import skill_manager +from .think import think_tool __all__ = [ - "tavily_search", "fetch_webpage_content", - "think_tool", "skill_manager", + "tavily_search", + "think_tool", ] diff --git a/EvoScientist/tools/search.py b/EvoScientist/tools/search.py index b649f2c..2c2e934 100644 --- a/EvoScientist/tools/search.py +++ b/EvoScientist/tools/search.py @@ -5,13 +5,12 @@ using Tavily for URL discovery and fetching full webpage content. """ import asyncio -from typing import Literal +from typing import Annotated, Literal import httpx from langchain_core.tools import InjectedToolArg, tool from markdownify import markdownify from tavily import TavilyClient -from typing_extensions import Annotated # Lazy initialization - only create client when needed _tavily_client = None @@ -49,7 +48,7 @@ async def fetch_webpage_content(url: str, timeout: float = 10.0) -> str: response.raise_for_status() return markdownify(response.text) except Exception as e: - return f"Error fetching content from {url}: {str(e)}" + return f"Error fetching content from {url}: {e!s}" @tool(parse_docstring=True) @@ -109,4 +108,4 @@ async def tavily_search( {"".join(result_texts)}""" except Exception as e: - return f"Search failed: {str(e)}" + return f"Search failed: {e!s}" diff --git a/EvoScientist/tools/skill_manager.py b/EvoScientist/tools/skill_manager.py index 824c8a7..e686964 100644 --- a/EvoScientist/tools/skill_manager.py +++ b/EvoScientist/tools/skill_manager.py @@ -52,11 +52,11 @@ def skill_manager( Result message """ from .skills_manager import ( + fetch_remote_skill_index, + get_skill_info, install_skill, list_skills, uninstall_skill, - get_skill_info, - fetch_remote_skill_index, ) if action == "install": diff --git a/pyproject.toml b/pyproject.toml index 323b127..50d085c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,3 +97,65 @@ testpaths = ["tests"] filterwarnings = [ "ignore::UserWarning:langchain_nvidia_ai_endpoints", ] + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +line-length = 88 +indent-width = 4 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "E", "W", # pycodestyle (Error & Warning) + "F", # Pyflakes (Logical errors) + "I", # isort (Import sorting) + "B", # flake8-bugbear (Common bugs) + "C4", # flake8-comprehensions (List/Dict perf) + "UP", # pyupgrade (Modern Python syntax) + "PT", # flake8-pytest-style (If using Pytest) + "PLE", # Pylint Error + "RUF", # Ruff specific rules + "FURB", # Refurb rules +] +ignore = [ + "E501", # Formatter takes care of that +] +fixable = ["ALL"] +unfixable = [] +allowed-confusables = ["–", "❯"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false diff --git a/tests/test_additional_channel_smoke.py b/tests/test_additional_channel_smoke.py index 3928358..c443c73 100644 --- a/tests/test_additional_channel_smoke.py +++ b/tests/test_additional_channel_smoke.py @@ -10,7 +10,6 @@ from EvoScientist.channels.imessage.channel_rpc import ( ) from EvoScientist.channels.qq.channel import QQChannel, QQConfig from EvoScientist.channels.signal.channel import SignalChannel, SignalConfig - from tests.conftest import run_async as _run diff --git a/tests/test_ask_user.py b/tests/test_ask_user.py index 7e9518e..b7b5c97 100644 --- a/tests/test_ask_user.py +++ b/tests/test_ask_user.py @@ -1,8 +1,8 @@ """Tests for the ask_user middleware, stream events, state, and UI helpers.""" -import pytest from unittest.mock import patch +import pytest # --------------------------------------------------------------------------- # Middleware data types diff --git a/tests/test_backends.py b/tests/test_backends.py index dd4116b..a488d13 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -4,12 +4,11 @@ import re from pathlib import Path from EvoScientist.backends import ( - validate_command, - convert_virtual_paths_in_command, CustomSandboxBackend, + convert_virtual_paths_in_command, + validate_command, ) - # === validate_command === diff --git a/tests/test_bus_integration.py b/tests/test_bus_integration.py index 345bc95..d7b7f03 100644 --- a/tests/test_bus_integration.py +++ b/tests/test_bus_integration.py @@ -7,12 +7,10 @@ thread to set a response via ``_set_channel_response()``. import asyncio +from EvoScientist.channels.base import Channel, OutgoingMessage from EvoScientist.channels.bus.events import InboundMessage from EvoScientist.channels.bus.message_bus import MessageBus from EvoScientist.channels.channel_manager import ChannelManager -from EvoScientist.channels.base import Channel, OutgoingMessage - - from tests.conftest import run_async as _run @@ -52,7 +50,7 @@ class FakeChannel(Channel): try: msg = await asyncio.wait_for(self._queue.get(), timeout=0.5) yield msg - except asyncio.TimeoutError: + except TimeoutError: return async def send(self, message: OutgoingMessage) -> bool: diff --git a/tests/test_ccproxy_manager.py b/tests/test_ccproxy_manager.py index 6ae0716..652abd5 100644 --- a/tests/test_ccproxy_manager.py +++ b/tests/test_ccproxy_manager.py @@ -6,18 +6,17 @@ from unittest.mock import MagicMock, patch import pytest from EvoScientist.ccproxy_manager import ( - is_ccproxy_available, check_ccproxy_auth, - is_ccproxy_running, - start_ccproxy, - stop_ccproxy, ensure_ccproxy, + is_ccproxy_available, + is_ccproxy_running, + maybe_start_ccproxy, setup_ccproxy_env, setup_codex_env, - maybe_start_ccproxy, + start_ccproxy, + stop_ccproxy, ) - # ============================================================================= # is_ccproxy_available # ============================================================================= diff --git a/tests/test_channel_comprehensive.py b/tests/test_channel_comprehensive.py index ecf0938..705628b 100644 --- a/tests/test_channel_comprehensive.py +++ b/tests/test_channel_comprehensive.py @@ -25,27 +25,27 @@ import pytest from EvoScientist.channels.base import ( Channel, ChannelError, - OutboundMessage, InboundMessage, + OutboundMessage, RawIncoming, chunk_text, ) from EvoScientist.channels.bus.events import ( InboundMessage as BusInbound, +) +from EvoScientist.channels.bus.events import ( OutboundMessage as BusOutbound, ) from EvoScientist.channels.bus.message_bus import MessageBus from EvoScientist.channels.channel_manager import ChannelManager from EvoScientist.channels.consumer import InboundConsumer +from EvoScientist.channels.formatter import convert_markdown from EvoScientist.channels.middleware import DedupCache from EvoScientist.channels.retry import RetryConfig, RetryInfo, retry_async -from EvoScientist.channels.formatter import convert_markdown - # ═══════════════════════════════════════════════════════════════════ # Helpers # ═══════════════════════════════════════════════════════════════════ - from tests.conftest import run_async as _run @@ -297,23 +297,28 @@ class TestChunkText: text = "Before.\n\n" + code + "\n\nAfter some text here." # Use a limit that forces a split inside the code block chunks = chunk_text(text, 25) - + # Verify we get multiple chunks and none are empty assert len(chunks) >= 2 assert all(c.strip() for c in chunks) - + # Check that the code block was properly re-fenced # The first chunk should open the block but not close it (if it splits mid-block) # Actually, the new implementation adds closing fences to the split part and opens the next. # Let's just check that all chunks are valid markdown and the code is preserved. - reconstructed = "".join(c for c in chunks).replace("```python\n", "").replace("\n```", "").replace("```\n", "") + reconstructed = ( + "".join(c for c in chunks) + .replace("```python\n", "") + .replace("\n```", "") + .replace("```\n", "") + ) assert "print('hello')" in reconstructed assert "print('world')" in reconstructed - + # At least one chunk should have a re-fenced code block if it split has_refence = any("```python\n" in c and c.count("```") == 2 for c in chunks) assert has_refence, "No chunks were properly re-fenced" - + # It's possible it split exactly on the fence, so we can't assert has_refence strictly without knowing the exact cut, # but we can assert that every chunk has balanced or correctly formatted fences. for c in chunks: diff --git a/tests/test_cli_run_name.py b/tests/test_cli_run_name.py index 55353b9..6e36ba4 100644 --- a/tests/test_cli_run_name.py +++ b/tests/test_cli_run_name.py @@ -2,10 +2,8 @@ from pathlib import Path - from EvoScientist.cli import _deduplicate_run_name - # ============================================================================= # _deduplicate_run_name # ============================================================================= diff --git a/tests/test_cli_tui_dispatch.py b/tests/test_cli_tui_dispatch.py index 2f08290..862bf4d 100644 --- a/tests/test_cli_tui_dispatch.py +++ b/tests/test_cli_tui_dispatch.py @@ -6,12 +6,12 @@ from EvoScientist.cli.interactive import cmd_interactive def test_cmd_interactive_dispatches_to_textual(monkeypatch): captured: dict[str, object] = {} - def _fake_resolve_ui_backend(value, *, warn_fallback=False): # noqa: ANN001 + def _fake_resolve_ui_backend(value, *, warn_fallback=False): captured["resolved_input"] = value captured["warn_fallback"] = warn_fallback return "tui" - def _fake_run_textual_interactive(**kwargs): # noqa: ANN003 + def _fake_run_textual_interactive(**kwargs): captured["kwargs"] = kwargs monkeypatch.setattr( diff --git a/tests/test_compact_command.py b/tests/test_compact_command.py index d6ab5a5..9035ff5 100644 --- a/tests/test_compact_command.py +++ b/tests/test_compact_command.py @@ -153,9 +153,10 @@ class TestCompactNegligibleSavings: def test_still_compacts_when_few_messages_but_high_tokens(self, _run): """2 messages but they account for >2% of tokens — should compact.""" - from EvoScientist.cli.commands import compact_conversation from langchain_core.messages import HumanMessage + from EvoScientist.cli.commands import compact_conversation + agent = MagicMock() msgs = [MagicMock() for _ in range(10)] snapshot = SimpleNamespace( @@ -211,9 +212,10 @@ class TestCompactSuccess: """Normal compaction flow.""" def test_successful_compaction(self, _run): - from EvoScientist.cli.commands import compact_conversation from langchain_core.messages import HumanMessage + from EvoScientist.cli.commands import compact_conversation + agent = MagicMock() msgs = [MagicMock() for _ in range(20)] snapshot = SimpleNamespace( @@ -281,9 +283,10 @@ class TestCompactSuccess: def test_offload_failure_non_fatal(self, _run): """Offload failure should not prevent compaction.""" - from EvoScientist.cli.commands import compact_conversation from langchain_core.messages import HumanMessage + from EvoScientist.cli.commands import compact_conversation + agent = MagicMock() msgs = [MagicMock() for _ in range(10)] snapshot = SimpleNamespace( diff --git a/tests/test_config.py b/tests/test_config.py index 3439aaa..7f4eab5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,19 +8,18 @@ import yaml from EvoScientist.config import ( EvoScientistConfig, + apply_config_to_env, get_config_dir, get_config_path, - load_config, - save_config, - reset_config, get_config_value, - set_config_value, - list_config, get_effective_config, - apply_config_to_env, + list_config, + load_config, + reset_config, + save_config, + set_config_value, ) - # ============================================================================= # Fixtures # ============================================================================= diff --git a/tests/test_diff_format.py b/tests/test_diff_format.py index 001d728..6e0b451 100644 --- a/tests/test_diff_format.py +++ b/tests/test_diff_format.py @@ -12,7 +12,6 @@ from EvoScientist.stream.diff_format import ( format_diff_rich, ) - # --------------------------------------------------------------------------- # _escape_markup # --------------------------------------------------------------------------- diff --git a/tests/test_dingtalk_channel.py b/tests/test_dingtalk_channel.py index 8cf3562..0d76051 100644 --- a/tests/test_dingtalk_channel.py +++ b/tests/test_dingtalk_channel.py @@ -5,10 +5,8 @@ from unittest.mock import AsyncMock, MagicMock import pytest -from EvoScientist.channels.dingtalk.channel import DingTalkChannel, DingTalkConfig from EvoScientist.channels.base import ChannelError, OutboundMessage - - +from EvoScientist.channels.dingtalk.channel import DingTalkChannel, DingTalkConfig from tests.conftest import run_async as _run diff --git a/tests/test_event_loop.py b/tests/test_event_loop.py index 72087f5..88eac24 100644 --- a/tests/test_event_loop.py +++ b/tests/test_event_loop.py @@ -3,7 +3,7 @@ import asyncio from unittest.mock import Mock, patch -from EvoScientist.stream.display import _get_event_loop, _create_event_loop +from EvoScientist.stream.display import _create_event_loop, _get_event_loop class TestCreateEventLoop: diff --git a/tests/test_feishu_channel.py b/tests/test_feishu_channel.py index 6b914c8..f322dd5 100644 --- a/tests/test_feishu_channel.py +++ b/tests/test_feishu_channel.py @@ -5,16 +5,14 @@ from unittest.mock import AsyncMock, MagicMock import pytest +from EvoScientist.channels.base import ChannelError, OutboundMessage from EvoScientist.channels.feishu.channel import ( FeishuChannel, FeishuConfig, _markdown_to_feishu_post, - _parse_inline_text, _parse_inline_elements, + _parse_inline_text, ) -from EvoScientist.channels.base import ChannelError, OutboundMessage - - from tests.conftest import run_async as _run diff --git a/tests/test_hitl.py b/tests/test_hitl.py index f56fa4f..8796964 100644 --- a/tests/test_hitl.py +++ b/tests/test_hitl.py @@ -1,12 +1,11 @@ """Tests for HITL (Human-in-the-Loop) approval mechanism.""" import asyncio -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch -from EvoScientist.stream.emitter import StreamEventEmitter, StreamEvent +from EvoScientist.stream.emitter import StreamEvent, StreamEventEmitter from EvoScientist.stream.state import StreamState - # ============================================================================= # StreamEventEmitter.interrupt() # ============================================================================= @@ -161,8 +160,8 @@ class TestResolveHitlApproval: disp._session_auto_approve = original def test_config_auto_approve(self): - from EvoScientist.stream.display import _resolve_hitl_approval import EvoScientist.stream.display as disp + from EvoScientist.stream.display import _resolve_hitl_approval original = disp._session_auto_approve try: @@ -185,8 +184,8 @@ class TestResolveHitlApproval: disp._session_auto_approve = original def test_non_execute_tool_auto_approves(self): - from EvoScientist.stream.display import _resolve_hitl_approval import EvoScientist.stream.display as disp + from EvoScientist.stream.display import _resolve_hitl_approval original = disp._session_auto_approve try: @@ -209,8 +208,8 @@ class TestResolveHitlApproval: disp._session_auto_approve = original def test_execute_with_matching_allow_list(self): - from EvoScientist.stream.display import _resolve_hitl_approval import EvoScientist.stream.display as disp + from EvoScientist.stream.display import _resolve_hitl_approval original = disp._session_auto_approve try: @@ -233,8 +232,8 @@ class TestResolveHitlApproval: disp._session_auto_approve = original def test_execute_not_in_allow_list_prompts(self): - from EvoScientist.stream.display import _resolve_hitl_approval import EvoScientist.stream.display as disp + from EvoScientist.stream.display import _resolve_hitl_approval original = disp._session_auto_approve try: @@ -310,6 +309,7 @@ class TestInterruptEventParsing: def test_interrupt_from_updates_mode(self): """__interrupt__ in updates mode yields interrupt event.""" from langchain_core.messages import AIMessageChunk + from EvoScientist.stream.events import stream_agent_events mock_agent = MagicMock() @@ -512,9 +512,9 @@ class TestConsumerHitlHelpers: class TestChannelHitlIntercept: def test_register_and_set_hitl_reply(self): from EvoScientist.cli.channel import ( + _pop_hitl_reply, _register_hitl_wait, _try_set_hitl_reply, - _pop_hitl_reply, ) event = _register_hitl_wait("telegram", "chat123") @@ -541,8 +541,8 @@ class TestChannelHitlIntercept: def test_hitl_reply_timeout(self): from EvoScientist.cli.channel import ( - _register_hitl_wait, _pop_hitl_reply, + _register_hitl_wait, ) event = _register_hitl_wait("telegram", "timeout_chat") @@ -561,8 +561,8 @@ class TestChannelHitlIntercept: class TestResolveHitlApprovalWithPromptFn: def test_prompt_fn_called_for_execute(self): - from EvoScientist.stream.display import _resolve_hitl_approval import EvoScientist.stream.display as disp + from EvoScientist.stream.display import _resolve_hitl_approval original = disp._session_auto_approve try: @@ -589,8 +589,8 @@ class TestResolveHitlApprovalWithPromptFn: disp._session_auto_approve = original def test_prompt_fn_not_called_for_auto_approve(self): - from EvoScientist.stream.display import _resolve_hitl_approval import EvoScientist.stream.display as disp + from EvoScientist.stream.display import _resolve_hitl_approval original = disp._session_auto_approve try: @@ -615,8 +615,8 @@ class TestResolveHitlApprovalWithPromptFn: disp._session_auto_approve = original def test_prompt_fn_not_called_for_non_execute(self): - from EvoScientist.stream.display import _resolve_hitl_approval import EvoScientist.stream.display as disp + from EvoScientist.stream.display import _resolve_hitl_approval original = disp._session_auto_approve try: diff --git a/tests/test_llm.py b/tests/test_llm.py index 540f8ff..1148726 100644 --- a/tests/test_llm.py +++ b/tests/test_llm.py @@ -3,16 +3,15 @@ from unittest.mock import patch from EvoScientist.llm import ( - MODELS, DEFAULT_MODEL, + MODELS, get_chat_model, + get_model_info, get_models_for_provider, list_models, - get_model_info, ) from EvoScientist.llm.models import _MODEL_ENTRIES - # ============================================================================= # Test MODELS registry # ============================================================================= diff --git a/tests/test_mcp_client.py b/tests/test_mcp_client.py index 46dfd7c..a5447ce 100644 --- a/tests/test_mcp_client.py +++ b/tests/test_mcp_client.py @@ -7,20 +7,19 @@ import pytest import yaml from EvoScientist.mcp.client import ( - _interpolate_env, - _filter_tools, - _route_tools, - _resolve_command, _build_connections, - load_mcp_config, + _filter_tools, + _interpolate_env, + _resolve_command, + _route_tools, add_mcp_server, edit_mcp_server, - remove_mcp_server, + load_mcp_config, parse_mcp_add_args, parse_mcp_edit_args, + remove_mcp_server, ) - # ---- _interpolate_env ---- @@ -48,7 +47,7 @@ class TestInterpolateEnv: # ---- load_mcp_config ---- -@pytest.fixture() +@pytest.fixture def mcp_config_file(monkeypatch, tmp_path): """Point USER_MCP_CONFIG to a temp file for isolated testing.""" cfg = tmp_path / "mcp.yaml" @@ -126,7 +125,9 @@ class TestResolveCommand: fake_exe.chmod(0o755) monkeypatch.setattr("shutil.which", lambda _: None) - monkeypatch.setattr("EvoScientist.mcp.client.sys.executable", str(bin_dir / "python")) + monkeypatch.setattr( + "EvoScientist.mcp.client.sys.executable", str(bin_dir / "python") + ) assert _resolve_command("my-mcp-tool") == str(fake_exe) @@ -474,7 +475,7 @@ class TestRouteTools: # ---- add_mcp_server / remove_mcp_server ---- -@pytest.fixture() +@pytest.fixture def user_mcp_dir(tmp_path, monkeypatch): """Redirect user MCP config to a temp directory.""" cfg_dir = tmp_path / "config" diff --git a/tests/test_memory_merge.py b/tests/test_memory_merge.py index c188686..cf20c40 100644 --- a/tests/test_memory_merge.py +++ b/tests/test_memory_merge.py @@ -2,7 +2,7 @@ import pytest -from EvoScientist.middleware.memory import _merge_memory, DEFAULT_MEMORY_TEMPLATE +from EvoScientist.middleware.memory import DEFAULT_MEMORY_TEMPLATE, _merge_memory class TestMergeMemoryBackslashSafety: diff --git a/tests/test_minimax_integration.py b/tests/test_minimax_integration.py index fe0aeaf..41371e0 100644 --- a/tests/test_minimax_integration.py +++ b/tests/test_minimax_integration.py @@ -12,7 +12,6 @@ import pytest from EvoScientist.llm import get_chat_model - pytestmark = pytest.mark.skipif( not os.environ.get("MINIMAX_API_KEY"), reason="MINIMAX_API_KEY not set", diff --git a/tests/test_onboard.py b/tests/test_onboard.py index 313fed0..df0bc83 100644 --- a/tests/test_onboard.py +++ b/tests/test_onboard.py @@ -1,20 +1,19 @@ """Tests for EvoScientist onboarding wizard.""" import subprocess -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch import pytest +from EvoScientist.config import EvoScientistConfig from EvoScientist.config.onboard import ( - IntegerValidator, - ChoiceValidator, + CONFIRM_STYLE, STEPS, WIZARD_STYLE, - CONFIRM_STYLE, + ChoiceValidator, + IntegerValidator, render_progress, ) -from EvoScientist.config import EvoScientistConfig - # ============================================================================= # Test STEPS and WIZARD_STYLE constants @@ -526,7 +525,7 @@ class TestStepSkills: def test_installs_selected_skills(self): """Test skills step installs selected skills and returns sources.""" - from EvoScientist.config.onboard import _step_skills, _RECOMMENDED_SKILLS + from EvoScientist.config.onboard import _RECOMMENDED_SKILLS, _step_skills source = _RECOMMENDED_SKILLS[0]["source"] @@ -544,7 +543,7 @@ class TestStepSkills: def test_handles_install_failure(self): """Test skills step handles installation errors gracefully.""" - from EvoScientist.config.onboard import _step_skills, _RECOMMENDED_SKILLS + from EvoScientist.config.onboard import _RECOMMENDED_SKILLS, _step_skills source = _RECOMMENDED_SKILLS[0]["source"] diff --git a/tests/test_prompts.py b/tests/test_prompts.py index fde171b..15e6182 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -1,9 +1,9 @@ """Tests for EvoScientist/prompts.py.""" from EvoScientist.prompts import ( - get_system_prompt, - EXPERIMENT_WORKFLOW, DELEGATION_STRATEGY, + EXPERIMENT_WORKFLOW, + get_system_prompt, ) diff --git a/tests/test_sessions.py b/tests/test_sessions.py index ebf4a13..6d17740 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -4,6 +4,7 @@ import json import os import tempfile import unittest +from datetime import UTC from unittest.mock import patch from EvoScientist.sessions import ( @@ -18,8 +19,6 @@ from EvoScientist.sessions import ( list_threads, thread_exists, ) - - from tests.conftest import run_async as _run @@ -53,37 +52,37 @@ class TestFormatRelativeTime(unittest.TestCase): self.assertEqual(_format_relative_time("not-a-date"), "") def test_recent(self): - from datetime import datetime, timezone + from datetime import datetime - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() result = _format_relative_time(now) self.assertIn("just now", result) def test_minutes(self): - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta - ts = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat() + ts = (datetime.now(UTC) - timedelta(minutes=5)).isoformat() result = _format_relative_time(ts) self.assertIn("min ago", result) def test_hours(self): - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta - ts = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat() + ts = (datetime.now(UTC) - timedelta(hours=2)).isoformat() result = _format_relative_time(ts) self.assertIn("hour", result) def test_days(self): - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta - ts = (datetime.now(timezone.utc) - timedelta(days=3)).isoformat() + ts = (datetime.now(UTC) - timedelta(days=3)).isoformat() result = _format_relative_time(ts) self.assertIn("day", result) def test_months(self): - from datetime import datetime, timedelta, timezone + from datetime import datetime, timedelta - ts = (datetime.now(timezone.utc) - timedelta(days=65)).isoformat() + ts = (datetime.now(UTC) - timedelta(days=65)).isoformat() result = _format_relative_time(ts) self.assertIn("month", result) diff --git a/tests/test_skills_manager.py b/tests/test_skills_manager.py index 1cb1143..553dd7b 100644 --- a/tests/test_skills_manager.py +++ b/tests/test_skills_manager.py @@ -6,19 +6,18 @@ from unittest.mock import patch import pytest from EvoScientist.tools.skills_manager import ( - _parse_skill_md, - _parse_github_url, _is_github_url, + _parse_github_url, + _parse_skill_md, _validate_skill_dir, + fetch_remote_skill_index, + get_all_tags, install_skill, list_skills, list_skills_by_tag, - get_all_tags, uninstall_skill, - fetch_remote_skill_index, ) - # ============================================================================= # Fixtures # ============================================================================= diff --git a/tests/test_slack_channel.py b/tests/test_slack_channel.py index 565a786..c168f7f 100644 --- a/tests/test_slack_channel.py +++ b/tests/test_slack_channel.py @@ -2,10 +2,8 @@ import pytest -from EvoScientist.channels.slack.channel import SlackChannel, SlackConfig from EvoScientist.channels.base import ChannelError - - +from EvoScientist.channels.slack.channel import SlackChannel, SlackConfig from tests.conftest import run_async as _run diff --git a/tests/test_stream_emitter.py b/tests/test_stream_emitter.py index a856d8f..5585260 100644 --- a/tests/test_stream_emitter.py +++ b/tests/test_stream_emitter.py @@ -1,6 +1,6 @@ """Tests for EvoScientist/stream/emitter.py.""" -from EvoScientist.stream.emitter import StreamEventEmitter, StreamEvent +from EvoScientist.stream.emitter import StreamEvent, StreamEventEmitter class TestStreamEventEmitter: diff --git a/tests/test_stream_state.py b/tests/test_stream_state.py index d2ec3cc..0fe8b14 100644 --- a/tests/test_stream_state.py +++ b/tests/test_stream_state.py @@ -1,13 +1,12 @@ """Tests for StreamState, SubAgentState, and display helpers from cli.py.""" from EvoScientist.cli import ( - SubAgentState, StreamState, - _parse_todo_items, + SubAgentState, _build_todo_stats, + _parse_todo_items, ) - # ============================================================================= # SubAgentState # ============================================================================= diff --git a/tests/test_stream_utils.py b/tests/test_stream_utils.py index 386c1ab..1ac5f67 100644 --- a/tests/test_stream_utils.py +++ b/tests/test_stream_utils.py @@ -1,16 +1,15 @@ """Tests for EvoScientist/stream/utils.py pure functions.""" from EvoScientist.stream.utils import ( - is_success, - format_tool_compact, - truncate, - has_args, - count_lines, - truncate_with_line_hint, _shorten_path, + count_lines, + format_tool_compact, + has_args, + is_success, + truncate, + truncate_with_line_hint, ) - # === is_success === diff --git a/tests/test_summarization.py b/tests/test_summarization.py index 1ec845d..4af6844 100644 --- a/tests/test_summarization.py +++ b/tests/test_summarization.py @@ -4,7 +4,6 @@ from EvoScientist.stream.emitter import StreamEventEmitter from EvoScientist.stream.events import _extract_summarization_text from EvoScientist.stream.state import StreamState - # --------------------------------------------------------------------------- # Emitter # --------------------------------------------------------------------------- diff --git a/tests/test_telegram_channel.py b/tests/test_telegram_channel.py index dfa6b1e..e30bafc 100644 --- a/tests/test_telegram_channel.py +++ b/tests/test_telegram_channel.py @@ -2,10 +2,8 @@ import pytest -from EvoScientist.channels.telegram.channel import TelegramChannel, TelegramConfig from EvoScientist.channels.base import ChannelError - - +from EvoScientist.channels.telegram.channel import TelegramChannel, TelegramConfig from tests.conftest import run_async as _run diff --git a/tests/test_thread_selector.py b/tests/test_thread_selector.py index b23fa4e..f41ed03 100644 --- a/tests/test_thread_selector.py +++ b/tests/test_thread_selector.py @@ -11,7 +11,6 @@ from EvoScientist.cli.widgets.thread_selector import ( build_row_text, ) - # --------------------------------------------------------------------------- # Sample data # --------------------------------------------------------------------------- diff --git a/tests/test_tool_error_handler.py b/tests/test_tool_error_handler.py index 9b0e8e4..4d07dd7 100644 --- a/tests/test_tool_error_handler.py +++ b/tests/test_tool_error_handler.py @@ -11,7 +11,6 @@ from EvoScientist.middleware.tool_error_handler import ( _build_error_message, ) - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/tests/test_tui_widgets.py b/tests/test_tui_widgets.py index 7594421..eb8caf0 100644 --- a/tests/test_tui_widgets.py +++ b/tests/test_tui_widgets.py @@ -9,7 +9,6 @@ from __future__ import annotations import importlib import unittest - # --------------------------------------------------------------------------- # Textual might not be installed — skip entire module if missing # --------------------------------------------------------------------------- @@ -429,14 +428,14 @@ class TestWidgetImports(unittest.TestCase): def test_all_imports(self): from EvoScientist.cli.widgets import ( - LoadingWidget, - ThinkingWidget, AssistantMessage, - ToolCallWidget, + LoadingWidget, SubAgentWidget, - TodoWidget, - UserMessage, SystemMessage, + ThinkingWidget, + TodoWidget, + ToolCallWidget, + UserMessage, ) # All should be classes @@ -484,9 +483,10 @@ class TestClipboardPaste(unittest.TestCase): with patch.dict("sys.modules", {"pyperclip": mock_pyperclip}): # Re-import to pick up the mock - from EvoScientist.cli import clipboard import importlib + from EvoScientist.cli import clipboard + importlib.reload(clipboard) result = clipboard.get_clipboard_text() @@ -499,9 +499,10 @@ class TestClipboardPaste(unittest.TestCase): from unittest.mock import patch with patch.dict("sys.modules", {"pyperclip": None}): - from EvoScientist.cli import clipboard import importlib + from EvoScientist.cli import clipboard + importlib.reload(clipboard) # Should not raise, returns None or string diff --git a/tests/test_ui_runtime.py b/tests/test_ui_runtime.py index f6961b2..31addee 100644 --- a/tests/test_ui_runtime.py +++ b/tests/test_ui_runtime.py @@ -49,7 +49,7 @@ def test_resolve_ui_backend_keeps_tui_when_available(monkeypatch): class _BrokenBackend: name: str = "tui" - def run_streaming(self, **kwargs): # noqa: ANN003, ANN201 + def run_streaming(self, **kwargs): raise RuntimeError("boom") @@ -59,7 +59,7 @@ def test_run_streaming_falls_back_to_cli_on_runtime_error(monkeypatch): ) class _RichStub: - def run_streaming(self, **kwargs): # noqa: ANN003, ANN201 + def run_streaming(self, **kwargs): return "fallback-ok" monkeypatch.setattr( diff --git a/tests/test_wechat_channel.py b/tests/test_wechat_channel.py index 9161dbe..2822328 100644 --- a/tests/test_wechat_channel.py +++ b/tests/test_wechat_channel.py @@ -7,24 +7,21 @@ import xml.etree.ElementTree as ET import pytest +from EvoScientist.channels.base import ChannelError from EvoScientist.channels.wechat.channel import ( WeChatChannel, - WeComConfig, WeChatMPConfig, + WeComConfig, _strip_markdown, ) from EvoScientist.channels.wechat.crypto import ( WeChatCrypto, - parse_xml, _pkcs7_pad, _pkcs7_unpad, + parse_xml, ) -from EvoScientist.channels.base import ChannelError - - from tests.conftest import run_async as _run - # ── Config tests ────────────────────────────────────────────────── @@ -241,12 +238,12 @@ class TestWeChatCrypto: # Skip encryption tests when no crypto backend is available _has_crypto = False try: - from Crypto.Cipher import AES as _aes # noqa: F401 + from Crypto.Cipher import AES as _aes _has_crypto = True except ImportError: try: - import pyaes as _pyaes # noqa: F401 + import pyaes as _pyaes _has_crypto = True except ImportError: