chore: add ruff lint rules and turn on formatting

This commit is contained in:
Jan Piotrowski
2026-03-19 12:06:03 +01:00
parent 793b3f32af
commit 4a3d6c0318
134 changed files with 629 additions and 588 deletions
+4 -2
View File
@@ -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 .
+14 -10
View File
@@ -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()
-1
View File
@@ -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"),
+1 -1
View File
@@ -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", "")
+5 -5
View File
@@ -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
+18 -18
View File
@@ -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:
+1 -1
View File
@@ -3,4 +3,4 @@
from .events import InboundMessage, OutboundMessage
from .message_bus import MessageBus
__all__ = ["MessageBus", "InboundMessage", "OutboundMessage"]
__all__ = ["InboundMessage", "MessageBus", "OutboundMessage"]
+2 -2
View File
@@ -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:
+6 -5
View File
@@ -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
+6 -5
View File
@@ -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)
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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__)
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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"]
+5 -5
View File
@@ -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"<br\s*/?>", "\n", text, flags=re.I)
text = re.sub(r"<p[^>]*>", "\n", text, flags=re.I)
text = re.sub(r"</p>", "\n", text, flags=re.I)
text = re.sub(r"<br\s*/?>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"<p[^>]*>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"</p>", "\n", text, flags=re.IGNORECASE)
text = re.sub(r"<[^>]+>", "", text)
return html.unescape(text).strip()
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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,
+2 -3
View File
@@ -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)
+12 -12
View File
@@ -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",
]
@@ -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__)
+5 -4
View File
@@ -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))
+1 -1
View File
@@ -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__)
+4 -4
View File
@@ -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)
+1 -1
View File
@@ -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):
-1
View File
@@ -15,7 +15,6 @@ from typing import Any, Protocol, runtime_checkable
from .capabilities import ChannelCapabilities
# ── Channel metadata ─────────────────────────────────────────────────
+1 -1
View File
@@ -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"]
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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"]
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+4 -4
View File
@@ -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 "
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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,
+4 -4
View File
@@ -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
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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":
+7 -6
View File
@@ -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:
+1 -1
View File
@@ -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,
@@ -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
+7 -8
View File
@@ -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():
+2 -2
View File
@@ -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 "",
}
+8 -8
View File
@@ -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
+43 -49
View File
@@ -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:
+22 -16
View File
@@ -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__)
+1 -4
View File
@@ -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]"
+2
View File
@@ -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 <name> --field value ...``."""
import shlex
from ..mcp import parse_mcp_edit_args
if not args_str.strip():
+1 -1
View File
@@ -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)
+2 -1
View File
@@ -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
+49 -36
View File
@@ -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"
+2 -1
View File
@@ -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
+20 -20
View File
@@ -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",
]
+1 -3
View File
@@ -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
+4 -5
View File
@@ -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
@@ -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
@@ -3,7 +3,6 @@
from __future__ import annotations
from rich.text import Text
from textual.containers import Vertical
from textual.widgets import Static
@@ -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
@@ -3,7 +3,6 @@
from __future__ import annotations
from rich.text import Text
from textual.widgets import Static
@@ -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
@@ -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
-1
View File
@@ -8,7 +8,6 @@ from __future__ import annotations
from rich.panel import Panel
from rich.text import Text
from textual.widgets import Static
@@ -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
-1
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
from rich.text import Text
from textual.widgets import Static
-1
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
from rich.text import Text
from textual.widgets import Static
+3 -3
View File
@@ -7,11 +7,11 @@ from .manager import CommandManager, manager
__all__ = [
"Argument",
"ChannelCommandUI",
"Command",
"CommandContext",
"CommandUI",
"manager",
"CommandManager",
"ChannelCommandUI",
"CommandUI",
"implementation",
"manager",
]
+4 -4
View File
@@ -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:
@@ -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"]
+4 -1
View File
@@ -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
@@ -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.",
+3 -4
View File
@@ -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 = []
+7 -7
View File
@@ -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__ = [
+10 -10
View File
@@ -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}):",
+2 -3
View File
@@ -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
+4 -4
View File
@@ -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",
]
+1
View File
@@ -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
+10 -15
View File
@@ -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",
-1
View File
@@ -18,7 +18,6 @@ from typing import Any
import yaml
logger = logging.getLogger(__name__)
# =============================================================================
+6 -8
View File
@@ -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
+13 -13
View File
@@ -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:
+3 -3
View File
@@ -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:
+20 -20
View File
@@ -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__ = [
-1
View File
@@ -14,7 +14,6 @@ import os
import re
import sys
# ---------------------------------------------------------------------------
# Charset detection (simplified from upstream config.py)
# ---------------------------------------------------------------------------
+5 -4
View File
@@ -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
+4 -4
View File
@@ -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(
+6 -2
View File
@@ -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
+10 -9
View File
@@ -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"),
+6 -7
View File
@@ -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)
+1 -2
View File
@@ -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]"
+4 -4
View File
@@ -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",
]
+3 -4
View File
@@ -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}"
+2 -2
View File
@@ -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":
+62
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More