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: