From 2ec8d2b42ffecec2128f1004c0a8451378057d42 Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 11 May 2026 11:13:25 -0700 Subject: [PATCH] =?UTF-8?q?chore:=20ruff=20auto-fix=20PLR6201=20=E2=80=94?= =?UTF-8?q?=20tuple=20=E2=86=92=20set=20in=20membership=20tests=20(#23937)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace with for all literal-tuple membership tests. Set lookup is O(1) vs O(n) for tuple — consistent micro-optimization across the codebase. 608 instances fixed via `ruff --fix --unsafe-fixes`, 0 remaining. 133 files, +626/-626 (net zero). --- acp_adapter/tools.py | 4 +- agent/account_usage.py | 2 +- agent/anthropic_adapter.py | 4 +- agent/auxiliary_client.py | 34 ++--- agent/context_compressor.py | 14 +- agent/credential_pool.py | 2 +- agent/error_classifier.py | 16 +-- agent/gemini_cloudcode_adapter.py | 2 +- agent/image_routing.py | 8 +- agent/memory_manager.py | 4 +- agent/model_metadata.py | 12 +- agent/moonshot_schema.py | 4 +- agent/redact.py | 2 +- agent/shell_hooks.py | 10 +- agent/skill_commands.py | 2 +- agent/transports/chat_completions.py | 4 +- batch_runner.py | 2 +- cli.py | 36 +++--- cron/jobs.py | 10 +- cron/scheduler.py | 2 +- environments/agentic_opd_env.py | 2 +- .../terminalbench_2/terminalbench2_env.py | 4 +- environments/hermes_base_env.py | 2 +- environments/tool_context.py | 2 +- gateway/config.py | 28 ++-- gateway/display_config.py | 6 +- gateway/platforms/api_server.py | 8 +- gateway/platforms/base.py | 6 +- gateway/platforms/bluebubbles.py | 2 +- gateway/platforms/dingtalk.py | 4 +- gateway/platforms/discord.py | 24 ++-- gateway/platforms/email.py | 4 +- gateway/platforms/feishu.py | 10 +- gateway/platforms/feishu_comment.py | 6 +- gateway/platforms/homeassistant.py | 4 +- gateway/platforms/matrix.py | 26 ++-- gateway/platforms/mattermost.py | 12 +- gateway/platforms/qqbot/adapter.py | 32 ++--- gateway/platforms/qqbot/chunked_upload.py | 2 +- gateway/platforms/signal.py | 6 +- gateway/platforms/slack.py | 22 ++-- gateway/platforms/telegram.py | 30 ++--- gateway/platforms/wecom.py | 8 +- gateway/platforms/weixin.py | 8 +- gateway/platforms/whatsapp.py | 8 +- gateway/platforms/yuanbao.py | 10 +- gateway/platforms/yuanbao_media.py | 4 +- gateway/platforms/yuanbao_proto.py | 2 +- gateway/run.py | 122 +++++++++--------- gateway/session.py | 8 +- gateway/status.py | 2 +- hermes_cli/auth.py | 18 +-- hermes_cli/auth_commands.py | 2 +- hermes_cli/backup.py | 4 +- hermes_cli/checkpoints.py | 2 +- hermes_cli/claw.py | 2 +- hermes_cli/codex_models.py | 4 +- hermes_cli/config.py | 22 ++-- hermes_cli/copilot_auth.py | 2 +- hermes_cli/curator.py | 4 +- hermes_cli/curses_ui.py | 24 ++-- hermes_cli/dingtalk_auth.py | 2 +- hermes_cli/doctor.py | 10 +- hermes_cli/fallback_cmd.py | 6 +- hermes_cli/gateway.py | 10 +- hermes_cli/goals.py | 6 +- hermes_cli/hooks.py | 6 +- hermes_cli/kanban.py | 18 +-- hermes_cli/kanban_db.py | 2 +- hermes_cli/kanban_diagnostics.py | 12 +- hermes_cli/main.py | 62 ++++----- hermes_cli/mcp_config.py | 8 +- hermes_cli/model_switch.py | 4 +- hermes_cli/models.py | 6 +- hermes_cli/plugins.py | 12 +- hermes_cli/plugins_cmd.py | 18 +-- hermes_cli/profiles.py | 4 +- hermes_cli/pty_bridge.py | 4 +- hermes_cli/runtime_provider.py | 12 +- hermes_cli/setup.py | 12 +- hermes_cli/skills_hub.py | 10 +- hermes_cli/status.py | 2 +- hermes_cli/stdio.py | 2 +- hermes_cli/tools_config.py | 16 +-- hermes_cli/uninstall.py | 4 +- hermes_cli/web_server.py | 20 +-- hermes_cli/webhook.py | 6 +- hermes_state.py | 6 +- mcp_serve.py | 8 +- model_tools.py | 2 +- rl_cli.py | 2 +- run_agent.py | 64 ++++----- scripts/build_skills_index.py | 2 +- scripts/profile-tui.py | 2 +- tools/approval.py | 14 +- tools/browser_providers/browser_use.py | 2 +- tools/browser_providers/browserbase.py | 2 +- tools/browser_providers/firecrawl.py | 2 +- tools/browser_supervisor.py | 6 +- tools/browser_tool.py | 4 +- tools/checkpoint_manager.py | 2 +- tools/code_execution_tool.py | 2 +- tools/computer_use/cua_backend.py | 2 +- tools/computer_use/tool.py | 8 +- tools/delegate_tool.py | 8 +- tools/environments/daytona.py | 4 +- tools/environments/vercel_sandbox.py | 4 +- tools/file_operations.py | 4 +- tools/file_tools.py | 2 +- tools/image_generation_tool.py | 2 +- tools/kanban_tools.py | 6 +- tools/memory_tool.py | 2 +- tools/osv_check.py | 4 +- tools/patch_parser.py | 4 +- tools/process_registry.py | 4 +- tools/rl_training_tool.py | 2 +- tools/send_message_tool.py | 20 +-- tools/skill_manager_tool.py | 2 +- tools/skills_guard.py | 2 +- tools/skills_hub.py | 4 +- tools/skills_tool.py | 4 +- tools/terminal_tool.py | 22 ++-- tools/tirith_security.py | 8 +- tools/todo_tool.py | 2 +- tools/tts_tool.py | 6 +- tools/url_safety.py | 4 +- tools/vision_tools.py | 2 +- tools/web_tools.py | 10 +- tools/yuanbao_tools.py | 2 +- tui_gateway/entry.py | 2 +- tui_gateway/server.py | 36 +++--- website/scripts/extract-skills.py | 2 +- website/scripts/generate-llms-txt.py | 2 +- 133 files changed, 626 insertions(+), 626 deletions(-) diff --git a/acp_adapter/tools.py b/acp_adapter/tools.py index e7e53a6277b..31ae943a056 100644 --- a/acp_adapter/tools.py +++ b/acp_adapter/tools.py @@ -769,8 +769,8 @@ def _build_patch_mode_content(patch_text: str) -> List[Any]: old_chunks: list[str] = [] new_chunks: list[str] = [] for hunk in op.hunks: - old_lines = [line.content for line in hunk.lines if line.prefix in (" ", "-")] - new_lines = [line.content for line in hunk.lines if line.prefix in (" ", "+")] + old_lines = [line.content for line in hunk.lines if line.prefix in {" ", "-"}] + new_lines = [line.content for line in hunk.lines if line.prefix in {" ", "+"}] if old_lines or new_lines: old_chunks.append("\n".join(old_lines)) new_chunks.append("\n".join(new_lines)) diff --git a/agent/account_usage.py b/agent/account_usage.py index 0e9562dcc9e..be03646021e 100644 --- a/agent/account_usage.py +++ b/agent/account_usage.py @@ -47,7 +47,7 @@ def _title_case_slug(value: Optional[str]) -> Optional[str]: def _parse_dt(value: Any) -> Optional[datetime]: - if value in (None, ""): + if value in {None, ""}: return None if isinstance(value, (int, float)): return datetime.fromtimestamp(float(value), tz=timezone.utc) diff --git a/agent/anthropic_adapter.py b/agent/anthropic_adapter.py index d9429c659f2..78444bcb54b 100644 --- a/agent/anthropic_adapter.py +++ b/agent/anthropic_adapter.py @@ -1537,7 +1537,7 @@ def convert_messages_to_anthropic( # downgraded to a spurious text block on the last assistant message. reasoning_content = m.get("reasoning_content") _already_has_thinking = any( - isinstance(b, dict) and b.get("type") in ("thinking", "redacted_thinking") + isinstance(b, dict) and b.get("type") in {"thinking", "redacted_thinking"} for b in blocks ) if isinstance(reasoning_content, str) and not _already_has_thinking: @@ -1688,7 +1688,7 @@ def convert_messages_to_anthropic( if isinstance(m["content"], list): m["content"] = [ b for b in m["content"] - if not (isinstance(b, dict) and b.get("type") in ("thinking", "redacted_thinking")) + if not (isinstance(b, dict) and b.get("type") in {"thinking", "redacted_thinking"}) ] prev_blocks = fixed[-1]["content"] curr_blocks = m["content"] diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 48b4984b4b4..7b53566a927 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -175,7 +175,7 @@ def _normalize_aux_provider(provider: Optional[str]) -> str: # Resolve to the user's actual main provider so named custom providers # and non-aggregator providers (DeepSeek, Alibaba, etc.) work correctly. main_prov = (_read_main_provider() or "").strip().lower() - if main_prov and main_prov not in ("auto", "main", ""): + if main_prov and main_prov not in {"auto", "main", ""}: normalized = main_prov else: return "custom" @@ -578,7 +578,7 @@ def _convert_content_for_responses(content: Any) -> Any: if detail: entry["detail"] = detail converted.append(entry) - elif ptype in ("input_text", "input_image"): + elif ptype in {"input_text", "input_image"}: # Already in Responses format — pass through converted.append(part) else: @@ -798,7 +798,7 @@ class _CodexCompletionsAdapter: if item_type == "message": for part in (_item_get(item, "content") or []): ptype = _item_get(part, "type") - if ptype in ("output_text", "text"): + if ptype in {"output_text", "text"}: text_parts.append(_item_get(part, "text", "")) elif item_type == "function_call": tool_calls_raw.append(SimpleNamespace( @@ -1960,7 +1960,7 @@ def _is_payment_error(exc: Exception) -> bool: err_lower = str(exc).lower() # OpenRouter and other providers include "credits" or "afford" in 402 bodies, # but sometimes wrap them in 429 or other codes. - if status in (402, 429, None): + if status in {402, 429, None}: if any(kw in err_lower for kw in ("credits", "insufficient funds", "can only afford", "billing", "payment required")): @@ -2157,7 +2157,7 @@ def _pool_cache_hint( if normalized == "auto": runtime = _normalize_main_runtime(main_runtime) normalized = _normalize_aux_provider(runtime.get("provider") or _read_main_provider()) - if normalized in ("", "auto", "custom"): + if normalized in {"", "auto", "custom"}: return "" entry = _peek_pool_entry(normalized) if entry is None: @@ -2179,7 +2179,7 @@ def _pool_error_context(exc: Exception) -> Dict[str, Any]: def _recoverable_pool_provider(resolved_provider: str, client: Any) -> Optional[str]: """Infer which provider pool can recover the current auxiliary client.""" normalized = _normalize_aux_provider(resolved_provider) - if normalized not in ("", "auto", "custom"): + if normalized not in {"", "auto", "custom"}: return normalized base = str(getattr(client, "base_url", "") or "") if base_url_host_matches(base, "chatgpt.com"): @@ -2496,7 +2496,7 @@ def _resolve_auto(main_runtime: Optional[Dict[str, Any]] = None) -> Tuple[Option main_provider = runtime_provider or _read_main_provider() main_model = runtime_model or _read_main_model() if (main_provider and main_model - and main_provider not in ("auto", "")): + and main_provider not in {"auto", ""}): resolved_provider = main_provider explicit_base_url = None explicit_api_key = None @@ -3157,7 +3157,7 @@ def resolve_provider_client( return (_to_async_client(client, final_model, is_vision=is_vision) if async_mode else (client, final_model)) - elif pconfig.auth_type in ("oauth_device_code", "oauth_external"): + elif pconfig.auth_type in {"oauth_device_code", "oauth_external"}: # OAuth providers — route through their specific try functions if provider == "nous": return resolve_provider_client("nous", model, async_mode) @@ -3266,7 +3266,7 @@ def get_available_vision_backends() -> List[str]: available: List[str] = [] # 1. Active provider — if the user configured a provider, try it first. main_provider = _read_main_provider() - if main_provider and main_provider not in ("auto", ""): + if main_provider and main_provider not in {"auto", ""}: if main_provider in _VISION_AUTO_PROVIDER_ORDER: if _strict_vision_backend_available(main_provider): available.append(main_provider) @@ -3312,7 +3312,7 @@ def resolve_vision_provider_client( if resolved_base_url: provider_for_base_override = ( - requested if requested and requested not in ("", "auto") else "custom" + requested if requested and requested not in {"", "auto"} else "custom" ) client, final_model = resolve_provider_client( provider_for_base_override, @@ -3340,7 +3340,7 @@ def resolve_vision_provider_client( # 4. Stop main_provider = _read_main_provider() main_model = _read_main_model() - if main_provider and main_provider not in ("auto", ""): + if main_provider and main_provider not in {"auto", ""}: vision_model = _PROVIDER_VISION_MODELS.get(main_provider, main_model) if main_provider == "nous": sync_client, default_model = _resolve_strict_vision_backend( @@ -4146,7 +4146,7 @@ def call_llm( # credentials were found, fail fast instead of silently routing # through OpenRouter (which causes confusing 404s). _explicit = (resolved_provider or "").strip().lower() - if _explicit and _explicit not in ("auto", "openrouter", "custom"): + if _explicit and _explicit not in {"auto", "openrouter", "custom"}: raise RuntimeError( f"Provider '{_explicit}' is set in config.yaml but no API key " f"was found. Set the {_explicit.upper()}_API_KEY environment " @@ -4276,7 +4276,7 @@ def call_llm( # ── Auth refresh retry ─────────────────────────────────────── if (_is_auth_error(first_err) - and resolved_provider not in ("auto", "", None) + and resolved_provider not in {"auto", "", None} and not client_is_nous): if _refresh_provider_credentials(resolved_provider): logger.info( @@ -4359,7 +4359,7 @@ def call_llm( # Only try alternative providers when the user didn't explicitly # configure this task's provider. Explicit provider = hard constraint; # auto (the default) = best-effort fallback chain. (#7559) - is_auto = resolved_provider in ("auto", "", None) + is_auto = resolved_provider in {"auto", "", None} if should_fallback and is_auto: if _is_payment_error(first_err): reason = "payment error" @@ -4515,7 +4515,7 @@ async def async_call_llm( ) if client is None: _explicit = (resolved_provider or "").strip().lower() - if _explicit and _explicit not in ("auto", "openrouter", "custom"): + if _explicit and _explicit not in {"auto", "openrouter", "custom"}: raise RuntimeError( f"Provider '{_explicit}' is set in config.yaml but no API key " f"was found. Set the {_explicit.upper()}_API_KEY environment " @@ -4626,7 +4626,7 @@ async def async_call_llm( # ── Auth refresh retry (mirrors sync call_llm) ─────────────── if (_is_auth_error(first_err) - and resolved_provider not in ("auto", "", None) + and resolved_provider not in {"auto", "", None} and not client_is_nous): if _refresh_provider_credentials(resolved_provider): logger.info( @@ -4688,7 +4688,7 @@ async def async_call_llm( or _is_connection_error(first_err) or _is_rate_limit_error(first_err) ) - is_auto = resolved_provider in ("auto", "", None) + is_auto = resolved_provider in {"auto", "", None} if should_fallback and is_auto: if _is_payment_error(first_err): reason = "payment error" diff --git a/agent/context_compressor.py b/agent/context_compressor.py index 466f128976b..d16236737c4 100644 --- a/agent/context_compressor.py +++ b/agent/context_compressor.py @@ -167,7 +167,7 @@ def _strip_image_parts_from_parts(parts: Any) -> Any: out.append(part) continue ptype = part.get("type") - if ptype in ("image", "image_url", "input_image"): + if ptype in {"image", "image_url", "input_image"}: had_image = True out.append({"type": "text", "text": "[screenshot removed to save context]"}) else: @@ -274,8 +274,8 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) -> mode = args.get("mode", "replace") return f"[patch] {mode} in {path} ({content_len:,} chars result)" - if tool_name in ("browser_navigate", "browser_click", "browser_snapshot", - "browser_type", "browser_scroll", "browser_vision"): + if tool_name in {"browser_navigate", "browser_click", "browser_snapshot", + "browser_type", "browser_scroll", "browser_vision"}: url = args.get("url", "") ref = args.get("ref", "") detail = f" {url}" if url else (f" ref={ref}" if ref else "") @@ -304,7 +304,7 @@ def _summarize_tool_result(tool_name: str, tool_args: str, tool_content: str) -> code_preview += "..." return f"[execute_code] `{code_preview}` ({line_count} lines output)" - if tool_name in ("skill_view", "skills_list", "skill_manage"): + if tool_name in {"skill_view", "skills_list", "skill_manage"}: name = args.get("name", "?") return f"[{tool_name}] name={name} ({content_len:,} chars)" @@ -979,13 +979,13 @@ The user has requested that this compaction PRIORITISE preserving all informatio _status = getattr(e, "status_code", None) or getattr(getattr(e, "response", None), "status_code", None) _err_str = str(e).lower() _is_model_not_found = ( - _status in (404, 503) + _status in {404, 503} or "model_not_found" in _err_str or "does not exist" in _err_str or "no available channel" in _err_str ) _is_timeout = ( - _status in (408, 429, 502, 504) + _status in {408, 429, 502, 504} or "timeout" in _err_str ) # Non-JSON / malformed-body responses from misconfigured providers @@ -1479,7 +1479,7 @@ The user has requested that this compaction PRIORITISE preserving all informatio first_tail_role = messages[compress_end].get("role", "user") if compress_end < n_messages else "user" # Pick a role that avoids consecutive same-role with both neighbors. # Priority: avoid colliding with head (already committed), then tail. - if last_head_role in ("assistant", "tool"): + if last_head_role in {"assistant", "tool"}: summary_role = "user" else: summary_role = "assistant" diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 0043c70ca29..aeda76225c8 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -149,7 +149,7 @@ class PooledCredential: } result: Dict[str, Any] = {} for field_def in fields(self): - if field_def.name in ("provider", "extra"): + if field_def.name in {"provider", "extra"}: continue value = getattr(self, field_def.name) if value is not None or field_def.name in _ALWAYS_EMIT: diff --git a/agent/error_classifier.py b/agent/error_classifier.py index 1a42a9589ee..d29a2e34ac6 100644 --- a/agent/error_classifier.py +++ b/agent/error_classifier.py @@ -83,7 +83,7 @@ class ClassifiedError: @property def is_auth(self) -> bool: - return self.reason in (FailoverReason.auth, FailoverReason.auth_permanent) + return self.reason in {FailoverReason.auth, FailoverReason.auth_permanent} @@ -688,10 +688,10 @@ def _classify_by_status( result_fn=result_fn, ) - if status_code in (500, 502): + if status_code in {500, 502}: return result_fn(FailoverReason.server_error, retryable=True) - if status_code in (503, 529): + if status_code in {503, 529}: return result_fn(FailoverReason.overloaded, retryable=True) # Other 4xx — non-retryable @@ -810,7 +810,7 @@ def _classify_400( # Responses API (and some providers) use flat body: {"message": "..."} if not err_body_msg: err_body_msg = str(body.get("message") or "").strip().lower() - is_generic = len(err_body_msg) < 30 or err_body_msg in ("error", "") + is_generic = len(err_body_msg) < 30 or err_body_msg in {"error", ""} # Absolute token/message-count thresholds are only a proxy for smaller # context windows. Large-context sessions can have many messages while # still being far below their actual token budget. @@ -841,14 +841,14 @@ def _classify_by_error_code( """Classify by structured error codes from the response body.""" code_lower = error_code.lower() - if code_lower in ("resource_exhausted", "throttled", "rate_limit_exceeded"): + if code_lower in {"resource_exhausted", "throttled", "rate_limit_exceeded"}: return result_fn( FailoverReason.rate_limit, retryable=True, should_rotate_credential=True, ) - if code_lower in ("insufficient_quota", "billing_not_active", "payment_required"): + if code_lower in {"insufficient_quota", "billing_not_active", "payment_required"}: return result_fn( FailoverReason.billing, retryable=False, @@ -856,14 +856,14 @@ def _classify_by_error_code( should_fallback=True, ) - if code_lower in ("model_not_found", "model_not_available", "invalid_model"): + if code_lower in {"model_not_found", "model_not_available", "invalid_model"}: return result_fn( FailoverReason.model_not_found, retryable=False, should_fallback=True, ) - if code_lower in ("context_length_exceeded", "max_tokens_exceeded"): + if code_lower in {"context_length_exceeded", "max_tokens_exceeded"}: return result_fn( FailoverReason.context_overflow, retryable=True, diff --git a/agent/gemini_cloudcode_adapter.py b/agent/gemini_cloudcode_adapter.py index 64c51cf9d81..5bc42e3aad7 100644 --- a/agent/gemini_cloudcode_adapter.py +++ b/agent/gemini_cloudcode_adapter.py @@ -77,7 +77,7 @@ def _coerce_content_to_text(content: Any) -> str: if p.get("type") == "text" and isinstance(p.get("text"), str): pieces.append(p["text"]) # Multimodal (image_url, etc.) — stub for now; log and skip - elif p.get("type") in ("image_url", "input_audio"): + elif p.get("type") in {"image_url", "input_audio"}: logger.debug("Dropping multimodal part (not yet supported): %s", p.get("type")) return "\n".join(pieces) return str(content) diff --git a/agent/image_routing.py b/agent/image_routing.py index 0b6687787a0..d5247ab222f 100644 --- a/agent/image_routing.py +++ b/agent/image_routing.py @@ -76,7 +76,7 @@ def _explicit_aux_vision_override(cfg: Optional[Dict[str, Any]]) -> bool: base_url = str(vision.get("base_url") or "").strip() # "auto" / "" / blank = not explicit - if provider in ("", "auto") and not model and not base_url: + if provider in {"", "auto"} and not model and not base_url: return False return True @@ -163,7 +163,7 @@ def _sniff_mime_from_bytes(raw: bytes) -> Optional[str]: if raw.startswith(b"\xff\xd8\xff"): return "image/jpeg" # GIF87a / GIF89a - if raw[:6] in (b"GIF87a", b"GIF89a"): + if raw[:6] in {b"GIF87a", b"GIF89a"}: return "image/gif" # WEBP: "RIFF" .... "WEBP" if len(raw) >= 12 and raw[:4] == b"RIFF" and raw[8:12] == b"WEBP": @@ -172,9 +172,9 @@ def _sniff_mime_from_bytes(raw: bytes) -> Optional[str]: if raw.startswith(b"BM"): return "image/bmp" # HEIC/HEIF: ftypheic / ftypheix / ftypmif1 / ftypmsf1 etc. - if len(raw) >= 12 and raw[4:8] == b"ftyp" and raw[8:12] in ( + if len(raw) >= 12 and raw[4:8] == b"ftyp" and raw[8:12] in { b"heic", b"heix", b"hevc", b"hevx", b"mif1", b"msf1", b"heim", b"heis", - ): + }: return "image/heic" return None diff --git a/agent/memory_manager.py b/agent/memory_manager.py index 1319681d3b1..7eda64fba4d 100644 --- a/agent/memory_manager.py +++ b/agent/memory_manager.py @@ -470,11 +470,11 @@ class MemoryManager: accepted = [ p for p in params - if p.kind in ( + if p.kind in { inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY, - ) + } ] if len(accepted) >= 4: return "positional" diff --git a/agent/model_metadata.py b/agent/model_metadata.py index cdca9ae5b2f..e19ef1cbdb1 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -571,7 +571,7 @@ def _extract_pricing(payload: Dict[str, Any]) -> Dict[str, Any]: pricing: Dict[str, Any] = {} for target, aliases in alias_map.items(): for alias in aliases: - if alias in normalized and normalized[alias] not in (None, ""): + if alias in normalized and normalized[alias] not in {None, ""}: pricing[target] = normalized[alias] break if pricing: @@ -1423,7 +1423,7 @@ def get_model_context_length( # (e.g. claude-opus-4.6 is 1M on Anthropic but 128K on GitHub Copilot). # If provider is generic (openrouter/custom/empty), try to infer from URL. effective_provider = provider - if not effective_provider or effective_provider in ("openrouter", "custom"): + if not effective_provider or effective_provider in {"openrouter", "custom"}: if base_url: inferred = _infer_provider_from_url(base_url) if inferred: @@ -1433,7 +1433,7 @@ def get_model_context_length( # This catches account-specific models (e.g. claude-opus-4.6-1m) that # don't exist in models.dev. For models that ARE in models.dev, this # returns the provider-enforced limit which is what users can actually use. - if effective_provider in ("copilot", "copilot-acp", "github-copilot"): + if effective_provider in {"copilot", "copilot-acp", "github-copilot"}: try: from hermes_cli.models import get_copilot_model_context ctx = get_copilot_model_context(model, api_key=api_key) @@ -1533,7 +1533,7 @@ def _count_image_tokens(msg: Dict[str, Any], cost_per_image: int) -> int: if not isinstance(part, dict): continue ptype = part.get("type") - if ptype in ("image", "image_url", "input_image"): + if ptype in {"image", "image_url", "input_image"}: count += 1 stashed = msg.get("_anthropic_content_blocks") if isinstance(msg, dict) else None if isinstance(stashed, list): @@ -1545,7 +1545,7 @@ def _count_image_tokens(msg: Dict[str, Any], cost_per_image: int) -> int: inner = content.get("content") if isinstance(inner, list): for part in inner: - if isinstance(part, dict) and part.get("type") in ("image", "image_url"): + if isinstance(part, dict) and part.get("type") in {"image", "image_url"}: count += 1 return count * cost_per_image @@ -1567,7 +1567,7 @@ def _estimate_message_chars(msg: Dict[str, Any]) -> int: cleaned = [] for part in v: if isinstance(part, dict): - if part.get("type") in ("image", "image_url", "input_image"): + if part.get("type") in {"image", "image_url", "input_image"}: cleaned.append({"type": part.get("type"), "image": "[stripped]"}) else: cleaned.append(part) diff --git a/agent/moonshot_schema.py b/agent/moonshot_schema.py index aeefd4a0cee..f22176f936e 100644 --- a/agent/moonshot_schema.py +++ b/agent/moonshot_schema.py @@ -122,7 +122,7 @@ def _repair_schema(node: Any, is_schema: bool = True) -> Any: # empty, drop it entirely. if "enum" in repaired and isinstance(repaired["enum"], list): node_type = repaired.get("type") - if node_type in ("string", "integer", "number", "boolean"): + if node_type in {"string", "integer", "number", "boolean"}: cleaned = [v for v in repaired["enum"] if v is not None and v != ""] if cleaned: @@ -135,7 +135,7 @@ def _repair_schema(node: Any, is_schema: bool = True) -> Any: def _fill_missing_type(node: Dict[str, Any]) -> Dict[str, Any]: """Infer a reasonable ``type`` if this schema node has none.""" - if "type" in node and node["type"] not in (None, ""): + if "type" in node and node["type"] not in {None, ""}: return node # Heuristic: presence of ``properties`` → object, ``items`` → array, ``enum`` diff --git a/agent/redact.py b/agent/redact.py index 1ac284cffd4..c6643304a9d 100644 --- a/agent/redact.py +++ b/agent/redact.py @@ -64,7 +64,7 @@ _SENSITIVE_BODY_KEYS = frozenset({ # cli.py) or `HERMES_REDACT_SECRETS=false` in ~/.hermes/.env. An opt-out # warning is logged at gateway and CLI startup so operators see the # downgrade — see `_log_redaction_status()` in gateway/run.py and cli.py. -_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "true").lower() in ("1", "true", "yes", "on") +_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "true").lower() in {"1", "true", "yes", "on"} # Known API key prefixes -- match the prefix + contiguous token chars _PREFIX_PATTERNS = [ diff --git a/agent/shell_hooks.py b/agent/shell_hooks.py index d45851fea6c..bad5388f88b 100644 --- a/agent/shell_hooks.py +++ b/agent/shell_hooks.py @@ -312,7 +312,7 @@ def _parse_single_entry( ) matcher = None - if matcher is not None and event not in ("pre_tool_call", "post_tool_call"): + if matcher is not None and event not in {"pre_tool_call", "post_tool_call"}: logger.warning( "hooks.%s[%d].matcher=%r will be ignored at runtime — the " "matcher field is only honored for pre_tool_call / " @@ -423,7 +423,7 @@ def _make_callback(spec: ShellHookSpec) -> Callable[..., Optional[Dict[str, Any] def _callback(**kwargs: Any) -> Optional[Dict[str, Any]]: # Matcher gate — only meaningful for tool-scoped events. - if spec.event in ("pre_tool_call", "post_tool_call"): + if spec.event in {"pre_tool_call", "post_tool_call"}: if not spec.matches_tool(kwargs.get("tool_name")): return None @@ -658,7 +658,7 @@ def _prompt_and_record( print() # keep the terminal tidy after ^C return False - if answer in ("y", "yes"): + if answer in {"y", "yes"}: _record_approval(event, command) return True @@ -752,13 +752,13 @@ def _resolve_effective_accept( if accept_hooks_arg: return True env = os.environ.get("HERMES_ACCEPT_HOOKS", "").strip().lower() - if env in ("1", "true", "yes", "on"): + if env in {"1", "true", "yes", "on"}: return True cfg_val = cfg.get("hooks_auto_accept", False) if isinstance(cfg_val, bool): return cfg_val if isinstance(cfg_val, str): - return cfg_val.strip().lower() in ("1", "true", "yes", "on") + return cfg_val.strip().lower() in {"1", "true", "yes", "on"} return False diff --git a/agent/skill_commands.py b/agent/skill_commands.py index 0276d5fc9ac..c8b7d039c46 100644 --- a/agent/skill_commands.py +++ b/agent/skill_commands.py @@ -261,7 +261,7 @@ def scan_skill_commands() -> Dict[str, Dict[str, Any]]: for scan_dir in dirs_to_scan: for skill_md in iter_skill_index_files(scan_dir, "SKILL.md"): - if any(part in ('.git', '.github', '.hub', '.archive') for part in skill_md.parts): + if any(part in {'.git', '.github', '.hub', '.archive'} for part in skill_md.parts): continue try: content = skill_md.read_text(encoding='utf-8') diff --git a/agent/transports/chat_completions.py b/agent/transports/chat_completions.py index 9b0dc32e5cc..7edb69e42c7 100644 --- a/agent/transports/chat_completions.py +++ b/agent/transports/chat_completions.py @@ -279,7 +279,7 @@ class ChatCompletionsTransport(ProviderTransport): _kimi_effort = "medium" if reasoning_config and isinstance(reasoning_config, dict): _e = (reasoning_config.get("effort") or "").strip().lower() - if _e in ("low", "medium", "high"): + if _e in {"low", "medium", "high"}: _kimi_effort = _e api_kwargs["reasoning_effort"] = _kimi_effort @@ -294,7 +294,7 @@ class ChatCompletionsTransport(ProviderTransport): _tokenhub_effort = "high" if reasoning_config and isinstance(reasoning_config, dict): _e = (reasoning_config.get("effort") or "").strip().lower() - if _e in ("low", "medium", "high"): + if _e in {"low", "medium", "high"}: _tokenhub_effort = _e api_kwargs["reasoning_effort"] = _tokenhub_effort diff --git a/batch_runner.py b/batch_runner.py index 9d6838288d4..a67037171bf 100644 --- a/batch_runner.py +++ b/batch_runner.py @@ -795,7 +795,7 @@ class BatchRunner: conversations = entry.get("conversations", []) for msg in conversations: role = msg.get("role") or msg.get("from") - if role in ("user", "human"): + if role in {"user", "human"}: prompt_text = (msg.get("content") or msg.get("value", "")).strip() break diff --git a/cli.py b/cli.py index a92d08124b8..7843882c2c4 100644 --- a/cli.py +++ b/cli.py @@ -1741,7 +1741,7 @@ def _detect_file_drop(user_input: str) -> "dict | None": or stripped.startswith("./") or stripped.startswith("../") or stripped.startswith("file://") - or (len(stripped) >= 3 and stripped[1] == ":" and stripped[2] in ("\\", "/") and stripped[0].isalpha()) + or (len(stripped) >= 3 and stripped[1] == ":" and stripped[2] in {"\\", "/"} and stripped[0].isalpha()) or stripped.startswith('"/') or stripped.startswith('"~') or stripped.startswith("'/") @@ -1750,7 +1750,7 @@ def _detect_file_drop(user_input: str) -> "dict | None": or stripped.startswith('"../') or stripped.startswith("'./") or stripped.startswith("'../") - or (len(stripped) >= 4 and stripped[0] in ("'", '"') and stripped[2] == ":" and stripped[3] in ("\\", "/") and stripped[1].isalpha()) + or (len(stripped) >= 4 and stripped[0] in {"'", '"'} and stripped[2] == ":" and stripped[3] in {"\\", "/"} and stripped[1].isalpha()) ) if not starts_like_path: return None @@ -2487,7 +2487,7 @@ class HermesCLI: _or_cfg = CLI_CONFIG.get("openrouter", {}) or {} _raw_score = _or_cfg.get("min_coding_score") self._openrouter_min_coding_score: Optional[float] = None - if _raw_score not in (None, ""): + if _raw_score not in {None, ""}: try: _f = float(_raw_score) if 0.0 <= _f <= 1.0: @@ -4663,7 +4663,7 @@ class HermesCLI: parts = command.split() subcmd = parts[1].lower() if len(parts) > 1 else "list" - if subcmd in ("list", "ls"): + if subcmd in {"list", "ls"}: snaps = list_quick_snapshots() if not snaps: print(" No state snapshots yet.") @@ -4691,7 +4691,7 @@ class HermesCLI: else: print(" No state files found to snapshot.") - elif subcmd in ("restore", "rewind"): + elif subcmd in {"restore", "rewind"}: if len(parts) < 3: print(" Usage: /snapshot restore ") # Show hint with most recent snapshot @@ -5230,7 +5230,7 @@ class HermesCLI: parts = cmd.split() subcommand = parts[1] if len(parts) > 1 else "" - if subcommand not in ("list", "disable", "enable"): + if subcommand not in {"list", "disable", "enable"}: self.show_tools() return @@ -6814,7 +6814,7 @@ class HermesCLI: # Set personality personality_name = parts[1].strip().lower() - if personality_name in ("none", "default", "neutral"): + if personality_name in {"none", "default", "neutral"}: self.system_prompt = "" self.agent = None # Force re-init if save_config_value("agent.system_prompt", ""): @@ -7222,7 +7222,7 @@ class HermesCLI: _cmd_def = _resolve_cmd(_base_word) canonical = _cmd_def.name if _cmd_def else _base_word - if canonical in ("quit", "exit"): + if canonical in {"quit", "exit"}: return False elif canonical == "help": self.show_help() @@ -8096,7 +8096,7 @@ class HermesCLI: ) return - if lower in ("clear", "stop", "done"): + if lower in {"clear", "stop", "done"}: had = mgr.has_goal() mgr.clear() if had: @@ -8186,7 +8186,7 @@ class HermesCLI: parts = [ p.get("text", "") for p in content - if isinstance(p, dict) and p.get("type") in ("text", "output_text") + if isinstance(p, dict) and p.get("type") in {"text", "output_text"} ] last_response = "\n".join(t for t in parts if t) else: @@ -8281,7 +8281,7 @@ class HermesCLI: current = bool(footer_cfg.get("enabled", False)) fields = footer_cfg.get("fields") or ["model", "context_pct", "cwd"] - if arg in ("status", "?"): + if arg in {"status", "?"}: state = "ON" if current else "OFF" _cprint( f" {_Colors.BOLD}Runtime footer:{_Colors.RESET} {state}\n" @@ -8289,9 +8289,9 @@ class HermesCLI: ) return - if arg in ("on", "enable", "true", "1"): + if arg in {"on", "enable", "true", "1"}: new_state = True - elif arg in ("off", "disable", "false", "0"): + elif arg in {"off", "disable", "false", "0"}: new_state = False elif arg == "": new_state = not current @@ -8384,7 +8384,7 @@ class HermesCLI: arg = parts[1].strip().lower() # Display toggle - if arg in ("show", "on"): + if arg in {"show", "on"}: self.show_reasoning = True if self.agent: self.agent.reasoning_callback = self._current_reasoning_callback() @@ -8392,7 +8392,7 @@ class HermesCLI: _cprint(f" {_ACCENT}✓ Reasoning display: ON (saved){_RST}") _cprint(f" {_DIM} Model thinking will be shown during and after each response.{_RST}") return - if arg in ("hide", "off"): + if arg in {"hide", "off"}: self.show_reasoning = False if self.agent: self.agent.reasoning_callback = self._current_reasoning_callback() @@ -9154,7 +9154,7 @@ class HermesCLI: if event_type == "tool.completed": self._tool_start_time = 0.0 # Print stacked scrollback line for "all" / "new" modes - if function_name and self.tool_progress_mode in ("all", "new"): + if function_name and self.tool_progress_mode in {"all", "new"}: duration = kwargs.get("duration", 0.0) is_error = kwargs.get("is_error", False) # Pop stored args from tool.started for this function @@ -10806,7 +10806,7 @@ class HermesCLI: try: from hermes_cli.profiles import get_active_profile_name profile = get_active_profile_name() - if profile not in ("default", "custom"): + if profile not in {"default", "custom"}: symbol = f"{profile} {symbol}" except Exception: pass @@ -11010,7 +11010,7 @@ class HermesCLI: # see that they're running without the safety net. try: _redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true") - if _redact_raw.lower() not in ("1", "true", "yes", "on"): + if _redact_raw.lower() not in {"1", "true", "yes", "on"}: self._console_print( "[bold red]⚠ Secret redaction is DISABLED[/] " f"(HERMES_REDACT_SECRETS={_redact_raw}). " diff --git a/cron/jobs.py b/cron/jobs.py index f9cf9fe2de0..6b3bc0e66f9 100644 --- a/cron/jobs.py +++ b/cron/jobs.py @@ -664,7 +664,7 @@ def update_job(job_id: str, updates: Dict[str, Any]) -> Optional[Dict[str, Any]] # None both mean "clear the field" (restore old behaviour). if "workdir" in updates: _wd = updates["workdir"] - if _wd in (None, "", False): + if _wd in {None, "", False}: updates["workdir"] = None else: updates["workdir"] = _normalize_workdir(_wd) @@ -811,7 +811,7 @@ def mark_job_run(job_id: str, success: bool, error: Optional[str] = None, # schedule quietly goes off. See issue #16265. if job["next_run_at"] is None: kind = job.get("schedule", {}).get("kind") - if kind in ("cron", "interval"): + if kind in {"cron", "interval"}: job["state"] = "error" if not job.get("last_error"): job["last_error"] = ( @@ -855,7 +855,7 @@ def advance_next_run(job_id: str) -> bool: for job in jobs: if job["id"] == job_id: kind = job.get("schedule", {}).get("kind") - if kind not in ("cron", "interval"): + if kind not in {"cron", "interval"}: return False now = _hermes_now().isoformat() new_next = compute_next_run(job["schedule"], now) @@ -909,7 +909,7 @@ def _get_due_jobs_locked() -> List[Dict[str, Any]]: # next_run_at unset. Without this branch, such jobs are # silently skipped forever; recompute next_run_at from the # schedule so they pick up at their next scheduled tick. - if not recovered_next and kind in ("cron", "interval"): + if not recovered_next and kind in {"cron", "interval"}: recovered_next = compute_next_run(schedule, now.isoformat()) if recovered_next: recovery_kind = kind @@ -940,7 +940,7 @@ def _get_due_jobs_locked() -> List[Dict[str, Any]]: # (gateway was down and missed the window). Fast-forward to # the next future occurrence instead of firing a stale run. grace = _compute_grace_seconds(schedule) - if kind in ("cron", "interval") and (now - next_run_dt).total_seconds() > grace: + if kind in {"cron", "interval"} and (now - next_run_dt).total_seconds() > grace: # Job is past its catch-up grace window — this is a stale missed run. # Grace scales with schedule period: daily=2h, hourly=30m, 10min=5m. new_next = compute_next_run(schedule, now.isoformat()) diff --git a/cron/scheduler.py b/cron/scheduler.py index 90683b6cc1c..7e39df578bb 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -754,7 +754,7 @@ def _run_job_script(script_path: str) -> tuple[bool, str]: # shebang: the scripts dir is trusted, but keeping the interpreter # choice explicit here keeps the allowed surface small and auditable. suffix = path.suffix.lower() - if suffix in (".sh", ".bash"): + if suffix in {".sh", ".bash"}: # Resolve bash dynamically so Windows (Git Bash) and Linux/macOS # all work. On native Windows without Git for Windows installed # shutil.which returns None — fall back to a clear error rather diff --git a/environments/agentic_opd_env.py b/environments/agentic_opd_env.py index 44311f55144..c6ed88756bf 100644 --- a/environments/agentic_opd_env.py +++ b/environments/agentic_opd_env.py @@ -264,7 +264,7 @@ def _parse_hint_result(text: str) -> tuple[int | None, str]: """Parse the judge's boxed decision and hint text.""" boxed = _BOXED_RE.findall(text) score = int(boxed[-1]) if boxed else None - if score not in (1, -1): + if score not in {1, -1}: score = None hint_matches = _HINT_RE.findall(text) hint = hint_matches[-1].strip() if hint_matches else "" diff --git a/environments/benchmarks/terminalbench_2/terminalbench2_env.py b/environments/benchmarks/terminalbench_2/terminalbench2_env.py index 0e88ac347fa..7742059768a 100644 --- a/environments/benchmarks/terminalbench_2/terminalbench2_env.py +++ b/environments/benchmarks/terminalbench_2/terminalbench2_env.py @@ -162,7 +162,7 @@ def _normalize_tar_member_parts(member_name: str) -> list: ): raise ValueError(f"Unsafe archive member path: {member_name}") - parts = [part for part in posix_path.parts if part not in ("", ".")] + parts = [part for part in posix_path.parts if part not in {"", "."}] if not parts or any(part == ".." for part in parts): raise ValueError(f"Unsafe archive member path: {member_name}") return parts @@ -561,7 +561,7 @@ class TerminalBench2EvalEnv(HermesAgentBaseEnv): # --- 5. Verify -- run test suite in the agent's sandbox --- # Skip verification if the agent produced no meaningful output only_system_and_user = all( - msg.get("role") in ("system", "user") for msg in result.messages + msg.get("role") in {"system", "user"} for msg in result.messages ) if result.turns_used == 0 or only_system_and_user: logger.warning( diff --git a/environments/hermes_base_env.py b/environments/hermes_base_env.py index ededab355f0..adefa9b7c3c 100644 --- a/environments/hermes_base_env.py +++ b/environments/hermes_base_env.py @@ -571,7 +571,7 @@ class HermesAgentBaseEnv(BaseEnv): # (e.g., API call failed on turn 1). No point spinning up a Modal sandbox # just to verify files that were never created. only_system_and_user = all( - msg.get("role") in ("system", "user") for msg in result.messages + msg.get("role") in {"system", "user"} for msg in result.messages ) if result.turns_used == 0 or only_system_and_user: logger.warning( diff --git a/environments/tool_context.py b/environments/tool_context.py index 550c5e851c1..9756dadaf7c 100644 --- a/environments/tool_context.py +++ b/environments/tool_context.py @@ -179,7 +179,7 @@ class ToolContext: # Ensure parent directory exists in the sandbox parent = str(_Path(remote_path).parent) - if parent not in (".", "/"): + if parent not in {".", "/"}: self.terminal(f"mkdir -p {parent}", timeout=10) # For small files, single command is fine diff --git a/gateway/config.py b/gateway/config.py index a8836ba0c90..16e2662e819 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -28,9 +28,9 @@ def _coerce_bool(value: Any, default: bool = True) -> bool: return default if isinstance(value, str): lowered = value.strip().lower() - if lowered in ("true", "1", "yes", "on"): + if lowered in {"true", "1", "yes", "on"}: return True - if lowered in ("false", "0", "no", "off"): + if lowered in {"false", "0", "no", "off"}: return False return default return is_truthy_value(value, default=default) @@ -799,7 +799,7 @@ def load_gateway_config() -> GatewayConfig: bridged["group_allow_admin_from"] = platform_cfg["group_allow_admin_from"] if "group_user_allowed_commands" in platform_cfg: bridged["group_user_allowed_commands"] = platform_cfg["group_user_allowed_commands"] - if plat in (Platform.DISCORD, Platform.SLACK) and "channel_skill_bindings" in platform_cfg: + if plat in {Platform.DISCORD, Platform.SLACK} and "channel_skill_bindings" in platform_cfg: bridged["channel_skill_bindings"] = platform_cfg["channel_skill_bindings"] if "channel_prompts" in platform_cfg: channel_prompts = platform_cfg["channel_prompts"] @@ -1179,7 +1179,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None: # Reply threading mode for Telegram (off/first/all) telegram_reply_mode = os.getenv("TELEGRAM_REPLY_TO_MODE", "").lower() - if telegram_reply_mode in ("off", "first", "all"): + if telegram_reply_mode in {"off", "first", "all"}: if Platform.TELEGRAM not in config.platforms: config.platforms[Platform.TELEGRAM] = PlatformConfig() config.platforms[Platform.TELEGRAM].reply_to_mode = telegram_reply_mode @@ -1220,14 +1220,14 @@ def _apply_env_overrides(config: GatewayConfig) -> None: # Reply threading mode for Discord (off/first/all) discord_reply_mode = os.getenv("DISCORD_REPLY_TO_MODE", "").lower() - if discord_reply_mode in ("off", "first", "all"): + if discord_reply_mode in {"off", "first", "all"}: if Platform.DISCORD not in config.platforms: config.platforms[Platform.DISCORD] = PlatformConfig() config.platforms[Platform.DISCORD].reply_to_mode = discord_reply_mode # WhatsApp (typically uses different auth mechanism) - whatsapp_enabled = os.getenv("WHATSAPP_ENABLED", "").lower() in ("true", "1", "yes") - whatsapp_disabled_explicitly = os.getenv("WHATSAPP_ENABLED", "").lower() in ("false", "0", "no") + whatsapp_enabled = os.getenv("WHATSAPP_ENABLED", "").lower() in {"true", "1", "yes"} + whatsapp_disabled_explicitly = os.getenv("WHATSAPP_ENABLED", "").lower() in {"false", "0", "no"} if Platform.WHATSAPP in config.platforms: # YAML config exists — respect explicit disable wa_cfg = config.platforms[Platform.WHATSAPP] @@ -1285,7 +1285,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None: config.platforms[Platform.SIGNAL].extra.update({ "http_url": signal_url, "account": signal_account, - "ignore_stories": os.getenv("SIGNAL_IGNORE_STORIES", "true").lower() in ("true", "1", "yes"), + "ignore_stories": os.getenv("SIGNAL_IGNORE_STORIES", "true").lower() in {"true", "1", "yes"}, }) signal_home = os.getenv("SIGNAL_HOME_CHANNEL") if signal_home and Platform.SIGNAL in config.platforms: @@ -1334,7 +1334,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None: matrix_password = os.getenv("MATRIX_PASSWORD", "") if matrix_password: config.platforms[Platform.MATRIX].extra["password"] = matrix_password - matrix_e2ee = os.getenv("MATRIX_ENCRYPTION", "").lower() in ("true", "1", "yes") + matrix_e2ee = os.getenv("MATRIX_ENCRYPTION", "").lower() in {"true", "1", "yes"} config.platforms[Platform.MATRIX].extra["encryption"] = matrix_e2ee matrix_device_id = os.getenv("MATRIX_DEVICE_ID", "") if matrix_device_id: @@ -1399,7 +1399,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None: ) # API Server - api_server_enabled = os.getenv("API_SERVER_ENABLED", "").lower() in ("true", "1", "yes") + api_server_enabled = os.getenv("API_SERVER_ENABLED", "").lower() in {"true", "1", "yes"} api_server_key = os.getenv("API_SERVER_KEY", "") api_server_cors_origins = os.getenv("API_SERVER_CORS_ORIGINS", "") api_server_port = os.getenv("API_SERVER_PORT") @@ -1426,7 +1426,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None: config.platforms[Platform.API_SERVER].extra["model_name"] = api_server_model_name # Webhook platform - webhook_enabled = os.getenv("WEBHOOK_ENABLED", "").lower() in ("true", "1", "yes") + webhook_enabled = os.getenv("WEBHOOK_ENABLED", "").lower() in {"true", "1", "yes"} webhook_port = os.getenv("WEBHOOK_PORT") webhook_secret = os.getenv("WEBHOOK_SECRET", "") if webhook_enabled: @@ -1442,11 +1442,11 @@ def _apply_env_overrides(config: GatewayConfig) -> None: config.platforms[Platform.WEBHOOK].extra["secret"] = webhook_secret # Microsoft Graph webhook platform - msgraph_webhook_enabled = os.getenv("MSGRAPH_WEBHOOK_ENABLED", "").lower() in ( + msgraph_webhook_enabled = os.getenv("MSGRAPH_WEBHOOK_ENABLED", "").lower() in { "true", "1", "yes", - ) + } msgraph_webhook_port = os.getenv("MSGRAPH_WEBHOOK_PORT") msgraph_webhook_client_state = os.getenv("MSGRAPH_WEBHOOK_CLIENT_STATE", "") msgraph_webhook_resources = os.getenv("MSGRAPH_WEBHOOK_ACCEPTED_RESOURCES", "") @@ -1640,7 +1640,7 @@ def _apply_env_overrides(config: GatewayConfig) -> None: "webhook_host": os.getenv("BLUEBUBBLES_WEBHOOK_HOST", "127.0.0.1"), "webhook_port": int(os.getenv("BLUEBUBBLES_WEBHOOK_PORT", "8645")), "webhook_path": os.getenv("BLUEBUBBLES_WEBHOOK_PATH", "/bluebubbles-webhook"), - "send_read_receipts": os.getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true").lower() in ("true", "1", "yes"), + "send_read_receipts": os.getenv("BLUEBUBBLES_SEND_READ_RECEIPTS", "true").lower() in {"true", "1", "yes"}, }) bluebubbles_home = os.getenv("BLUEBUBBLES_HOME_CHANNEL") if bluebubbles_home and Platform.BLUEBUBBLES in config.platforms: diff --git a/gateway/display_config.py b/gateway/display_config.py index 2d8f40f115f..eab6bebc783 100644 --- a/gateway/display_config.py +++ b/gateway/display_config.py @@ -190,13 +190,13 @@ def _normalise(setting: str, value: Any) -> Any: if value is True: return "all" return str(value).lower() - if setting in ("show_reasoning", "streaming"): + if setting in {"show_reasoning", "streaming"}: if isinstance(value, str): - return value.lower() in ("true", "1", "yes", "on") + return value.lower() in {"true", "1", "yes", "on"} return bool(value) if setting == "cleanup_progress": if isinstance(value, str): - return value.lower() in ("true", "1", "yes", "on") + return value.lower() in {"true", "1", "yes", "on"} return bool(value) if setting == "tool_preview_length": try: diff --git a/gateway/platforms/api_server.py b/gateway/platforms/api_server.py index 357ecbd4785..497adbd19c6 100644 --- a/gateway/platforms/api_server.py +++ b/gateway/platforms/api_server.py @@ -449,7 +449,7 @@ if AIOHTTP_AVAILABLE: @web.middleware async def body_limit_middleware(request, handler): """Reject overly large request bodies early based on Content-Length.""" - if request.method in ("POST", "PUT", "PATCH"): + if request.method in {"POST", "PUT", "PATCH"}: cl = request.headers.get("Content-Length") if cl is not None: try: @@ -646,7 +646,7 @@ class APIServerAdapter(BasePlatformAdapter): try: from hermes_cli.profiles import get_active_profile_name profile = get_active_profile_name() - if profile and profile not in ("default", "custom"): + if profile and profile not in {"default", "custom"}: return profile except Exception: pass @@ -1003,7 +1003,7 @@ class APIServerAdapter(BasePlatformAdapter): system_prompt = content else: system_prompt = system_prompt + "\n" + content - elif role in ("user", "assistant"): + elif role in {"user", "assistant"}: try: content = _normalize_multimodal_content(raw_content) except ValueError as exc: @@ -2381,7 +2381,7 @@ class APIServerAdapter(BasePlatformAdapter): if cron_err: return cron_err try: - include_disabled = request.query.get("include_disabled", "").lower() in ("true", "1") + include_disabled = request.query.get("include_disabled", "").lower() in {"true", "1"} jobs = _cron_list(include_disabled=include_disabled) return web.json_response({"jobs": jobs}) except Exception as e: diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 8e1f83c9b2a..ec0323d4738 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -560,7 +560,7 @@ def _looks_like_image(data: bytes) -> bool: return True if data[:3] == b"\xff\xd8\xff": return True - if data[:6] in (b"GIF87a", b"GIF89a"): + if data[:6] in {b"GIF87a", b"GIF89a"}: return True if data[:2] == b"BM": return True @@ -859,7 +859,7 @@ def cache_document_from_bytes(data: bytes, filename: str) -> str: # Sanitize: strip directory components, null bytes, and control characters safe_name = Path(filename).name if filename else "document" safe_name = safe_name.replace("\x00", "").strip() - if not safe_name or safe_name in (".", ".."): + if not safe_name or safe_name in {".", ".."}: safe_name = "document" cached_name = f"doc_{uuid.uuid4().hex[:12]}_{safe_name}" filepath = cache_dir / cached_name @@ -2793,7 +2793,7 @@ class BasePlatformAdapter(ABC): # and preserve ordering of queued follow-ups. Route those # through the dedicated handoff path that serializes # cancellation + runner response + pending drain. - if cmd in ("stop", "new", "reset"): + if cmd in {"stop", "new", "reset"}: try: await self._dispatch_active_session_command(event, session_key, cmd) except Exception as e: diff --git a/gateway/platforms/bluebubbles.py b/gateway/platforms/bluebubbles.py index 31120785c09..7a4af3ad685 100644 --- a/gateway/platforms/bluebubbles.py +++ b/gateway/platforms/bluebubbles.py @@ -223,7 +223,7 @@ class BlueBubblesAdapter(BasePlatformAdapter): def _webhook_url(self) -> str: """Compute the external webhook URL for BlueBubbles registration.""" host = self.webhook_host - if host in ("0.0.0.0", "127.0.0.1", "localhost", "::"): + if host in {"0.0.0.0", "127.0.0.1", "localhost", "::"}: host = "localhost" return f"http://{host}:{self.webhook_port}{self.webhook_path}" diff --git a/gateway/platforms/dingtalk.py b/gateway/platforms/dingtalk.py index 5c2285f24bb..579c382c704 100644 --- a/gateway/platforms/dingtalk.py +++ b/gateway/platforms/dingtalk.py @@ -353,9 +353,9 @@ class DingTalkAdapter(BasePlatformAdapter): configured = self.config.extra.get("require_mention") if configured is not None: if isinstance(configured, str): - return configured.lower() in ("true", "1", "yes", "on") + return configured.lower() in {"true", "1", "yes", "on"} return bool(configured) - return os.getenv("DINGTALK_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on") + return os.getenv("DINGTALK_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"} def _dingtalk_free_response_chats(self) -> Set[str]: raw = self.config.extra.get("free_response_chats") diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index e11a6093319..a96e97815c8 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -115,7 +115,7 @@ def _build_allowed_mentions(): raw = os.getenv(name, "").strip().lower() if not raw: return default - return raw in ("true", "1", "yes", "on") + return raw in {"true", "1", "yes", "on"} return discord.AllowedMentions( everyone=_b("DISCORD_ALLOW_MENTION_EVERYONE", False), @@ -708,7 +708,7 @@ class DiscordAdapter(BasePlatformAdapter): # Ignore Discord system messages (thread renames, pins, member joins, etc.) # Allow both default and reply types — replies have a distinct MessageType. - if message.type not in (discord.MessageType.default, discord.MessageType.reply): + if message.type not in {discord.MessageType.default, discord.MessageType.reply}: return # Bot message filtering (DISCORD_ALLOW_BOTS): @@ -769,7 +769,7 @@ class DiscordAdapter(BasePlatformAdapter): # answer regardless of who is mentioned. _ignore_no_mention = os.getenv( "DISCORD_IGNORE_NO_MENTION", "true" - ).lower() in ("true", "1", "yes") + ).lower() in {"true", "1", "yes"} if _ignore_no_mention and not _self_mentioned and not _other_bots_mentioned: _channel_id = str(message.channel.id) _parent_id = None @@ -1317,7 +1317,7 @@ class DiscordAdapter(BasePlatformAdapter): def _reactions_enabled(self) -> bool: """Check if message reactions are enabled via config/env.""" - return os.getenv("DISCORD_REACTIONS", "true").lower() not in ("false", "0", "no") + return os.getenv("DISCORD_REACTIONS", "true").lower() not in {"false", "0", "no"} async def on_processing_start(self, event: MessageEvent) -> None: """Add an in-progress reaction for normal Discord message events.""" @@ -3137,9 +3137,9 @@ class DiscordAdapter(BasePlatformAdapter): # UX so users don't see commands they can't invoke. Off by default # to preserve the slash UX for deployments that intentionally allow # everyone in the guild. - if os.getenv("DISCORD_HIDE_SLASH_COMMANDS", "false").strip().lower() in ( + if os.getenv("DISCORD_HIDE_SLASH_COMMANDS", "false").strip().lower() in { "true", "1", "yes", "on", - ): + }: self._apply_owner_only_visibility(tree) def _apply_owner_only_visibility(self, tree) -> None: @@ -3526,9 +3526,9 @@ class DiscordAdapter(BasePlatformAdapter): configured = self.config.extra.get("require_mention") if configured is not None: if isinstance(configured, str): - return configured.lower() not in ("false", "0", "no", "off") + return configured.lower() not in {"false", "0", "no", "off"} return bool(configured) - return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off") + return os.getenv("DISCORD_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"} def _discord_free_response_channels(self) -> set: """Return Discord channel IDs where no bot mention is required. @@ -4200,7 +4200,7 @@ class DiscordAdapter(BasePlatformAdapter): no_thread_channels_raw = os.getenv("DISCORD_NO_THREAD_CHANNELS", "") no_thread_channels = {ch.strip() for ch in no_thread_channels_raw.split(",") if ch.strip()} skip_thread = bool(channel_ids & no_thread_channels) - auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in ("true", "1", "yes") + auto_thread = os.getenv("DISCORD_AUTO_THREAD", "true").lower() in {"true", "1", "yes"} is_reply_message = getattr(message, "type", None) == discord.MessageType.reply if auto_thread and not skip_thread and not is_voice_linked_channel and not is_reply_message: thread = await self._auto_create_thread(message) @@ -4282,7 +4282,7 @@ class DiscordAdapter(BasePlatformAdapter): try: # Determine extension from content type (image/png -> .png) ext = "." + content_type.split("/")[-1].split(";")[0] - if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"): + if ext not in {".jpg", ".jpeg", ".png", ".gif", ".webp"}: ext = ".jpg" cached_path = await self._cache_discord_image(att, ext) media_urls.append(cached_path) @@ -4296,7 +4296,7 @@ class DiscordAdapter(BasePlatformAdapter): elif content_type.startswith("audio/"): try: ext = "." + content_type.split("/")[-1].split(";")[0] - if ext not in (".ogg", ".mp3", ".wav", ".webm", ".m4a"): + if ext not in {".ogg", ".mp3", ".wav", ".webm", ".m4a"}: ext = ".ogg" cached_path = await self._cache_discord_audio(att, ext) media_urls.append(cached_path) @@ -4339,7 +4339,7 @@ class DiscordAdapter(BasePlatformAdapter): logger.info("[Discord] Cached user document: %s", cached_path) # Inject text content for plain-text documents (capped at 100 KB) MAX_TEXT_INJECT_BYTES = 100 * 1024 - if ext in (".md", ".txt", ".log") and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES: + if ext in {".md", ".txt", ".log"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES: try: text_content = raw_bytes.decode("utf-8") display_name = att.filename or f"document{ext}" diff --git a/gateway/platforms/email.py b/gateway/platforms/email.py index fb44ad308e7..0fffb82d0b9 100644 --- a/gateway/platforms/email.py +++ b/gateway/platforms/email.py @@ -54,7 +54,7 @@ _NOREPLY_PATTERNS = ( # RFC headers that indicate bulk/automated mail _AUTOMATED_HEADERS = { "Auto-Submitted": lambda v: v.lower() != "no", - "Precedence": lambda v: v.lower() in ("bulk", "list", "junk"), + "Precedence": lambda v: v.lower() in {"bulk", "list", "junk"}, "X-Auto-Response-Suppress": lambda v: bool(v), "List-Unsubscribe": lambda v: bool(v), } @@ -203,7 +203,7 @@ def _extract_attachments( continue # Skip text/plain and text/html body parts content_type = part.get_content_type() - if content_type in ("text/plain", "text/html") and "attachment" not in disposition: + if content_type in {"text/plain", "text/html"} and "attachment" not in disposition: continue filename = part.get_filename() diff --git a/gateway/platforms/feishu.py b/gateway/platforms/feishu.py index 46604fa1e30..6d8222692db 100644 --- a/gateway/platforms/feishu.py +++ b/gateway/platforms/feishu.py @@ -428,7 +428,7 @@ RejectReason = Literal[ def _is_bot_sender(sender: Any) -> bool: # receive_v1 docs say {user, bot}; accept "app" defensively. - return getattr(sender, "sender_type", "") in ("bot", "app") + return getattr(sender, "sender_type", "") in {"bot", "app"} def _sender_identity(sender: Any) -> frozenset: @@ -1443,7 +1443,7 @@ class FeishuAdapter(BasePlatformAdapter): # Env-only so adapter and gateway auth bypass share one source; yaml # feishu.allow_bots is bridged to this env var at config load. allow_bots = os.getenv("FEISHU_ALLOW_BOTS", "none").strip().lower() - if allow_bots not in ("none", "mentions", "all"): + if allow_bots not in {"none", "mentions", "all"}: logger.warning( "[Feishu] Unknown allow_bots=%r, falling back to 'none'. Valid: none, mentions, all.", allow_bots, @@ -2752,7 +2752,7 @@ class FeishuAdapter(BasePlatformAdapter): # ========================================================================= def _reactions_enabled(self) -> bool: - return os.getenv("FEISHU_REACTIONS", "true").strip().lower() not in ("false", "0", "no") + return os.getenv("FEISHU_REACTIONS", "true").strip().lower() not in {"false", "0", "no"} async def _add_reaction(self, message_id: str, emoji_type: str) -> Optional[str]: """Return the reaction_id on success, else None. The id is needed later for deletion.""" @@ -3219,7 +3219,7 @@ class FeishuAdapter(BasePlatformAdapter): self._on_bot_added_to_chat(data) elif event_type == "im.chat.member.bot.deleted_v1": self._on_bot_removed_from_chat(data) - elif event_type in ("im.message.reaction.created_v1", "im.message.reaction.deleted_v1"): + elif event_type in {"im.message.reaction.created_v1", "im.message.reaction.deleted_v1"}: self._on_reaction_event(event_type, data) elif event_type == "card.action.trigger": self._on_card_action_trigger(data) @@ -4815,7 +4815,7 @@ def _poll_registration( # Terminal errors error = res.get("error", "") - if error in ("access_denied", "expired_token"): + if error in {"access_denied", "expired_token"}: if poll_count > 0: print() logger.warning("[Feishu onboard] Registration %s", error) diff --git a/gateway/platforms/feishu_comment.py b/gateway/platforms/feishu_comment.py index 08cd35185c6..4d757cc7646 100644 --- a/gateway/platforms/feishu_comment.py +++ b/gateway/platforms/feishu_comment.py @@ -690,7 +690,7 @@ def _extract_docs_links(replies: List[Dict[str, Any]]) -> List[Dict[str, str]]: except (json.JSONDecodeError, TypeError): continue for elem in content.get("elements", []): - if elem.get("type") not in ("docs_link", "link"): + if elem.get("type") not in {"docs_link", "link"}: continue link_data = elem.get("docs_link") or elem.get("link") or {} url = link_data.get("url", "") @@ -1031,7 +1031,7 @@ def _save_session_history(key: str, messages: List[Dict[str, Any]]) -> None: # Only keep user/assistant messages (strip system messages and tool internals) cleaned = [ m for m in messages - if m.get("role") in ("user", "assistant") and m.get("content") + if m.get("role") in {"user", "assistant"} and m.get("content") ] # Keep last N if len(cleaned) > _SESSION_MAX_MESSAGES: @@ -1170,7 +1170,7 @@ async def handle_drive_comment_event( rule = resolve_rule(comments_cfg, file_type, file_token) # If no exact match and config has wiki keys, try reverse-lookup - if rule.match_source in ("wildcard", "top") and has_wiki_keys(comments_cfg): + if rule.match_source in {"wildcard", "top"} and has_wiki_keys(comments_cfg): wiki_token = await _reverse_lookup_wiki_token(client, file_type, file_token) if wiki_token: rule = resolve_rule(comments_cfg, file_type, file_token, wiki_token=wiki_token) diff --git a/gateway/platforms/homeassistant.py b/gateway/platforms/homeassistant.py index 6bc9ae6eb61..e7ea762e2e7 100644 --- a/gateway/platforms/homeassistant.py +++ b/gateway/platforms/homeassistant.py @@ -256,7 +256,7 @@ class HomeAssistantAdapter(BasePlatformAdapter): await self._handle_ha_event(data.get("event", {})) except json.JSONDecodeError: logger.debug("Invalid JSON from HA WS: %s", ws_msg.data[:200]) - elif ws_msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR): + elif ws_msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}: break async def _handle_ha_event(self, event: Dict[str, Any]) -> None: @@ -361,7 +361,7 @@ class HomeAssistantAdapter(BasePlatformAdapter): f"(was {'triggered' if old_val == 'on' else 'cleared'})" ) - if domain in ("light", "switch", "fan"): + if domain in {"light", "switch", "fan"}: return ( f"[Home Assistant] {friendly_name}: turned " f"{'on' if new_val == 'on' else 'off'}" diff --git a/gateway/platforms/matrix.py b/gateway/platforms/matrix.py index 12e840b69c4..0133dc2dac7 100644 --- a/gateway/platforms/matrix.py +++ b/gateway/platforms/matrix.py @@ -245,11 +245,11 @@ def check_matrix_requirements() -> bool: # If encryption is requested, verify E2EE deps are available at startup # rather than silently degrading to plaintext-only at connect time. - encryption_requested = os.getenv("MATRIX_ENCRYPTION", "").lower() in ( + encryption_requested = os.getenv("MATRIX_ENCRYPTION", "").lower() in { "true", "1", "yes", - ) + } if encryption_requested and not _check_e2ee_deps(): logger.error( "Matrix: MATRIX_ENCRYPTION=true but E2EE dependencies are missing. %s. " @@ -312,7 +312,7 @@ class MatrixAdapter(BasePlatformAdapter): ) self._encryption: bool = config.extra.get( "encryption", - os.getenv("MATRIX_ENCRYPTION", "").lower() in ("true", "1", "yes"), + os.getenv("MATRIX_ENCRYPTION", "").lower() in {"true", "1", "yes"}, ) self._device_id: str = config.extra.get("device_id", "") or os.getenv( "MATRIX_DEVICE_ID", "" @@ -343,7 +343,7 @@ class MatrixAdapter(BasePlatformAdapter): # Mention/thread gating — parsed once from env vars. self._require_mention: bool = os.getenv( "MATRIX_REQUIRE_MENTION", "true" - ).lower() not in ("false", "0", "no") + ).lower() not in {"false", "0", "no"} free_rooms_raw = config.extra.get("free_response_rooms") if free_rooms_raw is None: free_rooms_raw = os.getenv("MATRIX_FREE_RESPONSE_ROOMS", "") @@ -367,22 +367,22 @@ class MatrixAdapter(BasePlatformAdapter): self._allowed_rooms: Set[str] = { r.strip() for r in str(allowed_rooms_raw).split(",") if r.strip() } - self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in ( + self._auto_thread: bool = os.getenv("MATRIX_AUTO_THREAD", "true").lower() in { "true", "1", "yes", - ) + } self._dm_auto_thread: bool = os.getenv( "MATRIX_DM_AUTO_THREAD", "false" - ).lower() in ("true", "1", "yes") + ).lower() in {"true", "1", "yes"} self._dm_mention_threads: bool = os.getenv( "MATRIX_DM_MENTION_THREADS", "false" - ).lower() in ("true", "1", "yes") + ).lower() in {"true", "1", "yes"} # Reactions: configurable via MATRIX_REACTIONS (default: true). self._reactions_enabled: bool = os.getenv( "MATRIX_REACTIONS", "true" - ).lower() not in ("false", "0", "no") + ).lower() not in {"false", "0", "no"} self._pending_reactions: dict[tuple[str, str], str] = {} # Delay before redacting reactions so Matrix homeservers have time to # deliver the final message event without tripping "missing event" @@ -1771,9 +1771,9 @@ class MatrixAdapter(BasePlatformAdapter): # Cache media locally when downstream tools need a real file path. cached_path = None - should_cache_locally = msg_type in ( + should_cache_locally = msg_type in { MessageType.PHOTO, MessageType.AUDIO, MessageType.VIDEO, MessageType.DOCUMENT, - ) or is_voice_message or is_encrypted_media + } or is_voice_message or is_encrypted_media if should_cache_locally and url: try: file_bytes = await self._client.download_media(ContentURI(url)) @@ -1834,7 +1834,7 @@ class MatrixAdapter(BasePlatformAdapter): ext = ext_map.get(media_type, ".jpg") cached_path = cache_image_from_bytes(file_bytes, ext=ext) logger.info("[Matrix] Cached user image at %s", cached_path) - elif msg_type in (MessageType.AUDIO, MessageType.VOICE): + elif msg_type in {MessageType.AUDIO, MessageType.VOICE}: ext = ( Path( body @@ -2602,7 +2602,7 @@ class MatrixAdapter(BasePlatformAdapter): """Sanitize a URL for use in an href attribute.""" stripped = url.strip() scheme = stripped.split(":", 1)[0].lower().strip() if ":" in stripped else "" - if scheme in ("javascript", "data", "vbscript"): + if scheme in {"javascript", "data", "vbscript"}: return "" return stripped.replace('"', """) diff --git a/gateway/platforms/mattermost.py b/gateway/platforms/mattermost.py index 3ffd74326d3..9487f8a1edf 100644 --- a/gateway/platforms/mattermost.py +++ b/gateway/platforms/mattermost.py @@ -611,7 +611,7 @@ class MattermostAdapter(BasePlatformAdapter): # succeed on retry — stop reconnecting instead of looping forever. import aiohttp err_str = str(exc).lower() - if isinstance(exc, aiohttp.WSServerHandshakeError) and exc.status in (401, 403): + if isinstance(exc, aiohttp.WSServerHandshakeError) and exc.status in {401, 403}: logger.error("Mattermost WS auth failed (HTTP %d) — stopping reconnect", exc.status) return if "401" in err_str or "403" in err_str or "unauthorized" in err_str: @@ -649,21 +649,21 @@ class MattermostAdapter(BasePlatformAdapter): if self._closing: return - if raw_msg.type in ( + if raw_msg.type in { raw_msg.type.TEXT, raw_msg.type.BINARY, - ): + }: try: event = json.loads(raw_msg.data) except (json.JSONDecodeError, TypeError): continue await self._handle_ws_event(event) - elif raw_msg.type in ( + elif raw_msg.type in { raw_msg.type.ERROR, raw_msg.type.CLOSE, raw_msg.type.CLOSING, raw_msg.type.CLOSED, - ): + }: logger.info("Mattermost: WebSocket closed (%s)", raw_msg.type) break @@ -732,7 +732,7 @@ class MattermostAdapter(BasePlatformAdapter): require_mention = os.getenv( "MATTERMOST_REQUIRE_MENTION", "true" - ).lower() not in ("false", "0", "no") + ).lower() not in {"false", "0", "no"} free_channels_raw = os.getenv("MATTERMOST_FREE_RESPONSE_CHANNELS", "") free_channels = {ch.strip() for ch in free_channels_raw.split(",") if ch.strip()} diff --git a/gateway/platforms/qqbot/adapter.py b/gateway/platforms/qqbot/adapter.py index 12caef0f144..b7a306f9b69 100644 --- a/gateway/platforms/qqbot/adapter.py +++ b/gateway/platforms/qqbot/adapter.py @@ -513,7 +513,7 @@ class QQAdapter(BasePlatformAdapter): self._fail_pending("Connection closed") # Stop reconnecting for fatal codes - if code in (4914, 4915): + if code in {4914, 4915}: desc = "offline/sandbox-only" if code == 4914 else "banned" logger.error( "[%s] Bot is %s. Check QQ Open Platform.", self._log_tag, desc @@ -550,7 +550,7 @@ class QQAdapter(BasePlatformAdapter): self._token_expires_at = 0.0 # Session invalid → clear session, will re-identify on next Hello - if code in ( + if code in { 4006, 4007, 4009, @@ -568,7 +568,7 @@ class QQAdapter(BasePlatformAdapter): 4911, 4912, 4913, - ): + }: logger.info( "[%s] Session error (%d), clearing session for re-identify", self._log_tag, @@ -637,12 +637,12 @@ class QQAdapter(BasePlatformAdapter): payload = self._parse_json(msg.data) if payload: self._dispatch_payload(payload) - elif msg.type in (aiohttp.WSMsgType.PING,): + elif msg.type in {aiohttp.WSMsgType.PING,}: # aiohttp auto-replies with PONG pass elif msg.type == aiohttp.WSMsgType.CLOSE: raise QQCloseError(msg.data, msg.extra) - elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR): + elif msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}: raise RuntimeError("WebSocket closed") async def _heartbeat_loop(self) -> None: @@ -783,13 +783,13 @@ class QQAdapter(BasePlatformAdapter): self._handle_ready(d) elif t == "RESUMED": logger.info("[%s] Session resumed", self._log_tag) - elif t in ( + elif t in { "C2C_MESSAGE_CREATE", "GROUP_AT_MESSAGE_CREATE", "DIRECT_MESSAGE_CREATE", "GUILD_MESSAGE_CREATE", "GUILD_AT_MESSAGE_CREATE", - ): + }: asyncio.create_task(self._on_message(t, d)) elif t == "INTERACTION_CREATE": self._create_task(self._on_interaction(d)) @@ -859,9 +859,9 @@ class QQAdapter(BasePlatformAdapter): # Route by event type if event_type == "C2C_MESSAGE_CREATE": await self._handle_c2c_message(d, msg_id, content, author, timestamp) - elif event_type in ("GROUP_AT_MESSAGE_CREATE",): + elif event_type in {"GROUP_AT_MESSAGE_CREATE",}: await self._handle_group_message(d, msg_id, content, author, timestamp) - elif event_type in ("GUILD_MESSAGE_CREATE", "GUILD_AT_MESSAGE_CREATE"): + elif event_type in {"GUILD_MESSAGE_CREATE", "GUILD_AT_MESSAGE_CREATE"}: await self._handle_guild_message(d, msg_id, content, author, timestamp) elif event_type == "DIRECT_MESSAGE_CREATE": await self._handle_dm_message(d, msg_id, content, author, timestamp) @@ -1864,7 +1864,7 @@ class QQAdapter(BasePlatformAdapter): return ".wav" if data[:4] == b"fLaC": return ".flac" - if data[:2] in (b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"): + if data[:2] in {b"\xff\xfb", b"\xff\xf3", b"\xff\xf2"}: return ".mp3" if data[:4] == b"\x30\x26\xb2\x75" or data[:4] == b"\x4f\x67\x67\x53": return ".ogg" @@ -2033,7 +2033,7 @@ class QQAdapter(BasePlatformAdapter): "base_url": base_url, "api_key": api_key, "model": model - or ("glm-asr" if provider in ("zai", "glm") else "whisper-1"), + or ("glm-asr" if provider in {"zai", "glm"} else "whisper-1"), } # 2. QQ-specific env vars (set by `hermes setup gateway` / `hermes gateway`) @@ -2115,7 +2115,7 @@ class QQAdapter(BasePlatformAdapter): if urlparse(source_url).path else "" ) - if not ext or ext not in ( + if not ext or ext not in { ".silk", ".amr", ".mp3", @@ -2124,7 +2124,7 @@ class QQAdapter(BasePlatformAdapter): ".m4a", ".aac", ".flac", - ): + }: ext = self._guess_ext_from_data(audio_data) with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp_src: @@ -2870,7 +2870,7 @@ class QQAdapter(BasePlatformAdapter): raise ValueError("Media source is required") parsed = urlparse(source) - if parsed.scheme in ("http", "https"): + if parsed.scheme in {"http", "https"}: # For URLs, pass through directly to the upload API content_type = mimetypes.guess_type(source)[0] or "application/octet-stream" resolved_name = file_name or Path(parsed.path).name or "media" @@ -2966,7 +2966,7 @@ class QQAdapter(BasePlatformAdapter): chat_type = self._guess_chat_type(chat_id) return { "name": chat_id, - "type": "group" if chat_type in ("group", "guild") else "dm", + "type": "group" if chat_type in {"group", "guild"} else "dm", } # ------------------------------------------------------------------ @@ -2975,7 +2975,7 @@ class QQAdapter(BasePlatformAdapter): @staticmethod def _is_url(source: str) -> bool: - return urlparse(str(source)).scheme in ("http", "https") + return urlparse(str(source)).scheme in {"http", "https"} def _guess_chat_type(self, chat_id: str) -> str: """Determine chat type from stored inbound metadata, fallback to 'c2c'.""" diff --git a/gateway/platforms/qqbot/chunked_upload.py b/gateway/platforms/qqbot/chunked_upload.py index 5f6ed9dd267..416dfc52a98 100644 --- a/gateway/platforms/qqbot/chunked_upload.py +++ b/gateway/platforms/qqbot/chunked_upload.py @@ -239,7 +239,7 @@ class ChunkedUploader: :raises UploadFileTooLargeError: When the file exceeds the platform limit. :raises RuntimeError: On other API or I/O failures. """ - if chat_type not in ("c2c", "group"): + if chat_type not in {"c2c", "group"}: raise ValueError( f"ChunkedUploader: unsupported chat_type {chat_type!r}" ) diff --git a/gateway/platforms/signal.py b/gateway/platforms/signal.py index a0053317f7e..118eb688cc9 100644 --- a/gateway/platforms/signal.py +++ b/gateway/platforms/signal.py @@ -99,11 +99,11 @@ def _guess_extension(data: bytes) -> str: def _is_image_ext(ext: str) -> bool: - return ext.lower() in (".jpg", ".jpeg", ".png", ".gif", ".webp") + return ext.lower() in {".jpg", ".jpeg", ".png", ".gif", ".webp"} def _is_audio_ext(ext: str) -> bool: - return ext.lower() in (".mp3", ".wav", ".ogg", ".m4a", ".aac") + return ext.lower() in {".mp3", ".wav", ".ogg", ".m4a", ".aac"} _EXT_TO_MIME = { @@ -1449,7 +1449,7 @@ class SignalAdapter(BasePlatformAdapter): contacts from seeing the 👀 reaction (which fires before run.py's auth gate and would otherwise reveal that a bot is listening). """ - if os.getenv("SIGNAL_REACTIONS", "true").lower() in ("false", "0", "no"): + if os.getenv("SIGNAL_REACTIONS", "true").lower() in {"false", "0", "no"}: return False if event is not None: sender = getattr(getattr(event, "source", None), "user_id", None) diff --git a/gateway/platforms/slack.py b/gateway/platforms/slack.py index 60912bc18e0..7fbefd446ca 100644 --- a/gateway/platforms/slack.py +++ b/gateway/platforms/slack.py @@ -935,7 +935,7 @@ class SlackAdapter(BasePlatformAdapter): raw = self.config.extra.get("dm_top_level_threads_as_sessions") if raw is None: return True # default: each DM thread is its own session - return str(raw).strip().lower() in ("1", "true", "yes", "on") + return str(raw).strip().lower() in {"1", "true", "yes", "on"} def _resolve_thread_ts( self, @@ -1300,7 +1300,7 @@ class SlackAdapter(BasePlatformAdapter): def _reactions_enabled(self) -> bool: """Check if message reactions are enabled via config/env.""" - return os.getenv("SLACK_REACTIONS", "true").lower() not in ("false", "0", "no") + return os.getenv("SLACK_REACTIONS", "true").lower() not in {"false", "0", "no"} async def on_processing_start(self, event: MessageEvent) -> None: """Add an in-progress reaction when message processing begins.""" @@ -1773,7 +1773,7 @@ class SlackAdapter(BasePlatformAdapter): # Ignore message edits and deletions subtype = event.get("subtype") - if subtype in ("message_changed", "message_deleted"): + if subtype in {"message_changed", "message_deleted"}: return original_text = event.get("text", "") @@ -1892,7 +1892,7 @@ class SlackAdapter(BasePlatformAdapter): channel_type = event.get("channel_type", "") if not channel_type and channel_id.startswith("D"): channel_type = "im" - is_dm = channel_type in ("im", "mpim") # Both 1:1 and group DMs + is_dm = channel_type in {"im", "mpim"} # Both 1:1 and group DMs # Build thread_ts for session keying. # In channels: fall back to ts so each top-level @mention starts a @@ -2033,7 +2033,7 @@ class SlackAdapter(BasePlatformAdapter): if mimetype.startswith("image/") and url: try: ext = "." + mimetype.split("/")[-1].split(";")[0] - if ext not in (".jpg", ".jpeg", ".png", ".gif", ".webp"): + if ext not in {".jpg", ".jpeg", ".png", ".gif", ".webp"}: ext = ".jpg" # Slack private URLs require the bot token as auth header cached = await self._download_slack_file(url, ext, team_id=team_id) @@ -2049,7 +2049,7 @@ class SlackAdapter(BasePlatformAdapter): elif mimetype.startswith("audio/") and url: try: ext = "." + mimetype.split("/")[-1].split(";")[0] - if ext not in (".ogg", ".mp3", ".wav", ".webm", ".m4a"): + if ext not in {".ogg", ".mp3", ".wav", ".webm", ".m4a"}: ext = ".ogg" cached = await self._download_slack_file(url, ext, audio=True, team_id=team_id) media_urls.append(cached) @@ -2737,7 +2737,7 @@ class SlackAdapter(BasePlatformAdapter): if team_id and channel_id: self._channel_team[channel_id] = team_id - if slash_name in ("hermes", ""): + if slash_name in {"hermes", ""}: # Legacy /hermes [args] routing + free-form questions. # Empty slash_name falls into this branch for backward compat # with any caller that didn't populate command["command"]. @@ -2932,9 +2932,9 @@ class SlackAdapter(BasePlatformAdapter): configured = self.config.extra.get("require_mention") if configured is not None: if isinstance(configured, str): - return configured.lower() not in ("false", "0", "no", "off") + return configured.lower() not in {"false", "0", "no", "off"} return bool(configured) - return os.getenv("SLACK_REQUIRE_MENTION", "true").lower() not in ("false", "0", "no", "off") + return os.getenv("SLACK_REQUIRE_MENTION", "true").lower() not in {"false", "0", "no", "off"} def _slack_strict_mention(self) -> bool: """When true, channel threads require an explicit @-mention on every @@ -2944,9 +2944,9 @@ class SlackAdapter(BasePlatformAdapter): configured = self.config.extra.get("strict_mention") if configured is not None: if isinstance(configured, str): - return configured.lower() in ("true", "1", "yes", "on") + return configured.lower() in {"true", "1", "yes", "on"} return bool(configured) - return os.getenv("SLACK_STRICT_MENTION", "false").lower() in ("true", "1", "yes", "on") + return os.getenv("SLACK_STRICT_MENTION", "false").lower() in {"true", "1", "yes", "on"} def _slack_free_response_channels(self) -> set: """Return channel IDs where no @mention is required.""" diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index c1f312783a4..8e937d7573f 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -616,7 +616,7 @@ class TelegramAdapter(BasePlatformAdapter): def _looks_like_network_error(error: Exception) -> bool: """Return True for transient network errors that warrant a reconnect attempt.""" name = error.__class__.__name__.lower() - if name in ("networkerror", "timedout", "connectionerror"): + if name in {"networkerror", "timedout", "connectionerror"}: return True try: from telegram.error import NetworkError, TimedOut @@ -632,9 +632,9 @@ class TelegramAdapter(BasePlatformAdapter): return default if isinstance(value, str): lowered = value.strip().lower() - if lowered in ("true", "1", "yes", "on"): + if lowered in {"true", "1", "yes", "on"}: return True - if lowered in ("false", "0", "no", "off"): + if lowered in {"false", "0", "no", "off"}: return False return default return bool(value) @@ -1171,7 +1171,7 @@ class TelegramAdapter(BasePlatformAdapter): "write_timeout": _env_float("HERMES_TELEGRAM_HTTP_WRITE_TIMEOUT", 20.0), } - disable_fallback = (os.getenv("HERMES_TELEGRAM_DISABLE_FALLBACK_IPS", "").strip().lower() in ("1", "true", "yes", "on")) + disable_fallback = (os.getenv("HERMES_TELEGRAM_DISABLE_FALLBACK_IPS", "").strip().lower() in {"1", "true", "yes", "on"}) fallback_ips = self._fallback_ips() if not fallback_ips: fallback_ips = await discover_fallback_ips() @@ -1917,7 +1917,7 @@ class TelegramAdapter(BasePlatformAdapter): """ if not self._bot or not hasattr(self._bot, "send_message_draft"): return False - return (chat_type or "").lower() in ("dm", "private") + return (chat_type or "").lower() in {"dm", "private"} async def send_draft( self, @@ -2723,7 +2723,7 @@ class TelegramAdapter(BasePlatformAdapter): with open(audio_path, "rb") as audio_file: ext = os.path.splitext(audio_path)[1].lower() # .ogg / .opus files -> send as voice (round playable bubble) - if ext in (".ogg", ".opus"): + if ext in {".ogg", ".opus"}: _voice_thread = self._metadata_thread_id(metadata) reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata) voice_thread_kwargs = self._thread_kwargs_for_send( @@ -2747,7 +2747,7 @@ class TelegramAdapter(BasePlatformAdapter): "voice", reset_media=lambda: audio_file.seek(0), ) - elif ext in (".mp3", ".m4a"): + elif ext in {".mp3", ".m4a"}: # Telegram's Bot API sendAudio only accepts MP3 / M4A. _audio_thread = self._metadata_thread_id(metadata) reply_to_id = self._reply_to_message_id_for_send(reply_to, metadata) @@ -3498,18 +3498,18 @@ class TelegramAdapter(BasePlatformAdapter): configured = self.config.extra.get("require_mention") if configured is not None: if isinstance(configured, str): - return configured.lower() in ("true", "1", "yes", "on") + return configured.lower() in {"true", "1", "yes", "on"} return bool(configured) - return os.getenv("TELEGRAM_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on") + return os.getenv("TELEGRAM_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"} def _telegram_guest_mode(self) -> bool: """Return whether non-allowlisted groups may trigger via direct @mention.""" configured = self.config.extra.get("guest_mode") if configured is not None: if isinstance(configured, str): - return configured.lower() in ("true", "1", "yes", "on") + return configured.lower() in {"true", "1", "yes", "on"} return bool(configured) - return os.getenv("TELEGRAM_GUEST_MODE", "false").lower() in ("true", "1", "yes", "on") + return os.getenv("TELEGRAM_GUEST_MODE", "false").lower() in {"true", "1", "yes", "on"} def _telegram_free_response_chats(self) -> set[str]: raw = self.config.extra.get("free_response_chats") @@ -3598,7 +3598,7 @@ class TelegramAdapter(BasePlatformAdapter): if not chat: return False chat_type = str(getattr(chat, "type", "")).split(".")[-1].lower() - return chat_type in ("group", "supergroup") + return chat_type in {"group", "supergroup"} def _is_reply_to_bot(self, message: Message) -> bool: if not self._bot or not getattr(message, "reply_to_message", None): @@ -4157,7 +4157,7 @@ class TelegramAdapter(BasePlatformAdapter): # For text files, inject content into event.text (capped at 100 KB) MAX_TEXT_INJECT_BYTES = 100 * 1024 - if ext in (".md", ".txt") and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES: + if ext in {".md", ".txt"} and len(raw_bytes) <= MAX_TEXT_INJECT_BYTES: try: text_content = raw_bytes.decode("utf-8") display_name = original_filename or f"document{ext}" @@ -4396,7 +4396,7 @@ class TelegramAdapter(BasePlatformAdapter): # Determine chat type chat_type = "dm" - if chat.type in (ChatType.GROUP, ChatType.SUPERGROUP): + if chat.type in {ChatType.GROUP, ChatType.SUPERGROUP}: chat_type = "group" elif chat.type == ChatType.CHANNEL: chat_type = "channel" @@ -4512,7 +4512,7 @@ class TelegramAdapter(BasePlatformAdapter): def _reactions_enabled(self) -> bool: """Check if message reactions are enabled via config/env.""" - return os.getenv("TELEGRAM_REACTIONS", "false").lower() not in ("false", "0", "no") + return os.getenv("TELEGRAM_REACTIONS", "false").lower() not in {"false", "0", "no"} async def _set_reaction(self, chat_id: str, message_id: str, emoji: str) -> bool: """Set a single emoji reaction on a Telegram message.""" diff --git a/gateway/platforms/wecom.py b/gateway/platforms/wecom.py index 769743794df..d7a5c1d9a49 100644 --- a/gateway/platforms/wecom.py +++ b/gateway/platforms/wecom.py @@ -295,7 +295,7 @@ class WeComAdapter(BasePlatformAdapter): auth_payload = await self._wait_for_handshake(req_id) errcode = auth_payload.get("errcode", 0) - if errcode not in (0, None): + if errcode not in {0, None}: errmsg = auth_payload.get("errmsg", "authentication failed") raise RuntimeError(f"{errmsg} (errcode={errcode})") @@ -320,7 +320,7 @@ class WeComAdapter(BasePlatformAdapter): if self._payload_req_id(payload) == req_id: return payload logger.debug("[%s] Ignoring pre-auth payload: %s", self.name, payload.get("cmd")) - elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR): + elif msg.type in {aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.ERROR}: raise RuntimeError("WeCom websocket closed during authentication") async def _listen_loop(self) -> None: @@ -360,7 +360,7 @@ class WeComAdapter(BasePlatformAdapter): payload = self._parse_json(msg.data) if payload: await self._dispatch_payload(payload) - elif msg.type in (aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR): + elif msg.type in {aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR}: raise RuntimeError("WeCom websocket closed") async def _heartbeat_loop(self) -> None: @@ -998,7 +998,7 @@ class WeComAdapter(BasePlatformAdapter): @staticmethod def _response_error(response: Dict[str, Any]) -> Optional[str]: errcode = response.get("errcode", 0) - if errcode in (0, None): + if errcode in {0, None}: return None errmsg = str(response.get("errmsg") or "unknown error") return f"WeCom errcode {errcode}: {errmsg}" diff --git a/gateway/platforms/weixin.py b/gateway/platforms/weixin.py index 1c20b3f2902..1c9fec0af7f 100644 --- a/gateway/platforms/weixin.py +++ b/gateway/platforms/weixin.py @@ -605,7 +605,7 @@ def _assert_weixin_cdn_url(url: str) -> None: except Exception as exc: # noqa: BLE001 raise ValueError(f"Unparseable media URL: {url!r}") from exc - if scheme not in ("http", "https"): + if scheme not in {"http", "https"}: raise ValueError( f"Media URL has disallowed scheme {scheme!r}; only http/https are permitted." ) @@ -983,7 +983,7 @@ def _extract_text(item_list: List[Dict[str, Any]]) -> str: ref = item.get("ref_msg") or {} ref_item = ref.get("message_item") or {} ref_type = ref_item.get("type") - if ref_type in (ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE): + if ref_type in {ITEM_IMAGE, ITEM_VIDEO, ITEM_FILE, ITEM_VOICE}: title = ref.get("title") or "" prefix = f"[引用媒体: {title}]\n" if title else "[引用媒体]\n" return f"{prefix}{text}".strip() @@ -1331,7 +1331,7 @@ class WeixinAdapter(BasePlatformAdapter): ret = response.get("ret", 0) errcode = response.get("errcode", 0) - if ret not in (0, None) or errcode not in (0, None): + if ret not in {0, None} or errcode not in {0, None}: if (ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE or _is_stale_session_ret(ret, errcode, response.get("errmsg"))): logger.error("[%s] Session expired; pausing for 10 minutes", self.name) @@ -1601,7 +1601,7 @@ class WeixinAdapter(BasePlatformAdapter): if resp and isinstance(resp, dict): ret = resp.get("ret") errcode = resp.get("errcode") - if (ret is not None and ret not in (0,)) or (errcode is not None and errcode not in (0,)): + if (ret is not None and ret not in {0,}) or (errcode is not None and errcode not in {0,}): is_session_expired = ( ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE diff --git a/gateway/platforms/whatsapp.py b/gateway/platforms/whatsapp.py index 8e21736441c..2fb6fc13329 100644 --- a/gateway/platforms/whatsapp.py +++ b/gateway/platforms/whatsapp.py @@ -301,9 +301,9 @@ class WhatsAppAdapter(BasePlatformAdapter): configured = self.config.extra.get("require_mention") if configured is not None: if isinstance(configured, str): - return configured.lower() in ("true", "1", "yes", "on") + return configured.lower() in {"true", "1", "yes", "on"} return bool(configured) - return os.getenv("WHATSAPP_REQUIRE_MENTION", "false").lower() in ("true", "1", "yes", "on") + return os.getenv("WHATSAPP_REQUIRE_MENTION", "false").lower() in {"true", "1", "yes", "on"} def _whatsapp_free_response_chats(self) -> set[str]: raw = self.config.extra.get("free_response_chats") @@ -679,7 +679,7 @@ class WhatsAppAdapter(BasePlatformAdapter): # getattr-with-default keeps tests that construct the adapter via # ``WhatsAppAdapter.__new__`` (bypassing __init__) working without # every _make_adapter() helper having to seed the attribute. - if getattr(self, "_shutting_down", False) and returncode in (0, -2, -15): + if getattr(self, "_shutting_down", False) and returncode in {0, -2, -15}: logger.info( "[%s] Bridge exited during shutdown (code %d).", self.name, @@ -1183,7 +1183,7 @@ class WhatsAppAdapter(BasePlatformAdapter): if msg_type == MessageType.DOCUMENT and cached_urls: for doc_path in cached_urls: ext = Path(doc_path).suffix.lower() - if ext in (".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".py", ".js", ".ts", ".html", ".css"): + if ext in {".txt", ".md", ".csv", ".json", ".xml", ".yaml", ".yml", ".log", ".py", ".js", ".ts", ".html", ".css"}: try: file_size = Path(doc_path).stat().st_size if file_size > MAX_TEXT_INJECT_BYTES: diff --git a/gateway/platforms/yuanbao.py b/gateway/platforms/yuanbao.py index f08f7266e19..d79da7856ae 100644 --- a/gateway/platforms/yuanbao.py +++ b/gateway/platforms/yuanbao.py @@ -2228,7 +2228,7 @@ class MediaResolveMiddleware(InboundMiddleware): resp.raise_for_status() payload = resp.json() code = payload.get("code") - if code not in (None, 0): + if code not in {None, 0}: raise RuntimeError( f"resource/v1/download failed: code={code}, msg={payload.get('msg', '')}" ) @@ -2391,7 +2391,7 @@ class MediaResolveMiddleware(InboundMiddleware): rid = m.group(2) kind, _, filename = head.partition(":") kind = kind.strip() - if kind not in ("image", "file"): + if kind not in {"image", "file"}: continue if rid in seen: continue @@ -2993,10 +2993,10 @@ class ConnectionManager: # Fire-and-forget heartbeat ACKs — server always responds but callers don't # wait on these; silently discard to avoid "Unmatched Response" noise. - if cmd_type == CMD_TYPE["Response"] and cmd in ( + if cmd_type == CMD_TYPE["Response"] and cmd in { "send_group_heartbeat", "send_private_heartbeat", - ): + }: logger.debug("[%s] Heartbeat ACK received: cmd=%s msg_id=%s", adapter.name, cmd, msg_id) return @@ -3369,7 +3369,7 @@ class MediaSendHandler(ABC): # Remove keys already passed explicitly to avoid "multiple values" TypeError fwd_kwargs = { k: v for k, v in kwargs.items() - if k not in ("file_uuid", "filename", "content_type") + if k not in {"file_uuid", "filename", "content_type"} } msg_body = self.build_msg_body( upload_result, diff --git a/gateway/platforms/yuanbao_media.py b/gateway/platforms/yuanbao_media.py index 39f8d88d8a3..87eefcddae2 100644 --- a/gateway/platforms/yuanbao_media.py +++ b/gateway/platforms/yuanbao_media.py @@ -150,7 +150,7 @@ def _parse_jpeg_size(buf: bytes) -> Optional[dict[str, int]]: i += 1 continue marker = buf[i + 1] - if marker in (0xC0, 0xC2): + if marker in {0xC0, 0xC2}: h = struct.unpack(">H", buf[i + 5: i + 7])[0] w = struct.unpack(">H", buf[i + 7: i + 9])[0] return {"width": w, "height": h} @@ -165,7 +165,7 @@ def _parse_gif_size(buf: bytes) -> Optional[dict[str, int]]: if len(buf) < 10: return None sig = buf[:6].decode("ascii", errors="replace") - if sig not in ("GIF87a", "GIF89a"): + if sig not in {"GIF87a", "GIF89a"}: return None w = struct.unpack(" Optional[dict]: "trace_id": trace_id, } # 过滤空值(保持 API 整洁) - return {k: v for k, v in result.items() if v or k in ("msg_body", "msg_seq")} + return {k: v for k, v in result.items() if v or k in {"msg_body", "msg_seq"}} except Exception as e: if DEBUG_MODE: logger.debug("[yuanbao_proto] decode_inbound_push failed: %s", e) diff --git a/gateway/run.py b/gateway/run.py index 2ced433aa0e..1da45e3f03f 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -288,7 +288,7 @@ def _last_transcript_timestamp(history: Optional[List[Dict[str, Any]]]) -> Any: if not isinstance(msg, dict): continue role = msg.get("role") - if not role or role in ("session_meta", "system"): + if not role or role in {"session_meta", "system"}: continue ts = msg.get("timestamp") if ts is not None: @@ -472,7 +472,7 @@ if _config_path.exists(): # gateway resolves these to Path.home() later (line ~255). # Writing the raw placeholder here would just be noise. # Only bridge explicit absolute paths from config.yaml. - if _cfg_key == "cwd" and str(_val) in (".", "auto", "cwd"): + if _cfg_key == "cwd" and str(_val) in {".", "auto", "cwd"}: continue # Expand shell tilde in cwd so subprocess.Popen never # receives a literal "~/" which the kernel rejects. @@ -616,7 +616,7 @@ os.environ["HERMES_EXEC_ASK"] = "1" # to home directory. MESSAGING_CWD is accepted as a backward-compat # fallback (deprecated — the warning above tells users to migrate). _configured_cwd = os.environ.get("TERMINAL_CWD", "") -if not _configured_cwd or _configured_cwd in (".", "auto", "cwd"): +if not _configured_cwd or _configured_cwd in {".", "auto", "cwd"}: _fallback = os.getenv("MESSAGING_CWD") or str(Path.home()) os.environ["TERMINAL_CWD"] = _fallback @@ -849,7 +849,7 @@ def _skill_slug_from_frontmatter(skill_md: Path) -> tuple[str | None, str | None if line.startswith("name:"): raw = line.split(":", 1)[1].strip() # Strip YAML quote wrappers if present - if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in ('"', "'"): + if len(raw) >= 2 and raw[0] == raw[-1] and raw[0] in {'"', "'"}: raw = raw[1:-1] declared_name = raw.strip() break @@ -891,7 +891,7 @@ def _check_unavailable_skill(command_name: str) -> str | None: if not skills_dir.exists(): continue for skill_md in skills_dir.rglob("SKILL.md"): - if any(part in ('.git', '.github', '.hub', '.archive') for part in skill_md.parts): + if any(part in {'.git', '.github', '.hub', '.archive'} for part in skill_md.parts): continue slug, declared_name = _skill_slug_from_frontmatter(skill_md) if not slug or not declared_name: @@ -1033,7 +1033,7 @@ def _parse_session_key(session_key: str) -> "dict | None": "chat_type": parts[3], "chat_id": parts[4], } - if len(parts) > 5 and parts[3] in ("dm", "thread"): + if len(parts) > 5 and parts[3] in {"dm", "thread"}: result["thread_id"] = parts[5] return result return None @@ -1561,7 +1561,7 @@ class GatewayRunner: enabled_chats.clear() enabled_chats.update( key[len(prefix):] for key, mode in self._voice_mode.items() - if mode in ("voice_only", "all") and key.startswith(prefix) + if mode in {"voice_only", "all"} and key.startswith(prefix) ) async def _safe_adapter_disconnect(self, adapter, platform) -> None: @@ -1991,7 +1991,7 @@ class GatewayRunner: # Both "queue" and "steer" modes imply the user doesn't want messages # to be lost during restart — queue them for the newly-spawned gateway # process to pick up. "interrupt" mode drops them (current behaviour). - return self._restart_requested and self._busy_input_mode in ("queue", "steer") + return self._restart_requested and self._busy_input_mode in {"queue", "steer"} # -------- /queue FIFO helpers -------------------------------------- # /queue must produce one full agent turn per invocation, in FIFO @@ -2401,7 +2401,7 @@ class GatewayRunner: raw = cfg_get(cfg, "display", "background_process_notifications") if raw is False: mode = "off" - elif raw not in (None, ""): + elif raw not in {None, ""}: mode = str(raw) except Exception: pass @@ -3247,7 +3247,7 @@ class GatewayRunner: # for this process's lifetime. try: _redact_raw = os.getenv("HERMES_REDACT_SECRETS", "true") - _redact_on = _redact_raw.lower() in ("1", "true", "yes", "on") + _redact_on = _redact_raw.lower() in {"1", "true", "yes", "on"} if _redact_on: logger.info( "Secret redaction: ENABLED (tool output, logs, and chat " @@ -3329,8 +3329,8 @@ class GatewayRunner: _any_allowlist = any( os.getenv(v) for v in _builtin_allowed_vars + _plugin_allowed_vars ) - _allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes") or any( - os.getenv(v, "").lower() in ("true", "1", "yes") + _allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"} or any( + os.getenv(v, "").lower() in {"true", "1", "yes"} for v in _builtin_allow_all_vars + _plugin_allow_all_vars ) if not _any_allowlist and not _allow_all: @@ -4379,7 +4379,7 @@ class GatewayRunner: # dispatcher respawns the task and it cycles into the # same state. See the longer comment on TERMINAL_KINDS # above for the failure mode this prevents. - task_terminal = task and task.status in ("done", "archived") + task_terminal = task and task.status in {"done", "archived"} if task_terminal: await asyncio.to_thread( self._kanban_unsub, sub, board_slug, @@ -4479,7 +4479,7 @@ class GatewayRunner: logger.warning("kanban dispatcher: config loader unavailable; disabled") return env_override = os.environ.get("HERMES_KANBAN_DISPATCH_IN_GATEWAY", "").strip().lower() - if env_override in ("0", "false", "no", "off"): + if env_override in {"0", "false", "no", "off"}: logger.info("kanban dispatcher: disabled via HERMES_KANBAN_DISPATCH_IN_GATEWAY env") return @@ -5156,12 +5156,12 @@ class GatewayRunner: try: _gw_cfg = _load_gateway_config() _raw = cfg_get(_gw_cfg, "display", "platforms", "telegram", "notifications") - if _raw not in (None, ""): + if _raw not in {None, ""}: _notify_mode = str(_raw).strip().lower() except Exception: pass _notify_mode = _notify_mode or "important" - if _notify_mode not in ("all", "important"): + if _notify_mode not in {"all", "important"}: logger.warning( "Unknown telegram notifications mode '%s', " "defaulting to 'important' (valid: all, important)", @@ -5338,7 +5338,7 @@ class GatewayRunner: # connection, so HA events are always authorized. # Webhook events are authenticated via HMAC signature validation in # the adapter itself — no user allowlist applies. - if source.platform in (Platform.HOMEASSISTANT, Platform.WEBHOOK): + if source.platform in {Platform.HOMEASSISTANT, Platform.WEBHOOK}: return True user_id = source.user_id @@ -5411,12 +5411,12 @@ class GatewayRunner: # Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true) platform_allow_all_var = platform_allow_all_map.get(source.platform, "") - if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in ("true", "1", "yes"): + if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in {"true", "1", "yes"}: return True if getattr(source, "is_bot", False): allow_bots_var = platform_allow_bots_map.get(source.platform) - if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in ("mentions", "all"): + if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in {"mentions", "all"}: return True # Discord role-based access (DISCORD_ALLOWED_ROLES): the adapter's @@ -5447,7 +5447,7 @@ class GatewayRunner: if not platform_allowlist and not group_user_allowlist and not group_chat_allowlist and not global_allowlist: # No allowlists configured -- check global allow-all flag - return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes") + return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in {"true", "1", "yes"} # Telegram can optionally authorize group traffic by chat ID. # Keep this separate from TELEGRAM_GROUP_ALLOWED_USERS, which gates @@ -5742,9 +5742,9 @@ class GatewayRunner: raw = (event.text or "").strip() # Accept /approve and /deny as shorthand for yes/no cmd = event.get_command() - if cmd in ("approve", "yes"): + if cmd in {"approve", "yes"}: response_text = "y" - elif cmd in ("deny", "no"): + elif cmd in {"deny", "no"}: response_text = "n" else: _recognized_cmd = None @@ -5826,17 +5826,17 @@ class GatewayRunner: _raw_reply = (event.text or "").strip() _cmd_reply = event.get_command() _confirm_choice = None - if _cmd_reply in ("approve", "yes", "ok", "confirm"): + if _cmd_reply in {"approve", "yes", "ok", "confirm"}: _confirm_choice = "once" - elif _cmd_reply in ("always", "remember"): + elif _cmd_reply in {"always", "remember"}: _confirm_choice = "always" - elif _cmd_reply in ("cancel", "no", "deny", "nevermind"): + elif _cmd_reply in {"cancel", "no", "deny", "nevermind"}: _confirm_choice = "cancel" - elif _raw_reply.lower() in ("approve", "approve once", "once"): + elif _raw_reply.lower() in {"approve", "approve once", "once"}: _confirm_choice = "once" - elif _raw_reply.lower() in ("always", "always approve"): + elif _raw_reply.lower() in {"always", "always approve"}: _confirm_choice = "always" - elif _raw_reply.lower() in ("cancel", "nevermind", "no"): + elif _raw_reply.lower() in {"cancel", "nevermind", "no"}: _confirm_choice = "cancel" if _confirm_choice is not None: _resolved = await _slash_confirm_mod.resolve( @@ -5972,7 +5972,7 @@ class GatewayRunner: # Semantics: each /queue invocation produces its own full agent # turn, processed in FIFO order after the current run (and any # earlier /queue items) finishes. Messages are NOT merged. - if event.get_command() in ("queue", "q"): + if event.get_command() in {"queue", "q"}: queued_text = event.get_command_args().strip() if not queued_text: return "Usage: /queue " @@ -6045,7 +6045,7 @@ class GatewayRunner: # The agent thread is blocked on a threading.Event inside # tools/approval.py — sending an interrupt won't unblock it. # Route directly to the approval handler so the event is signalled. - if _cmd_def_inner and _cmd_def_inner.name in ("approve", "deny"): + if _cmd_def_inner and _cmd_def_inner.name in {"approve", "deny"}: if _cmd_def_inner.name == "approve": return await self._handle_approve_command(event) return await self._handle_deny_command(event) @@ -6076,7 +6076,7 @@ class GatewayRunner: # continuation prompt against the current turn. if _cmd_def_inner and _cmd_def_inner.name == "goal": _goal_arg = (event.get_command_args() or "").strip().lower() - if not _goal_arg or _goal_arg in ("status", "pause", "resume", "clear", "stop", "done"): + if not _goal_arg or _goal_arg in {"status", "pause", "resume", "clear", "stop", "done"}: return await self._handle_goal_command(event) return "Agent is running — use /goal status / pause / clear mid-run, or /stop before setting a new goal." @@ -6088,7 +6088,7 @@ class GatewayRunner: # /fast and /reasoning are config-only and take effect next # message, so they fall through to the catch-all busy response # below — users should wait and set them between turns. - if _cmd_def_inner and _cmd_def_inner.name in ("yolo", "verbose"): + if _cmd_def_inner and _cmd_def_inner.name in {"yolo", "verbose"}: if _cmd_def_inner.name == "yolo": return await self._handle_yolo_command(event) if _cmd_def_inner.name == "verbose": @@ -6711,7 +6711,7 @@ class GatewayRunner: mtype = event.media_types[i] if i < len(event.media_types) else "" if mtype.startswith("image/") or event.message_type == MessageType.PHOTO: image_paths.append(path) - if mtype.startswith("audio/") or event.message_type in (MessageType.VOICE, MessageType.AUDIO): + if mtype.startswith("audio/") or event.message_type in {MessageType.VOICE, MessageType.AUDIO}: audio_paths.append(path) if image_paths: @@ -6780,7 +6780,7 @@ class GatewayRunner: _TEXT_EXTENSIONS = {".txt", ".md", ".csv", ".log", ".json", ".xml", ".yaml", ".yml", ".toml", ".ini", ".cfg"} for i, path in enumerate(event.media_urls): mtype = event.media_types[i] if i < len(event.media_types) else "" - if mtype in ("", "application/octet-stream"): + if mtype in {"", "application/octet-stream"}: _ext = os.path.splitext(path)[1].lower() if _ext in _TEXT_EXTENSIONS: mtype = "text/plain" @@ -7164,7 +7164,7 @@ class GatewayRunner: if isinstance(_comp_cfg, dict): _hyg_compression_enabled = str( _comp_cfg.get("enabled", True) - ).lower() in ("true", "1", "yes") + ).lower() in {"true", "1", "yes"} _raw_hard_limit = _comp_cfg.get("hygiene_hard_message_limit") if _raw_hard_limit is not None: try: @@ -7287,7 +7287,7 @@ class GatewayRunner: _hyg_msgs = [ {"role": m.get("role"), "content": m.get("content")} for m in history - if m.get("role") in ("user", "assistant") + if m.get("role") in {"user", "assistant"} and m.get("content") ] @@ -7651,7 +7651,7 @@ class GatewayRunner: while not _pr.completion_queue.empty(): evt = _pr.completion_queue.get_nowait() evt_type = evt.get("type", "completion") - if evt_type in ("watch_match", "watch_disabled"): + if evt_type in {"watch_match", "watch_disabled"}: _watch_events.append(evt) # else: completion events are handled by the watcher task for evt in _watch_events: @@ -7893,7 +7893,7 @@ class GatewayRunner: status_hint = " You are being rate-limited. Please wait a moment and try again." elif status_code == 529: status_hint = " The API is temporarily overloaded. Please try again shortly." - elif status_code in (400, 500): + elif status_code in {400, 500}: # 400 with a large session is context overflow. # 500 with a large session often means the payload is too large # for the API to process — treat it the same way. @@ -8255,7 +8255,7 @@ class GatewayRunner: policy = _policy_for_source(self.config, source) platform = source.platform.value if source and source.platform else "?" chat_type = (source.chat_type if source else "") or "dm" - scope = "DM" if chat_type.lower() in ("dm", "direct", "private", "") else "group/channel" + scope = "DM" if chat_type.lower() in {"dm", "direct", "private", ""} else "group/channel" user_id = (source.user_id if source else None) or "?" if not policy.enabled: @@ -9193,7 +9193,7 @@ class GatewayRunner: return "\n".join(p for p in parts if p) return str(value) - if args in ("none", "default", "neutral"): + if args in {"none", "default", "neutral"}: try: if "agent" not in config or not isinstance(config.get("agent"), dict): config["agent"] = {} @@ -9345,7 +9345,7 @@ class GatewayRunner: return t("gateway.goal.no_resume") return t("gateway.goal.resumed", goal=state.goal) - if lower in ("clear", "stop", "done"): + if lower in {"clear", "stop", "done"}: had = mgr.has_goal() mgr.clear() try: @@ -9598,13 +9598,13 @@ class GatewayRunner: adapter = self.adapters.get(platform) - if args in ("on", "enable"): + if args in {"on", "enable"}: self._voice_mode[voice_key] = "voice_only" self._save_voice_modes() if adapter: self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True) return t("gateway.voice.enabled_voice_only") - elif args in ("off", "disable"): + elif args in {"off", "disable"}: self._voice_mode[voice_key] = "off" self._save_voice_modes() if adapter: @@ -9616,7 +9616,7 @@ class GatewayRunner: if adapter: self._set_adapter_auto_tts_enabled(adapter, chat_id, enabled=True) return t("gateway.voice.tts_enabled") - elif args in ("channel", "join"): + elif args in {"channel", "join"}: return await self._handle_voice_channel_join(event) elif args == "leave": return await self._handle_voice_channel_leave(event) @@ -10390,12 +10390,12 @@ class GatewayRunner: # Display toggle (per-platform) platform_key = _platform_config_key(event.source.platform) - if args in ("show", "on"): + if args in {"show", "on"}: self._show_reasoning = True _save_config_key(f"display.platforms.{platform_key}.show_reasoning", True) return t("gateway.reasoning.display_set_on", platform=platform_key) - if args in ("hide", "off"): + if args in {"hide", "off"}: self._show_reasoning = False _save_config_key(f"display.platforms.{platform_key}.show_reasoning", False) return t("gateway.reasoning.display_set_off", platform=platform_key) @@ -10411,7 +10411,7 @@ class GatewayRunner: return t("gateway.reasoning.reset_done") if effort == "none": parsed = {"enabled": False} - elif effort in ("minimal", "low", "medium", "high", "xhigh"): + elif effort in {"minimal", "low", "medium", "high", "xhigh"}: parsed = {"enabled": True, "effort": effort} else: return t( @@ -10603,7 +10603,7 @@ class GatewayRunner: effective = resolve_footer_config(user_config, platform_key) - if arg in ("status", "?"): + if arg in {"status", "?"}: state = t("gateway.footer.state_on") if effective["enabled"] else t("gateway.footer.state_off") fields = ", ".join(effective.get("fields") or []) return t( @@ -10613,9 +10613,9 @@ class GatewayRunner: platform=platform_key, ) - if arg in ("on", "enable", "true", "1"): + if arg in {"on", "enable", "true", "1"}: new_state = True - elif arg in ("off", "disable", "false", "0"): + elif arg in {"off", "disable", "false", "0"}: new_state = False elif arg == "": new_state = not effective["enabled"] @@ -10683,7 +10683,7 @@ class GatewayRunner: msgs = [ {"role": m.get("role"), "content": m.get("content")} for m in history - if m.get("role") in ("user", "assistant") and m.get("content") + if m.get("role") in {"user", "assistant"} and m.get("content") ] tmp_agent = AIAgent( @@ -11597,7 +11597,7 @@ class GatewayRunner: history = self.session_store.load_transcript(session_entry.session_id) if history: from agent.model_metadata import estimate_messages_tokens_rough - msgs = [m for m in history if m.get("role") in ("user", "assistant") and m.get("content")] + msgs = [m for m in history if m.get("role") in {"user", "assistant"} and m.get("content")] approx = estimate_messages_tokens_rough(msgs) lines = [ t("gateway.usage.header_session_info"), @@ -12151,9 +12151,9 @@ class GatewayRunner: resolve_all = "all" in args remaining = [a for a in args if a != "all"] - if any(a in ("always", "permanent", "permanently") for a in remaining): + if any(a in {"always", "permanent", "permanently"} for a in remaining): choice = "always" - elif any(a in ("session", "ses") for a in remaining): + elif any(a in {"session", "ses"} for a in remaining): choice = "session" else: choice = "once" @@ -13270,8 +13270,8 @@ class GatewayRunner: # --- Normal text-only notification --- # Decide whether to notify based on mode should_notify = ( - notify_mode in ("all", "result") - or (notify_mode == "error" and session.exit_code not in (0, None)) + notify_mode in {"all", "result"} + or (notify_mode == "error" and session.exit_code not in {0, None}) ) if should_notify: new_output = session.output_buffer[-1000:] if session.output_buffer else "" @@ -13866,7 +13866,7 @@ class GatewayRunner: for msg in history: role = msg.get("role") content = msg.get("content") - if role in ("user", "assistant") and content: + if role in {"user", "assistant"} and content: api_messages.append({"role": role, "content": content}) api_messages.append({"role": "user", "content": message}) @@ -14257,7 +14257,7 @@ class GatewayRunner: # Only act on tool.started events (ignore tool.completed, reasoning.available, etc.) - if event_type not in ("tool.started",): + if event_type not in {"tool.started",}: return # Suppress tool-progress bubbles once the user has sent `stop`. @@ -14954,7 +14954,7 @@ class GatewayRunner: # Skip metadata entries (tool definitions, session info) # -- these are for transcript logging, not for the LLM - if role in ("session_meta",): + if role in {"session_meta",}: continue # Skip system messages -- the agent rebuilds its own system prompt @@ -14991,7 +14991,7 @@ class GatewayRunner: # even if the message list shrinks, we know which paths are old. _history_media_paths: set = set() for _hm in agent_history: - if _hm.get("role") in ("tool", "function"): + if _hm.get("role") in {"tool", "function"}: _hc = _hm.get("content", "") if "MEDIA:" in _hc: for _match in re.finditer(r'MEDIA:(\S+)', _hc): @@ -15263,7 +15263,7 @@ class GatewayRunner: media_tags = [] has_voice_directive = False for msg in result.get("messages", []): - if msg.get("role") in ("tool", "function"): + if msg.get("role") in {"tool", "function"}: content = msg.get("content", "") if "MEDIA:" in content: for match in re.finditer(r'MEDIA:(\S+)', content): diff --git a/gateway/session.py b/gateway/session.py index c145625ae44..ac6f95eec63 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -764,12 +764,12 @@ class SessionStore: now = _now() - if policy.mode in ("idle", "both"): + if policy.mode in {"idle", "both"}: idle_deadline = entry.updated_at + timedelta(minutes=policy.idle_minutes) if now > idle_deadline: return True - if policy.mode in ("daily", "both"): + if policy.mode in {"daily", "both"}: today_reset = now.replace( hour=policy.at_hour, minute=0, second=0, microsecond=0, @@ -805,12 +805,12 @@ class SessionStore: now = _now() - if policy.mode in ("idle", "both"): + if policy.mode in {"idle", "both"}: idle_deadline = entry.updated_at + timedelta(minutes=policy.idle_minutes) if now > idle_deadline: return "idle" - if policy.mode in ("daily", "both"): + if policy.mode in {"daily", "both"}: today_reset = now.replace( hour=policy.at_hour, minute=0, diff --git a/gateway/status.py b/gateway/status.py index 25df7dc02eb..2849e775080 100644 --- a/gateway/status.py +++ b/gateway/status.py @@ -604,7 +604,7 @@ def acquire_scoped_lock(scope: str, identity: str, metadata: Optional[dict[str, for _line in _proc_status.read_text(encoding="utf-8").splitlines(): if _line.startswith("State:"): _state = _line.split()[1] - if _state in ("T", "t"): # stopped or tracing stop + if _state in {"T", "t"}: # stopped or tracing stop stale = True break except (OSError, PermissionError): diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 42e2f720874..7db897cb55b 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -1450,7 +1450,7 @@ def resolve_provider( # whose availability isn't implied by LM_API_KEY presence (it may be # offline, and the no-auth setup uses a placeholder value), so it # also requires explicit selection. - if pid in ("copilot", "lmstudio"): + if pid in {"copilot", "lmstudio"}: continue for env_var in pconfig.api_key_env_vars: if has_usable_secret(os.getenv(env_var, "")): @@ -2541,7 +2541,7 @@ def refresh_codex_oauth_pure( # A 401/403 from the token endpoint always means the refresh token # is invalid/expired — force relogin even if the body error code # wasn't one of the known strings above. - if response.status_code in (401, 403) and not relogin_required: + if response.status_code in {401, 403} and not relogin_required: relogin_required = True raise AuthError( message, @@ -2947,7 +2947,7 @@ def _merge_shared_nous_oauth_state(state: Dict[str, Any]) -> bool: "expires_at", ): value = shared.get(key) - if value not in (None, ""): + if value not in {None, ""}: state[key] = value return True @@ -3986,7 +3986,7 @@ def get_api_key_provider_status(provider_id: str) -> Dict[str, Any]: if pconfig.base_url_env_var: env_url = os.getenv(pconfig.base_url_env_var, "").strip() - if provider_id in ("kimi-coding", "kimi-coding-cn"): + if provider_id in {"kimi-coding", "kimi-coding-cn"}: base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url) elif env_url: base_url = env_url @@ -4090,7 +4090,7 @@ def resolve_api_key_provider_credentials(provider_id: str) -> Dict[str, Any]: if pconfig.base_url_env_var: env_url = os.getenv(pconfig.base_url_env_var, "").strip() - if provider_id in ("kimi-coding", "kimi-coding-cn"): + if provider_id in {"kimi-coding", "kimi-coding-cn"}: base_url = _resolve_kimi_base_url(api_key, pconfig.inference_base_url, env_url) elif provider_id == "zai": base_url = _resolve_zai_base_url(api_key, pconfig.inference_base_url, env_url) @@ -4510,7 +4510,7 @@ def _login_openai_codex( reuse = input("Use existing credentials? [Y/n]: ").strip().lower() except (EOFError, KeyboardInterrupt): reuse = "y" - if reuse in ("", "y", "yes"): + if reuse in {"", "y", "yes"}: config_path = _update_config_for_provider("openai-codex", existing.get("base_url", DEFAULT_CODEX_BASE_URL)) print() print("Login successful!") @@ -4531,7 +4531,7 @@ def _login_openai_codex( do_import = input("Import these credentials? (a separate login is recommended) [y/N]: ").strip().lower() except (EOFError, KeyboardInterrupt): do_import = "n" - if do_import in ("y", "yes"): + if do_import in {"y", "yes"}: _save_codex_tokens(cli_tokens) base_url = os.getenv("HERMES_CODEX_BASE_URL", "").strip().rstrip("/") or DEFAULT_CODEX_BASE_URL config_path = _update_config_for_provider("openai-codex", base_url) @@ -4623,7 +4623,7 @@ def _codex_device_code_login() -> Dict[str, Any]: if poll_resp.status_code == 200: code_resp = poll_resp.json() break - elif poll_resp.status_code in (403, 404): + elif poll_resp.status_code in {403, 404}: continue # User hasn't completed login yet else: raise AuthError( @@ -5188,7 +5188,7 @@ def _login_nous(args, pconfig: ProviderConfig) -> None: do_import = input("Import these credentials? [Y/n]: ").strip().lower() except (EOFError, KeyboardInterrupt): do_import = "y" - if do_import in ("", "y", "yes"): + if do_import in {"", "y", "yes"}: print("Rehydrating Nous session from shared credentials...") auth_state = _try_import_shared_nous_state( timeout_seconds=timeout_seconds, diff --git a/hermes_cli/auth_commands.py b/hermes_cli/auth_commands.py index 4312f688a3f..b701a54725a 100644 --- a/hermes_cli/auth_commands.py +++ b/hermes_cli/auth_commands.py @@ -266,7 +266,7 @@ def auth_add_command(args) -> None: do_import = input("Import these credentials? [Y/n]: ").strip().lower() except (EOFError, KeyboardInterrupt): do_import = "y" - if do_import in ("", "y", "yes"): + if do_import in {"", "y", "yes"}: print("Rehydrating Nous session from shared credentials...") rehydrated = auth_mod._try_import_shared_nous_state( timeout_seconds=getattr(args, "timeout", None) or 15.0, diff --git a/hermes_cli/backup.py b/hermes_cli/backup.py index 728f1fd89ce..a137509d7b1 100644 --- a/hermes_cli/backup.py +++ b/hermes_cli/backup.py @@ -298,7 +298,7 @@ def _detect_prefix(zf: zipfile.ZipFile) -> str: if len(first_parts) == 1: prefix = first_parts.pop() # Only strip if it looks like a hermes dir name - if prefix in (".hermes", "hermes"): + if prefix in {".hermes", "hermes"}: return prefix + "/" return "" @@ -349,7 +349,7 @@ def run_import(args) -> None: except (EOFError, KeyboardInterrupt): print("\nAborted.") sys.exit(1) - if answer not in ("y", "yes"): + if answer not in {"y", "yes"}: print("Aborted.") return diff --git a/hermes_cli/checkpoints.py b/hermes_cli/checkpoints.py index cac5cd0979f..2c0d3dd107b 100644 --- a/hermes_cli/checkpoints.py +++ b/hermes_cli/checkpoints.py @@ -139,7 +139,7 @@ def _confirm(prompt: str) -> bool: except (EOFError, KeyboardInterrupt): print() return False - return resp in ("y", "yes") + return resp in {"y", "yes"} def cmd_clear(args: argparse.Namespace) -> int: diff --git a/hermes_cli/claw.py b/hermes_cli/claw.py index 513fcc7d7ea..909b046f1f7 100644 --- a/hermes_cli/claw.py +++ b/hermes_cli/claw.py @@ -298,7 +298,7 @@ def claw_command(args): if action == "migrate": _cmd_migrate(args) - elif action in ("cleanup", "clean"): + elif action in {"cleanup", "clean"}: _cmd_cleanup(args) else: print("Usage: hermes claw [options]") diff --git a/hermes_cli/codex_models.py b/hermes_cli/codex_models.py index 8e50004c2d6..e45ba33f8eb 100644 --- a/hermes_cli/codex_models.py +++ b/hermes_cli/codex_models.py @@ -101,7 +101,7 @@ def _fetch_models_from_api(access_token: str) -> List[str]: # Some valid Codex CLI models (for example gpt-5.3-codex-spark) are # marked false here but are still accepted by the Codex route. visibility = item.get("visibility", "") - if isinstance(visibility, str) and visibility.strip().lower() in ("hide", "hidden"): + if isinstance(visibility, str) and visibility.strip().lower() in {"hide", "hidden"}: continue priority = item.get("priority") rank = int(priority) if isinstance(priority, (int, float)) else 10_000 @@ -152,7 +152,7 @@ def _read_cache_models(codex_home: Path) -> List[str]: # public OpenAI API, while Hermes openai-codex talks to the same # OAuth-backed Codex backend as Codex CLI. visibility = item.get("visibility") - if isinstance(visibility, str) and visibility.strip().lower() in ("hide", "hidden"): + if isinstance(visibility, str) and visibility.strip().lower() in {"hide", "hidden"}: continue priority = item.get("priority") rank = int(priority) if isinstance(priority, (int, float)) else 10_000 diff --git a/hermes_cli/config.py b/hermes_cli/config.py index feeb10892f2..f4cedcb75bc 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -3202,7 +3202,7 @@ def warn_deprecated_cwd_env_vars(config: Optional[Dict[str, Any]] = None) -> Non terminal_cfg = config.get("terminal", {}) config_cwd = terminal_cfg.get("cwd", ".") if isinstance(terminal_cfg, dict) else "." # Only warn if config.yaml doesn't have an explicit path - config_has_explicit_cwd = config_cwd not in (".", "auto", "cwd", "") + config_has_explicit_cwd = config_cwd not in {".", "auto", "cwd", ""} lines: list[str] = [] if messaging_cwd: @@ -3262,10 +3262,10 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A if "tool_progress" not in display: old_enabled = get_env_value("HERMES_TOOL_PROGRESS") old_mode = get_env_value("HERMES_TOOL_PROGRESS_MODE") - if old_enabled and old_enabled.lower() in ("false", "0", "no"): + if old_enabled and old_enabled.lower() in {"false", "0", "no"}: display["tool_progress"] = "off" results["config_added"].append("display.tool_progress=off (from HERMES_TOOL_PROGRESS=false)") - elif old_mode and old_mode.lower() in ("new", "all"): + elif old_mode and old_mode.lower() in {"new", "all"}: display["tool_progress"] = old_mode.lower() results["config_added"].append(f"display.tool_progress={old_mode.lower()} (from HERMES_TOOL_PROGRESS_MODE)") else: @@ -3344,7 +3344,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A new_entry = {"api": old_url} if old_name: new_entry["name"] = old_name - if old_key and old_key not in ("no-key", "no-key-required", ""): + if old_key and old_key not in {"no-key", "no-key-required", ""}: new_entry["api_key"] = old_key # Carry over model and api_mode if present @@ -3402,7 +3402,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A stt.pop("model", None) # Place it in the appropriate provider section only if the # user didn't already set a model there - if provider in ("local", "local_command"): + if provider in {"local", "local_command"}: # Don't migrate an OpenAI model name into the local section _local_models = { "tiny.en", "tiny", "base.en", "base", "small.en", "small", @@ -3486,7 +3486,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A if not aux_comp.get("model"): aux_comp["model"] = str(s_model).strip() migrated_keys.append(f"model={s_model}") - if s_provider and str(s_provider).strip() not in ("", "auto"): + if s_provider and str(s_provider).strip() not in {"", "auto"}: aux = config.setdefault("auxiliary", {}) aux_comp = aux.setdefault("compression", {}) if not aux_comp.get("provider") or aux_comp.get("provider") == "auto": @@ -3717,7 +3717,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A except (EOFError, KeyboardInterrupt): answer = "n" - if answer in ("y", "yes"): + if answer in {"y", "yes"}: print() for name, info in new_and_unset: if info.get("url"): @@ -3778,7 +3778,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A except (EOFError, KeyboardInterrupt): answer = "n" - if answer in ("y", "yes"): + if answer in {"y", "yes"}: print() config = load_config() try: @@ -4860,9 +4860,9 @@ def set_config_value(key: str, value: str): # inline navigation here silently overwrote lists with dicts. # Convert value to appropriate type - if value.lower() in ('true', 'yes', 'on'): + if value.lower() in {'true', 'yes', 'on'}: value = True - elif value.lower() in ('false', 'no', 'off'): + elif value.lower() in {'false', 'no', 'off'}: value = False elif value.isdigit(): value = int(value) @@ -5067,7 +5067,7 @@ def _inject_profile_env_vars() -> None: try: from providers import list_providers for _pp in list_providers(): - if _pp.auth_type not in ("api_key",): + if _pp.auth_type not in {"api_key",}: continue for _var in _pp.env_vars: if _var in OPTIONAL_ENV_VARS: diff --git a/hermes_cli/copilot_auth.py b/hermes_cli/copilot_auth.py index 7475f80a2b1..e6f63a1557c 100644 --- a/hermes_cli/copilot_auth.py +++ b/hermes_cli/copilot_auth.py @@ -128,7 +128,7 @@ def _try_gh_cli_token() -> Optional[str]: # Build a clean env so gh doesn't short-circuit on GITHUB_TOKEN / GH_TOKEN clean_env = {k: v for k, v in os.environ.items() - if k not in ("GITHUB_TOKEN", "GH_TOKEN")} + if k not in {"GITHUB_TOKEN", "GH_TOKEN"}} for gh_path in _gh_cli_candidates(): cmd = [gh_path, "auth", "token"] diff --git a/hermes_cli/curator.py b/hermes_cli/curator.py index 38675b93ab8..190a052b48e 100644 --- a/hermes_cli/curator.py +++ b/hermes_cli/curator.py @@ -347,7 +347,7 @@ def _cmd_prune(args) -> int: except (EOFError, KeyboardInterrupt): print("\ncurator: aborted") return 1 - if reply not in ("y", "yes"): + if reply not in {"y", "yes"}: print("curator: aborted") return 1 @@ -449,7 +449,7 @@ def _cmd_rollback(args) -> int: except (EOFError, KeyboardInterrupt): print("\ncancelled") return 1 - if ans not in ("y", "yes"): + if ans not in {"y", "yes"}: print("cancelled") return 1 diff --git a/hermes_cli/curses_ui.py b/hermes_cli/curses_ui.py index 01d759d3872..57607cc31dd 100644 --- a/hermes_cli/curses_ui.py +++ b/hermes_cli/curses_ui.py @@ -139,16 +139,16 @@ def curses_checklist( stdscr.refresh() key = stdscr.getch() - if key in (curses.KEY_UP, ord("k")): + if key in {curses.KEY_UP, ord("k")}: cursor = (cursor - 1) % len(items) - elif key in (curses.KEY_DOWN, ord("j")): + elif key in {curses.KEY_DOWN, ord("j")}: cursor = (cursor + 1) % len(items) elif key == ord(" "): chosen.symmetric_difference_update({cursor}) - elif key in (curses.KEY_ENTER, 10, 13): + elif key in {curses.KEY_ENTER, 10, 13}: result_holder[0] = set(chosen) return - elif key in (27, ord("q")): + elif key in {27, ord("q")}: result_holder[0] = cancel_returns return @@ -265,14 +265,14 @@ def curses_radiolist( stdscr.refresh() key = stdscr.getch() - if key in (curses.KEY_UP, ord("k")): + if key in {curses.KEY_UP, ord("k")}: cursor = (cursor - 1) % len(items) - elif key in (curses.KEY_DOWN, ord("j")): + elif key in {curses.KEY_DOWN, ord("j")}: cursor = (cursor + 1) % len(items) - elif key in (ord(" "), curses.KEY_ENTER, 10, 13): + elif key in {ord(" "), curses.KEY_ENTER, 10, 13}: result_holder[0] = cursor return - elif key in (27, ord("q")): + elif key in {27, ord("q")}: result_holder[0] = cancel_returns return @@ -388,14 +388,14 @@ def curses_single_select( stdscr.refresh() key = stdscr.getch() - if key in (curses.KEY_UP, ord("k")): + if key in {curses.KEY_UP, ord("k")}: cursor = (cursor - 1) % len(all_items) - elif key in (curses.KEY_DOWN, ord("j")): + elif key in {curses.KEY_DOWN, ord("j")}: cursor = (cursor + 1) % len(all_items) - elif key in (curses.KEY_ENTER, 10, 13): + elif key in {curses.KEY_ENTER, 10, 13}: result_holder[0] = cursor return - elif key in (27, ord("q")): + elif key in {27, ord("q")}: result_holder[0] = None return diff --git a/hermes_cli/dingtalk_auth.py b/hermes_cli/dingtalk_auth.py index 798ce46fcb7..50d56e845ea 100644 --- a/hermes_cli/dingtalk_auth.py +++ b/hermes_cli/dingtalk_auth.py @@ -93,7 +93,7 @@ def poll_registration(device_code: str) -> dict: """ data = _api_post("/app/registration/poll", {"device_code": device_code}) status_raw = str(data.get("status", "")).strip().upper() - if status_raw not in ("WAITING", "SUCCESS", "FAIL", "EXPIRED"): + if status_raw not in {"WAITING", "SUCCESS", "FAIL", "EXPIRED"}: status_raw = "UNKNOWN" return { "status": status_raw, diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 2cefcbb7378..13f58a8509f 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -473,7 +473,7 @@ def run_doctor(args): if ( provider and _resolve_auth_provider is not None - and provider not in ("auto", "custom") + and provider not in {"auto", "custom"} ): try: runtime_provider = _resolve_auth_provider(provider) @@ -485,7 +485,7 @@ def run_doctor(args): if ( provider and _resolve_provider_full is not None - and provider not in ("auto", "custom") + and provider not in {"auto", "custom"} ): provider_def = _resolve_provider_full(provider, user_providers, custom_providers) catalog_provider = provider_def.id if provider_def is not None else None @@ -542,7 +542,7 @@ def run_doctor(args): # own env-var checks elsewhere in doctor, and get_auth_status() # returns a bare {logged_in: False} for anything it doesn't # explicitly dispatch, which would produce false positives. - if runtime_provider and runtime_provider not in ("auto", "custom", "openrouter"): + if runtime_provider and runtime_provider not in {"auto", "custom", "openrouter"}: try: from hermes_cli.auth import PROVIDER_REGISTRY, get_auth_status pconfig = PROVIDER_REGISTRY.get(runtime_provider) @@ -1010,7 +1010,7 @@ def run_doctor(args): issues.append(f"Set TERMINAL_VERCEL_RUNTIME to one of: {supported}") disk = os.getenv("TERMINAL_CONTAINER_DISK", "51200").strip() - if disk in ("", "0", "51200"): + if disk in {"", "0", "51200"}: check_ok("Vercel disk setting", "(uses platform default)") else: check_fail("Vercel custom disk unsupported", "(reset terminal.container_disk to 51200)") @@ -1036,7 +1036,7 @@ def run_doctor(args): for line in auth_status.detail_lines: check_info(f"Vercel auth {line}") - persistent = os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in ("1", "true", "yes", "on") + persistent = os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in {"1", "true", "yes", "on"} if persistent: check_info("Vercel persistence: snapshot filesystem only; live processes do not survive sandbox recreation") else: diff --git a/hermes_cli/fallback_cmd.py b/hermes_cli/fallback_cmd.py index 02c0a01c39d..9f2e6b97d46 100644 --- a/hermes_cli/fallback_cmd.py +++ b/hermes_cli/fallback_cmd.py @@ -307,7 +307,7 @@ def cmd_fallback_clear(args) -> None: # noqa: ARG001 print() print(" Cancelled.") return - if resp not in ("y", "yes"): + if resp not in {"y", "yes"}: print(" Cancelled — no change.") return @@ -347,11 +347,11 @@ def _numbered_pick(question: str, choices: List[str]) -> Optional[int]: def cmd_fallback(args) -> None: """Top-level dispatcher for ``hermes fallback [subcommand]``.""" sub = getattr(args, "fallback_command", None) - if sub in (None, "", "list", "ls"): + if sub in {None, "", "list", "ls"}: cmd_fallback_list(args) elif sub == "add": cmd_fallback_add(args) - elif sub in ("remove", "rm"): + elif sub in {"remove", "rm"}: cmd_fallback_remove(args) elif sub == "clear": cmd_fallback_clear(args) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index b43161bfac9..c3e1344556e 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -1194,7 +1194,7 @@ def _systemd_operational(system: bool = False) -> bool: ) # "running", "degraded", "starting" all mean systemd is PID 1 status = result.stdout.strip().lower() - return status in ("running", "degraded", "starting", "initializing") + return status in {"running", "degraded", "starting", "initializing"} except (RuntimeError, subprocess.TimeoutExpired, OSError): return False @@ -2915,7 +2915,7 @@ def launchd_start(): try: subprocess.run(["launchctl", "kickstart", f"{_launchd_domain()}/{label}"], check=True, timeout=30) except subprocess.CalledProcessError as e: - if e.returncode not in (3, 113): + if e.returncode not in {3, 113}: raise print("↻ launchd job was unloaded; reloading service definition") subprocess.run(["launchctl", "bootstrap", _launchd_domain(), str(plist_path)], check=True, timeout=30) @@ -2939,7 +2939,7 @@ def launchd_stop(): try: subprocess.run(["launchctl", "bootout", target], check=True, timeout=90) except subprocess.CalledProcessError as e: - if e.returncode in (3, 113): + if e.returncode in {3, 113}: pass # Already unloaded — nothing to stop. else: raise @@ -3011,7 +3011,7 @@ def launchd_restart(): subprocess.run(["launchctl", "kickstart", "-k", target], check=True, timeout=90) print("✓ Service restarted") except subprocess.CalledProcessError as e: - if e.returncode not in (3, 113): + if e.returncode not in {3, 113}: raise # Job not loaded — bootstrap and start fresh print("↻ launchd job was unloaded; reloading") @@ -3749,7 +3749,7 @@ def _platform_status(platform: dict) -> str: password = get_env_value("MATRIX_PASSWORD") if (val or password) and homeserver: e2ee = get_env_value("MATRIX_ENCRYPTION") - suffix = " + E2EE" if e2ee and e2ee.lower() in ("true", "1", "yes") else "" + suffix = " + E2EE" if e2ee and e2ee.lower() in {"true", "1", "yes"} else "" return f"configured{suffix}" if val or password or homeserver: return "partially configured" diff --git a/hermes_cli/goals.py b/hermes_cli/goals.py index 894cdddb01b..9e8742e08ae 100644 --- a/hermes_cli/goals.py +++ b/hermes_cli/goals.py @@ -270,7 +270,7 @@ def _parse_judge_response(raw: str) -> Tuple[bool, str, bool]: done_val = data.get("done") if isinstance(done_val, str): - done = done_val.strip().lower() in ("true", "yes", "1", "done") + done = done_val.strip().lower() in {"true", "yes", "1", "done"} else: done = bool(done_val) reason = str(data.get("reason") or "").strip() @@ -389,11 +389,11 @@ class GoalManager: return self._state is not None and self._state.status == "active" def has_goal(self) -> bool: - return self._state is not None and self._state.status in ("active", "paused") + return self._state is not None and self._state.status in {"active", "paused"} def status_line(self) -> str: s = self._state - if s is None or s.status in ("cleared",): + if s is None or s.status in {"cleared",}: return "No active goal. Set one with /goal ." turns = f"{s.turns_used}/{s.max_turns} turns" if s.status == "active": diff --git a/hermes_cli/hooks.py b/hermes_cli/hooks.py index 45b3fc63745..9bbec9997fe 100644 --- a/hermes_cli/hooks.py +++ b/hermes_cli/hooks.py @@ -32,11 +32,11 @@ def hooks_command(args) -> None: print("Run 'hermes hooks --help' for details.") return - if sub in ("list", "ls"): + if sub in {"list", "ls"}: _cmd_list(args) elif sub == "test": _cmd_test(args) - elif sub in ("revoke", "remove", "rm"): + elif sub in {"revoke", "remove", "rm"}: _cmd_revoke(args) elif sub == "doctor": _cmd_doctor(args) @@ -220,7 +220,7 @@ def _cmd_test(args) -> None: if getattr(args, "for_tool", None): specs = [ s for s in specs - if s.event not in ("pre_tool_call", "post_tool_call") + if s.event not in {"pre_tool_call", "post_tool_call"} or s.matches_tool(args.for_tool) ] diff --git a/hermes_cli/kanban.py b/hermes_cli/kanban.py index 862870c7d9e..76f95db4fac 100644 --- a/hermes_cli/kanban.py +++ b/hermes_cli/kanban.py @@ -82,7 +82,7 @@ def _parse_workspace_flag(value: str) -> tuple[str, Optional[str]]: if not value: return ("scratch", None) v = value.strip() - if v in ("scratch", "worktree"): + if v in {"scratch", "worktree"}: return (v, None) if v.startswith("dir:"): path = v[len("dir:"):].strip() @@ -788,15 +788,15 @@ def _dispatch_boards(args: argparse.Namespace) -> int: can still run ``boards create`` / ``boards list``. """ sub = getattr(args, "boards_action", None) or "list" - if sub in ("list", "ls"): + if sub in {"list", "ls"}: return _cmd_boards_list(args) - if sub in ("create", "new"): + if sub in {"create", "new"}: return _cmd_boards_create(args) - if sub in ("rm", "remove", "delete"): + if sub in {"rm", "remove", "delete"}: return _cmd_boards_rm(args) - if sub in ("switch", "use"): + if sub in {"switch", "use"}: return _cmd_boards_switch(args) - if sub in ("show", "current"): + if sub in {"show", "current"}: return _cmd_boards_show(args) if sub == "rename": return _cmd_boards_rename(args) @@ -1301,7 +1301,7 @@ def _cmd_show(args: argparse.Namespace) -> int: def _cmd_assign(args: argparse.Namespace) -> int: - profile = None if args.profile.lower() in ("none", "-", "null") else args.profile + profile = None if args.profile.lower() in {"none", "-", "null"} else args.profile with kb.connect() as conn: ok = kb.assign_task(conn, args.task_id, profile) if not ok: @@ -1328,7 +1328,7 @@ def _cmd_reclaim(args: argparse.Namespace) -> int: def _cmd_reassign(args: argparse.Namespace) -> int: - profile = None if args.profile.lower() in ("none", "-", "null") else args.profile + profile = None if args.profile.lower() in {"none", "-", "null"} else args.profile with kb.connect() as conn: ok = kb.reassign_task( conn, args.task_id, profile, @@ -2230,7 +2230,7 @@ def run_slash(rest: str) -> str: out = buf_out.getvalue().rstrip() err = buf_err.getvalue().rstrip() # Help dump (exit 0) → return the captured help text directly. - if exc.code in (0, None) and out: + if exc.code in {0, None} and out: return out body = err or out return f"⚠ /kanban usage error\n{body}" if body else "⚠ /kanban usage error" diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index 4746c85518a..0db694ff5b1 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -1844,7 +1844,7 @@ def recompute_ready(conn: sqlite3.Connection) -> int: "WHERE l.child_id = ?", (task_id,), ).fetchall() - if all(p["status"] in ("done", "archived") for p in parents): + if all(p["status"] in {"done", "archived"} for p in parents): conn.execute( "UPDATE tasks SET status = 'ready' WHERE id = ? AND status = 'todo'", (task_id,), diff --git a/hermes_cli/kanban_diagnostics.py b/hermes_cli/kanban_diagnostics.py index cf9b4b7708f..42c0c2043f2 100644 --- a/hermes_cli/kanban_diagnostics.py +++ b/hermes_cli/kanban_diagnostics.py @@ -177,7 +177,7 @@ def _active_hallucination_events( active: list[Any] = [] for ev in events: k = _event_kind(ev) - if k in ("completed", "edited"): + if k in {"completed", "edited"}: active.clear() elif k == kind: active.append(ev) @@ -193,7 +193,7 @@ def _latest_clean_event_ts(events: Iterable[Any]) -> int: """ latest = 0 for ev in events: - if _event_kind(ev) in ("completed", "edited"): + if _event_kind(ev) in {"completed", "edited"}: t = _event_ts(ev) latest = max(latest, t) return latest @@ -355,7 +355,7 @@ def _rule_repeated_failures(task, events, runs, now, cfg) -> list[Diagnostic]: most_recent_outcome = None for r in reversed(ordered_runs): oc = _task_field(r, "outcome") - if oc in ("spawn_failed", "timed_out", "crashed"): + if oc in {"spawn_failed", "timed_out", "crashed"}: most_recent_outcome = oc break @@ -373,7 +373,7 @@ def _rule_repeated_failures(task, events, runs, now, cfg) -> list[Diagnostic]: label=f"Fix profile auth: hermes -p {assignee} auth", payload={"command": f"hermes -p {assignee} auth"}, )) - elif most_recent_outcome in ("timed_out", "crashed"): + elif most_recent_outcome in {"timed_out", "crashed"}: # Worker got off the ground but died. Logs are the right place # to diagnose; reclaim/reassign are the recovery levers. task_id = _task_field(task, "id") @@ -466,7 +466,7 @@ def _rule_repeated_crashes(task, events, runs, now, cfg) -> list[Diagnostic]: consecutive += 1 if last_err is None: last_err = _task_field(r, "error") - elif outcome in ("completed", "reclaimed"): + elif outcome in {"completed", "reclaimed"}: # A success (or manual reclaim) breaks the streak. break else: @@ -541,7 +541,7 @@ def _rule_stuck_in_blocked(task, events, runs, now, cfg) -> list[Diagnostic]: return [] # Any comment / unblock after the block breaks the "stale" signal. for ev in events: - if _event_kind(ev) in ("commented", "unblocked") and _event_ts(ev) > last_blocked_ts: + if _event_kind(ev) in {"commented", "unblocked"} and _event_ts(ev) > last_blocked_ts: return [] actions: list[DiagnosticAction] = [ DiagnosticAction( diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 2bde766273d..e2c47835310 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -124,7 +124,7 @@ def _apply_profile_override() -> None: # 1. Check for explicit -p / --profile flag for i, arg in enumerate(argv): - if arg in ("--profile", "-p") and i + 1 < len(argv): + if arg in {"--profile", "-p"} and i + 1 < len(argv): profile_name = argv[i + 1] consume = 2 break @@ -192,7 +192,7 @@ def _apply_profile_override() -> None: # Strip the flag from argv so argparse doesn't choke if consume > 0: for i, arg in enumerate(argv): - if arg in ("--profile", "-p"): + if arg in {"--profile", "-p"}: start = i + 1 # +1 because argv is sys.argv[1:] sys.argv = sys.argv[:start] + sys.argv[start + consume :] break @@ -567,13 +567,13 @@ def _session_browse_picker(sessions: list) -> Optional[str]: stdscr.refresh() key = stdscr.getch() - if key in (curses.KEY_UP,): + if key in {curses.KEY_UP,}: if filtered: cursor = (cursor - 1) % len(filtered) - elif key in (curses.KEY_DOWN,): + elif key in {curses.KEY_DOWN,}: if filtered: cursor = (cursor + 1) % len(filtered) - elif key in (curses.KEY_ENTER, 10, 13): + elif key in {curses.KEY_ENTER, 10, 13}: if filtered: result_holder[0] = filtered[cursor]["id"] return @@ -587,7 +587,7 @@ def _session_browse_picker(sessions: list) -> Optional[str]: else: # Second Esc exits return - elif key in (curses.KEY_BACKSPACE, 127, 8): + elif key in {curses.KEY_BACKSPACE, 127, 8}: if search_text: search_text = search_text[:-1] if search_text: @@ -626,7 +626,7 @@ def _session_browse_picker(sessions: list) -> Optional[str]: while True: try: val = input(f"\n Select [1-{len(sessions)}]: ").strip() - if not val or val.lower() in ("q", "quit", "exit"): + if not val or val.lower() in {"q", "quit", "exit"}: return None idx = int(val) - 1 if 0 <= idx < len(sessions): @@ -1303,7 +1303,7 @@ def _launch_tui( except KeyboardInterrupt: code = 130 - if code in (0, 130): + if code in {0, 130}: _print_tui_exit_summary(resume_session_id, active_session_file) finally: try: @@ -1403,7 +1403,7 @@ def cmd_chat(args): reply = input("Run setup now? [Y/n] ").strip().lower() except (EOFError, KeyboardInterrupt): reply = "n" - if reply in ("", "y", "yes"): + if reply in {"", "y", "yes"}: cmd_setup(args) return print() @@ -1583,7 +1583,7 @@ def cmd_whatsapp(args): response = input("\n Update allowed users? [y/N] ").strip() except (EOFError, KeyboardInterrupt): response = "n" - if response.lower() in ("y", "yes"): + if response.lower() in {"y", "yes"}: if wa_mode == "bot": phone = input( " Phone numbers that can message the bot (comma-separated): " @@ -1658,7 +1658,7 @@ def cmd_whatsapp(args): ).strip() except (EOFError, KeyboardInterrupt): response = "n" - if response.lower() in ("y", "yes"): + if response.lower() in {"y", "yes"}: shutil.rmtree(session_dir, ignore_errors=True) session_dir.mkdir(parents=True, exist_ok=True) print(" ✓ Session cleared") @@ -2012,7 +2012,7 @@ def select_provider_and_model(args=None): _model_flow_bedrock(config, current_model) elif selected_provider == "azure-foundry": _model_flow_azure_foundry(config, current_model) - elif selected_provider in ( + elif selected_provider in { "gemini", "deepseek", "xai", @@ -2032,18 +2032,18 @@ def select_provider_and_model(args=None): "ollama-cloud", "tencent-tokenhub", "lmstudio", - ) or _is_profile_api_key_provider(selected_provider): + } or _is_profile_api_key_provider(selected_provider): _model_flow_api_key_provider(config, selected_provider, current_model) # ── Post-switch cleanup: clear stale OPENAI_BASE_URL ────────────── # When the user switches to a named provider (anything except "custom"), # a leftover OPENAI_BASE_URL in ~/.hermes/.env can poison auxiliary # clients that use provider:auto. Clear it proactively. (#5161) - if selected_provider not in ( + if selected_provider not in { "custom", "cancel", "remove-custom", - ) and not selected_provider.startswith("custom:"): + } and not selected_provider.startswith("custom:"): _clear_stale_openai_base_url() @@ -2169,7 +2169,7 @@ def _reset_aux_to_auto() -> int: entry = {} aux[task] = entry changed = False - if entry.get("provider") not in (None, "", "auto"): + if entry.get("provider") not in {None, "", "auto"}: entry["provider"] = "auto" changed = True for field in ("model", "base_url", "api_key"): @@ -3080,7 +3080,7 @@ def _model_flow_custom(config): _add_v1 = input(" Add /v1? [Y/n]: ").strip().lower() except (KeyboardInterrupt, EOFError): _add_v1 = "n" - if _add_v1 in ("", "y", "yes"): + if _add_v1 in {"", "y", "yes"}: effective_url = effective_url.rstrip("/") + "/v1" if base_url: base_url = effective_url @@ -3124,7 +3124,7 @@ def _model_flow_custom(config): if len(detected_models) == 1: print(f" Detected model: {detected_models[0]}") confirm = input(" Use this model? [Y/n]: ").strip().lower() - if confirm in ("", "y", "yes"): + if confirm in {"", "y", "yes"}: model_name = detected_models[0] else: model_name = input("Model name (e.g. gpt-4, llama-3-70b): ").strip() @@ -3957,7 +3957,7 @@ def _model_flow_copilot(config, current_model=""): api_key = creds.get("api_key", "") source = creds.get("source", "") else: - if source in ("GITHUB_TOKEN", "GH_TOKEN"): + if source in {"GITHUB_TOKEN", "GH_TOKEN"}: print(f" GitHub token: {api_key[:8]}... ✓ ({source})") elif source == "gh auth token": print(" GitHub token: ✓ (from `gh auth token`)") @@ -5277,7 +5277,7 @@ def cmd_slack(args): command registered as a first-class slash. """ sub = getattr(args, "slack_command", None) - if sub in (None, ""): + if sub in {None, ""}: # No subcommand — print usage hint. print( "usage: hermes slack \n" @@ -5424,7 +5424,7 @@ def _clear_bytecode_cache(root: Path) -> int: dirnames[:] = [ d for d in dirnames - if d not in ("venv", ".venv", "node_modules", ".git", ".worktrees") + if d not in {"venv", ".venv", "node_modules", ".git", ".worktrees"} ] if os.path.basename(dirpath) == "__pycache__": try: @@ -6219,7 +6219,7 @@ def _restore_stashed_changes( response = input_fn("Restore local changes now? [Y/n]", "y") else: response = input().strip().lower() - if response not in ("", "y", "yes"): + if response not in {"", "y", "yes"}: print("Skipped restoring local changes.") print("Your changes are still preserved in git stash.") print(f"Restore manually with: git stash apply {stash_ref}") @@ -6462,7 +6462,7 @@ def _sync_with_upstream_if_needed(git_cmd: list[str], cwd: Path) -> None: print() response = "n" - if response in ("", "y", "yes"): + if response in {"", "y", "yes"}: print("→ Adding upstream remote...") if _add_upstream_remote(git_cmd, cwd): print( @@ -7521,7 +7521,7 @@ def _cmd_update_impl(args, gateway_mode: bool): prompt_user=prompt_for_restore, input_fn=gw_input_fn, ) - if current_branch not in ("main", "HEAD"): + if current_branch not in {"main", "HEAD"}: subprocess.run( git_cmd + ["checkout", current_branch], cwd=PROJECT_ROOT, @@ -7805,7 +7805,7 @@ def _cmd_update_impl(args, gateway_mode: bool): except EOFError: response = "n" - if response in ("", "y", "yes", "auto"): + if response in {"", "y", "yes", "auto"}: print() # Gateway mode, --yes, and non-interactive update contexts # (dashboard / web server actions) cannot prompt for API keys. @@ -8866,7 +8866,7 @@ def cmd_profile(args): answer = input("\nProceed with install? [y/N] ").strip().lower() except (EOFError, KeyboardInterrupt): answer = "" - if answer not in ("y", "yes"): + if answer not in {"y", "yes"}: print("Install cancelled.") return @@ -8925,7 +8925,7 @@ def cmd_profile(args): answer = input("\nProceed? [y/N] ").strip().lower() except (EOFError, KeyboardInterrupt): answer = "" - if answer not in ("y", "yes"): + if answer not in {"y", "yes"}: print("Update cancelled.") return @@ -10713,9 +10713,9 @@ Examples: mem_dir = get_hermes_home() / "memories" target = getattr(args, "target", "all") files_to_reset = [] - if target in ("all", "memory"): + if target in {"all", "memory"}: files_to_reset.append(("MEMORY.md", "agent notes")) - if target in ("all", "user"): + if target in {"all", "user"}: files_to_reset.append(("USER.md", "user profile")) # Check what exists @@ -10826,7 +10826,7 @@ Examples: def cmd_tools(args): action = getattr(args, "tools_action", None) - if action in ("list", "disable", "enable"): + if action in {"list", "disable", "enable"}: from hermes_cli.tools_config import tools_disable_enable_command tools_disable_enable_command(args) @@ -11035,7 +11035,7 @@ Examples: def _confirm_prompt(prompt: str) -> bool: """Prompt for y/N confirmation, safe against non-TTY environments.""" try: - return input(prompt).strip().lower() in ("y", "yes") + return input(prompt).strip().lower() in {"y", "yes"} except (EOFError, KeyboardInterrupt): return False diff --git a/hermes_cli/mcp_config.py b/hermes_cli/mcp_config.py index 0e1e6c5a87d..8c12ad70758 100644 --- a/hermes_cli/mcp_config.py +++ b/hermes_cli/mcp_config.py @@ -63,7 +63,7 @@ def _confirm(question: str, default: bool = True) -> bool: return default if not val: return default - return val in ("y", "yes") + return val in {"y", "yes"} def _prompt(question: str, *, password: bool = False, default: str = "") -> str: @@ -375,11 +375,11 @@ def cmd_mcp_add(args): _info("Cancelled.") return - if choice in ("n", "no"): + if choice in {"n", "no"}: _info("Cancelled — server not saved.") return - if choice in ("s", "select"): + if choice in {"s", "select"}: # Interactive tool selection from hermes_cli.curses_ui import curses_checklist @@ -509,7 +509,7 @@ def cmd_mcp_list(args=None): # Enabled status enabled = cfg.get("enabled", True) if isinstance(enabled, str): - enabled = enabled.lower() in ("true", "1", "yes") + enabled = enabled.lower() in {"true", "1", "yes"} status = color("✓ enabled", Colors.GREEN) if enabled else color("✗ disabled", Colors.DIM) print(f" {name:<16} {transport:<30} {tools_str:<12} {status}") diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index 1bdb76b0f0f..fec1f33d092 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -825,7 +825,7 @@ def switch_model( # --- Step e: detect_provider_for_model() as last resort --- _base = current_base_url or "" - is_custom = current_provider in ("custom", "local") or ( + is_custom = current_provider in {"custom", "local"} or ( "localhost" in _base or "127.0.0.1" in _base ) @@ -1525,7 +1525,7 @@ def list_authenticated_providers( api_key = os.environ.get(key_env, "").strip() if key_env else "" discover = ep_cfg.get("discover_models", True) if isinstance(discover, str): - discover = discover.lower() not in ("false", "no", "0") + discover = discover.lower() not in {"false", "no", "0"} if api_url and api_key and discover: try: from hermes_cli.models import fetch_api_models diff --git a/hermes_cli/models.py b/hermes_cli/models.py index cf693ae28b4..c23bd397e3f 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -818,7 +818,7 @@ try: for _pp in _list_providers_for_canonical(): if _pp.name in _canonical_slugs: continue - if _pp.auth_type in ("oauth_device_code", "oauth_external", "external_process", "aws_sdk", "copilot"): + if _pp.auth_type in {"oauth_device_code", "oauth_external", "external_process", "aws_sdk", "copilot"}: continue # non-api-key flows need bespoke picker UX; skip auto-inject _label = _pp.display_name or _pp.name _desc = _pp.description or f"{_label} (direct API)" @@ -2335,7 +2335,7 @@ def _lmstudio_fetch_raw_models( with urllib.request.urlopen(request, timeout=timeout) as resp: payload = json.loads(resp.read().decode()) except urllib.error.HTTPError as exc: - if exc.code in (401, 403): + if exc.code in {401, 403}: from hermes_cli.auth import AuthError raise AuthError( f"LM Studio rejected the request with HTTP {exc.code}.", @@ -3270,7 +3270,7 @@ def validate_requested_model( # MiniMax providers don't expose a /models endpoint — validate against # the static catalog instead, similar to openai-codex. - if normalized in ("minimax", "minimax-cn"): + if normalized in {"minimax", "minimax-cn"}: try: catalog_models = provider_model_ids(normalized) except Exception: diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index 3a58baa0695..70b0dc9cd7f 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -86,9 +86,9 @@ logger = logging.getLogger(__name__) # The env var is read once at import time; tests that need to flip it # mid-process can call ``_install_plugin_debug_handler(force=True)``. -_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in ( +_PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in { "1", "true", "yes", "on", -) +} _DEBUG_HANDLER_INSTALLED = False @@ -100,9 +100,9 @@ def _install_plugin_debug_handler(force: bool = False) -> None: """ global _DEBUG_HANDLER_INSTALLED, _PLUGINS_DEBUG if force: - _PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in ( + _PLUGINS_DEBUG = os.getenv("HERMES_PLUGINS_DEBUG", "").strip().lower() in { "1", "true", "yes", "on", - ) + } if not _PLUGINS_DEBUG or _DEBUG_HANDLER_INSTALLED: return handler = logging.StreamHandler(sys.stderr) @@ -824,7 +824,7 @@ class PluginManager: # Bundled platform plugins (gateway adapters like IRC) auto-load # for the same reason: every platform Hermes ships must be # available out of the box without the user having to opt in. - if manifest.source == "bundled" and manifest.kind in ("backend", "platform"): + if manifest.source == "bundled" and manifest.kind in {"backend", "platform"}: self._load_plugin(manifest) continue @@ -1075,7 +1075,7 @@ class PluginManager: ) try: - if manifest.source in ("user", "project", "bundled"): + if manifest.source in {"user", "project", "bundled"}: module = self._load_directory_module(manifest) else: module = self._load_entrypoint_module(manifest) diff --git a/hermes_cli/plugins_cmd.py b/hermes_cli/plugins_cmd.py index 38aefe18789..675989d170e 100644 --- a/hermes_cli/plugins_cmd.py +++ b/hermes_cli/plugins_cmd.py @@ -85,7 +85,7 @@ def _sanitize_plugin_name(name: str, plugins_dir: Path) -> Path: if not name: raise ValueError("Plugin name must not be empty.") - if name in (".", ".."): + if name in {".", ".."}: raise ValueError( f"Invalid plugin name '{name}': must not reference the plugins directory itself." ) @@ -491,7 +491,7 @@ def cmd_install( answer = input( f" Enable '{installed_name}' now? [y/N]: ", ).strip().lower() - should_enable = answer in ("y", "yes") + should_enable = answer in {"y", "yes"} except (EOFError, KeyboardInterrupt): should_enable = False else: @@ -731,7 +731,7 @@ def _discover_all_plugins() -> list: for d in sorted(base.iterdir()): if not d.is_dir(): continue - if source == "bundled" and d.name in ("memory", "context_engine"): + if source == "bundled" and d.name in {"memory", "context_engine"}: continue manifest_file = d / "plugin.yaml" if not manifest_file.exists(): @@ -1129,10 +1129,10 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected, stdscr.refresh() key = stdscr.getch() - if key in (curses.KEY_UP, ord("k")): + if key in {curses.KEY_UP, ord("k")}: if total_items > 0: cursor = (cursor - 1) % total_items - elif key in (curses.KEY_DOWN, ord("j")): + elif key in {curses.KEY_DOWN, ord("j")}: if total_items > 0: cursor = (cursor + 1) % total_items elif key == ord(" "): @@ -1168,7 +1168,7 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected, curses.init_pair(3, curses.COLOR_CYAN, -1) curses.init_pair(4, 8, -1) curses.curs_set(0) - elif key in (curses.KEY_ENTER, 10, 13): + elif key in {curses.KEY_ENTER, 10, 13}: if cursor < n_plugins: # ENTER on a plugin checkbox — confirm and exit result_holder["plugins_changed"] = True @@ -1200,7 +1200,7 @@ def _run_composite_ui(curses, plugin_names, plugin_labels, plugin_selected, curses.init_pair(3, curses.COLOR_CYAN, -1) curses.init_pair(4, 8, -1) curses.curs_set(0) - elif key in (27, ord("q")): + elif key in {27, ord("q")}: # Save plugin changes on exit result_holder["plugins_changed"] = True return @@ -1569,13 +1569,13 @@ def plugins_command(args) -> None: ) elif action == "update": cmd_update(args.name) - elif action in ("remove", "rm", "uninstall"): + elif action in {"remove", "rm", "uninstall"}: cmd_remove(args.name) elif action == "enable": cmd_enable(args.name) elif action == "disable": cmd_disable(args.name) - elif action in ("list", "ls"): + elif action in {"list", "ls"}: cmd_list() elif action is None: cmd_toggle() diff --git a/hermes_cli/profiles.py b/hermes_cli/profiles.py index d111159c013..468a4599f84 100644 --- a/hermes_cli/profiles.py +++ b/hermes_cli/profiles.py @@ -989,7 +989,7 @@ def _default_export_ignore(root_dir: Path): if entry == "__pycache__" or entry.endswith((".sock", ".tmp")): ignored.add(entry) # npm lockfiles can appear at root - elif entry in ("package.json", "package-lock.json"): + elif entry in {"package.json", "package-lock.json"}: ignored.add(entry) # Root-level exclusions if Path(directory) == root_dir: @@ -1057,7 +1057,7 @@ def _normalize_profile_archive_parts(member_name: str) -> List[str]: ): raise ValueError(f"Unsafe archive member path: {member_name}") - parts = [part for part in posix_path.parts if part not in ("", ".")] + parts = [part for part in posix_path.parts if part not in {"", "."}] if not parts or any(part == ".." for part in parts): raise ValueError(f"Unsafe archive member path: {member_name}") return parts diff --git a/hermes_cli/pty_bridge.py b/hermes_cli/pty_bridge.py index f2ef8d0876d..a1779aa1dd2 100644 --- a/hermes_cli/pty_bridge.py +++ b/hermes_cli/pty_bridge.py @@ -164,7 +164,7 @@ class PtyBridge: data = os.read(self._fd, 65536) except OSError as exc: # EIO on Linux = slave side closed. EBADF = already closed. - if exc.errno in (errno.EIO, errno.EBADF): + if exc.errno in {errno.EIO, errno.EBADF}: return None raise if not data: @@ -181,7 +181,7 @@ class PtyBridge: try: n = os.write(self._fd, view) except OSError as exc: - if exc.errno in (errno.EIO, errno.EBADF, errno.EPIPE): + if exc.errno in {errno.EIO, errno.EBADF, errno.EPIPE}: return raise if n <= 0: diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index fe996d1e399..1cc41ceae95 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -260,7 +260,7 @@ def _resolve_runtime_from_pool_entry( if cfg_base_url: base_url = cfg_base_url configured_mode = _parse_api_mode(model_cfg.get("api_mode")) - if provider in ("opencode-zen", "opencode-go"): + if provider in {"opencode-zen", "opencode-go"}: # Re-derive api_mode from the effective model rather than the # persisted api_mode: the opencode providers serve both # anthropic_messages and chat_completions models, so the previous @@ -282,7 +282,7 @@ def _resolve_runtime_from_pool_entry( # Anthropic SDK prepends its own /v1/messages to the base_url. Strip the # trailing /v1 so the SDK constructs the correct path (e.g. # https://opencode.ai/zen/go/v1/messages instead of .../v1/v1/messages). - if api_mode == "anthropic_messages" and provider in ("opencode-zen", "opencode-go"): + if api_mode == "anthropic_messages" and provider in {"opencode-zen", "opencode-go"}: base_url = re.sub(r"/v1/?$", "", base_url) return { @@ -859,7 +859,7 @@ def _resolve_explicit_runtime( base_url = explicit_base_url if not base_url: - if provider in ("kimi-coding", "kimi-coding-cn"): + if provider in {"kimi-coding", "kimi-coding-cn"}: creds = resolve_api_key_provider_credentials(provider) base_url = creds.get("base_url", "").rstrip("/") else: @@ -1223,7 +1223,7 @@ def resolve_runtime_provider( # trust boto3's credential chain — it handles IMDS, ECS task roles, # Lambda execution roles, SSO, and other implicit sources that our # env-var check can't detect. - is_explicit = requested_provider in ("bedrock", "aws", "aws-bedrock", "amazon-bedrock", "amazon") + is_explicit = requested_provider in {"bedrock", "aws", "aws-bedrock", "amazon-bedrock", "amazon"} if not is_explicit and not has_aws_credentials(): raise AuthError( "No AWS credentials found for Bedrock. Configure one of:\n" @@ -1303,7 +1303,7 @@ def resolve_runtime_provider( configured_provider = str(model_cfg.get("provider") or "").strip().lower() # Only honor persisted api_mode when it belongs to the same provider family. configured_mode = _parse_api_mode(model_cfg.get("api_mode")) - if provider in ("opencode-zen", "opencode-go"): + if provider in {"opencode-zen", "opencode-go"}: # opencode-zen/go must always re-derive api_mode from the # target model (not the stale persisted api_mode), because # the same provider serves both anthropic_messages @@ -1325,7 +1325,7 @@ def resolve_runtime_provider( if detected: api_mode = detected # Strip trailing /v1 for OpenCode Anthropic models (see comment above). - if api_mode == "anthropic_messages" and provider in ("opencode-zen", "opencode-go"): + if api_mode == "anthropic_messages" and provider in {"opencode-zen", "opencode-go"}: base_url = re.sub(r"/v1/?$", "", base_url) return { "provider": provider, diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 73ef596324b..df4e88e0006 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -292,9 +292,9 @@ def prompt_yes_no(question: str, default: bool = True) -> bool: if not value: return default - if value in ("y", "yes"): + if value in {"y", "yes"}: return True - if value in ("n", "no"): + if value in {"n", "no"}: return False print_error("Please enter 'y' or 'n'") @@ -641,7 +641,7 @@ def _prompt_container_resources(config: dict): persist_str = prompt( " Persist filesystem across sessions? (yes/no)", persist_label ) - terminal["container_persistent"] = persist_str.lower() in ("yes", "true", "y", "1") + terminal["container_persistent"] = persist_str.lower() in {"yes", "true", "y", "1"} # CPU current_cpu = terminal.get("container_cpu", 1) @@ -692,7 +692,7 @@ def _prompt_vercel_sandbox_settings(config: dict): persist_label = "yes" if current_persist else "no" terminal["container_persistent"] = prompt( " Persist filesystem with snapshots? (yes/no)", persist_label - ).lower() in ("yes", "true", "y", "1") + ).lower() in {"yes", "true", "y", "1"} current_cpu = terminal.get("container_cpu", 1) cpu_str = prompt(" CPU cores", str(current_cpu)) @@ -708,7 +708,7 @@ def _prompt_vercel_sandbox_settings(config: dict): except ValueError: pass - if terminal.get("container_disk", 51200) not in (0, 51200): + if terminal.get("container_disk", 51200) not in {0, 51200}: print_warning("Vercel Sandbox does not support custom disk sizing; resetting container_disk to 51200.") terminal["container_disk"] = 51200 @@ -1729,7 +1729,7 @@ def setup_agent_settings(config: dict): current_mode = cfg_get(config, "display", "tool_progress", default="all") mode = prompt("Tool progress mode", current_mode) - if mode.lower() in ("off", "new", "all", "verbose"): + if mode.lower() in {"off", "new", "all", "verbose"}: if "display" not in config: config["display"] = {} config["display"]["tool_progress"] = mode.lower() diff --git a/hermes_cli/skills_hub.py b/hermes_cli/skills_hub.py index 3bfb0631cc4..96c02feb732 100644 --- a/hermes_cli/skills_hub.py +++ b/hermes_cli/skills_hub.py @@ -593,7 +593,7 @@ def do_install(identifier: str, category: str = "", force: bool = False, answer = input("Confirm [y/N]: ").strip().lower() except (EOFError, KeyboardInterrupt): answer = "n" - if answer not in ("y", "yes"): + if answer not in {"y", "yes"}: c.print("[dim]Installation cancelled.[/]\n") shutil.rmtree(q_path, ignore_errors=True) return @@ -948,7 +948,7 @@ def do_uninstall(name: str, console: Optional[Console] = None, answer = input("Confirm [y/N]: ").strip().lower() except (EOFError, KeyboardInterrupt): answer = "n" - if answer not in ("y", "yes"): + if answer not in {"y", "yes"}: c.print("[dim]Cancelled.[/]\n") return @@ -984,7 +984,7 @@ def do_reset(name: str, restore: bool = False, answer = input("Confirm [y/N]: ").strip().lower() except (EOFError, KeyboardInterrupt): answer = "n" - if answer not in ("y", "yes"): + if answer not in {"y", "yes"}: c.print("[dim]Cancelled.[/]\n") return @@ -1138,7 +1138,7 @@ def _github_publish(skill_path: Path, skill_name: str, target_repo: str, f"https://api.github.com/repos/{target_repo}/forks", headers=headers, timeout=30, ) - if resp.status_code in (200, 202): + if resp.status_code in {200, 202}: fork = resp.json() fork_repo = fork["full_name"] elif resp.status_code == 403: @@ -1564,7 +1564,7 @@ def handle_skills_slash(cmd: str, console: Optional[Console] = None) -> None: repo = args[1] if len(args) > 1 else "" do_tap(tap_action, repo=repo, console=c) - elif action in ("help", "--help", "-h"): + elif action in {"help", "--help", "-h"}: _print_skills_help(c) else: diff --git a/hermes_cli/status.py b/hermes_cli/status.py index 9a40c8d9b78..b4417091ca7 100644 --- a/hermes_cli/status.py +++ b/hermes_cli/status.py @@ -367,7 +367,7 @@ def show_status(args): if persist is None: persist_enabled = bool(terminal_cfg.get("container_persistent", True)) else: - persist_enabled = persist.lower() in ("1", "true", "yes", "on") + persist_enabled = persist.lower() in {"1", "true", "yes", "on"} auth_status = describe_vercel_auth() sdk_ok = importlib.util.find_spec("vercel") is not None sdk_label = "installed" if sdk_ok else "missing (install: pip install 'hermes-agent[vercel]')" diff --git a/hermes_cli/stdio.py b/hermes_cli/stdio.py index 51c3f7ba530..a1733f0fe0b 100644 --- a/hermes_cli/stdio.py +++ b/hermes_cli/stdio.py @@ -105,7 +105,7 @@ def configure_windows_stdio() -> bool: _CONFIGURED = True return False - if os.environ.get("HERMES_DISABLE_WINDOWS_UTF8") in ("1", "true", "True", "yes"): + if os.environ.get("HERMES_DISABLE_WINDOWS_UTF8") in {"1", "true", "True", "yes"}: _CONFIGURED = True return False diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 96b3d4e3be5..81e4d327c0b 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -594,7 +594,7 @@ def _pip_install( def _run_post_setup(post_setup_key: str): """Run post-setup hooks for tools that need extra installation steps.""" import shutil - if post_setup_key in ("agent_browser", "browserbase"): + if post_setup_key in {"agent_browser", "browserbase"}: node_modules = PROJECT_ROOT / "node_modules" / "agent-browser" npm_bin = shutil.which("npm") npx_bin = shutil.which("npx") @@ -1631,7 +1631,7 @@ def _is_provider_active(provider: dict, config: dict) -> bool: image_cfg = config.get("image_gen", {}) if isinstance(image_cfg, dict): configured_provider = image_cfg.get("provider") - if configured_provider not in (None, "", "fal"): + if configured_provider not in {None, "", "fal"}: return False if image_cfg.get("use_gateway") is not None and not is_truthy_value(image_cfg.get("use_gateway"), default=False): return False @@ -1664,7 +1664,7 @@ def _is_provider_active(provider: dict, config: dict) -> bool: configured_provider = image_cfg.get("provider") return ( provider["imagegen_backend"] == "fal" - and configured_provider in (None, "", "fal") + and configured_provider in {None, "", "fal"} and not is_truthy_value(image_cfg.get("use_gateway"), default=False) ) return False @@ -1914,7 +1914,7 @@ def _configure_provider(provider: dict, config: dict): # For tools without a specific config key (e.g. image_gen), still # track use_gateway so the runtime knows the user's intent. - if managed_feature and managed_feature not in ("web", "tts", "browser"): + if managed_feature and managed_feature not in {"web", "tts", "browser"}: config.setdefault(managed_feature, {})["use_gateway"] = True elif not managed_feature: # User picked a non-gateway provider — find which category this @@ -1946,7 +1946,7 @@ def _configure_provider(provider: dict, config: dict): # image_gen.provider clear so the dispatch shim falls through # to the legacy FAL path. img_cfg = config.setdefault("image_gen", {}) - if isinstance(img_cfg, dict) and img_cfg.get("provider") not in (None, "", "fal"): + if isinstance(img_cfg, dict) and img_cfg.get("provider") not in {None, "", "fal"}: img_cfg["provider"] = "fal" return @@ -1991,7 +1991,7 @@ def _configure_provider(provider: dict, config: dict): if backend: _configure_imagegen_model(backend, config) img_cfg = config.setdefault("image_gen", {}) - if isinstance(img_cfg, dict) and img_cfg.get("provider") not in (None, "", "fal"): + if isinstance(img_cfg, dict) and img_cfg.get("provider") not in {None, "", "fal"}: img_cfg["provider"] = "fal" @@ -2186,7 +2186,7 @@ def _reconfigure_provider(provider: dict, config: dict): web_cfg["use_gateway"] = bool(managed_feature) _print_success(f" Web backend set to: {provider['web_backend']}") - if managed_feature and managed_feature not in ("web", "tts", "browser"): + if managed_feature and managed_feature not in {"web", "tts", "browser"}: section = config.setdefault(managed_feature, {}) if not isinstance(section, dict): section = {} @@ -2535,7 +2535,7 @@ def _configure_mcp_tools_interactive(config: dict): # Count enabled servers enabled_names = [ k for k, v in mcp_servers.items() - if v.get("enabled", True) not in (False, "false", "0", "no", "off") + if v.get("enabled", True) not in {False, "false", "0", "no", "off"} ] if not enabled_names: _print_info("All MCP servers are disabled.") diff --git a/hermes_cli/uninstall.py b/hermes_cli/uninstall.py index f14c2358750..2d781e754ae 100644 --- a/hermes_cli/uninstall.py +++ b/hermes_cli/uninstall.py @@ -490,7 +490,7 @@ def run_uninstall(args): print("Cancelled.") return - if choice == "3" or choice.lower() in ("c", "cancel", "q", "quit", "n", "no"): + if choice == "3" or choice.lower() in {"c", "cancel", "q", "quit", "n", "no"}: print() print("Uninstall cancelled.") return @@ -517,7 +517,7 @@ def run_uninstall(args): print() print("Cancelled.") return - remove_profiles = resp in ("y", "yes") + remove_profiles = resp in {"y", "yes"} # Final confirmation print() diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 0975d03682d..9f434819dff 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -179,7 +179,7 @@ def _is_accepted_host(host_header: str, bound_host: str) -> bool: # 0.0.0.0 bind means operator explicitly opted into all-interfaces # (requires --insecure per web_server.start_server). No Host-layer # defence can protect that mode; rely on operator network controls. - if bound_host in ("0.0.0.0", "::"): + if bound_host in {"0.0.0.0", "::"}: return True # Loopback bind: accept the loopback names @@ -385,7 +385,7 @@ def _build_schema_from_config( full_key = f"{prefix}.{key}" if prefix else key # Skip internal / version keys - if full_key in ("_config_version",): + if full_key in {"_config_version",}: continue # Category is the first path component for nested keys, or "general" @@ -576,13 +576,13 @@ async def get_status(): gateway_exit_reason = runtime.get("exit_reason") gateway_updated_at = runtime.get("updated_at") if not gateway_running: - gateway_state = gateway_state if gateway_state in ("stopped", "startup_failed") else "stopped" + gateway_state = gateway_state if gateway_state in {"stopped", "startup_failed"} else "stopped" gateway_platforms = {} elif gateway_running and remote_health_body is not None: # The health probe confirmed the gateway is alive, but the local # runtime status file may be stale (cross-container). Override # stopped/None state so the dashboard shows the correct badge. - if gateway_state in (None, "stopped"): + if gateway_state in {None, "stopped"}: gateway_state = "running" # If there was no runtime info at all but the health probe confirmed alive, @@ -1075,7 +1075,7 @@ async def set_model_assignment(body: ModelAssignment): model = (body.model or "").strip() task = (body.task or "").strip().lower() - if scope not in ("main", "auxiliary"): + if scope not in {"main", "auxiliary"}: raise HTTPException(status_code=400, detail="scope must be 'main' or 'auxiliary'") try: @@ -1568,7 +1568,7 @@ async def disconnect_oauth_provider(provider_id: str, request: Request): # AND forget the Claude Code import. We don't touch ~/.claude/* directly # — that's owned by the Claude Code CLI; users can re-auth there if they # want to undo a disconnect. - if provider_id in ("anthropic", "claude-code"): + if provider_id in {"anthropic", "claude-code"}: try: from agent.anthropic_adapter import _HERMES_OAUTH_FILE if _HERMES_OAUTH_FILE.exists(): @@ -2024,7 +2024,7 @@ def _codex_full_login_worker(session_id: str) -> None: if poll.status_code == 200: code_resp = poll.json() break - if poll.status_code in (403, 404): + if poll.status_code in {403, 404}: continue # user hasn't authorized yet raise RuntimeError(f"deviceauth/token poll returned {poll.status_code}") @@ -3003,7 +3003,7 @@ _LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1", "localhost", "testclient"}) def _is_public_bind() -> bool: """True when bound to all-interfaces (operator used --insecure).""" - return getattr(app.state, "bound_host", "") in ("0.0.0.0", "::") + return getattr(app.state, "bound_host", "") in {"0.0.0.0", "::"} def _ws_client_is_allowed(ws: "WebSocket") -> bool: @@ -3585,7 +3585,7 @@ def _normalise_theme_definition(data: Dict[str, Any]) -> Optional[Dict[str, Any] if isinstance(radius, str) and radius.strip(): layout["radius"] = radius density = layout_src.get("density") - if isinstance(density, str) and density in ("compact", "comfortable", "spacious"): + if isinstance(density, str) and density in {"compact", "comfortable", "spacious"}: layout["density"] = density # Color overrides — keep only valid keys with string values. @@ -3918,7 +3918,7 @@ def _merged_plugins_hub() -> Dict[str, Any]: pass can_remove_update = ( - source in ("user", "git") and under_user_tree and Path(dir_str).is_dir() + source in {"user", "git"} and under_user_tree and Path(dir_str).is_dir() ) # Check if this plugin provides tools that require auth diff --git a/hermes_cli/webhook.py b/hermes_cli/webhook.py index 4b74204bcc4..621acc82e27 100644 --- a/hermes_cli/webhook.py +++ b/hermes_cli/webhook.py @@ -124,11 +124,11 @@ def webhook_command(args): if not _require_webhook_enabled(): return - if sub in ("subscribe", "add"): + if sub in {"subscribe", "add"}: _cmd_subscribe(args) - elif sub in ("list", "ls"): + elif sub in {"list", "ls"}: _cmd_list(args) - elif sub in ("remove", "rm"): + elif sub in {"remove", "rm"}: _cmd_remove(args) elif sub == "test": _cmd_test(args) diff --git a/hermes_state.py b/hermes_state.py index 7fdf875c30f..494ab3b2e7a 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -1967,7 +1967,7 @@ class SessionDB: # Route to LIKE when any non-operator CJK token is <3 CJK chars. _tokens_for_check = [ t for t in raw_query.split() - if t.upper() not in ("AND", "OR", "NOT") and self._contains_cjk(t) + if t.upper() not in {"AND", "OR", "NOT"} and self._contains_cjk(t) ] _any_short_cjk = any( self._count_cjk(t) < 3 for t in _tokens_for_check @@ -1980,7 +1980,7 @@ class SessionDB: tokens = raw_query.split() parts = [] for tok in tokens: - if tok.upper() in ("AND", "OR", "NOT"): + if tok.upper() in {"AND", "OR", "NOT"}: parts.append(tok) else: parts.append('"' + tok.replace('"', '""') + '"') @@ -2031,7 +2031,7 @@ class SessionDB: # is matched independently (#20494). non_op_tokens = [ t for t in raw_query.split() - if t.upper() not in ("AND", "OR", "NOT") + if t.upper() not in {"AND", "OR", "NOT"} ] or [raw_query] token_clauses = [] like_params: list = [] diff --git a/mcp_serve.py b/mcp_serve.py index d10306fb5c7..5ae0261d9af 100644 --- a/mcp_serve.py +++ b/mcp_serve.py @@ -169,7 +169,7 @@ def _extract_attachments(msg: dict) -> List[dict]: url = part.get("url", part.get("source", {}).get("url", "")) if url: attachments.append({"type": "image", "url": url}) - elif ptype not in ("text",): + elif ptype not in {"text",}: # Unknown non-text content type attachments.append({"type": ptype, "data": part}) @@ -414,7 +414,7 @@ class EventBridge: for msg in messages: ts = _ts_float(msg.get("timestamp", 0)) role = msg.get("role", "") - if role not in ("user", "assistant"): + if role not in {"user", "assistant"}: continue if ts > last_seen: new_messages.append(msg) @@ -594,7 +594,7 @@ def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "FastMCP": filtered = [] for msg in all_messages: role = msg.get("role", "") - if role in ("user", "assistant"): + if role in {"user", "assistant"}: content = _extract_message_content(msg) if content: filtered.append({ @@ -847,7 +847,7 @@ def create_mcp_server(event_bridge: Optional[EventBridge] = None) -> "FastMCP": id: The approval ID from permissions_list_open decision: One of "allow-once", "allow-always", or "deny" """ - if decision not in ("allow-once", "allow-always", "deny"): + if decision not in {"allow-once", "allow-always", "deny"}: return json.dumps({ "error": f"Invalid decision: {decision}. " f"Must be allow-once, allow-always, or deny" diff --git a/model_tools.py b/model_tools.py index e2ab401aa05..0b9178111a5 100644 --- a/model_tools.py +++ b/model_tools.py @@ -598,7 +598,7 @@ def _coerce_value(value: str, expected_type, schema: dict | None = None): return result return value - if expected_type in ("integer", "number"): + if expected_type in {"integer", "number"}: return _coerce_number(value, integer_only=(expected_type == "integer")) if expected_type == "boolean": return _coerce_boolean(value) diff --git a/rl_cli.py b/rl_cli.py index d494c1addb2..e3996a29df6 100644 --- a/rl_cli.py +++ b/rl_cli.py @@ -392,7 +392,7 @@ def main( if not user_input: continue - if user_input.lower() in ('quit', 'exit', 'q'): + if user_input.lower() in {'quit', 'exit', 'q'}: print("\n👋 Goodbye!") break diff --git a/run_agent.py b/run_agent.py index 72115a5e719..80a38809a34 100644 --- a/run_agent.py +++ b/run_agent.py @@ -539,7 +539,7 @@ def _trajectory_normalize_msg(msg: Dict[str, Any]) -> Dict[str, Any]: if isinstance(content, list): cleaned = [] for p in content: - if isinstance(p, dict) and p.get("type") in ("image", "image_url", "input_image"): + if isinstance(p, dict) and p.get("type") in {"image", "image_url", "input_image"}: cleaned.append({"type": "text", "text": "[screenshot]"}) else: cleaned.append(p) @@ -903,7 +903,7 @@ def _strip_images_from_messages(messages: list) -> bool: continue new_parts = [] for part in content: - if isinstance(part, dict) and part.get("type") in ("image_url", "image", "input_image"): + if isinstance(part, dict) and part.get("type") in {"image_url", "image", "input_image"}: found = True else: new_parts.append(part) @@ -1393,7 +1393,7 @@ class AIAgent: _pc_cfg = _load_pc_cfg().get("prompt_caching", {}) or {} _ttl = _pc_cfg.get("cache_ttl", "5m") - if _ttl in ("5m", "1h"): + if _ttl in {"5m", "1h"}: self._cache_ttl = _ttl except Exception: pass @@ -1640,7 +1640,7 @@ class AIAgent: # but no credentials were found, fail fast with a clear # message instead of silently routing through OpenRouter. _explicit = (self.provider or "").strip().lower() - if _explicit and _explicit not in ("auto", "openrouter", "custom"): + if _explicit and _explicit not in {"auto", "openrouter", "custom"}: # Look up the actual env var name from the provider # config — some providers use non-standard names # (e.g. alibaba → DASHSCOPE_API_KEY, not ALIBABA_API_KEY). @@ -2029,7 +2029,7 @@ class AIAgent: compression_threshold = _model_cthresh except Exception: pass - compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in ("true", "1", "yes") + compression_enabled = str(_compression_cfg.get("enabled", True)).lower() in {"true", "1", "yes"} compression_target_ratio = float(_compression_cfg.get("target_ratio", 0.20)) compression_protect_last = int(_compression_cfg.get("protect_last_n", 20)) @@ -2543,7 +2543,7 @@ class AIAgent: # tests) can't reintroduce the double-/v1 404 bug. if ( api_mode == "anthropic_messages" - and new_provider in ("opencode-zen", "opencode-go") + and new_provider in {"opencode-zen", "opencode-go"} and isinstance(base_url, str) and base_url ): @@ -4280,7 +4280,7 @@ class AIAgent: metadata["task_id"] = task_id if tool_call_id: metadata["tool_call_id"] = tool_call_id - return {k: v for k, v in metadata.items() if v not in (None, "")} + return {k: v for k, v in metadata.items() if v not in {None, ""}} def _apply_persist_user_message_override(self, messages: List[Dict]) -> None: """Rewrite the current-turn user message before persistence/return. @@ -4494,7 +4494,7 @@ class AIAgent: for p in content: if isinstance(p, dict) and p.get("type") == "text": _txt.append(str(p.get("text", ""))) - elif isinstance(p, dict) and p.get("type") in ("image", "image_url", "input_image"): + elif isinstance(p, dict) and p.get("type") in {"image", "image_url", "input_image"}: _txt.append("[screenshot]") content = "\n".join(_txt) if _txt else None tool_calls_data = None @@ -4853,11 +4853,11 @@ class AIAgent: context["message"] = message.strip() for key in ("resets_at", "reset_at"): value = payload.get(key) - if value not in (None, ""): + if value not in {None, ""}: context["reset_at"] = value break retry_after = payload.get("retry_after") - if retry_after not in (None, "") and "reset_at" not in context: + if retry_after not in {None, ""} and "reset_at" not in context: try: context["reset_at"] = time.time() + float(retry_after) except (TypeError, ValueError): @@ -5678,9 +5678,9 @@ class AIAgent: if self.valid_tool_names: _enforce = self._tool_use_enforcement _inject = False - if _enforce is True or (isinstance(_enforce, str) and _enforce.lower() in ("true", "always", "yes", "on")): + if _enforce is True or (isinstance(_enforce, str) and _enforce.lower() in {"true", "always", "yes", "on"}): _inject = True - elif _enforce is False or (isinstance(_enforce, str) and _enforce.lower() in ("false", "never", "no", "off")): + elif _enforce is False or (isinstance(_enforce, str) and _enforce.lower() in {"false", "never", "no", "off"}): _inject = False elif isinstance(_enforce, list): model_lower = (self.model or "").lower() @@ -5935,7 +5935,7 @@ class AIAgent: return False continue btype = block.get("type") - if btype in ("thinking", "redacted_thinking"): + if btype in {"thinking", "redacted_thinking"}: continue if btype == "text": text = block.get("text", "") @@ -6665,7 +6665,7 @@ class AIAgent: if done_item is not None: collected_output_items.append(done_item) # Log non-completed terminal events for diagnostics - elif event_type in ("response.incomplete", "response.failed"): + elif event_type in {"response.incomplete", "response.failed"}: resp_obj = getattr(event, "response", None) status = getattr(resp_obj, "status", None) if resp_obj else None incomplete_details = getattr(resp_obj, "incomplete_details", None) if resp_obj else None @@ -6767,7 +6767,7 @@ class AIAgent: done_item = event.get("item") if done_item is not None: collected_output_items.append(done_item) - elif event_type in ("response.output_text.delta",): + elif event_type in {"response.output_text.delta",}: delta = getattr(event, "delta", "") if not delta and isinstance(event, dict): delta = event.get("delta", "") @@ -7063,7 +7063,7 @@ class AIAgent: effective_reason = FailoverReason.billing elif status_code == 429: effective_reason = FailoverReason.rate_limit - elif status_code in (401, 403): + elif status_code in {401, 403}: effective_reason = FailoverReason.auth if effective_reason == FailoverReason.billing: @@ -8384,7 +8384,7 @@ class AIAgent: auth resolution and client construction — no duplicated provider→key mappings. """ - if reason in (FailoverReason.rate_limit, FailoverReason.billing): + if reason in {FailoverReason.rate_limit, FailoverReason.billing}: # Only start cooldown when leaving the primary provider. If we're # already on a fallback and chain-switching, the primary wasn't the # source of the 429 so the cooldown should not be reset/extended. @@ -8710,7 +8710,7 @@ class AIAgent: if self._is_openrouter_url(): return False provider_lower = (self.provider or "").strip().lower() - if provider_lower in ("nous", "nous-research"): + if provider_lower in {"nous", "nous-research"}: return False try: @@ -10304,7 +10304,7 @@ class AIAgent: store=self._memory_store, ) # Bridge: notify external memory provider of built-in memory writes - if self._memory_manager and function_args.get("action") in ("add", "replace"): + if self._memory_manager and function_args.get("action") in {"add", "replace"}: try: self._memory_manager.on_memory_write( function_args.get("action", ""), @@ -10403,7 +10403,7 @@ class AIAgent: function_args = {} # Checkpoint for file-mutating tools - if function_name in ("write_file", "patch") and self._checkpoint_mgr.enabled: + if function_name in {"write_file", "patch"} and self._checkpoint_mgr.enabled: try: file_path = function_args.get("path", "") if file_path: @@ -10860,7 +10860,7 @@ class AIAgent: logging.debug(f"Tool start callback error: {cb_err}") # Checkpoint: snapshot working dir before file-mutating tools - if not _execution_blocked and function_name in ("write_file", "patch") and self._checkpoint_mgr.enabled: + if not _execution_blocked and function_name in {"write_file", "patch"} and self._checkpoint_mgr.enabled: try: file_path = function_args.get("path", "") if file_path: @@ -10932,7 +10932,7 @@ class AIAgent: store=self._memory_store, ) # Bridge: notify external memory provider of built-in memory writes - if self._memory_manager and function_args.get("action") in ("add", "replace"): + if self._memory_manager and function_args.get("action") in {"add", "replace"}: try: self._memory_manager.on_memory_write( function_args.get("action", ""), @@ -12462,9 +12462,9 @@ class AIAgent: _failure_hint = f"upstream gateway timeout (504, {api_duration:.0f}s)" elif _resp_error_code == 429: _failure_hint = f"rate limited by upstream provider (429)" - elif _resp_error_code in (500, 502): + elif _resp_error_code in {500, 502}: _failure_hint = f"upstream server error ({_resp_error_code}, {api_duration:.0f}s)" - elif _resp_error_code in (503, 529): + elif _resp_error_code in {503, 529}: _failure_hint = f"upstream provider overloaded ({_resp_error_code})" elif _resp_error_code is not None: _failure_hint = f"upstream error (code {_resp_error_code}, {api_duration:.0f}s)" @@ -12652,7 +12652,7 @@ class AIAgent: "error": _exhaust_error, } - if self.api_mode in ("chat_completions", "bedrock_converse", "anthropic_messages"): + if self.api_mode in {"chat_completions", "bedrock_converse", "anthropic_messages"}: assistant_message = _trunc_msg if assistant_message is not None and not _trunc_has_tool_calls: length_continue_retries += 1 @@ -12692,7 +12692,7 @@ class AIAgent: "error": "Response remained truncated after 3 continuation attempts", } - if self.api_mode in ("chat_completions", "bedrock_converse", "anthropic_messages"): + if self.api_mode in {"chat_completions", "bedrock_converse", "anthropic_messages"}: assistant_message = _trunc_msg if assistant_message is not None and _trunc_has_tool_calls: if truncated_tool_call_retries < 1: @@ -13524,10 +13524,10 @@ class AIAgent: # When a fallback model is configured, switch immediately instead # of burning through retries with exponential backoff -- the # primary provider won't recover within the retry window. - is_rate_limited = classified.reason in ( + is_rate_limited = classified.reason in { FailoverReason.rate_limit, FailoverReason.billing, - ) + } if is_rate_limited and self._fallback_index < len(self._fallback_chain): # Don't eagerly fallback if credential pool rotation may # still recover. See _pool_may_recover_from_rate_limit @@ -13852,7 +13852,7 @@ class AIAgent: or ( not classified.retryable and not classified.should_compress - and classified.reason not in ( + and classified.reason not in { FailoverReason.rate_limit, FailoverReason.billing, FailoverReason.overloaded, @@ -13860,7 +13860,7 @@ class AIAgent: FailoverReason.payload_too_large, FailoverReason.long_context_tier, FailoverReason.thinking_signature, - ) + } ) ) and not is_context_length_error @@ -15307,9 +15307,9 @@ def main( info = get_toolset_info(name) if info: entry = (name, info) - if name in ["web", "terminal", "vision", "creative", "reasoning"]: + if name in {"web", "terminal", "vision", "creative", "reasoning"}: basic_toolsets.append(entry) - elif name in ["research", "development", "analysis", "content_creation", "full_stack"]: + elif name in {"research", "development", "analysis", "content_creation", "full_stack"}: composite_toolsets.append(entry) else: scenario_toolsets.append(entry) diff --git a/scripts/build_skills_index.py b/scripts/build_skills_index.py index 96a0b637596..206a8012436 100644 --- a/scripts/build_skills_index.py +++ b/scripts/build_skills_index.py @@ -147,7 +147,7 @@ def batch_resolve_paths(skills: list, auth: GitHubAuth) -> list: 4. Match skills to their resolved paths """ # Filter to skills.sh entries that need resolution - skills_sh = [s for s in skills if s["source"] in ("skills.sh", "skills-sh")] + skills_sh = [s for s in skills if s["source"] in {"skills.sh", "skills-sh"}] if not skills_sh: return skills diff --git a/scripts/profile-tui.py b/scripts/profile-tui.py index edbdf2ee453..13ce21fb3fc 100755 --- a/scripts/profile-tui.py +++ b/scripts/profile-tui.py @@ -360,7 +360,7 @@ def format_diff(before: dict[str, float], after: dict[str, float]) -> str: b = before.get(k, 0.0) a = after.get(k, 0.0) d = a - b - pct_change = ((a / b) - 1) * 100 if b not in (0, 0.0) else float("inf") if a else 0 + pct_change = ((a / b) - 1) * 100 if b not in {0, 0.0} else float("inf") if a else 0 # Flag improvements vs regressions. For _p99 / _max / _total / gaps_over / # patches / writeBytes / backpressure, LOWER is better. For fps / gaps_under, diff --git a/tools/approval.py b/tools/approval.py index bd3fb504b33..d6db5a05a0e 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -759,13 +759,13 @@ def prompt_dangerous_approval(command: str, description: str, return "deny" choice = result["choice"] - if choice in ('o', 'once'): + if choice in {'o', 'once'}: print(t("approval.allowed_once")) return "once" - elif choice in ('s', 'session'): + elif choice in {'s', 'session'}: print(t("approval.allowed_session")) return "session" - elif choice in ('a', 'always'): + elif choice in {'a', 'always'}: if not allow_permanent: print(t("approval.allowed_session")) return "session" @@ -831,7 +831,7 @@ def _get_cron_approval_mode() -> str: from hermes_cli.config import load_config config = load_config() mode = str(cfg_get(config, "approvals", "cron_mode", default="deny")).lower().strip() - if mode in ("approve", "off", "allow", "yes"): + if mode in {"approve", "off", "allow", "yes"}: return "approve" return "deny" except Exception: @@ -900,7 +900,7 @@ def check_dangerous_command(command: str, env_type: str, Returns: {"approved": True/False, "message": str or None, ...} """ - if env_type in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"): + if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}: return {"approved": True, "message": None} # Hardline floor: commands with no recovery path (rm -rf /, mkfs, dd @@ -1025,7 +1025,7 @@ def check_all_command_guards(command: str, env_type: str, other was shown to the user. """ # Skip containers for both checks - if env_type in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"): + if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}: return {"approved": True, "message": None} # Hardline floor: unconditional block for catastrophic commands @@ -1104,7 +1104,7 @@ def check_all_command_guards(command: str, env_type: str, # Previously, tirith "block" was a hard block with no approval prompt. # Now both block and warn go through the approval flow so users can # inspect the explanation and approve if they understand the risk. - if tirith_result["action"] in ("block", "warn"): + if tirith_result["action"] in {"block", "warn"}: findings = tirith_result.get("findings") or [] rule_id = findings[0].get("rule_id", "unknown") if findings else "unknown" tirith_key = f"tirith:{rule_id}" diff --git a/tools/browser_providers/browser_use.py b/tools/browser_providers/browser_use.py index f8e9a8d9fa4..260249ef0bb 100644 --- a/tools/browser_providers/browser_use.py +++ b/tools/browser_providers/browser_use.py @@ -184,7 +184,7 @@ class BrowserUseProvider(CloudBrowserProvider): json={"action": "stop"}, timeout=10, ) - if response.status_code in (200, 201, 204): + if response.status_code in {200, 201, 204}: logger.debug("Successfully closed Browser Use session %s", session_id) return True else: diff --git a/tools/browser_providers/browserbase.py b/tools/browser_providers/browserbase.py index 338ebf89895..5076af4c7a6 100644 --- a/tools/browser_providers/browserbase.py +++ b/tools/browser_providers/browserbase.py @@ -180,7 +180,7 @@ class BrowserbaseProvider(CloudBrowserProvider): }, timeout=10, ) - if response.status_code in (200, 201, 204): + if response.status_code in {200, 201, 204}: logger.debug("Successfully closed Browserbase session %s", session_id) return True else: diff --git a/tools/browser_providers/firecrawl.py b/tools/browser_providers/firecrawl.py index 3f8556fc124..17001f72f1d 100644 --- a/tools/browser_providers/firecrawl.py +++ b/tools/browser_providers/firecrawl.py @@ -79,7 +79,7 @@ class FirecrawlProvider(CloudBrowserProvider): headers=self._headers(), timeout=10, ) - if response.status_code in (200, 201, 204): + if response.status_code in {200, 201, 204}: logger.debug("Successfully closed Firecrawl session %s", session_id) return True else: diff --git a/tools/browser_supervisor.py b/tools/browser_supervisor.py index 371210350ff..af8d40ee185 100644 --- a/tools/browser_supervisor.py +++ b/tools/browser_supervisor.py @@ -412,7 +412,7 @@ class CDPSupervisor: ``{"ok": False, "error": "..."}`` on a recoverable error (no dialog, ambiguous dialog_id, supervisor inactive). """ - if action not in ("accept", "dismiss"): + if action not in {"accept", "dismiss"}: return {"ok": False, "error": f"action must be 'accept' or 'dismiss', got {action!r}"} with self._state_lock: @@ -1206,7 +1206,7 @@ class CDPSupervisor: info = params.get("targetInfo") or {} sid = params.get("sessionId") target_type = info.get("type") - if not sid or target_type not in ("iframe", "worker"): + if not sid or target_type not in {"iframe", "worker"}: return self._child_sessions[sid] = {"info": info, "type": target_type} @@ -1290,7 +1290,7 @@ class CDPSupervisor: event = ConsoleEvent(ts=time.time(), level="exception", text=text, url=url) else: raw_level = str(params.get("type") or "log") - level = "error" if raw_level in ("error", "assert") else ( + level = "error" if raw_level in {"error", "assert"} else ( "warning" if raw_level == "warning" else "log" ) args = params.get("args") or [] diff --git a/tools/browser_tool.py b/tools/browser_tool.py index b1986f7b64b..40ba7cab25c 100644 --- a/tools/browser_tool.py +++ b/tools/browser_tool.py @@ -918,7 +918,7 @@ def _url_is_private(url: str) -> bool: # Hostname — must resolve to confirm it's private (bare "localhost" # resolves to 127.0.0.1 via /etc/hosts). Short-circuit on obvious # names to avoid a DNS hop. - if hostname in ("localhost",) or hostname.endswith(".localhost"): + if hostname in {"localhost",} or hostname.endswith(".localhost"): return True if hostname.endswith(".local") or hostname.endswith(".lan") or hostname.endswith(".internal"): return True @@ -2499,7 +2499,7 @@ def browser_scroll(direction: str, task_id: Optional[str] = None) -> str: JSON string with scroll result """ # Validate direction - if direction not in ["up", "down"]: + if direction not in {"up", "down"}: return json.dumps({ "success": False, "error": f"Invalid direction '{direction}'. Use 'up' or 'down'." diff --git a/tools/checkpoint_manager.py b/tools/checkpoint_manager.py index 4616e4f8556..16ce12fc60e 100644 --- a/tools/checkpoint_manager.py +++ b/tools/checkpoint_manager.py @@ -639,7 +639,7 @@ class CheckpointManager: abs_dir = str(_normalize_path(working_dir)) # Skip root, home, and other overly broad directories - if abs_dir in ("/", str(Path.home())): + if abs_dir in {"/", str(Path.home())}: logger.debug("Checkpoint skipped: directory too broad (%s)", abs_dir) return False diff --git a/tools/code_execution_tool.py b/tools/code_execution_tool.py index 092f7e37e97..3822ce539f2 100644 --- a/tools/code_execution_tool.py +++ b/tools/code_execution_tool.py @@ -612,7 +612,7 @@ def _get_or_create_env(task_id: str): cwd = overrides.get("cwd") or config["cwd"] container_config = None - if env_type in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"): + if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}: container_config = { "container_cpu": config.get("container_cpu", 1), "container_memory": config.get("container_memory", 5120), diff --git a/tools/computer_use/cua_backend.py b/tools/computer_use/cua_backend.py index ba50c57987c..df1162c5d79 100644 --- a/tools/computer_use/cua_backend.py +++ b/tools/computer_use/cua_backend.py @@ -673,5 +673,5 @@ def _parse_element(d: Dict[str, Any]) -> UIElement: pid=int(d.get("pid", 0) or 0), window_id=int(d.get("windowId", 0) or 0), attributes={k: v for k, v in d.items() - if k not in ("index", "role", "label", "bounds", "app", "pid", "windowId")}, + if k not in {"index", "role", "label", "bounds", "app", "pid", "windowId"}}, ) diff --git a/tools/computer_use/tool.py b/tools/computer_use/tool.py index 51c7656fc1a..63a5076c171 100644 --- a/tools/computer_use/tool.py +++ b/tools/computer_use/tool.py @@ -131,7 +131,7 @@ def _get_backend() -> ComputerUseBackend: with _backend_lock: if _backend is None: backend_name = os.environ.get("HERMES_COMPUTER_USE_BACKEND", "cua").lower() - if backend_name in ("cua", "cua-driver", ""): + if backend_name in {"cua", "cua-driver", ""}: from tools.computer_use.cua_backend import CuaDriverBackend _backend = CuaDriverBackend() elif backend_name == "noop": # pragma: no cover @@ -286,7 +286,7 @@ def _request_approval(action: str, args: Dict[str, Any]) -> Optional[str]: def _summarize_action(action: str, args: Dict[str, Any]) -> str: - if action in ("click", "double_click", "right_click", "middle_click"): + if action in {"click", "double_click", "right_click", "middle_click"}: if args.get("element") is not None: return f"{action} element #{args['element']}" coord = args.get("coordinate") @@ -314,7 +314,7 @@ def _dispatch(backend: ComputerUseBackend, action: str, args: Dict[str, Any]) -> if action == "capture": mode = str(args.get("mode", "som")) - if mode not in ("som", "vision", "ax"): + if mode not in {"som", "vision", "ax"}: return json.dumps({"error": f"bad mode {mode!r}; use som|vision|ax"}) cap = backend.capture(mode=mode, app=args.get("app")) return _capture_response(cap) @@ -335,7 +335,7 @@ def _dispatch(backend: ComputerUseBackend, action: str, args: Dict[str, Any]) -> res = backend.focus_app(app, raise_window=bool(args.get("raise_window"))) return _maybe_follow_capture(backend, res, capture_after) - if action in ("click", "double_click", "right_click", "middle_click"): + if action in {"click", "double_click", "right_click", "middle_click"}: button = args.get("button") click_count = 1 if action == "double_click": diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 66a721697f8..b2c02aedaf8 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -315,7 +315,7 @@ def _normalize_role(r: Optional[str]) -> str: if r is None or not r: return "leaf" r_norm = str(r).strip().lower() - if r_norm in ("leaf", "orchestrator"): + if r_norm in {"leaf", "orchestrator"}: return r_norm logger.warning("Unknown delegate_task role=%r, coercing to 'leaf'", r) return "leaf" @@ -437,7 +437,7 @@ def _get_orchestrator_enabled() -> bool: return val # Accept "true"/"false" strings from YAML that doesn't auto-coerce. if isinstance(val, str): - return val.strip().lower() in ("true", "1", "yes", "on") + return val.strip().lower() in {"true", "1", "yes", "on"} return True @@ -2271,9 +2271,9 @@ def delegate_task( # total as "none" when the parent itself hadn't billed any calls # yet (rare but possible when the parent's only action this turn # was delegate_task). - if getattr(parent_agent, "session_cost_source", "none") in (None, "", "none"): + if getattr(parent_agent, "session_cost_source", "none") in {None, "", "none"}: parent_agent.session_cost_source = "subagent" - if getattr(parent_agent, "session_cost_status", "unknown") in (None, "", "unknown"): + if getattr(parent_agent, "session_cost_status", "unknown") in {None, "", "unknown"}: parent_agent.session_cost_status = "estimated" except Exception: logger.debug("Subagent cost rollup failed", exc_info=True) diff --git a/tools/environments/daytona.py b/tools/environments/daytona.py index 6eff002ae07..a32ec900c6a 100644 --- a/tools/environments/daytona.py +++ b/tools/environments/daytona.py @@ -124,7 +124,7 @@ class DaytonaEnvironment(BaseEnvironment): home = self._sandbox.process.exec("echo $HOME").result.strip() if home: self._remote_home = home - if requested_cwd in ("~", "/home/daytona"): + if requested_cwd in {"~", "/home/daytona"}: self.cwd = home except Exception: pass @@ -195,7 +195,7 @@ class DaytonaEnvironment(BaseEnvironment): def _ensure_sandbox_ready(self) -> None: """Restart sandbox if it was stopped (e.g., by a previous interrupt).""" self._sandbox.refresh_data() - if self._sandbox.state in (self._SandboxState.STOPPED, self._SandboxState.ARCHIVED): + if self._sandbox.state in {self._SandboxState.STOPPED, self._SandboxState.ARCHIVED}: self._sandbox.start() logger.info("Daytona: restarted sandbox %s", self._sandbox.id) diff --git a/tools/environments/vercel_sandbox.py b/tools/environments/vercel_sandbox.py index 2b434af1594..b381eb77cd2 100644 --- a/tools/environments/vercel_sandbox.py +++ b/tools/environments/vercel_sandbox.py @@ -254,7 +254,7 @@ class VercelSandboxEnvironment(BaseEnvironment): self.init_session() def _build_create_params(self, *, cpu: float, memory: int, disk: int) -> _SandboxCreateParams: - if disk not in (0, _DEFAULT_CONTAINER_DISK_MB): + if disk not in {0, _DEFAULT_CONTAINER_DISK_MB}: raise ValueError( "Vercel Sandbox does not support configurable container_disk. " "Use the default shared setting." @@ -336,7 +336,7 @@ class VercelSandboxEnvironment(BaseEnvironment): if requested_cwd == "~": self.cwd = self._remote_home - elif requested_cwd in ("", DEFAULT_VERCEL_CWD): + elif requested_cwd in {"", DEFAULT_VERCEL_CWD}: self.cwd = self._workspace_root else: self.cwd = requested_cwd diff --git a/tools/file_operations.py b/tools/file_operations.py index 022943d9f0e..91c5abae343 100644 --- a/tools/file_operations.py +++ b/tools/file_operations.py @@ -1244,7 +1244,7 @@ class ShellFileOperations(FileOperations): search_root = Path(path) has_hidden_path_ancestor = any( - part not in (".", "..") and part.startswith(".") + part not in {".", ".."} and part.startswith(".") for part in search_root.parts ) @@ -1305,7 +1305,7 @@ class ShellFileOperations(FileOperations): rel_parts = Path(file_path).resolve().relative_to(normalized_root).parts except ValueError: rel_parts = Path(file_path).parts - if any(part not in (".", "..") and part.startswith(".") for part in rel_parts): + if any(part not in {".", ".."} and part.startswith(".") for part in rel_parts): continue filtered_files.append(file_path) files = filtered_files[offset:offset + limit] diff --git a/tools/file_tools.py b/tools/file_tools.py index c197061ade1..2cedc4bcd5f 100644 --- a/tools/file_tools.py +++ b/tools/file_tools.py @@ -380,7 +380,7 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations: logger.info("Creating new %s environment for task %s...", env_type, task_id[:8]) container_config = None - if env_type in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"): + if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}: container_config = { "container_cpu": config.get("container_cpu", 1), "container_memory": config.get("container_memory", 5120), diff --git a/tools/image_generation_tool.py b/tools/image_generation_tool.py index 68f4af9ac0c..a545a85d9fc 100644 --- a/tools/image_generation_tool.py +++ b/tools/image_generation_tool.py @@ -575,7 +575,7 @@ def _build_fal_payload( payload: Dict[str, Any] = dict(meta.get("defaults", {})) payload["prompt"] = (prompt or "").strip() - if size_style in ("image_size_preset", "gpt_literal"): + if size_style in {"image_size_preset", "gpt_literal"}: payload["image_size"] = sizes[aspect] elif size_style == "aspect_ratio": payload["aspect_ratio"] = sizes[aspect] diff --git a/tools/kanban_tools.py b/tools/kanban_tools.py index 84105a9f839..fab0a68c92b 100644 --- a/tools/kanban_tools.py +++ b/tools/kanban_tools.py @@ -160,7 +160,7 @@ def _normalize_profile(value: Any) -> Optional[str]: if value is None: return None text = str(value).strip() - if not text or text.lower() in ("none", "-", "null"): + if not text or text.lower() in {"none", "-", "null"}: return None return text @@ -172,9 +172,9 @@ def _parse_bool_arg(args: dict, name: str, *, default: bool = False): if isinstance(value, bool): return value, None text = str(value).strip().lower() - if text in ("true", "1", "yes"): + if text in {"true", "1", "yes"}: return True, None - if text in ("false", "0", "no"): + if text in {"false", "0", "no"}: return False, None return default, f"{name} must be a boolean or 'true'/'false'" diff --git a/tools/memory_tool.py b/tools/memory_tool.py index 80ee3c63d67..37f9cd4a715 100644 --- a/tools/memory_tool.py +++ b/tools/memory_tool.py @@ -477,7 +477,7 @@ def memory_tool( if store is None: return tool_error("Memory is not available. It may be disabled in config or this environment.", success=False) - if target not in ("memory", "user"): + if target not in {"memory", "user"}: return tool_error(f"Invalid target '{target}'. Use 'memory' or 'user'.", success=False) if action == "add": diff --git a/tools/osv_check.py b/tools/osv_check.py index 52458fdd32a..e094b272104 100644 --- a/tools/osv_check.py +++ b/tools/osv_check.py @@ -65,9 +65,9 @@ def check_package_for_malware( def _infer_ecosystem(command: str) -> Optional[str]: """Infer package ecosystem from the command name.""" base = os.path.basename(command).lower() - if base in ("npx", "npx.cmd"): + if base in {"npx", "npx.cmd"}: return "npm" - if base in ("uvx", "uvx.cmd", "pipx"): + if base in {"uvx", "uvx.cmd", "pipx"}: return "PyPI" return None diff --git a/tools/patch_parser.py b/tools/patch_parser.py index d2a298fc9f8..dacc6e855c3 100644 --- a/tools/patch_parser.py +++ b/tools/patch_parser.py @@ -263,7 +263,7 @@ def _validate_operations( simulated = read_result.content for hunk in op.hunks: - search_lines = [l.content for l in hunk.lines if l.prefix in (' ', '-')] + search_lines = [l.content for l in hunk.lines if l.prefix in {' ', '-'}] if not search_lines: # Addition-only hunk: validate context hint uniqueness if hunk.context_hint: @@ -282,7 +282,7 @@ def _validate_operations( continue search_pattern = '\n'.join(search_lines) - replace_lines = [l.content for l in hunk.lines if l.prefix in (' ', '+')] + replace_lines = [l.content for l in hunk.lines if l.prefix in {' ', '+'}] replacement = '\n'.join(replace_lines) new_simulated, count, _strategy, match_error = fuzzy_find_and_replace( diff --git a/tools/process_registry.py b/tools/process_registry.py index 260ba4739fd..8bbe1f56b7c 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -1237,7 +1237,7 @@ class ProcessRegistry: killed = 0 for session in targets: result = self.kill_process(session.id) - if result.get("status") in ("killed", "already_exited"): + if result.get("status") in {"killed", "already_exited"}: killed += 1 return killed @@ -1446,7 +1446,7 @@ def _handle_process(args, **kw): if action == "list": return json.dumps({"processes": process_registry.list_sessions(task_id=task_id)}, ensure_ascii=False) - elif action in ("poll", "log", "wait", "kill", "write", "submit", "close"): + elif action in {"poll", "log", "wait", "kill", "write", "submit", "close"}: if not session_id: return tool_error(f"session_id is required for {action}") if action == "poll": diff --git a/tools/rl_training_tool.py b/tools/rl_training_tool.py index d2a5c3bfbb5..c7acb8012e1 100644 --- a/tools/rl_training_tool.py +++ b/tools/rl_training_tool.py @@ -919,7 +919,7 @@ async def rl_stop_training(run_id: str) -> str: run_state = _active_runs[run_id] - if run_state.status not in ("running", "starting"): + if run_state.status not in {"running", "starting"}: return json.dumps({ "message": f"Run '{run_id}' is not running (status: {run_state.status})", }, indent=2) diff --git a/tools/send_message_tool.py b/tools/send_message_tool.py index cc9b0a96c6f..c8d84fdf213 100644 --- a/tools/send_message_tool.py +++ b/tools/send_message_tool.py @@ -1034,7 +1034,7 @@ async def _send_discord(token, chat_id, message, thread_id=None, media_files=Non filename=os.path.basename(media_path), ) async with session.post(thread_url, headers=auth_headers, data=form, **_req_kw) as resp: - if resp.status not in (200, 201): + if resp.status not in {200, 201}: body = await resp.text() return _error(f"Discord forum thread creation error ({resp.status}): {body}") data = await resp.json() @@ -1052,7 +1052,7 @@ async def _send_discord(token, chat_id, message, thread_id=None, media_files=Non }, **_req_kw, ) as resp: - if resp.status not in (200, 201): + if resp.status not in {200, 201}: body = await resp.text() return _error(f"Discord forum thread creation error ({resp.status}): {body}") data = await resp.json() @@ -1076,7 +1076,7 @@ async def _send_discord(token, chat_id, message, thread_id=None, media_files=Non # Send text message (skip if empty and media is present) if message.strip() or not media_files: async with session.post(url, headers=json_headers, json={"content": message}, **_req_kw) as resp: - if resp.status not in (200, 201): + if resp.status not in {200, 201}: body = await resp.text() return _error(f"Discord API error ({resp.status}): {body}") last_data = await resp.json() @@ -1094,7 +1094,7 @@ async def _send_discord(token, chat_id, message, thread_id=None, media_files=Non with open(media_path, "rb") as f: form.add_field("files[0]", f, filename=filename) async with session.post(url, headers=auth_headers, data=form, **_req_kw) as resp: - if resp.status not in (200, 201): + if resp.status not in {200, 201}: body = await resp.text() warning = _sanitize_error_text(f"Failed to send media {media_path}: Discord API error ({resp.status}): {body}") logger.error(warning) @@ -1457,7 +1457,7 @@ async def _send_mattermost(token, extra, chat_id, message): headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session: async with session.post(url, headers=headers, json={"channel_id": chat_id, "message": message}) as resp: - if resp.status not in (200, 201): + if resp.status not in {200, 201}: body = await resp.text() return _error(f"Mattermost API error ({resp.status}): {body}") data = await resp.json() @@ -1501,7 +1501,7 @@ async def _send_matrix(token, extra, chat_id, message): async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session: async with session.put(url, headers=headers, json=payload) as resp: - if resp.status not in (200, 201): + if resp.status not in {200, 201}: body = await resp.text() return _error(f"Matrix API error ({resp.status}): {body}") data = await resp.json() @@ -1585,7 +1585,7 @@ async def _send_homeassistant(token, extra, chat_id, message): headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=30)) as session: async with session.post(url, headers=headers, json={"message": message, "target": chat_id}) as resp: - if resp.status not in (200, 201): + if resp.status not in {200, 201}: body = await resp.text() return _error(f"Home Assistant API error ({resp.status}): {body}") return {"success": True, "platform": "homeassistant", "chat_id": chat_id} @@ -1827,7 +1827,7 @@ async def _send_qqbot(pconfig, chat_id, message): # Try channel endpoint first (works for guild channels) url = f"https://api.sgroup.qq.com/channels/{chat_id}/messages" resp = await client.post(url, json=payload, headers=headers) - if resp.status_code in (200, 201): + if resp.status_code in {200, 201}: data = resp.json() return {"success": True, "platform": "qqbot", "chat_id": chat_id, "message_id": data.get("id")} @@ -1835,7 +1835,7 @@ async def _send_qqbot(pconfig, chat_id, message): # If channel endpoint failed (likely "频道不存在"), try C2C endpoint url_c2c = f"https://api.sgroup.qq.com/v2/users/{chat_id}/messages" resp_c2c = await client.post(url_c2c, json=payload, headers=headers) - if resp_c2c.status_code in (200, 201): + if resp_c2c.status_code in {200, 201}: data = resp_c2c.json() return {"success": True, "platform": "qqbot", "chat_id": chat_id, "message_id": data.get("id")} @@ -1843,7 +1843,7 @@ async def _send_qqbot(pconfig, chat_id, message): # If C2C also failed, try group endpoint url_group = f"https://api.sgroup.qq.com/v2/groups/{chat_id}/messages" resp_group = await client.post(url_group, json=payload, headers=headers) - if resp_group.status_code in (200, 201): + if resp_group.status_code in {200, 201}: data = resp_group.json() return {"success": True, "platform": "qqbot", "chat_id": chat_id, "message_id": data.get("id")} diff --git a/tools/skill_manager_tool.py b/tools/skill_manager_tool.py index d253cd2a7cd..caa30f321c6 100644 --- a/tools/skill_manager_tool.py +++ b/tools/skill_manager_tool.py @@ -780,7 +780,7 @@ def skill_manage( if action == "create": if is_background_review(): mark_agent_created(name) - elif action in ("patch", "edit", "write_file", "remove_file"): + elif action in {"patch", "edit", "write_file", "remove_file"}: bump_patch(name) elif action == "delete": forget(name) diff --git a/tools/skills_guard.py b/tools/skills_guard.py index ffb965b5212..46503a7eb15 100644 --- a/tools/skills_guard.py +++ b/tools/skills_guard.py @@ -814,7 +814,7 @@ def _check_structure(skill_dir: Path) -> List[Finding]: )) # Executable permission on non-script files - if ext not in ('.sh', '.bash', '.py', '.rb', '.pl') and f.stat().st_mode & 0o111: + if ext not in {'.sh', '.bash', '.py', '.rb', '.pl'} and f.stat().st_mode & 0o111: findings.append(Finding( pattern_id="unexpected_executable", severity="medium", diff --git a/tools/skills_hub.py b/tools/skills_hub.py index c070a7de5f9..3e2c27c338a 100644 --- a/tools/skills_hub.py +++ b/tools/skills_hub.py @@ -101,7 +101,7 @@ def _normalize_bundle_path(path_value: str, *, field_name: str, allow_nested: bo normalized = raw.replace("\\", "/") path = PurePosixPath(normalized) - parts = [part for part in path.parts if part not in ("", ".")] + parts = [part for part in path.parts if part not in {"", "."}] if normalized.startswith("/") or path.is_absolute(): raise ValueError(f"Unsafe {field_name}: {path_value}") @@ -1415,7 +1415,7 @@ class SkillsShSource(SkillSource): dir_name = entry["name"] if dir_name.startswith((".", "_")): continue - if dir_name in ("skills", ".agents", ".claude"): + if dir_name in {"skills", ".agents", ".claude"}: continue # already tried # Try direct: repo/dir/skill_token direct_id = f"{repo}/{dir_name}/{skill_token}" diff --git a/tools/skills_tool.py b/tools/skills_tool.py index 5da340c86b4..5d219771791 100644 --- a/tools/skills_tool.py +++ b/tools/skills_tool.py @@ -1133,7 +1133,7 @@ def skill_view( available_files["assets"].append(rel) elif rel.startswith("scripts/"): available_files["scripts"].append(rel) - elif f.suffix in [ + elif f.suffix in { ".md", ".py", ".yaml", @@ -1141,7 +1141,7 @@ def skill_view( ".json", ".tex", ".sh", - ]: + }: available_files["other"].append(rel) # Remove empty categories diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 3ff22e3f882..57bc42b2a22 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -139,7 +139,7 @@ def _check_vercel_sandbox_requirements(config: dict[str, Any]) -> bool: return False disk = config.get("container_disk", 51200) - if disk not in (0, 51200): + if disk not in {0, 51200}: logger.error( "Vercel Sandbox does not support custom TERMINAL_CONTAINER_DISK=%s. " "Use the default shared setting (51200 MB).", @@ -416,7 +416,7 @@ def _prompt_for_sudo_password(timeout_seconds: int = 45) -> str: chars = [] while True: c = msvcrt.getwch() - if c in ("\r", "\n"): + if c in {"\r", "\n"}: break if c == "\x03": raise KeyboardInterrupt @@ -432,7 +432,7 @@ def _prompt_for_sudo_password(timeout_seconds: int = 45) -> str: chars = [] while True: b = os.read(tty_fd, 1) - if not b or b in (b"\n", b"\r"): + if not b or b in {b"\n", b"\r"}: break chars.append(b) result["password"] = b"".join(chars).decode("utf-8", errors="replace") @@ -707,7 +707,7 @@ def _rewrite_compound_background(command: str) -> str: continue # Quoted tokens — consume whole string via the shared tokenizer. - if ch in ("'", '"'): + if ch in {"'", '"'}: _, next_i = _read_shell_token(command, i) i = max(next_i, i + 1) continue @@ -1009,7 +1009,7 @@ def _get_env_config() -> Dict[str, Any]: default_image = "nikolaik/python-nodejs:python3.11-nodejs20" env_type = os.getenv("TERMINAL_ENV", "local") - mount_docker_cwd = os.getenv("TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE", "false").lower() in ("true", "1", "yes") + mount_docker_cwd = os.getenv("TERMINAL_DOCKER_MOUNT_CWD_TO_WORKSPACE", "false").lower() in {"true", "1", "yes"} # Default cwd: local uses the host's current directory, ssh uses the # remote home, Vercel uses its documented workspace root, and everything @@ -1041,7 +1041,7 @@ def _get_env_config() -> Dict[str, Any]: ): host_cwd = candidate cwd = "/workspace" - elif env_type in ("modal", "docker", "singularity", "daytona", "vercel_sandbox") and cwd: + elif env_type in {"modal", "docker", "singularity", "daytona", "vercel_sandbox"} and cwd: # Host paths and relative paths that won't work inside containers is_host_path = any(cwd.startswith(p) for p in host_prefixes) is_relative = not os.path.isabs(cwd) # e.g. "." or "src/" @@ -1076,17 +1076,17 @@ def _get_env_config() -> Dict[str, Any]: "ssh_persistent": os.getenv( "TERMINAL_SSH_PERSISTENT", os.getenv("TERMINAL_PERSISTENT_SHELL", "true"), - ).lower() in ("true", "1", "yes"), - "local_persistent": os.getenv("TERMINAL_LOCAL_PERSISTENT", "false").lower() in ("true", "1", "yes"), + ).lower() in {"true", "1", "yes"}, + "local_persistent": os.getenv("TERMINAL_LOCAL_PERSISTENT", "false").lower() in {"true", "1", "yes"}, # Container resource config (applies to docker, singularity, modal, # daytona, and vercel_sandbox -- ignored for local/ssh) "container_cpu": _parse_env_var("TERMINAL_CONTAINER_CPU", "1", float, "number"), "container_memory": _parse_env_var("TERMINAL_CONTAINER_MEMORY", "5120"), # MB (default 5GB) "container_disk": _parse_env_var("TERMINAL_CONTAINER_DISK", "51200"), # MB (default 50GB) - "container_persistent": os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in ("true", "1", "yes"), + "container_persistent": os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in {"true", "1", "yes"}, "docker_volumes": _parse_env_var("TERMINAL_DOCKER_VOLUMES", "[]", json.loads, "valid JSON"), "docker_env": _parse_env_var("TERMINAL_DOCKER_ENV", "{}", json.loads, "valid JSON"), - "docker_run_as_host_user": os.getenv("TERMINAL_DOCKER_RUN_AS_HOST_USER", "false").lower() in ("true", "1", "yes"), + "docker_run_as_host_user": os.getenv("TERMINAL_DOCKER_RUN_AS_HOST_USER", "false").lower() in {"true", "1", "yes"}, "docker_extra_args": _parse_env_var("TERMINAL_DOCKER_EXTRA_ARGS", "[]", json.loads, "valid JSON"), } @@ -1782,7 +1782,7 @@ def terminal_tool( } container_config = None - if env_type in ("docker", "singularity", "modal", "daytona", "vercel_sandbox"): + if env_type in {"docker", "singularity", "modal", "daytona", "vercel_sandbox"}: container_config = { "container_cpu": config.get("container_cpu", 1), "container_memory": config.get("container_memory", 5120), diff --git a/tools/tirith_security.py b/tools/tirith_security.py index bad94c96f7f..350265d33a1 100644 --- a/tools/tirith_security.py +++ b/tools/tirith_security.py @@ -52,7 +52,7 @@ def _env_bool(key: str, default: bool) -> bool: val = os.getenv(key) if val is None: return default - return val.lower() in ("1", "true", "yes") + return val.lower() in {"1", "true", "yes"} def _env_int(key: str, default: int) -> int: @@ -189,14 +189,14 @@ def _detect_target() -> str | None: # Android (Termux) is ABI-compatible with Linux — reuse Linux binaries. if system == "Darwin": plat = "apple-darwin" - elif system in ("Linux", "Android"): + elif system in {"Linux", "Android"}: plat = "unknown-linux-gnu" else: return None - if machine in ("x86_64", "amd64"): + if machine in {"x86_64", "amd64"}: arch = "x86_64" - elif machine in ("aarch64", "arm64"): + elif machine in {"aarch64", "arm64"}: arch = "aarch64" else: return None diff --git a/tools/todo_tool.py b/tools/todo_tool.py index b0d38a23426..99d9ffe8515 100644 --- a/tools/todo_tool.py +++ b/tools/todo_tool.py @@ -109,7 +109,7 @@ class TodoStore: # cause the model to re-do finished work after compression. active_items = [ item for item in self._items - if item["status"] in ("pending", "in_progress") + if item["status"] in {"pending", "in_progress"} ] if not active_items: return None diff --git a/tools/tts_tool.py b/tools/tts_tool.py index 01b4dd3e5be..8c32d66de18 100644 --- a/tools/tts_tool.py +++ b/tools/tts_tool.py @@ -1612,7 +1612,7 @@ def text_to_speech_tool( file_path = out_dir / f"tts_{timestamp}.{fmt}" # Use .ogg for Telegram with providers that support native Opus output, # otherwise fall back to .mp3 (Edge TTS will attempt ffmpeg conversion later). - elif want_opus and provider in ("openai", "elevenlabs", "mistral", "gemini"): + elif want_opus and provider in {"openai", "elevenlabs", "mistral", "gemini"}: file_path = out_dir / f"tts_{timestamp}.ogg" else: file_path = out_dir / f"tts_{timestamp}.mp3" @@ -1762,12 +1762,12 @@ def text_to_speech_tool( if opus_path: file_str = opus_path voice_compatible = file_str.endswith(".ogg") - elif provider in ("edge", "neutts", "minimax", "xai", "kittentts", "piper") and not file_str.endswith(".ogg"): + elif provider in {"edge", "neutts", "minimax", "xai", "kittentts", "piper"} and not file_str.endswith(".ogg"): opus_path = _convert_to_opus(file_str) if opus_path: file_str = opus_path voice_compatible = True - elif provider in ("elevenlabs", "openai", "mistral", "gemini"): + elif provider in {"elevenlabs", "openai", "mistral", "gemini"}: voice_compatible = file_str.endswith(".ogg") file_size = os.path.getsize(file_str) diff --git a/tools/url_safety.py b/tools/url_safety.py index 723b1b0c7c3..743510b2757 100644 --- a/tools/url_safety.py +++ b/tools/url_safety.py @@ -96,10 +96,10 @@ def _global_allow_private_urls() -> bool: # 1. Env var override (highest priority) env_val = os.getenv("HERMES_ALLOW_PRIVATE_URLS", "").strip().lower() - if env_val in ("true", "1", "yes"): + if env_val in {"true", "1", "yes"}: _cached_allow_private = True return _cached_allow_private - if env_val in ("false", "0", "no"): + if env_val in {"false", "0", "no"}: # Explicit false — don't fall through to config return _cached_allow_private diff --git a/tools/vision_tools.py b/tools/vision_tools.py index d8c6f64f021..96401294748 100644 --- a/tools/vision_tools.py +++ b/tools/vision_tools.py @@ -346,7 +346,7 @@ def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None, data_url = _image_to_base64_data_url(image_path, mime_type=mime_type) return data_url # fall through to size-check in caller # Convert RGBA to RGB for JPEG output - if pil_format == "JPEG" and img.mode in ("RGBA", "P"): + if pil_format == "JPEG" and img.mode in {"RGBA", "P"}: img = img.convert("RGB") # Strategy: halve dimensions until base64 fits, up to 4 rounds. diff --git a/tools/web_tools.py b/tools/web_tools.py index 2768bd5ebcf..df16c31db78 100644 --- a/tools/web_tools.py +++ b/tools/web_tools.py @@ -126,7 +126,7 @@ def _get_backend() -> str: keys manually without running setup. """ configured = (_load_web_config().get("backend") or "").lower().strip() - if configured in ("parallel", "firecrawl", "tavily", "exa", "searxng", "brave-free", "ddgs"): + if configured in {"parallel", "firecrawl", "tavily", "exa", "searxng", "brave-free", "ddgs"}: return configured # Fallback for manual / legacy config — pick the highest-priority @@ -1074,7 +1074,7 @@ def _parallel_search(query: str, limit: int = 5) -> dict: return {"error": "Interrupted", "success": False} mode = os.getenv("PARALLEL_SEARCH_MODE", "agentic").lower().strip() - if mode not in ("fast", "one-shot", "agentic"): + if mode not in {"fast", "one-shot", "agentic"}: mode = "agentic" logger.info("Parallel search: '%s' (mode=%s, limit=%d)", query, mode, limit) @@ -1397,7 +1397,7 @@ async def web_extract_tool( "include_images": False, }) results = _normalize_tavily_documents(raw, fallback_url=safe_urls[0] if safe_urls else "") - elif backend in ("searxng", "brave-free", "ddgs"): + elif backend in {"searxng", "brave-free", "ddgs"}: # These backends are search-only — they cannot extract URL content _label = {"searxng": "SearXNG", "brave-free": "Brave Search (free tier)", "ddgs": "DuckDuckGo (ddgs)"}[backend] return json.dumps({ @@ -1781,7 +1781,7 @@ async def web_crawl_tool( return cleaned_result # SearXNG / Brave Search (free tier) / DuckDuckGo (ddgs) are search-only — they cannot crawl - if backend in ("searxng", "brave-free", "ddgs"): + if backend in {"searxng", "brave-free", "ddgs"}: _label = {"searxng": "SearXNG", "brave-free": "Brave Search (free tier)", "ddgs": "DuckDuckGo (ddgs)"}[backend] return json.dumps({ "error": f"{_label} is a search-only backend and cannot crawl URLs. " @@ -2084,7 +2084,7 @@ def check_firecrawl_api_key() -> bool: def check_web_api_key() -> bool: """Check whether the configured web backend is available.""" configured = _load_web_config().get("backend", "").lower().strip() - if configured in ("exa", "parallel", "firecrawl", "tavily", "searxng", "brave-free", "ddgs"): + if configured in {"exa", "parallel", "firecrawl", "tavily", "searxng", "brave-free", "ddgs"}: return _is_backend_available(configured) return any( _is_backend_available(backend) diff --git a/tools/yuanbao_tools.py b/tools/yuanbao_tools.py index e12307b85e0..6466458d34f 100644 --- a/tools/yuanbao_tools.py +++ b/tools/yuanbao_tools.py @@ -122,7 +122,7 @@ async def query_group_members( hint = {"mention_hint": MENTION_HINT} if mention else {} if action == "list_bots": - bots = [m for m in all_members if m["role"] in ("yuanbao_ai", "bot")] + bots = [m for m in all_members if m["role"] in {"yuanbao_ai", "bot"}] if not bots: return {"success": False, "error": "No bots found in this group."} return { diff --git a/tui_gateway/entry.py b/tui_gateway/entry.py index 12d53c6d2e5..0400a3fcbff 100644 --- a/tui_gateway/entry.py +++ b/tui_gateway/entry.py @@ -9,7 +9,7 @@ if _src_root and _src_root not in sys.path: sys.path.insert(0, _src_root) # Strip '' and '.' — both resolve to CWD at import time and can let a local # directory shadow installed packages. -sys.path = [p for p in sys.path if p not in ("", ".")] +sys.path = [p for p in sys.path if p not in {"", "."}] import json import signal diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 73b2fc1a811..6635a5cda3c 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1706,7 +1706,7 @@ def _available_personalities(cfg: dict | None = None) -> dict: def _validate_personality(value: str, cfg: dict | None = None) -> tuple[str, str]: raw = str(value or "").strip() name = raw.lower() - if not name or name in ("none", "default", "neutral"): + if not name or name in {"none", "default", "neutral"}: return "", "" personalities = _available_personalities(cfg) @@ -2053,7 +2053,7 @@ def _history_to_messages(history: list[dict]) -> list[dict]: if not isinstance(m, dict): continue role = m.get("role") - if role not in ("user", "assistant", "tool", "system"): + if role not in {"user", "assistant", "tool", "system"}: continue content_text = _content_display_text(m.get("content")) if role == "assistant" and m.get("tool_calls"): @@ -2496,7 +2496,7 @@ def _(rid, params: dict) -> dict: removed = 0 with session["history_lock"]: history = session.get("history", []) - while history and history[-1].get("role") in ("assistant", "tool"): + while history and history[-1].get("role") in {"assistant", "tool"}: history.pop() removed += 1 if history and history[-1].get("role") == "user": @@ -3668,7 +3668,7 @@ def _(rid, params: dict) -> dict: {"key": key, "value": "fast" if current_fast else "normal"}, ) - if raw in ("", "toggle"): + if raw in {"", "toggle"}: nv = "normal" if current_fast else "fast" elif raw in {"fast", "on"}: nv = "fast" @@ -3716,7 +3716,7 @@ def _(rid, params: dict) -> dict: if key == "busy": raw = str(value or "").strip().lower() - if raw in ("", "status"): + if raw in {"", "status"}: return _ok(rid, {"key": key, "value": _load_busy_input_mode()}) if raw not in {"queue", "steer", "interrupt"}: return _err(rid, 4002, f"unknown busy mode: {value}") @@ -3781,7 +3781,7 @@ def _(rid, params: dict) -> dict: from hermes_constants import parse_reasoning_effort arg = str(value or "").strip().lower() - if arg in ("show", "on"): + if arg in {"show", "on"}: cfg = _load_cfg() display = ( cfg.get("display") if isinstance(cfg.get("display"), dict) else {} @@ -3799,7 +3799,7 @@ def _(rid, params: dict) -> dict: if session: session["show_reasoning"] = True return _ok(rid, {"key": key, "value": "show"}) - if arg in ("hide", "off"): + if arg in {"hide", "off"}: cfg = _load_cfg() display = ( cfg.get("display") if isinstance(cfg.get("display"), dict) else {} @@ -3894,7 +3894,7 @@ def _(rid, params: dict) -> dict: cfg0 = _load_cfg() d0 = cfg0.get("display") if isinstance(cfg0.get("display"), dict) else {} cur_b = bool(d0.get("tui_compact", False)) - if raw in ("", "toggle"): + if raw in {"", "toggle"}: nv_b = not cur_b elif raw == "on": nv_b = True @@ -3911,7 +3911,7 @@ def _(rid, params: dict) -> dict: d0 = display if isinstance(display, dict) else {} current = _coerce_statusbar(d0.get("tui_statusbar", "top")) - if raw in ("", "toggle"): + if raw in {"", "toggle"}: nv = "top" if current == "off" else "off" elif raw == "on": nv = "top" @@ -3929,7 +3929,7 @@ def _(rid, params: dict) -> dict: display = cfg.get("display") if isinstance(cfg.get("display"), dict) else {} current = _display_mouse_tracking(display) - if raw in ("", "toggle"): + if raw in {"", "toggle"}: nv = not current elif raw == "on": nv = True @@ -3955,7 +3955,7 @@ def _(rid, params: dict) -> dict: _write_config_key("display.tui_status_indicator", raw) return _ok(rid, {"key": key, "value": raw}) - if key in ("prompt", "personality", "skin"): + if key in {"prompt", "personality", "skin"}: try: cfg = _load_cfg() if key == "prompt": @@ -4518,7 +4518,7 @@ def _(rid, params: dict) -> dict: # In the TUI the slash worker subprocess has no reader for that queue, # so we handle them here and return a structured payload. - if name in ("queue", "q"): + if name in {"queue", "q"}: if not arg: return _err(rid, 4004, "usage: /queue ") return _ok(rid, {"type": "send", "message": arg}) @@ -4617,7 +4617,7 @@ def _(rid, params: dict) -> dict: ), }, ) - if lower in ("clear", "stop", "done"): + if lower in {"clear", "stop", "done"}: had = mgr.has_goal() mgr.clear() return _ok( @@ -4648,7 +4648,7 @@ def _(rid, params: dict) -> dict: {"type": "send", "notice": notice, "message": state.goal}, ) - if name in ("snapshot", "snap"): + if name in {"snapshot", "snap"}: subcommand = arg.split(maxsplit=1)[0].lower() if arg else "" if subcommand in {"restore", "rewind"}: return _ok( @@ -4893,7 +4893,7 @@ def _(rid, params: dict) -> dict: # Accept both `@folder:path` and the bare `@folder` form so the user # sees directory listings as soon as they finish typing the keyword, # without first accepting the static `@folder:` hint. - if is_context and query in ("file", "folder"): + if is_context and query in {"file", "folder"}: prefix_tag, path_part = query, "" elif is_context and query.startswith(("file:", "folder:")): prefix_tag, _, tail = query.partition(":") @@ -5637,7 +5637,7 @@ def _(rid, params: dict) -> dict: return _ok(rid, payload) - if action in ("on", "off"): + if action in {"on", "off"}: enabled = action == "on" # Runtime-only flag (CLI parity) — no _write_config_key, so the # next TUI launch starts with voice OFF instead of auto-REC from a @@ -5870,7 +5870,7 @@ def _(rid, params: dict) -> dict: removed = 0 with session["history_lock"]: history = session.get("history", []) - while history and history[-1].get("role") in ("assistant", "tool"): + while history and history[-1].get("role") in {"assistant", "tool"}: history.pop() removed += 1 if history and history[-1].get("role") == "user": @@ -6428,7 +6428,7 @@ def _(rid, params: dict) -> dict: ) ), ) - if action in ("remove", "pause", "resume"): + if action in {"remove", "pause", "resume"}: return _ok(rid, json.loads(cronjob(action=action, job_id=jid))) return _err(rid, 4016, f"unknown cron action: {action}") except Exception as e: diff --git a/website/scripts/extract-skills.py b/website/scripts/extract-skills.py index 302fbe51c30..b508eb19872 100644 --- a/website/scripts/extract-skills.py +++ b/website/scripts/extract-skills.py @@ -309,7 +309,7 @@ MIN_CATEGORY_SIZE = 4 def _consolidate_small_categories(skills: list) -> list: for s in skills: - if s["category"] in ("uncategorized", ""): + if s["category"] in {"uncategorized", ""}: s["category"] = "other" s["categoryLabel"] = "Other" diff --git a/website/scripts/generate-llms-txt.py b/website/scripts/generate-llms-txt.py index 5bb2c65cb53..a34c57792a3 100644 --- a/website/scripts/generate-llms-txt.py +++ b/website/scripts/generate-llms-txt.py @@ -280,7 +280,7 @@ def emit_llms_full() -> str: rel = path.relative_to(DOCS) parts = rel.parts if len(parts) >= 3 and parts[0] == "user-guide" and parts[1] == "skills" \ - and parts[2] in ("bundled", "optional"): + and parts[2] in {"bundled", "optional"}: continue seen.add(path) meta, body = read_frontmatter(path)