From 6765c9469407fb993b390e8b509c9de904b7c7ec Mon Sep 17 00:00:00 2001 From: xeondesk Date: Fri, 6 Mar 2026 09:36:20 +0600 Subject: [PATCH] fix: add ruff configuration and fix linting issues - Add comprehensive ruff configuration to pyproject.toml - Auto-fix 816 linting errors using ruff --fix - Configure appropriate ignores for framework patterns - All ruff checks now pass (0 errors) Configured ignores for: - Test file import patterns (E402) - Framework-specific patterns (mutable defaults, function calls in defaults) - Style preferences and modern Python suggestions - Exception naming conventions and import aliases --- agents/_example/tools/example_tool.py | 4 +- agents/_example/tools/response.py | 4 +- backend/core/agent.py | 26 ++-- backend/core/events.py | 9 +- backend/core/exceptions.py | 7 +- backend/core/models.py | 75 ++++------ .../_40_handle_intervention_exception.py | 5 - .../_50_handle_repairable_exception.py | 3 - .../agent_init/_15_load_profile_settings.py | 2 +- .../_10_log_for_stream.py | 4 - .../hist_add_before/_10_mask_content.py | 2 +- .../_90_save_tool_call_file.py | 1 - .../_60_include_current_datetime.py | 4 +- .../_90_organize_history_wait.py | 2 +- .../monologue_start/_60_rename_chat.py | 2 +- .../process_chain_end/_50_process_queue.py | 2 +- .../reasoning_stream/_10_log_from_stream.py | 4 - .../reasoning_stream_chunk/_10_mask_stream.py | 2 +- .../reasoning_stream_end/_10_mask_end.py | 2 +- .../response_stream/_10_log_from_stream.py | 3 - .../response_stream/_20_live_response.py | 7 +- .../response_stream_chunk/_10_mask_stream.py | 3 +- .../response_stream_end/_10_mask_end.py | 3 +- .../_15_log_from_stream_end.py | 8 - .../system_prompt/_10_system_prompt.py | 2 +- .../user_message_ui/_10_update_check.py | 2 +- backend/interfaces/a2a/server.py | 8 +- .../interfaces/api/routes/agents/subagents.py | 4 +- .../routes/backup/backup_preview_grouped.py | 4 +- .../interfaces/api/routes/chat/chat_create.py | 2 +- .../interfaces/api/routes/chat/chat_export.py | 2 +- .../interfaces/api/routes/chat/chat_load.py | 2 +- .../interfaces/api/routes/chat/chat_remove.py | 2 +- .../interfaces/api/routes/chat/chat_reset.py | 2 +- .../api/routes/chat/ctx_window_get.py | 3 +- .../api/routes/files/delete_work_dir_file.py | 4 +- .../api/routes/files/edit_work_dir_file.py | 2 +- .../interfaces/api/routes/files/file_info.py | 2 +- .../api/routes/files/get_work_dir_files.py | 2 +- .../interfaces/api/routes/files/image_get.py | 2 +- .../api/routes/files/upload_work_dir_files.py | 3 +- .../interfaces/api/routes/media/synthesize.py | 4 +- .../interfaces/api/routes/media/transcribe.py | 4 +- .../interfaces/api/routes/plugins/plugins.py | 4 +- .../interfaces/api/routes/plugins/skills.py | 2 +- .../api/routes/plugins/skills_import.py | 1 - .../routes/plugins/skills_import_preview.py | 1 - .../api/routes/projects/projects.py | 8 +- .../api/routes/settings/csrf_token.py | 1 - .../api/routes/system/tunnel_proxy.py | 3 +- backend/interfaces/mcp/server.py | 7 +- .../websockets/dev_websocket_test_handler.py | 4 +- backend/interfaces/websockets/websocket.py | 15 +- .../websockets/websocket_manager.py | 58 ++++---- .../websocket_namespace_discovery.py | 2 +- backend/services/agent_service.py | 11 +- backend/services/chat_service.py | 10 +- backend/services/memory_service.py | 12 +- backend/services/project_service.py | 10 +- backend/services/skill_service.py | 10 +- backend/tools/browser/browser_agent.py | 15 +- backend/tools/system/input.py | 3 +- backend/tools/system/search_engine.py | 4 - backend/tools/system/skills_tool.py | 6 +- backend/tools/system/vision_load.py | 1 - backend/tools/system/wait.py | 7 +- backend/utils/api.py | 10 +- backend/utils/attachment_manager.py | 5 +- backend/utils/backup.py | 52 +++---- backend/utils/browser_use.py | 2 - backend/utils/call_llm.py | 3 +- backend/utils/context.py | 8 +- backend/utils/crypto.py | 1 - backend/utils/defer.py | 11 +- backend/utils/docker.py | 7 +- backend/utils/document_query.py | 25 ++-- backend/utils/email_client.py | 42 +++--- backend/utils/extension.py | 4 +- backend/utils/extract_tools.py | 8 +- backend/utils/faiss_monkey_patch.py | 1 - backend/utils/fasta2a_client.py | 22 +-- backend/utils/file_browser.py | 16 +- backend/utils/file_tree.py | 35 ++--- backend/utils/files.py | 11 +- backend/utils/history.py | 22 ++- backend/utils/job_loop.py | 1 - backend/utils/kokoro_tts.py | 1 - backend/utils/kvp.py | 2 +- backend/utils/localization.py | 10 +- backend/utils/log.py | 14 +- backend/utils/maintenance.py | 1 - backend/utils/mcp_handler.py | 56 +++---- backend/utils/message_queue.py | 1 - backend/utils/notification.py | 6 +- backend/utils/playwright.py | 1 - backend/utils/plugins.py | 26 ++-- backend/utils/print_catch.py | 5 +- backend/utils/projects.py | 4 +- backend/utils/providers.py | 26 ++-- backend/utils/rate_limiter.py | 6 +- backend/utils/rfc.py | 2 +- backend/utils/runtime.py | 9 +- backend/utils/secrets.py | 57 ++++--- backend/utils/security.py | 4 +- backend/utils/settings.py | 11 +- backend/utils/shell_local.py | 8 +- backend/utils/shell_ssh.py | 4 +- backend/utils/skills.py | 56 +++---- backend/utils/skills_cli.py | 31 ++-- backend/utils/skills_import.py | 40 +++-- backend/utils/state_monitor.py | 2 +- backend/utils/state_snapshot.py | 3 +- backend/utils/subagents.py | 2 +- backend/utils/task_scheduler.py | 139 +++++++++--------- backend/utils/timed_input.py | 2 +- backend/utils/tty_session.py | 3 +- backend/utils/vector_db.py | 11 +- backend/utils/wait.py | 10 +- backend/utils/whisper.py | 2 +- .../run/fs/exe/supervisor_event_listener.py | 6 +- initialize.py | 9 +- plugins/chat_branching/api/branch_chat.py | 6 +- .../_80_retry_critical_exception.py | 15 +- .../_10_reset_critical_exception_counter.py | 9 +- plugins/plugin_scan/api/plugin_scan_queue.py | 2 +- .../system_prompt/_15_text_editor_prompt.py | 4 +- plugins/text_editor/helpers/file_ops.py | 4 +- plugins/text_editor/tools/text_editor.py | 10 +- prompts/agent.system.main.tips.py | 11 +- prompts/agent.system.tool.call_sub.py | 9 +- prompts/agent.system.tools.py | 5 +- pyproject.toml | 13 ++ run_ui.py | 55 +++---- scripts/maintenance_tool.py | 39 ++--- scripts/preload.py | 6 +- scripts/prepare.py | 6 +- scripts/run_tunnel.py | 12 +- scripts/update_reqs.py | 8 +- test_litellm.py | 8 +- test_litellm2.py | 11 +- tests/chunk_parser_test.py | 3 +- tests/email_parser_test.py | 7 +- tests/rate_limiter_test.py | 6 +- tests/test_fasta2a_client.py | 6 +- tests/test_file_tree_visualize.py | 35 +++-- tests/test_http_auth_csrf.py | 2 - tests/test_settings_developer_sections.py | 1 - tests/test_snapshot_parity.py | 2 +- tests/test_snapshot_schema_v1.py | 1 - tests/test_socketio_library_semantics.py | 3 +- tests/test_socketio_unknown_namespace.py | 3 +- tests/test_state_sync_handler.py | 4 +- tests/test_state_sync_welcome_screen.py | 4 +- tests/test_websocket_client_api_surface.py | 1 - tests/test_websocket_handlers.py | 4 +- tests/test_websocket_harness.py | 2 +- tests/test_websocket_manager.py | 8 +- tests/test_websocket_namespace_discovery.py | 5 +- tests/test_websocket_namespace_security.py | 13 +- tests/test_websocket_namespaces.py | 7 +- .../test_websocket_namespaces_integration.py | 5 +- tests/test_websocket_root_namespace.py | 7 +- tests/test_webui_extension_surfaces.py | 3 +- tests/websocket_namespace_test_utils.py | 1 - 164 files changed, 786 insertions(+), 871 deletions(-) diff --git a/agents/_example/tools/example_tool.py b/agents/_example/tools/example_tool.py index f94fe983..e7806222 100644 --- a/agents/_example/tools/example_tool.py +++ b/agents/_example/tools/example_tool.py @@ -1,7 +1,7 @@ -from backend.utils.tool import Tool, Response +from backend.utils.tool import Response, Tool # this is an example tool class -# don't forget to include instructions in the system prompt by creating +# don't forget to include instructions in the system prompt by creating # agent.system.tool.example_tool.md file in prompts directory of your agent # see /backend/tools folder for all default tools diff --git a/agents/_example/tools/response.py b/agents/_example/tools/response.py index 07c8948e..6a955b90 100644 --- a/agents/_example/tools/response.py +++ b/agents/_example/tools/response.py @@ -1,4 +1,4 @@ -from backend.utils.tool import Tool, Response +from backend.utils.tool import Response, Tool # example of a tool redefinition # the original response tool is in backend/tools/response.py @@ -20,4 +20,4 @@ async def after_execution(self, response, **kwargs): if self.loop_data and "log_item_response" in self.loop_data.params_temporary: log = self.loop_data.params_temporary["log_item_response"] - log.update(finished=True) # mark the message as finished \ No newline at end of file + log.update(finished=True) # mark the message as finished diff --git a/backend/core/agent.py b/backend/core/agent.py index 4761ab5c..847c10b4 100644 --- a/backend/core/agent.py +++ b/backend/core/agent.py @@ -3,10 +3,11 @@ import string import threading from collections import OrderedDict +from collections.abc import Awaitable, Callable, Coroutine from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import Enum -from typing import Any, Awaitable, Callable, Coroutine, Dict, Literal +from typing import Any from langchain_core.messages import BaseMessage, SystemMessage from langchain_core.prompts import ChatPromptTemplate @@ -18,23 +19,16 @@ from backend.utils import context as context_helper from backend.utils import ( dirty_json, - errors, - extension, files, history, -) -from backend.utils import log as Log -from backend.utils import ( - print_style, subagents, tokens, ) +from backend.utils import log as Log from backend.utils.defer import DeferredTask from backend.utils.dirty_json import DirtyJson from backend.utils.errors import ( - HandledException, InterventionException, - RepairableException, ) from backend.utils.extension import call_extensions, extensible from backend.utils.extract_tools import json_parse_dirty, load_classes_from_file @@ -94,11 +88,11 @@ def __init__( self.paused = paused self.streaming_agent = streaming_agent self.task: DeferredTask | None = None - self.created_at = created_at or datetime.now(timezone.utc) + self.created_at = created_at or datetime.now(UTC) self.type = type AgentContext._counter += 1 self.no = AgentContext._counter - self.last_message = last_message or datetime.now(timezone.utc) + self.last_message = last_message or datetime.now(UTC) # initialize agent at last (context is complete now) self.ctx = ctx or Agent(0, self.config, self) @@ -284,7 +278,7 @@ def run_task(self, func: Callable[..., Coroutine[Any, Any, Any]], *args: Any, ** @extensible async def _process_chain(self, agent: "Agent", msg: "UserMessage|str", user=True): try: - msg_template = ( + ( agent.hist_add_user_message(msg) # type: ignore if user else agent.hist_add_tool_result( @@ -327,7 +321,7 @@ class AgentConfig: code_exec_ssh_port: int = 55022 code_exec_ssh_user: str = "root" code_exec_ssh_pass: str = "" - additional: Dict[str, Any] = field(default_factory=dict) + additional: dict[str, Any] = field(default_factory=dict) @dataclass @@ -627,7 +621,7 @@ def set_data(self, field: str, value): @extensible def hist_add_message(self, ai: bool, content: history.MessageContent, tokens: int = 0): - self.last_message = datetime.now(timezone.utc) + self.last_message = datetime.now(UTC) # Allow extensions to process content before adding to history content_data = {"content": content} asyncio.run(self.call_extensions("hist_add_before", content_data=content_data, ai=ai)) @@ -925,7 +919,7 @@ async def handle_response_stream(self, stream: str): parsed=response, ) - except Exception as e: + except Exception: pass @extensible diff --git a/backend/core/events.py b/backend/core/events.py index c84163cc..b7885140 100644 --- a/backend/core/events.py +++ b/backend/core/events.py @@ -6,9 +6,10 @@ """ import asyncio +from collections.abc import Callable from dataclasses import dataclass from datetime import datetime -from typing import Any, Callable, Dict, List +from typing import Any @dataclass @@ -16,7 +17,7 @@ class Event: """Represents an event in the system.""" name: str - data: Dict[str, Any] + data: dict[str, Any] timestamp: datetime source: str = "unknown" @@ -25,7 +26,7 @@ class EventManager: """Manages event subscription and publishing.""" def __init__(self): - self._subscribers: Dict[str, List[Callable]] = {} + self._subscribers: dict[str, list[Callable]] = {} self._lock = asyncio.Lock() async def subscribe(self, event_name: str, callback: Callable[[Event], None]) -> None: @@ -65,7 +66,7 @@ async def publish(self, event: Event) -> None: if tasks: await asyncio.gather(*tasks, return_exceptions=True) - def get_subscribed_events(self) -> List[str]: + def get_subscribed_events(self) -> list[str]: """Get list of all subscribed event names.""" return list(self._subscribers.keys()) diff --git a/backend/core/exceptions.py b/backend/core/exceptions.py index 046bcde0..b0a16478 100644 --- a/backend/core/exceptions.py +++ b/backend/core/exceptions.py @@ -5,13 +5,12 @@ the application to provide better error handling and debugging. """ -from typing import Any, Optional class CtxAIException(Exception): """Base exception class for all Ctx AI framework exceptions.""" - def __init__(self, message: str, details: Optional[dict] = None): + def __init__(self, message: str, details: dict | None = None): super().__init__(message) self.message = message self.details = details or {} @@ -63,7 +62,7 @@ class RateLimitException(CtxAIException): """Exception raised when rate limits are exceeded.""" def __init__( - self, message: str, retry_after: Optional[int] = None, details: Optional[dict] = None + self, message: str, retry_after: int | None = None, details: dict | None = None ): super().__init__(message, details) self.retry_after = retry_after @@ -73,7 +72,7 @@ class TimeoutException(CtxAIException): """Exception raised when operations timeout.""" def __init__( - self, message: str, timeout_seconds: Optional[float] = None, details: Optional[dict] = None + self, message: str, timeout_seconds: float | None = None, details: dict | None = None ): super().__init__(message, details) self.timeout_seconds = timeout_seconds diff --git a/backend/core/models.py b/backend/core/models.py index 1f14e1a9..a244417e 100644 --- a/backend/core/models.py +++ b/backend/core/models.py @@ -1,16 +1,10 @@ import logging import os +from collections.abc import AsyncIterator, Awaitable, Callable, Iterator from dataclasses import dataclass, field from enum import Enum from typing import ( Any, - AsyncIterator, - Awaitable, - Callable, - Iterator, - List, - Optional, - Tuple, TypedDict, ) @@ -30,7 +24,6 @@ ) from langchain_core.outputs.chat_generation import ChatGenerationChunk from litellm import acompletion, completion, embedding -from litellm.types.utils import ModelResponse # from sentence_transformers import SentenceTransformer # Temporarily disabled for Python 3.14 compatibility from pydantic import ConfigDict @@ -311,7 +304,7 @@ def __init__( self, model: str, provider: str, - model_config: Optional[ModelConfig] = None, + model_config: ModelConfig | None = None, **kwargs: Any, ): model_value = f"{provider}/{model}" @@ -324,8 +317,8 @@ def _llm_type(self) -> str: return "litellm-chat" def _convert_messages( - self, messages: List[BaseMessage], explicit_caching: bool = False - ) -> List[dict]: + self, messages: list[BaseMessage], explicit_caching: bool = False + ) -> list[dict]: result = [] # Map LangChain message types to LiteLLM roles role_mapping = { @@ -389,12 +382,11 @@ def _convert_messages( def _call( self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, **kwargs: Any, ) -> str: - import asyncio msgs = self._convert_messages(messages) @@ -413,12 +405,11 @@ def _call( def _stream( self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, **kwargs: Any, ) -> Iterator[ChatGenerationChunk]: - import asyncio msgs = self._convert_messages(messages) @@ -444,9 +435,9 @@ def _stream( async def _astream( self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: AsyncCallbackManagerForLLMRun | None = None, **kwargs: Any, ) -> AsyncIterator[ChatGenerationChunk]: msgs = self._convert_messages(messages) @@ -476,14 +467,14 @@ async def unified_call( self, system_message="", user_message="", - messages: List[BaseMessage] | None = None, + messages: list[BaseMessage] | None = None, response_callback: Callable[[str, str], Awaitable[None]] | None = None, reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None, tokens_callback: Callable[[str, int], Awaitable[None]] | None = None, rate_limiter_callback: Callable[[str, str, int, int], Awaitable[bool]] | None = None, explicit_caching: bool = False, **kwargs: Any, - ) -> Tuple[str, str]: + ) -> tuple[str, str]: turn_off_logging() @@ -618,11 +609,7 @@ def __init__(self, wrapper, *args, **kwargs): from browser_use.llm import ( - ChatAnthropic, ChatGoogle, - ChatGroq, - ChatOllama, - ChatOpenAI, ChatOpenRouter, ) @@ -654,9 +641,9 @@ def get_client(self, *args, **kwargs): # type: ignore async def _acall( self, - messages: List[BaseMessage], - stop: Optional[List[str]] = None, - run_manager: Optional[CallbackManagerForLLMRun] = None, + messages: list[BaseMessage], + stop: list[str] | None = None, + run_manager: CallbackManagerForLLMRun | None = None, **kwargs: Any, ): # Apply rate limiting if configured @@ -713,7 +700,7 @@ async def _acall( ): # type: ignore js = dirty_json.parse(resp.choices[0].message.content) # type: ignore resp.choices[0].message.content = dirty_json.stringify(js) # type: ignore - except Exception as e: + except Exception: pass return resp @@ -722,20 +709,20 @@ async def _acall( class LiteLLMEmbeddingWrapper(Embeddings): model_name: str kwargs: dict = {} - a0_model_conf: Optional[ModelConfig] = None + a0_model_conf: ModelConfig | None = None def __init__( self, model: str, provider: str, - model_config: Optional[ModelConfig] = None, + model_config: ModelConfig | None = None, **kwargs: Any, ): self.model_name = f"{provider}/{model}" if provider != "openai" else model self.kwargs = kwargs self.a0_model_conf = model_config - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str]) -> list[list[float]]: # Apply rate limiting if configured apply_rate_limiter_sync(self.a0_model_conf, " ".join(texts)) @@ -745,7 +732,7 @@ def embed_documents(self, texts: List[str]) -> List[List[float]]: for item in resp.data # type: ignore ] - def embed_query(self, text: str) -> List[float]: + def embed_query(self, text: str) -> list[float]: # Apply rate limiting if configured apply_rate_limiter_sync(self.a0_model_conf, text) @@ -761,7 +748,7 @@ def __init__( self, provider: str, model: str, - model_config: Optional[ModelConfig] = None, + model_config: ModelConfig | None = None, **kwargs: Any, ): # Clean common user-input mistakes @@ -786,14 +773,14 @@ def __init__( self.model_name = model self.a0_model_conf = model_config - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str]) -> list[list[float]]: # Apply rate limiting if configured apply_rate_limiter_sync(self.a0_model_conf, " ".join(texts)) embeddings = self.model.encode(texts, convert_to_tensor=False) # type: ignore return embeddings.tolist() if hasattr(embeddings, "tolist") else embeddings # type: ignore - def embed_query(self, text: str) -> List[float]: + def embed_query(self, text: str) -> list[float]: # Apply rate limiting if configured apply_rate_limiter_sync(self.a0_model_conf, text) @@ -806,7 +793,7 @@ def _get_litellm_chat( cls: type = LiteLLMChatWrapper, model_name: str = "", provider_name: str = "", - model_config: Optional[ModelConfig] = None, + model_config: ModelConfig | None = None, **kwargs: Any, ): # use api key from kwargs or env @@ -823,7 +810,7 @@ def _get_litellm_chat( def _get_litellm_embedding( model_name: str, provider_name: str, - model_config: Optional[ModelConfig] = None, + model_config: ModelConfig | None = None, **kwargs: Any, ): # Check if this is a local sentence-transformers model @@ -945,7 +932,7 @@ def _normalize_values(values: dict) -> dict: def get_chat_model( - provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any + provider: str, name: str, model_config: ModelConfig | None = None, **kwargs: Any ) -> LiteLLMChatWrapper: orig = provider.lower() provider_name, kwargs = _merge_provider_defaults("chat", orig, kwargs) @@ -953,7 +940,7 @@ def get_chat_model( def get_browser_model( - provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any + provider: str, name: str, model_config: ModelConfig | None = None, **kwargs: Any ) -> BrowserCompatibleChatWrapper: orig = provider.lower() provider_name, kwargs = _merge_provider_defaults("chat", orig, kwargs) @@ -963,7 +950,7 @@ def get_browser_model( def get_embedding_model( - provider: str, name: str, model_config: Optional[ModelConfig] = None, **kwargs: Any + provider: str, name: str, model_config: ModelConfig | None = None, **kwargs: Any ) -> LiteLLMEmbeddingWrapper | LocalSentenceTransformerWrapper: orig = provider.lower() provider_name, kwargs = _merge_provider_defaults("embedding", orig, kwargs) diff --git a/backend/extensions/agent_Agent_handle_exception_end/_40_handle_intervention_exception.py b/backend/extensions/agent_Agent_handle_exception_end/_40_handle_intervention_exception.py index ce65102f..67949e0c 100644 --- a/backend/extensions/agent_Agent_handle_exception_end/_40_handle_intervention_exception.py +++ b/backend/extensions/agent_Agent_handle_exception_end/_40_handle_intervention_exception.py @@ -1,11 +1,6 @@ -from datetime import datetime, timezone -from backend.core.agent import LoopData -from backend.utils import errors from backend.utils.errors import InterventionException from backend.utils.extension import Extension -from backend.utils.localization import Localization -from backend.utils.print_style import PrintStyle class HandleInterventionException(Extension): diff --git a/backend/extensions/agent_Agent_handle_exception_end/_50_handle_repairable_exception.py b/backend/extensions/agent_Agent_handle_exception_end/_50_handle_repairable_exception.py index f8e04c27..97e40ca3 100644 --- a/backend/extensions/agent_Agent_handle_exception_end/_50_handle_repairable_exception.py +++ b/backend/extensions/agent_Agent_handle_exception_end/_50_handle_repairable_exception.py @@ -1,10 +1,7 @@ -from datetime import datetime, timezone -from backend.core.agent import LoopData from backend.utils import errors from backend.utils.errors import RepairableException from backend.utils.extension import Extension -from backend.utils.localization import Localization from backend.utils.print_style import PrintStyle diff --git a/backend/extensions/agent_init/_15_load_profile_settings.py b/backend/extensions/agent_init/_15_load_profile_settings.py index ac026e75..42156f7d 100644 --- a/backend/extensions/agent_init/_15_load_profile_settings.py +++ b/backend/extensions/agent_init/_15_load_profile_settings.py @@ -1,4 +1,4 @@ -from backend.utils import dirty_json, files, projects, subagents +from backend.utils import dirty_json, files, subagents from backend.utils.extension import Extension from initialize import initialize_agent diff --git a/backend/extensions/before_main_llm_call/_10_log_for_stream.py b/backend/extensions/before_main_llm_call/_10_log_for_stream.py index f651da7a..d62f6bb4 100644 --- a/backend/extensions/before_main_llm_call/_10_log_for_stream.py +++ b/backend/extensions/before_main_llm_call/_10_log_for_stream.py @@ -1,10 +1,6 @@ -import asyncio -import math from backend.core.agent import LoopData -from backend.utils import log, persist_chat, tokens from backend.utils.extension import Extension -from backend.utils.log import LogItem class LogForStream(Extension): diff --git a/backend/extensions/hist_add_before/_10_mask_content.py b/backend/extensions/hist_add_before/_10_mask_content.py index 3c6f4c01..cfac0692 100644 --- a/backend/extensions/hist_add_before/_10_mask_content.py +++ b/backend/extensions/hist_add_before/_10_mask_content.py @@ -15,7 +15,7 @@ async def execute(self, **kwargs): # Mask the content before adding to history content_data["content"] = self._mask_content(content_data["content"], secrets_mgr) - except Exception as e: + except Exception: # If masking fails, proceed without masking pass diff --git a/backend/extensions/hist_add_tool_result/_90_save_tool_call_file.py b/backend/extensions/hist_add_tool_result/_90_save_tool_call_file.py index a28dbd36..9d938d27 100644 --- a/backend/extensions/hist_add_tool_result/_90_save_tool_call_file.py +++ b/backend/extensions/hist_add_tool_result/_90_save_tool_call_file.py @@ -1,5 +1,4 @@ import os -import re from typing import Any from backend.utils import files, persist_chat diff --git a/backend/extensions/message_loop_prompts_after/_60_include_current_datetime.py b/backend/extensions/message_loop_prompts_after/_60_include_current_datetime.py index fcc1888a..0c6b4478 100644 --- a/backend/extensions/message_loop_prompts_after/_60_include_current_datetime.py +++ b/backend/extensions/message_loop_prompts_after/_60_include_current_datetime.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from backend.core.agent import LoopData from backend.utils.extension import Extension @@ -9,7 +9,7 @@ class IncludeCurrentDatetime(Extension): async def execute(self, loop_data: LoopData = LoopData(), **kwargs): # get current datetime current_datetime = Localization.get().utc_dt_to_localtime_str( - datetime.now(timezone.utc), sep=" ", timespec="seconds" + datetime.now(UTC), sep=" ", timespec="seconds" ) # remove timezone offset if current_datetime and "+" in current_datetime: diff --git a/backend/extensions/message_loop_prompts_before/_90_organize_history_wait.py b/backend/extensions/message_loop_prompts_before/_90_organize_history_wait.py index 98a63b68..1745d2b4 100644 --- a/backend/extensions/message_loop_prompts_before/_90_organize_history_wait.py +++ b/backend/extensions/message_loop_prompts_before/_90_organize_history_wait.py @@ -1,6 +1,6 @@ from backend.core.agent import LoopData from backend.extensions.message_loop_end._10_organize_history import DATA_NAME_TASK -from backend.utils.defer import THREAD_BACKGROUND, DeferredTask +from backend.utils.defer import DeferredTask from backend.utils.extension import Extension diff --git a/backend/extensions/monologue_start/_60_rename_chat.py b/backend/extensions/monologue_start/_60_rename_chat.py index 3aae08bb..9c719c1e 100644 --- a/backend/extensions/monologue_start/_60_rename_chat.py +++ b/backend/extensions/monologue_start/_60_rename_chat.py @@ -34,5 +34,5 @@ async def change_name(self): # apply to context and save self.agent.context.name = new_name persist_chat.save_tmp_chat(self.agent.context) - except Exception as e: + except Exception: pass # non-critical diff --git a/backend/extensions/process_chain_end/_50_process_queue.py b/backend/extensions/process_chain_end/_50_process_queue.py index d80bcb57..5971592b 100644 --- a/backend/extensions/process_chain_end/_50_process_queue.py +++ b/backend/extensions/process_chain_end/_50_process_queue.py @@ -1,6 +1,6 @@ import asyncio -from backend.core.agent import Agent, AgentContext, LoopData +from backend.core.agent import AgentContext, LoopData from backend.utils import message_queue as mq from backend.utils.extension import Extension diff --git a/backend/extensions/reasoning_stream/_10_log_from_stream.py b/backend/extensions/reasoning_stream/_10_log_from_stream.py index e6af7887..97ffe32c 100644 --- a/backend/extensions/reasoning_stream/_10_log_from_stream.py +++ b/backend/extensions/reasoning_stream/_10_log_from_stream.py @@ -1,14 +1,10 @@ -import asyncio import math from backend.core.agent import LoopData from backend.extensions.before_main_llm_call._10_log_for_stream import ( - build_default_heading, build_heading, ) -from backend.utils import log, persist_chat, tokens from backend.utils.extension import Extension -from backend.utils.log import LogItem class LogFromStream(Extension): diff --git a/backend/extensions/reasoning_stream_chunk/_10_mask_stream.py b/backend/extensions/reasoning_stream_chunk/_10_mask_stream.py index 24f5c00f..2539ec69 100644 --- a/backend/extensions/reasoning_stream_chunk/_10_mask_stream.py +++ b/backend/extensions/reasoning_stream_chunk/_10_mask_stream.py @@ -34,6 +34,6 @@ async def execute(self, **kwargs): from backend.utils.print_style import PrintStyle PrintStyle().stream(processed_chunk) - except Exception as e: + except Exception: # If masking fails, proceed without masking pass diff --git a/backend/extensions/reasoning_stream_end/_10_mask_end.py b/backend/extensions/reasoning_stream_end/_10_mask_end.py index acaab60e..232c5697 100644 --- a/backend/extensions/reasoning_stream_end/_10_mask_end.py +++ b/backend/extensions/reasoning_stream_end/_10_mask_end.py @@ -23,6 +23,6 @@ async def execute(self, **kwargs): # Clean up the filter agent.set_data(filter_key, None) - except Exception as e: + except Exception: # If masking fails, proceed without masking pass diff --git a/backend/extensions/response_stream/_10_log_from_stream.py b/backend/extensions/response_stream/_10_log_from_stream.py index 77dfaff8..edd08795 100644 --- a/backend/extensions/response_stream/_10_log_from_stream.py +++ b/backend/extensions/response_stream/_10_log_from_stream.py @@ -1,4 +1,3 @@ -import asyncio import math from backend.core.agent import LoopData @@ -6,9 +5,7 @@ build_default_heading, build_heading, ) -from backend.utils import log, persist_chat, tokens from backend.utils.extension import Extension -from backend.utils.log import LogItem class LogFromStream(Extension): diff --git a/backend/extensions/response_stream/_20_live_response.py b/backend/extensions/response_stream/_20_live_response.py index fdd69e1a..fc3d0c2c 100644 --- a/backend/extensions/response_stream/_20_live_response.py +++ b/backend/extensions/response_stream/_20_live_response.py @@ -1,9 +1,6 @@ -import asyncio from backend.core.agent import LoopData -from backend.utils import log, persist_chat, tokens from backend.utils.extension import Extension -from backend.utils.log import LogItem class LiveResponse(Extension): @@ -17,7 +14,7 @@ async def execute( ): try: if ( - not "tool_name" in parsed + "tool_name" not in parsed or parsed["tool_name"] != "response" or "tool_args" not in parsed or "text" not in parsed["tool_args"] @@ -35,5 +32,5 @@ async def execute( # update log message log_item = loop_data.params_temporary["log_item_response"] log_item.update(content=parsed["tool_args"]["text"]) - except Exception as e: + except Exception: pass diff --git a/backend/extensions/response_stream_chunk/_10_mask_stream.py b/backend/extensions/response_stream_chunk/_10_mask_stream.py index 4c791125..88026a42 100644 --- a/backend/extensions/response_stream_chunk/_10_mask_stream.py +++ b/backend/extensions/response_stream_chunk/_10_mask_stream.py @@ -1,4 +1,3 @@ -from backend.core.agent import Agent, LoopData from backend.utils.extension import Extension from backend.utils.secrets import get_secrets_manager @@ -36,6 +35,6 @@ async def execute(self, **kwargs): from backend.utils.print_style import PrintStyle PrintStyle().stream(processed_chunk) - except Exception as e: + except Exception: # If masking fails, proceed without masking pass diff --git a/backend/extensions/response_stream_end/_10_mask_end.py b/backend/extensions/response_stream_end/_10_mask_end.py index b632614b..abfe5e13 100644 --- a/backend/extensions/response_stream_end/_10_mask_end.py +++ b/backend/extensions/response_stream_end/_10_mask_end.py @@ -1,5 +1,4 @@ from backend.utils.extension import Extension -from backend.utils.secrets import SecretsManager class MaskResponseStreamEnd(Extension): @@ -24,6 +23,6 @@ async def execute(self, **kwargs): # Clean up the filter agent.set_data(filter_key, None) - except Exception as e: + except Exception: # If masking fails, proceed without masking pass diff --git a/backend/extensions/response_stream_end/_15_log_from_stream_end.py b/backend/extensions/response_stream_end/_15_log_from_stream_end.py index 322b86ce..6f70aa03 100644 --- a/backend/extensions/response_stream_end/_15_log_from_stream_end.py +++ b/backend/extensions/response_stream_end/_15_log_from_stream_end.py @@ -1,14 +1,6 @@ -import asyncio -import math from backend.core.agent import LoopData -from backend.extensions.before_main_llm_call._10_log_for_stream import ( - build_default_heading, - build_heading, -) -from backend.utils import log, persist_chat, tokens from backend.utils.extension import Extension -from backend.utils.log import LogItem class LogFromStream(Extension): diff --git a/backend/extensions/system_prompt/_10_system_prompt.py b/backend/extensions/system_prompt/_10_system_prompt.py index 4e8b545b..3a6550f8 100644 --- a/backend/extensions/system_prompt/_10_system_prompt.py +++ b/backend/extensions/system_prompt/_10_system_prompt.py @@ -65,7 +65,7 @@ def get_secrets_prompt(agent: Agent): secrets = secrets_manager.get_secrets_for_prompt() vars = get_settings()["variables"] return agent.read_prompt("agent.system.secrets.md", secrets=secrets, vars=vars) - except Exception as e: + except Exception: # If secrets module is not available or has issues, return empty string return "" diff --git a/backend/extensions/user_message_ui/_10_update_check.py b/backend/extensions/user_message_ui/_10_update_check.py index 333a6861..1eb29fb1 100644 --- a/backend/extensions/user_message_ui/_10_update_check.py +++ b/backend/extensions/user_message_ui/_10_update_check.py @@ -45,7 +45,7 @@ async def execute(self, loop_data: LoopData = LoopData(), text: str = "", **kwar last_notification_id = notif.get("id") last_notification_time = datetime.datetime.now() self.send_notification(notif) - except Exception as e: + except Exception: pass # no need to log if the update server is inaccessible def send_notification(self, notif): diff --git a/backend/interfaces/a2a/server.py b/backend/interfaces/a2a/server.py index 8371aba9..77e376c3 100644 --- a/backend/interfaces/a2a/server.py +++ b/backend/interfaces/a2a/server.py @@ -4,7 +4,7 @@ import contextlib import threading import uuid -from typing import Any, List +from typing import Any from starlette.requests import Request @@ -144,11 +144,11 @@ async def cancel_task(self, params: Any) -> None: # params: TaskIdParams # Note: No context cleanup needed since contexts are always temporary and cleaned up in run_task - def build_message_history(self, history: List[Any]) -> List[Message]: # type: ignore + def build_message_history(self, history: list[Any]) -> list[Message]: # type: ignore # Not used in this simplified implementation return [] - def build_artifacts(self, result: Any) -> List[Artifact]: # type: ignore + def build_artifacts(self, result: Any) -> list[Artifact]: # type: ignore # No artifacts for now return [] @@ -218,7 +218,7 @@ def _configure(self): broker = InMemoryBroker() # type: ignore[arg-type] # Define Ctx AI's skills - skills: List[Skill] = [ + skills: list[Skill] = [ { # type: ignore "id": "general_assistance", "name": "General AI Assistant", diff --git a/backend/interfaces/api/routes/agents/subagents.py b/backend/interfaces/api/routes/agents/subagents.py index 462066b4..4a0d1c1e 100644 --- a/backend/interfaces/api/routes/agents/subagents.py +++ b/backend/interfaces/api/routes/agents/subagents.py @@ -1,10 +1,10 @@ from typing import TYPE_CHECKING from backend.utils import subagents -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request if TYPE_CHECKING: - from backend.utils import projects + pass class Subagents(ApiHandler): diff --git a/backend/interfaces/api/routes/backup/backup_preview_grouped.py b/backend/interfaces/api/routes/backup/backup_preview_grouped.py index 696d0d10..9627dd38 100644 --- a/backend/interfaces/api/routes/backup/backup_preview_grouped.py +++ b/backend/interfaces/api/routes/backup/backup_preview_grouped.py @@ -1,4 +1,4 @@ -from typing import Any, Dict +from typing import Any from backend.utils.api import ApiHandler, Request, Response from backend.utils.backup import BackupService @@ -61,7 +61,7 @@ async def process(self, input: dict, request: Request) -> dict | Response: all_files = [f for f in all_files if search_lower in f["path"].lower()] # Group files by directory structure - groups: Dict[str, Dict[str, Any]] = {} + groups: dict[str, dict[str, Any]] = {} total_size = 0 for file_info in all_files: diff --git a/backend/interfaces/api/routes/chat/chat_create.py b/backend/interfaces/api/routes/chat/chat_create.py index 510ba745..a87c1322 100644 --- a/backend/interfaces/api/routes/chat/chat_create.py +++ b/backend/interfaces/api/routes/chat/chat_create.py @@ -1,6 +1,6 @@ from backend.core.agent import AgentContext from backend.utils import guids, projects, settings -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request class CreateChat(ApiHandler): diff --git a/backend/interfaces/api/routes/chat/chat_export.py b/backend/interfaces/api/routes/chat/chat_export.py index 2ff26d68..22378649 100644 --- a/backend/interfaces/api/routes/chat/chat_export.py +++ b/backend/interfaces/api/routes/chat/chat_export.py @@ -1,5 +1,5 @@ from backend.utils import persist_chat -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request class ExportChat(ApiHandler): diff --git a/backend/interfaces/api/routes/chat/chat_load.py b/backend/interfaces/api/routes/chat/chat_load.py index 97387036..7a3fbc49 100644 --- a/backend/interfaces/api/routes/chat/chat_load.py +++ b/backend/interfaces/api/routes/chat/chat_load.py @@ -1,5 +1,5 @@ from backend.utils import persist_chat -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request class LoadChats(ApiHandler): diff --git a/backend/interfaces/api/routes/chat/chat_remove.py b/backend/interfaces/api/routes/chat/chat_remove.py index 13f617b5..6ad5ba92 100644 --- a/backend/interfaces/api/routes/chat/chat_remove.py +++ b/backend/interfaces/api/routes/chat/chat_remove.py @@ -1,6 +1,6 @@ from backend.core.agent import AgentContext from backend.utils import persist_chat -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request from backend.utils.task_scheduler import TaskScheduler diff --git a/backend/interfaces/api/routes/chat/chat_reset.py b/backend/interfaces/api/routes/chat/chat_reset.py index 8ec6b35d..4f582245 100644 --- a/backend/interfaces/api/routes/chat/chat_reset.py +++ b/backend/interfaces/api/routes/chat/chat_reset.py @@ -1,5 +1,5 @@ from backend.utils import persist_chat -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request from backend.utils.task_scheduler import TaskScheduler diff --git a/backend/interfaces/api/routes/chat/ctx_window_get.py b/backend/interfaces/api/routes/chat/ctx_window_get.py index c6108e98..bd8a6ce7 100644 --- a/backend/interfaces/api/routes/chat/ctx_window_get.py +++ b/backend/interfaces/api/routes/chat/ctx_window_get.py @@ -1,5 +1,4 @@ -from backend.utils import tokens -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request class GetCtxWindow(ApiHandler): diff --git a/backend/interfaces/api/routes/files/delete_work_dir_file.py b/backend/interfaces/api/routes/files/delete_work_dir_file.py index fe3e9425..ee0cedb1 100644 --- a/backend/interfaces/api/routes/files/delete_work_dir_file.py +++ b/backend/interfaces/api/routes/files/delete_work_dir_file.py @@ -1,7 +1,7 @@ from backend.api import get_work_dir_files -from backend.utils import files, runtime -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils import runtime +from backend.utils.api import ApiHandler, Input, Output, Request from backend.utils.file_browser import FileBrowser diff --git a/backend/interfaces/api/routes/files/edit_work_dir_file.py b/backend/interfaces/api/routes/files/edit_work_dir_file.py index da432110..62a4b5d1 100644 --- a/backend/interfaces/api/routes/files/edit_work_dir_file.py +++ b/backend/interfaces/api/routes/files/edit_work_dir_file.py @@ -75,7 +75,7 @@ async def load_file(file_path: str) -> dict: mime_type, _ = mimetypes.guess_type(full_path) try: - with open(full_path, "r", encoding="utf-8", errors="strict") as file: + with open(full_path, encoding="utf-8", errors="strict") as file: content = file.read() except UnicodeDecodeError: raise Exception("Unable to decode file as UTF-8; editing is not supported") diff --git a/backend/interfaces/api/routes/files/file_info.py b/backend/interfaces/api/routes/files/file_info.py index 84bb6421..290a818c 100644 --- a/backend/interfaces/api/routes/files/file_info.py +++ b/backend/interfaces/api/routes/files/file_info.py @@ -2,7 +2,7 @@ from typing import TypedDict from backend.utils import files, runtime -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request class FileInfoApi(ApiHandler): diff --git a/backend/interfaces/api/routes/files/get_work_dir_files.py b/backend/interfaces/api/routes/files/get_work_dir_files.py index 5712fe2e..8eae5b11 100644 --- a/backend/interfaces/api/routes/files/get_work_dir_files.py +++ b/backend/interfaces/api/routes/files/get_work_dir_files.py @@ -1,4 +1,4 @@ -from backend.utils import files, runtime +from backend.utils import runtime from backend.utils.api import ApiHandler, Request, Response from backend.utils.file_browser import FileBrowser diff --git a/backend/interfaces/api/routes/files/image_get.py b/backend/interfaces/api/routes/files/image_get.py index c29c1191..fe9a8601 100644 --- a/backend/interfaces/api/routes/files/image_get.py +++ b/backend/interfaces/api/routes/files/image_get.py @@ -16,7 +16,7 @@ def get_methods(cls) -> list[str]: async def process(self, input: dict, request: Request) -> dict | Response: # input data path = input.get("path", request.args.get("path", "")) - metadata = input.get("metadata", request.args.get("metadata", "false")).lower() == "true" + input.get("metadata", request.args.get("metadata", "false")).lower() == "true" if not path: raise ValueError("No path provided") diff --git a/backend/interfaces/api/routes/files/upload_work_dir_files.py b/backend/interfaces/api/routes/files/upload_work_dir_files.py index 642423d8..871bc565 100644 --- a/backend/interfaces/api/routes/files/upload_work_dir_files.py +++ b/backend/interfaces/api/routes/files/upload_work_dir_files.py @@ -1,10 +1,9 @@ import base64 -import os from backend.api import get_work_dir_files from werkzeug.datastructures import FileStorage -from backend.utils import files, runtime +from backend.utils import runtime from backend.utils.api import ApiHandler, Request, Response from backend.utils.file_browser import FileBrowser diff --git a/backend/interfaces/api/routes/media/synthesize.py b/backend/interfaces/api/routes/media/synthesize.py index de0bc151..702e590b 100644 --- a/backend/interfaces/api/routes/media/synthesize.py +++ b/backend/interfaces/api/routes/media/synthesize.py @@ -1,6 +1,6 @@ # api/synthesize.py -from backend.utils import kokoro_tts, runtime, settings +from backend.utils import kokoro_tts from backend.utils.api import ApiHandler, Request, Response @@ -10,7 +10,7 @@ async def process(self, input: dict, request: Request) -> dict | Response: ctxid = input.get("ctxid", "") if ctxid: - context = self.use_context(ctxid) + self.use_context(ctxid) # if not await kokoro_tts.is_downloaded(): # context.log.log(type="info", content="Kokoro TTS model is currently being initialized, please wait...") diff --git a/backend/interfaces/api/routes/media/transcribe.py b/backend/interfaces/api/routes/media/transcribe.py index 00eddb17..1a4137ca 100644 --- a/backend/interfaces/api/routes/media/transcribe.py +++ b/backend/interfaces/api/routes/media/transcribe.py @@ -1,4 +1,4 @@ -from backend.utils import runtime, settings, whisper +from backend.utils import settings, whisper from backend.utils.api import ApiHandler, Request, Response @@ -8,7 +8,7 @@ async def process(self, input: dict, request: Request) -> dict | Response: ctxid = input.get("ctxid", "") if ctxid: - context = self.use_context(ctxid) + self.use_context(ctxid) # if not await whisper.is_downloaded(): # context.log.log(type="info", content="Whisper STT model is currently being initialized, please wait...") diff --git a/backend/interfaces/api/routes/plugins/plugins.py b/backend/interfaces/api/routes/plugins/plugins.py index 5eb09615..0bb72cc8 100644 --- a/backend/interfaces/api/routes/plugins/plugins.py +++ b/backend/interfaces/api/routes/plugins/plugins.py @@ -2,7 +2,7 @@ import os import subprocess import sys -from datetime import datetime, timezone +from datetime import UTC, datetime from backend.utils import files, plugins from backend.utils.api import ApiHandler, Request, Response @@ -237,7 +237,7 @@ async def process(self, input: dict, request: Request) -> dict | Response: if not files.exists(init_script): return Response(status=404, response="initialize.py not found") - executed_at = datetime.now(timezone.utc).isoformat() + executed_at = datetime.now(UTC).isoformat() try: result = subprocess.run( [sys.executable, init_script], diff --git a/backend/interfaces/api/routes/plugins/skills.py b/backend/interfaces/api/routes/plugins/skills.py index c10038e9..d555c0b0 100644 --- a/backend/interfaces/api/routes/plugins/skills.py +++ b/backend/interfaces/api/routes/plugins/skills.py @@ -1,5 +1,5 @@ from backend.utils import files, projects, runtime, skills -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request class Skills(ApiHandler): diff --git a/backend/interfaces/api/routes/plugins/skills_import.py b/backend/interfaces/api/routes/plugins/skills_import.py index 99bf5eb1..372940ed 100644 --- a/backend/interfaces/api/routes/plugins/skills_import.py +++ b/backend/interfaces/api/routes/plugins/skills_import.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import time import uuid from pathlib import Path diff --git a/backend/interfaces/api/routes/plugins/skills_import_preview.py b/backend/interfaces/api/routes/plugins/skills_import_preview.py index a5552a7b..c895ca6f 100644 --- a/backend/interfaces/api/routes/plugins/skills_import_preview.py +++ b/backend/interfaces/api/routes/plugins/skills_import_preview.py @@ -1,6 +1,5 @@ from __future__ import annotations -import os import time import uuid from pathlib import Path diff --git a/backend/interfaces/api/routes/projects/projects.py b/backend/interfaces/api/routes/projects/projects.py index 0b22feba..a2b74155 100644 --- a/backend/interfaces/api/routes/projects/projects.py +++ b/backend/interfaces/api/routes/projects/projects.py @@ -1,5 +1,5 @@ from backend.utils import projects -from backend.utils.api import ApiHandler, Input, Output, Request, Response +from backend.utils.api import ApiHandler, Input, Output, Request from backend.utils.notification import NotificationManager, NotificationPriority, NotificationType @@ -72,10 +72,10 @@ def clone_project(self, project: dict | None): raise Exception("Git URL is required") # Progress notification - notification = NotificationManager.send_notification( + NotificationManager.send_notification( NotificationType.PROGRESS, NotificationPriority.NORMAL, - f"Cloning repository...", + "Cloning repository...", "Git Clone", display_time=999, group="git_clone", @@ -89,7 +89,7 @@ def clone_project(self, project: dict | None): NotificationManager.send_notification( NotificationType.SUCCESS, NotificationPriority.NORMAL, - f"Repository cloned successfully", + "Repository cloned successfully", "Git Clone", display_time=3, group="git_clone", diff --git a/backend/interfaces/api/routes/settings/csrf_token.py b/backend/interfaces/api/routes/settings/csrf_token.py index 32e95985..e0be0ed0 100644 --- a/backend/interfaces/api/routes/settings/csrf_token.py +++ b/backend/interfaces/api/routes/settings/csrf_token.py @@ -8,7 +8,6 @@ Input, Output, Request, - Response, session, ) diff --git a/backend/interfaces/api/routes/system/tunnel_proxy.py b/backend/interfaces/api/routes/system/tunnel_proxy.py index acd00b61..77a4b493 100644 --- a/backend/interfaces/api/routes/system/tunnel_proxy.py +++ b/backend/interfaces/api/routes/system/tunnel_proxy.py @@ -2,7 +2,6 @@ from backend.utils import dotenv, runtime from backend.utils.api import ApiHandler, Request, Response -from backend.utils.tunnel_manager import TunnelManager class TunnelProxy(ApiHandler): @@ -24,7 +23,7 @@ async def process(input: dict) -> dict | Response: response = requests.post(f"http://localhost:{tunnel_api_port}/", json={"action": "health"}) if response.status_code == 200: service_ok = True - except Exception as e: + except Exception: service_ok = False # forward this request to the tunnel service if OK diff --git a/backend/interfaces/mcp/server.py b/backend/interfaces/mcp/server.py index d5c932a9..acff384e 100644 --- a/backend/interfaces/mcp/server.py +++ b/backend/interfaces/mcp/server.py @@ -1,8 +1,7 @@ -import asyncio import contextvars import os import threading -from typing import Annotated, Literal, Union +from typing import Annotated, Literal from urllib.parse import urlparse import fastmcp @@ -127,7 +126,7 @@ async def send_message( | None ) = None, ) -> Annotated[ - Union[ToolResponse, ToolError], + ToolResponse | ToolError, Field(description="The response from the remote Ctx AI Instance", title="response"), ]: # Get project name from context variable (set in proxy __call__) @@ -218,7 +217,7 @@ async def finish_chat( ), ], ) -> Annotated[ - Union[ToolResponse, ToolError], + ToolResponse | ToolError, Field(description="The response from the remote Ctx AI Instance", title="response"), ]: if not chat_id: diff --git a/backend/interfaces/websockets/dev_websocket_test_handler.py b/backend/interfaces/websockets/dev_websocket_test_handler.py index 28a49e41..4fecff06 100644 --- a/backend/interfaces/websockets/dev_websocket_test_handler.py +++ b/backend/interfaces/websockets/dev_websocket_test_handler.py @@ -1,7 +1,7 @@ from __future__ import annotations import asyncio -from typing import Any, Dict +from typing import Any from backend.interfaces.websockets.websocket import WebSocketHandler, WebSocketResult from backend.utils import runtime @@ -25,7 +25,7 @@ def get_event_types(cls) -> list[str]: ] async def process_event( - self, event_type: str, data: Dict[str, Any], sid: str + self, event_type: str, data: dict[str, Any], sid: str ) -> dict[str, Any] | WebSocketResult | None: if event_type == "ws_event_console_subscribe": if not runtime.is_development(): diff --git a/backend/interfaces/websockets/websocket.py b/backend/interfaces/websockets/websocket.py index b5741cba..0d1dd89f 100644 --- a/backend/interfaces/websockets/websocket.py +++ b/backend/interfaces/websockets/websocket.py @@ -3,7 +3,8 @@ import re import threading from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Iterable, Optional +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any from urllib.parse import urlparse import socketio @@ -197,7 +198,7 @@ def ok( *, correlation_id: str | None = None, duration_ms: float | None = None, - ) -> "WebSocketResult": + ) -> WebSocketResult: if data is not None and not isinstance(data, dict): raise TypeError("WebSocketResult.ok data must be a dict or None") payload = dict(data) if data is not None else None @@ -217,7 +218,7 @@ def error( details: Any | None = None, correlation_id: str | None = None, duration_ms: float | None = None, - ) -> "WebSocketResult": + ) -> WebSocketResult: if not isinstance(code, str) or not code.strip(): raise ValueError("Error code must be a non-empty string") if not isinstance(message, str) or not message.strip(): @@ -277,8 +278,8 @@ class WebSocketHandler(ABC): conventions. """ - _instances: dict[type["WebSocketHandler"], "WebSocketHandler"] = {} - _construction_tokens: dict[type["WebSocketHandler"], bool] = {} + _instances: dict[type[WebSocketHandler], WebSocketHandler] = {} + _construction_tokens: dict[type[WebSocketHandler], bool] = {} _singleton_lock = threading.RLock() def __init__(self, socketio: socketio.AsyncServer, lock: threading.RLock) -> None: @@ -292,7 +293,7 @@ def __init__(self, socketio: socketio.AsyncServer, lock: threading.RLock) -> Non self.socketio: socketio.AsyncServer = socketio self.lock: threading.RLock = lock - self._manager: Optional[WebSocketManager] = None + self._manager: WebSocketManager | None = None self._namespace: str | None = None @classmethod @@ -302,7 +303,7 @@ def get_instance( lock: threading.RLock | None = None, *args: Any, **kwargs: Any, - ) -> "WebSocketHandler": + ) -> WebSocketHandler: """Return the singleton instance for ``cls``. Args: diff --git a/backend/interfaces/websockets/websocket_manager.py b/backend/interfaces/websockets/websocket_manager.py index 79193791..56de97c0 100644 --- a/backend/interfaces/websockets/websocket_manager.py +++ b/backend/interfaces/websockets/websocket_manager.py @@ -2,13 +2,13 @@ import asyncio import os -import threading import time import uuid from collections import defaultdict, deque +from collections.abc import Callable, Iterable from dataclasses import dataclass, field -from datetime import datetime, timedelta, timezone -from typing import Any, Callable, Deque, Dict, Iterable, List, Optional, Set +from datetime import UTC, datetime, timedelta +from typing import Any import socketio @@ -27,7 +27,7 @@ def _utcnow() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) @dataclass @@ -66,19 +66,19 @@ class WebSocketManager: def __init__(self, socketio: socketio.AsyncServer, lock) -> None: self.socketio = socketio self.lock = lock - self.handlers: defaultdict[str, defaultdict[str, List[WebSocketHandler]]] = defaultdict( + self.handlers: defaultdict[str, defaultdict[str, list[WebSocketHandler]]] = defaultdict( lambda: defaultdict(list) ) - self.connections: Dict[ConnectionIdentity, ConnectionInfo] = {} - self.buffers: defaultdict[ConnectionIdentity, Deque[BufferedEvent]] = defaultdict(deque) - self._known_sids: Set[ConnectionIdentity] = set() + self.connections: dict[ConnectionIdentity, ConnectionInfo] = {} + self.buffers: defaultdict[ConnectionIdentity, deque[BufferedEvent]] = defaultdict(deque) + self._known_sids: set[ConnectionIdentity] = set() self._identifier: str = f"{self.__class__.__module__}.{self.__class__.__name__}" # Session tracking (single-user default) - self.user_to_sids: defaultdict[str, Set[ConnectionIdentity]] = defaultdict(set) - self.sid_to_user: Dict[ConnectionIdentity, str | None] = {} + self.user_to_sids: defaultdict[str, set[ConnectionIdentity]] = defaultdict(set) + self.sid_to_user: dict[ConnectionIdentity, str | None] = {} self._ALL_USERS_BUCKET = "allUsers" self._server_restart_enabled: bool = False - self._diagnostic_watchers: Set[ConnectionIdentity] = set() + self._diagnostic_watchers: set[ConnectionIdentity] = set() self._diagnostics_enabled: bool = runtime.is_development() self._dispatcher_loop: asyncio.AbstractEventLoop | None = None self._handler_worker: DeferredTask | None = None @@ -170,7 +170,7 @@ def _summarize_payload(self, payload: dict[str, Any] | None) -> dict[str, Any]: summary["__sizeBytes__"] = len(str(payload).encode("utf-8")) return summary - def _summarize_results(self, results: List[dict[str, Any]]) -> dict[str, Any]: + def _summarize_results(self, results: list[dict[str, Any]]) -> dict[str, Any]: summary = {"ok": 0, "error": 0, "handlers": []} for result in results: handler_id = result.get("handlerId") @@ -239,7 +239,7 @@ async def _broadcast() -> None: asyncio.create_task(_broadcast()) - def _normalize_handler_filter(self, value: Any, field_name: str) -> Set[str] | None: + def _normalize_handler_filter(self, value: Any, field_name: str) -> set[str] | None: if value is None: return None if isinstance(value, str): @@ -249,19 +249,19 @@ def _normalize_handler_filter(self, value: Any, field_name: str) -> Set[str] | N except TypeError as exc: # pragma: no cover - defensive raise ValueError(f"{field_name} must be an array of handler identifiers") from exc - normalized: Set[str] = set() + normalized: set[str] = set() for item in iterator: if not isinstance(item, str): raise ValueError(f"{field_name} values must be handler identifier strings") normalized.add(item) return normalized - def _normalize_sid_filter(self, value: str | Iterable[str] | None) -> Set[str]: + def _normalize_sid_filter(self, value: str | Iterable[str] | None) -> set[str]: if value is None: return set() if isinstance(value, str): return {value} - normalized: Set[str] = set() + normalized: set[str] = set() for item in value: normalized.add(str(item)) return normalized @@ -271,9 +271,9 @@ def _select_handlers( namespace: str, event_type: str, *, - include: Set[str] | None, - exclude: Set[str] | None, - ) -> tuple[list[WebSocketHandler], Set[str]]: + include: set[str] | None, + exclude: set[str] | None, + ) -> tuple[list[WebSocketHandler], set[str]]: registered = self.handlers.get(namespace, {}).get(event_type, []) available_ids = {handler.identifier for handler in registered} @@ -457,10 +457,10 @@ async def route_event( event_type: str, data: dict[str, Any], sid: str, - ack: Optional[Callable[[Any], None]] = None, + ack: Callable[[Any], None] | None = None, *, - include_handlers: Set[str] | None = None, - exclude_handlers: Set[str] | None = None, + include_handlers: set[str] | None = None, + exclude_handlers: set[str] | None = None, allow_exclude: bool = False, handler_id: str | None = None, ) -> dict[str, Any]: @@ -599,7 +599,7 @@ async def route_event( ] ) - results: List[dict[str, Any]] = [] + results: list[dict[str, Any]] = [] for execution in executions: handler = execution.handler value = execution.value @@ -679,7 +679,7 @@ async def request_for_sid( data: dict[str, Any], timeout_ms: int = 0, handler_id: str | None = None, - include_handlers: Set[str] | None = None, + include_handlers: set[str] | None = None, ) -> dict[str, Any]: payload = dict(data or {}) correlation_id = self._resolve_correlation_id(payload) @@ -712,7 +712,7 @@ async def _invoke() -> dict[str, Any]: if timeout_ms and timeout_ms > 0: try: return await asyncio.wait_for(_invoke(), timeout=timeout_ms / 1000) - except asyncio.TimeoutError: + except TimeoutError: PrintStyle.warning(f"request timeout for sid {sid} event '{event_type}'") return { "correlationId": correlation_id, @@ -734,14 +734,14 @@ async def route_event_all( data: dict[str, Any], *, timeout_ms: int = 0, - exclude_handlers: Set[str] | None = None, + exclude_handlers: set[str] | None = None, handler_id: str | None = None, ) -> list[dict[str, Any]]: """Fan-out a request to all active connections and aggregate responses.""" base_payload = dict(data or {}) exclude_meta_raw = base_payload.pop("excludeHandlers", None) - exclude_combined: Set[str] | None = exclude_handlers + exclude_combined: set[str] | None = exclude_handlers correlation_id = self._resolve_correlation_id(base_payload) if exclude_meta_raw is not None: @@ -815,7 +815,7 @@ async def _dispatch() -> dict[str, Any]: try: task = asyncio.create_task(_dispatch()) return await asyncio.wait_for(asyncio.shield(task), timeout=timeout_seconds) - except asyncio.TimeoutError: + except TimeoutError: PrintStyle.warning( f"requestAll timeout for sid {target_sid} correlation={correlation_id}" ) @@ -1003,7 +1003,7 @@ async def broadcast( ) async def _run_lifecycle(self, namespace: str, fn: Callable[[WebSocketHandler], Any]) -> None: - seen: Set[WebSocketHandler] = set() + seen: set[WebSocketHandler] = set() coros: list[Any] = [] for handler_list in self.handlers.get(namespace, {}).values(): for handler in handler_list: diff --git a/backend/interfaces/websockets/websocket_namespace_discovery.py b/backend/interfaces/websockets/websocket_namespace_discovery.py index 81f31ef7..38a9f780 100644 --- a/backend/interfaces/websockets/websocket_namespace_discovery.py +++ b/backend/interfaces/websockets/websocket_namespace_discovery.py @@ -3,9 +3,9 @@ import importlib.util import inspect import os +from collections.abc import Iterable from dataclasses import dataclass from types import ModuleType -from typing import Iterable from backend.interfaces.websockets.websocket import WebSocketHandler from backend.utils.files import get_abs_path diff --git a/backend/services/agent_service.py b/backend/services/agent_service.py index a5490c65..307487dd 100644 --- a/backend/services/agent_service.py +++ b/backend/services/agent_service.py @@ -2,7 +2,6 @@ Agent service for managing agent operations. """ -from typing import Any, Dict, List, Optional # Temporary placeholder classes until core dependencies are resolved @@ -30,8 +29,8 @@ class AgentService: """Service for managing agent operations.""" def __init__(self): - self._agents: Dict[str, Agent] = {} - self._contexts: Dict[str, AgentContext] = {} + self._agents: dict[str, Agent] = {} + self._contexts: dict[str, AgentContext] = {} def create_agent(self, config: AgentConfig) -> Agent: """Create a new agent.""" @@ -39,7 +38,7 @@ def create_agent(self, config: AgentConfig) -> Agent: self._agents[agent.id] = agent return agent - def get_agent(self, agent_id: str) -> Optional[Agent]: + def get_agent(self, agent_id: str) -> Agent | None: """Get an agent by ID.""" return self._agents.get(agent_id) @@ -49,11 +48,11 @@ def create_context(self, agent: Agent) -> AgentContext: self._contexts[context.id] = context return context - def get_context(self, context_id: str) -> Optional[AgentContext]: + def get_context(self, context_id: str) -> AgentContext | None: """Get a context by ID.""" return self._contexts.get(context_id) - def list_agents(self) -> List[Agent]: + def list_agents(self) -> list[Agent]: """List all agents.""" return list(self._agents.values()) diff --git a/backend/services/chat_service.py b/backend/services/chat_service.py index 9d2ad9c9..41eac75a 100644 --- a/backend/services/chat_service.py +++ b/backend/services/chat_service.py @@ -3,14 +3,14 @@ """ from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any class ChatService: """Service for managing chat operations.""" def __init__(self): - self._chats: Dict[str, Dict[str, Any]] = {} + self._chats: dict[str, dict[str, Any]] = {} def create_chat(self, agent_id: str, title: str = None) -> str: """Create a new chat session.""" @@ -25,11 +25,11 @@ def create_chat(self, agent_id: str, title: str = None) -> str: } return chat_id - def get_chat(self, chat_id: str) -> Optional[Dict[str, Any]]: + def get_chat(self, chat_id: str) -> dict[str, Any] | None: """Get a chat by ID.""" return self._chats.get(chat_id) - def add_message(self, chat_id: str, message: Dict[str, Any]) -> bool: + def add_message(self, chat_id: str, message: dict[str, Any]) -> bool: """Add a message to a chat.""" if chat_id in self._chats: self._chats[chat_id]["messages"].append(message) @@ -37,7 +37,7 @@ def add_message(self, chat_id: str, message: Dict[str, Any]) -> bool: return True return False - def list_chats(self, agent_id: str = None) -> List[Dict[str, Any]]: + def list_chats(self, agent_id: str = None) -> list[dict[str, Any]]: """List chats, optionally filtered by agent.""" chats = list(self._chats.values()) if agent_id: diff --git a/backend/services/memory_service.py b/backend/services/memory_service.py index 68791d35..641d2521 100644 --- a/backend/services/memory_service.py +++ b/backend/services/memory_service.py @@ -3,16 +3,16 @@ """ from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any class MemoryService: """Service for managing memory operations.""" def __init__(self): - self._memories: Dict[str, Dict[str, Any]] = {} + self._memories: dict[str, dict[str, Any]] = {} - def create_memory(self, content: str, tags: List[str] = None, agent_id: str = None) -> str: + def create_memory(self, content: str, tags: list[str] = None, agent_id: str = None) -> str: """Create a new memory.""" memory_id = f"memory_{len(self._memories) + 1}" self._memories[memory_id] = { @@ -25,11 +25,11 @@ def create_memory(self, content: str, tags: List[str] = None, agent_id: str = No } return memory_id - def get_memory(self, memory_id: str) -> Optional[Dict[str, Any]]: + def get_memory(self, memory_id: str) -> dict[str, Any] | None: """Get a memory by ID.""" return self._memories.get(memory_id) - def search_memories(self, query: str, agent_id: str = None) -> List[Dict[str, Any]]: + def search_memories(self, query: str, agent_id: str = None) -> list[dict[str, Any]]: """Search memories by content.""" results = [] query_lower = query.lower() @@ -43,7 +43,7 @@ def search_memories(self, query: str, agent_id: str = None) -> List[Dict[str, An return results - def list_memories(self, agent_id: str = None, tags: List[str] = None) -> List[Dict[str, Any]]: + def list_memories(self, agent_id: str = None, tags: list[str] = None) -> list[dict[str, Any]]: """List memories, optionally filtered.""" memories = list(self._memories.values()) diff --git a/backend/services/project_service.py b/backend/services/project_service.py index 3f94a196..27974b66 100644 --- a/backend/services/project_service.py +++ b/backend/services/project_service.py @@ -3,14 +3,14 @@ """ from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any class ProjectService: """Service for managing project operations.""" def __init__(self): - self._projects: Dict[str, Dict[str, Any]] = {} + self._projects: dict[str, dict[str, Any]] = {} def create_project(self, name: str, description: str = "", repo_url: str = "") -> str: """Create a new project.""" @@ -26,15 +26,15 @@ def create_project(self, name: str, description: str = "", repo_url: str = "") - } return project_id - def get_project(self, project_id: str) -> Optional[Dict[str, Any]]: + def get_project(self, project_id: str) -> dict[str, Any] | None: """Get a project by ID.""" return self._projects.get(project_id) - def list_projects(self) -> List[Dict[str, Any]]: + def list_projects(self) -> list[dict[str, Any]]: """List all projects.""" return list(self._projects.values()) - def update_project(self, project_id: str, updates: Dict[str, Any]) -> bool: + def update_project(self, project_id: str, updates: dict[str, Any]) -> bool: """Update a project.""" if project_id in self._projects: self._projects[project_id].update(updates) diff --git a/backend/services/skill_service.py b/backend/services/skill_service.py index ffd69f90..fb150514 100644 --- a/backend/services/skill_service.py +++ b/backend/services/skill_service.py @@ -3,14 +3,14 @@ """ from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any class SkillService: """Service for managing skill operations.""" def __init__(self): - self._skills: Dict[str, Dict[str, Any]] = {} + self._skills: dict[str, dict[str, Any]] = {} def create_skill(self, name: str, description: str, code: str, category: str = "custom") -> str: """Create a new skill.""" @@ -27,11 +27,11 @@ def create_skill(self, name: str, description: str, code: str, category: str = " } return skill_id - def get_skill(self, skill_id: str) -> Optional[Dict[str, Any]]: + def get_skill(self, skill_id: str) -> dict[str, Any] | None: """Get a skill by ID.""" return self._skills.get(skill_id) - def list_skills(self, category: str = None, enabled_only: bool = False) -> List[Dict[str, Any]]: + def list_skills(self, category: str = None, enabled_only: bool = False) -> list[dict[str, Any]]: """List skills, optionally filtered.""" skills = list(self._skills.values()) @@ -43,7 +43,7 @@ def list_skills(self, category: str = None, enabled_only: bool = False) -> List[ return skills - def update_skill(self, skill_id: str, updates: Dict[str, Any]) -> bool: + def update_skill(self, skill_id: str, updates: dict[str, Any]) -> bool: """Update a skill.""" if skill_id in self._skills: self._skills[skill_id].update(updates) diff --git a/backend/tools/browser/browser_agent.py b/backend/tools/browser/browser_agent.py index 7ec0206d..cf56ea6d 100644 --- a/backend/tools/browser/browser_agent.py +++ b/backend/tools/browser/browser_agent.py @@ -1,8 +1,7 @@ import asyncio import time -import uuid from pathlib import Path -from typing import Optional, cast +from typing import cast from pydantic import BaseModel @@ -25,10 +24,10 @@ async def create(agent: Agent): def __init__(self, agent: Agent): self.agent = agent - self.browser_session: Optional[browser_use.BrowserSession] = None - self.task: Optional[defer.DeferredTask] = None - self.use_agent: Optional[browser_use.Agent] = None - self.secrets_dict: Optional[dict[str, str]] = None + self.browser_session: browser_use.BrowserSession | None = None + self.task: defer.DeferredTask | None = None + self.use_agent: browser_use.Agent | None = None + self.secrets_dict: dict[str, str] | None = None self.iter_no = 0 def __del__(self): @@ -259,7 +258,7 @@ async def execute(self, message="", reset="", **kwargs): try: update = await asyncio.wait_for(self.get_update(), timeout=10) fail_counter = 0 # reset on success - except asyncio.TimeoutError: + except TimeoutError: fail_counter += 1 PrintStyle().warning( self._mask(f"browser_agent.get_update timed out ({fail_counter}/3)") @@ -408,7 +407,7 @@ def update_progress(self, text): def _mask(self, text: str) -> str: try: return get_secrets_manager(self.agent.context).mask_values(text or "") - except Exception as e: + except Exception: return text or "" # def __del__(self): diff --git a/backend/tools/system/input.py b/backend/tools/system/input.py index 351d07b2..1504bfd9 100644 --- a/backend/tools/system/input.py +++ b/backend/tools/system/input.py @@ -1,6 +1,5 @@ -from backend.core.agent import Agent, UserMessage from backend.tools.execution.code_execution_tool import CodeExecution -from backend.utils.tool import Response, Tool +from backend.utils.tool import Tool class Input(Tool): diff --git a/backend/tools/system/search_engine.py b/backend/tools/system/search_engine.py index e67d6f44..ae9e30ab 100644 --- a/backend/tools/system/search_engine.py +++ b/backend/tools/system/search_engine.py @@ -1,9 +1,5 @@ -import asyncio -import os -from backend.utils import dotenv, duckduckgo_search, perplexity_search from backend.utils.errors import handle_error -from backend.utils.print_style import PrintStyle from backend.utils.searxng import search as searxng from backend.utils.tool import Response, Tool diff --git a/backend/tools/system/skills_tool.py b/backend/tools/system/skills_tool.py index 092005e2..8352cea7 100644 --- a/backend/tools/system/skills_tool.py +++ b/backend/tools/system/skills_tool.py @@ -1,9 +1,5 @@ from __future__ import annotations -from pathlib import Path -from typing import List - -from backend.utils import file_tree, files, projects, runtime from backend.utils import skills as skills_helper from backend.utils.tool import Response, Tool @@ -62,7 +58,7 @@ def _list(self) -> str: # Stable output: sort by name skills_sorted = sorted(skills, key=lambda s: s.name.lower()) - lines: List[str] = [] + lines: list[str] = [] lines.append(f"Available skills ({len(skills_sorted)}):") for s in skills_sorted: tags = f" tags={','.join(s.tags)}" if s.tags else "" diff --git a/backend/tools/system/vision_load.py b/backend/tools/system/vision_load.py index 510ba3be..b2126527 100644 --- a/backend/tools/system/vision_load.py +++ b/backend/tools/system/vision_load.py @@ -15,7 +15,6 @@ class VisionLoad(Tool): async def execute(self, paths: list[str] = [], **kwargs) -> Response: self.images_dict = {} - template: list[dict[str, str]] = [] # type: ignore for path in paths: if not await runtime.call_development_function(files.exists, str(path)): diff --git a/backend/tools/system/wait.py b/backend/tools/system/wait.py index 9284b610..4131f668 100644 --- a/backend/tools/system/wait.py +++ b/backend/tools/system/wait.py @@ -1,5 +1,4 @@ -import asyncio -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from backend.utils.localization import Localization from backend.utils.print_style import PrintStyle @@ -20,7 +19,7 @@ async def execute(self, **kwargs) -> Response: is_duration_wait = not bool(until_timestamp_str) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) target_time = None if until_timestamp_str: @@ -84,5 +83,5 @@ def get_log_object(self): def get_heading(self, text: str = "", done: bool = False): done_icon = " icon://done_all" if done else "" if not text: - text = f"Waiting..." + text = "Waiting..." return f"icon://timer Wait: {text}{done_icon}" diff --git a/backend/utils/api.py b/backend/utils/api.py index 0a9200fc..62b515a9 100644 --- a/backend/utils/api.py +++ b/backend/utils/api.py @@ -5,16 +5,14 @@ from abc import abstractmethod from functools import wraps from pathlib import Path -from typing import Any, Dict, TypedDict, Union +from typing import Any, TypedDict, Union from flask import ( Flask, Request, Response, - jsonify, redirect, request, - send_file, session, url_for, ) @@ -32,7 +30,7 @@ cache.toggle_area(CACHE_AREA, False) # cache off for now Input = dict -Output = Union[Dict[str, Any], Response, TypedDict] # type: ignore +Output = Union[dict[str, Any], Response, TypedDict] # type: ignore class ApiHandler: @@ -128,11 +126,11 @@ def is_loopback_address(address: str) -> bool: try: socket.inet_pton(socket.AF_INET6, address) address_type = "ipv6" - except socket.error: + except OSError: try: socket.inet_pton(socket.AF_INET, address) address_type = "ipv4" - except socket.error: + except OSError: address_type = "hostname" if address_type == "ipv4": diff --git a/backend/utils/attachment_manager.py b/backend/utils/attachment_manager.py index 01dd5977..c45b7182 100644 --- a/backend/utils/attachment_manager.py +++ b/backend/utils/attachment_manager.py @@ -1,7 +1,6 @@ import base64 import io import os -from typing import Dict, List, Optional, Tuple from PIL import Image from werkzeug.datastructures import FileStorage @@ -44,7 +43,7 @@ def validate_mime_type(self, file) -> bool: except AttributeError: return False - def save_file(self, file: FileStorage, name: str) -> Tuple[str, Dict]: + def save_file(self, file: FileStorage, name: str) -> tuple[str, dict]: """Save file and return path and metadata""" try: filename = safe_filename(name) @@ -74,7 +73,7 @@ def save_file(self, file: FileStorage, name: str) -> Tuple[str, Dict]: PrintStyle.error(f"Error saving file {name}: {e}") return None, {} # type: ignore - def generate_image_preview(self, image_path: str, max_size: int = 800) -> Optional[str]: + def generate_image_preview(self, image_path: str, max_size: int = 800) -> str | None: try: with Image.open(image_path) as img: # Convert image if needed diff --git a/backend/utils/backup.py b/backend/utils/backup.py index 9275c15d..42b6d8df 100644 --- a/backend/utils/backup.py +++ b/backend/utils/backup.py @@ -4,7 +4,7 @@ import platform import tempfile import zipfile -from typing import Any, Dict, List, Optional +from typing import Any from pathspec import PathSpec from pathspec.patterns.gitwildmatch import GitWildMatchPattern @@ -35,7 +35,7 @@ def __init__(self): self.ctxai_root: self.ctxai_root, } - def get_default_backup_metadata(self) -> Dict[str, Any]: + def get_default_backup_metadata(self) -> dict[str, Any]: """Get default backup patterns and metadata""" timestamp = datetime.datetime.now().isoformat() @@ -113,7 +113,7 @@ def _patterns_to_string(self, include_patterns: list[str], exclude_patterns: lis return "\n".join(patterns) - async def _get_system_info(self) -> Dict[str, Any]: + async def _get_system_info(self) -> dict[str, Any]: """Collect system information for metadata""" import psutil @@ -135,7 +135,7 @@ async def _get_system_info(self) -> Dict[str, Any]: except Exception as e: return {"error": f"Failed to collect system info: {str(e)}"} - async def _get_environment_info(self) -> Dict[str, Any]: + async def _get_environment_info(self) -> dict[str, Any]: """Collect environment information for metadata""" try: return { @@ -166,7 +166,7 @@ async def _get_backup_author(self) -> str: except Exception: return "unknown" - def _count_directories(self, matched_files: List[Dict[str, Any]]) -> int: + def _count_directories(self, matched_files: list[dict[str, Any]]) -> int: """Count unique directories in file list""" directories = set() for file_info in matched_files: @@ -175,7 +175,7 @@ def _count_directories(self, matched_files: List[Dict[str, Any]]) -> int: directories.add(dir_path) return len(directories) - def _get_explicit_patterns(self, include_patterns: List[str]) -> set[str]: + def _get_explicit_patterns(self, include_patterns: list[str]) -> set[str]: """Extract explicit (non-wildcard) patterns that should always be included""" explicit_patterns = set() @@ -199,8 +199,8 @@ def _is_explicitly_included(self, file_path: str, explicit_patterns: set[str]) - return relative_path in explicit_patterns def _translate_patterns( - self, patterns: List[str], backup_metadata: Dict[str, Any] - ) -> List[str]: + self, patterns: list[str], backup_metadata: dict[str, Any] + ) -> list[str]: """Translate patterns from backed up system to current system. Replaces the backed up Ctx AI root path with the current Ctx AI root path @@ -246,8 +246,8 @@ def _translate_patterns( return translated_patterns async def test_patterns( - self, metadata: Dict[str, Any], max_files: int = 1000 - ) -> List[Dict[str, Any]]: + self, metadata: dict[str, Any], max_files: int = 1000 + ) -> list[dict[str, Any]]: """Test backup patterns and return list of matched files""" include_patterns = metadata.get("include_patterns", []) exclude_patterns = metadata.get("exclude_patterns", []) @@ -325,7 +325,7 @@ async def test_patterns( } ) processed_count += 1 - except (OSError, IOError): + except OSError: # Skip files we can't access continue @@ -342,8 +342,8 @@ async def test_patterns( async def create_backup( self, - include_patterns: List[str], - exclude_patterns: List[str], + include_patterns: list[str], + exclude_patterns: list[str], include_hidden: bool = True, backup_name: str = "ctxai-backup", ) -> str: @@ -416,7 +416,7 @@ async def create_backup( try: if os.path.exists(real_path) and os.path.isfile(real_path): zipf.write(real_path, archive_path) - except (OSError, IOError) as e: + except OSError as e: # Log error but continue with other files PrintStyle().warning(f"Warning: Could not backup file {real_path}: {e}") continue @@ -429,7 +429,7 @@ async def create_backup( os.remove(zip_path) raise Exception(f"Error creating backup: {str(e)}") - async def inspect_backup(self, backup_file) -> Dict[str, Any]: + async def inspect_backup(self, backup_file) -> dict[str, Any]: """Inspect backup archive and return metadata""" # Save uploaded file temporarily @@ -467,12 +467,12 @@ async def inspect_backup(self, backup_file) -> Dict[str, Any]: async def preview_restore( self, backup_file, - restore_include_patterns: Optional[List[str]] = None, - restore_exclude_patterns: Optional[List[str]] = None, + restore_include_patterns: list[str] | None = None, + restore_exclude_patterns: list[str] | None = None, overwrite_policy: str = "overwrite", clean_before_restore: bool = False, - user_edited_metadata: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + user_edited_metadata: dict[str, Any] | None = None, + ) -> dict[str, Any]: """Preview which files would be restored based on patterns""" # Save uploaded file temporarily @@ -620,12 +620,12 @@ async def preview_restore( async def restore_backup( self, backup_file, - restore_include_patterns: Optional[List[str]] = None, - restore_exclude_patterns: Optional[List[str]] = None, + restore_include_patterns: list[str] | None = None, + restore_exclude_patterns: list[str] | None = None, overwrite_policy: str = "overwrite", clean_before_restore: bool = False, - user_edited_metadata: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + user_edited_metadata: dict[str, Any] | None = None, + ) -> dict[str, Any]: """Restore files from backup archive""" # Save uploaded file temporarily @@ -813,7 +813,7 @@ async def restore_backup( if os.path.exists(temp_dir): os.rmdir(temp_dir) - def _translate_restore_path(self, archive_path: str, backup_metadata: Dict[str, Any]) -> str: + def _translate_restore_path(self, archive_path: str, backup_metadata: dict[str, Any]) -> str: """Translate file path from backed up system to current system. Replaces the backed up Ctx AI root path with the current Ctx AI root path @@ -864,8 +864,8 @@ def _translate_restore_path(self, archive_path: str, backup_metadata: Dict[str, return absolute_archive_path async def _find_files_to_clean_with_user_metadata( - self, user_metadata: Dict[str, Any], original_metadata: Dict[str, Any] - ) -> List[Dict[str, Any]]: + self, user_metadata: dict[str, Any], original_metadata: dict[str, Any] + ) -> list[dict[str, Any]]: """Find existing files that match patterns from user-edited metadata for clean operations""" # Use user-edited patterns for what to clean user_include_patterns = user_metadata.get("include_patterns", []) diff --git a/backend/utils/browser_use.py b/backend/utils/browser_use.py index 90fa9761..3bfe6638 100644 --- a/backend/utils/browser_use.py +++ b/backend/utils/browser_use.py @@ -1,5 +1,3 @@ from backend.utils import dotenv dotenv.save_dotenv_value("ANONYMIZED_TELEMETRY", "false") -import browser_use -import browser_use.utils diff --git a/backend/utils/call_llm.py b/backend/utils/call_llm.py index 0b3e6f70..fe58230e 100644 --- a/backend/utils/call_llm.py +++ b/backend/utils/call_llm.py @@ -1,4 +1,5 @@ -from typing import Callable, TypedDict +from collections.abc import Callable +from typing import TypedDict from langchain.prompts import ( ChatPromptTemplate, diff --git a/backend/utils/context.py b/backend/utils/context.py index 9d243fc5..c18f0d85 100644 --- a/backend/utils/context.py +++ b/backend/utils/context.py @@ -1,13 +1,13 @@ from contextvars import ContextVar -from typing import Any, Dict, Optional, TypeVar, cast +from typing import Any, TypeVar, cast T = TypeVar("T") # no mutable default — None is safe -_context_data: ContextVar[Optional[Dict[str, Any]]] = ContextVar("_context_data", default=None) +_context_data: ContextVar[dict[str, Any] | None] = ContextVar("_context_data", default=None) -def _ensure_context() -> Dict[str, Any]: +def _ensure_context() -> dict[str, Any]: """Make sure a context dict exists, and return it.""" data = _context_data.get() if data is None: @@ -33,7 +33,7 @@ def delete_context_data(key: str): _context_data.set(data) -def get_context_data(key: Optional[str] = None, default: T = None) -> T: +def get_context_data(key: str | None = None, default: T = None) -> T: """Get a key from the current context, or the full dict if key is None.""" data = _ensure_context() if key is None: diff --git a/backend/utils/crypto.py b/backend/utils/crypto.py index 686681d4..60daca34 100644 --- a/backend/utils/crypto.py +++ b/backend/utils/crypto.py @@ -1,6 +1,5 @@ import hashlib import hmac -import os from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import padding, rsa diff --git a/backend/utils/defer.py b/backend/utils/defer.py index f5c5ddf1..86e1d11b 100644 --- a/backend/utils/defer.py +++ b/backend/utils/defer.py @@ -1,8 +1,9 @@ import asyncio import threading +from collections.abc import Awaitable, Callable, Coroutine from concurrent.futures import Future from dataclasses import dataclass -from typing import Any, Awaitable, Callable, Coroutine, Optional, TypeVar +from typing import Any, TypeVar T = TypeVar("T") @@ -21,7 +22,7 @@ def __init__(self, thread_name: str = THREAD_BACKGROUND) -> None: def __new__(cls, thread_name: str = THREAD_BACKGROUND): with cls._lock: if thread_name not in cls._instances: - instance = super(EventLoopThread, cls).__new__(cls) + instance = super().__new__(cls) cls._instances[thread_name] = instance return cls._instances[thread_name] @@ -86,7 +87,7 @@ def __init__( thread_name: str = THREAD_BACKGROUND, ): self.event_loop_thread = EventLoopThread(thread_name) - self._future: Optional[Future] = None + self._future: Future | None = None self.children: list[ChildTask] = [] def start_task(self, func: Callable[..., Coroutine[Any, Any, Any]], *args: Any, **kwargs: Any): @@ -114,7 +115,7 @@ async def _run(self): def is_ready(self) -> bool: return self._future.done() if self._future else False - def result_sync(self, timeout: Optional[float] = None) -> Any: + def result_sync(self, timeout: float | None = None) -> Any: if not self._future: raise RuntimeError("Task hasn't been started") try: @@ -122,7 +123,7 @@ def result_sync(self, timeout: Optional[float] = None) -> Any: except TimeoutError: raise TimeoutError("The task did not complete within the specified timeout.") - async def result(self, timeout: Optional[float] = None) -> Any: + async def result(self, timeout: float | None = None) -> Any: if not self._future: raise RuntimeError("Task hasn't been started") diff --git a/backend/utils/docker.py b/backend/utils/docker.py index e938caef..a84f4aa0 100644 --- a/backend/utils/docker.py +++ b/backend/utils/docker.py @@ -1,10 +1,7 @@ -import atexit import time -from typing import Optional import docker from backend.utils.errors import format_error -from backend.utils.files import get_abs_path from backend.utils.log import Log from backend.utils.print_style import PrintStyle @@ -14,8 +11,8 @@ def __init__( self, image: str, name: str, - ports: Optional[dict[str, int]] = None, - volumes: Optional[dict[str, dict[str, str]]] = None, + ports: dict[str, int] | None = None, + volumes: dict[str, dict[str, str]] | None = None, logger: Log | None = None, ): self.logger = logger diff --git a/backend/utils/document_query.py b/backend/utils/document_query.py index 59476319..53e62774 100644 --- a/backend/utils/document_query.py +++ b/backend/utils/document_query.py @@ -8,8 +8,8 @@ from backend.utils.vector_db import VectorDB os.environ["USER_AGENT"] = "@mixedbread-ai/unstructured" # noqa E402 +from collections.abc import Callable, Sequence from datetime import datetime -from typing import Callable, List, Optional, Sequence, Tuple from urllib.parse import urlparse from langchain.schema import HumanMessage, SystemMessage @@ -17,7 +17,6 @@ from langchain_community.document_loaders import AsyncHtmlLoader from langchain_community.document_loaders.parsers.images import TesseractBlobParser from langchain_community.document_loaders.pdf import PyMuPDFLoader -from langchain_community.document_loaders.text import TextLoader from langchain_community.document_transformers import MarkdownifyTransformer from langchain_core.documents import Document from langchain_unstructured import UnstructuredLoader # noqa E402 @@ -148,7 +147,7 @@ async def add_document( PrintStyle.error(f"Error adding document '{document_uri}': {err_text}") return False, [] - async def get_document(self, document_uri: str) -> Optional[Document]: + async def get_document(self, document_uri: str) -> Document | None: """ Retrieve a document by its URI. @@ -183,7 +182,7 @@ async def get_document(self, document_uri: str) -> Optional[Document]: return Document(page_content=full_content, metadata=metadata) - async def _get_document_chunks(self, document_uri: str) -> List[Document]: + async def _get_document_chunks(self, document_uri: str) -> list[Document]: """ Get all chunks for a document. @@ -268,7 +267,7 @@ async def delete_document(self, document_uri: str) -> bool: async def search_documents( self, query: str, limit: int = 10, threshold: float = 0.5, filter: str = "" - ) -> List[Document]: + ) -> list[Document]: """ Search for documents similar to the query across the entire store. @@ -303,7 +302,7 @@ async def search_documents( async def search_document( self, document_uri: str, query: str, limit: int = 10, threshold: float = 0.5 - ) -> List[Document]: + ) -> list[Document]: """ Search for content within a specific document. @@ -320,7 +319,7 @@ async def search_document( query, limit, threshold, f"document_uri == '{document_uri}'" ) - async def list_documents(self) -> List[str]: + async def list_documents(self) -> list[str]: """ Get a list of all document URIs in the store. @@ -351,8 +350,8 @@ def __init__(self, agent: Agent, progress_callback: Callable[[str], None] | None self.store_lock = asyncio.Lock() async def document_qa( - self, document_uris: List[str], questions: Sequence[str] - ) -> Tuple[bool, str]: + self, document_uris: list[str], questions: Sequence[str] + ) -> tuple[bool, str]: self.progress_callback(f"Starting Q&A process for {len(document_uris)} documents") await self.agent.handle_intervention() @@ -412,12 +411,12 @@ async def document_qa( explicit_caching=False, ) - self.progress_callback(f"Q&A process completed") + self.progress_callback("Q&A process completed") return True, str(ai_response) async def document_get_content(self, document_uri: str, add_to_db: bool = False) -> str: - self.progress_callback(f"Fetching document content") + self.progress_callback("Fetching document content") await self.agent.handle_intervention() url = urlparse(document_uri) scheme = url.scheme or "file" @@ -492,14 +491,14 @@ async def document_get_content(self, document_uri: str, add_to_db: bool = False) else: document_content = self.handle_unstructured_document(document_uri, scheme) if add_to_db: - self.progress_callback(f"Indexing document") + self.progress_callback("Indexing document") await self.agent.handle_intervention() async with self.store_lock: success, ids = await self.store.add_document( document_content, document_uri_norm ) if not success: - self.progress_callback(f"Failed to index document") + self.progress_callback("Failed to index document") raise ValueError( f"DocumentQueryHelper::document_get_content: Failed to index document: {document_uri_norm}" ) diff --git a/backend/utils/email_client.py b/backend/utils/email_client.py index d4b1fddb..68433d1e 100644 --- a/backend/utils/email_client.py +++ b/backend/utils/email_client.py @@ -7,7 +7,7 @@ from email.header import decode_header from email.message import Message as EmailMessage from fnmatch import fnmatch -from typing import Any, Dict, List, Optional, Tuple +from typing import Any import html2text from bs4 import BeautifulSoup @@ -25,7 +25,7 @@ class Message: sender: str subject: str body: str - attachments: List[str] + attachments: list[str] class EmailClient: @@ -41,7 +41,7 @@ def __init__( port: int = 993, username: str = "", password: str = "", - options: Optional[Dict[str, Any]] = None, + options: dict[str, Any] | None = None, ): """ Initialize email client with connection parameters. @@ -67,7 +67,7 @@ def __init__( self.ssl = self.options.get("ssl", True) self.timeout = self.options.get("timeout", 30) - self.client: Optional[IMAPClient] = None + self.client: IMAPClient | None = None self.exchange_account = None async def connect(self) -> None: @@ -143,8 +143,8 @@ async def disconnect(self) -> None: async def read_messages( self, download_folder: str, - filter: Optional[Dict[str, Any]] = None, - ) -> List[Message]: + filter: dict[str, Any] | None = None, + ) -> list[Message]: """ Read messages based on filter criteria. @@ -171,14 +171,14 @@ async def read_messages( async def _fetch_imap_messages( self, download_folder: str, - filter: Dict[str, Any], - ) -> List[Message]: + filter: dict[str, Any], + ) -> list[Message]: """Fetch messages from IMAP server.""" if not self.client: raise RepairableException("IMAP client not connected. Call connect() first.") loop = asyncio.get_event_loop() - messages: List[Message] = [] + messages: list[Message] = [] def _sync_fetch(): # Select inbox @@ -224,8 +224,8 @@ async def _fetch_and_parse_imap_message( self, msg_id: int, download_folder: str, - filter: Dict[str, Any], - ) -> Optional[Message]: + filter: dict[str, Any], + ) -> Message | None: """Fetch and parse a single IMAP message with retry logic for large messages.""" loop = asyncio.get_event_loop() @@ -285,8 +285,8 @@ def _sync_fetch(): async def _fetch_exchange_messages( self, download_folder: str, - filter: Dict[str, Any], - ) -> List[Message]: + filter: dict[str, Any], + ) -> list[Message]: """Fetch messages from Exchange server.""" if not self.exchange_account: raise RepairableException("Exchange account not connected. Call connect() first.") @@ -294,7 +294,7 @@ async def _fetch_exchange_messages( from exchangelib import Q loop = asyncio.get_event_loop() - messages: List[Message] = [] + messages: list[Message] = [] def _sync_fetch(): # Build query @@ -383,9 +383,9 @@ async def _parse_message( # Extract body and attachments body = "" - attachment_paths: List[str] = [] - cid_map: Dict[str, str] = {} # Map Content-ID to file paths - body_parts: List[str] = [] # Track parts in order + attachment_paths: list[str] = [] + cid_map: dict[str, str] = {} # Map Content-ID to file paths + body_parts: list[str] = [] # Track parts in order if email_msg.is_multipart(): # Process parts in order to maintain attachment positions @@ -452,7 +452,7 @@ async def _parse_message( return Message(sender=sender, subject=subject, body=body, attachments=attachment_paths) - def _html_to_text(self, html_content: str, cid_map: Optional[Dict[str, str]] = None) -> str: + def _html_to_text(self, html_content: str, cid_map: dict[str, str] | None = None) -> str: """ Convert HTML to plain text with inline attachment references. @@ -538,9 +538,9 @@ async def read_messages( username: str = "", password: str = "", download_folder: str = "usr/email", - options: Optional[Dict[str, Any]] = None, - filter: Optional[Dict[str, Any]] = None, -) -> List[Message]: + options: dict[str, Any] | None = None, + filter: dict[str, Any] | None = None, +) -> list[Message]: """ Convenience wrapper for reading email messages. diff --git a/backend/utils/extension.py b/backend/utils/extension.py index ddaef07d..dfd23326 100644 --- a/backend/utils/extension.py +++ b/backend/utils/extension.py @@ -138,7 +138,7 @@ def _inner_sync(*args, **kwargs): class Extension: def __init__(self, agent: "Agent|None", **kwargs): - self.agent: "Agent|None" = agent + self.agent: Agent|None = agent self.kwargs = kwargs @abstractmethod @@ -147,7 +147,7 @@ async def execute(self, **kwargs) -> Any: async def call_extensions(extension_point: str, agent: "Agent|None" = None, **kwargs) -> Any: - from backend.utils import plugins, projects, subagents + from backend.utils import plugins, subagents # search for extension folders in all agent's paths paths = subagents.get_paths(agent, "extensions", extension_point, default_root="python") diff --git a/backend/utils/extract_tools.py b/backend/utils/extract_tools.py index 6803e7cb..1096efa8 100644 --- a/backend/utils/extract_tools.py +++ b/backend/utils/extract_tools.py @@ -5,12 +5,12 @@ import re from fnmatch import fnmatch from types import ModuleType -from typing import Any, Type, TypeVar +from typing import Any, TypeVar import regex from .dirty_json import DirtyJson -from .files import deabsolute_path, get_abs_path +from .files import get_abs_path def json_parse_dirty(json: str) -> dict[str, Any] | None: @@ -89,8 +89,8 @@ def import_module(file_path: str) -> ModuleType: def load_classes_from_folder( - folder: str, name_pattern: str, base_class: Type[T], one_per_file: bool = True -) -> list[Type[T]]: + folder: str, name_pattern: str, base_class: type[T], one_per_file: bool = True +) -> list[type[T]]: classes = [] abs_folder = get_abs_path(folder) diff --git a/backend/utils/faiss_monkey_patch.py b/backend/utils/faiss_monkey_patch.py index 45822798..7cc55fad 100644 --- a/backend/utils/faiss_monkey_patch.py +++ b/backend/utils/faiss_monkey_patch.py @@ -40,4 +40,3 @@ with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) - import faiss diff --git a/backend/utils/fasta2a_client.py b/backend/utils/fasta2a_client.py index 88c1c46c..b46cb3bb 100644 --- a/backend/utils/fasta2a_client.py +++ b/backend/utils/fasta2a_client.py @@ -1,5 +1,5 @@ import uuid -from typing import Any, Dict, List, Optional +from typing import Any from backend.utils.print_style import PrintStyle @@ -18,7 +18,7 @@ class AgentConnection: """Helper class for connecting to and communicating with other Ctx AI instances via FastA2A.""" - def __init__(self, agent_url: str, timeout: int = 30, token: Optional[str] = None): + def __init__(self, agent_url: str, timeout: int = 30, token: str | None = None): """Initialize connection to an agent. Args: @@ -45,11 +45,11 @@ def __init__(self, agent_url: str, timeout: int = 30, token: Optional[str] = Non headers["X-API-KEY"] = token self._http_client = httpx.AsyncClient(timeout=timeout, headers=headers) # type: ignore self._a2a_client = A2AClient(base_url=self.agent_url, http_client=self._http_client) # type: ignore - self._agent_card: Optional[Dict[str, Any]] = None + self._agent_card: dict[str, Any] | None = None # Track conversation context automatically - self._context_id: Optional[str] = None + self._context_id: str | None = None - async def get_agent_card(self) -> Dict[str, Any]: + async def get_agent_card(self) -> dict[str, Any]: """Retrieve the agent card from the remote agent.""" if self._agent_card is None: try: @@ -80,10 +80,10 @@ async def get_agent_card(self) -> Dict[str, Any]: async def send_message( self, message: str, - attachments: Optional[List[str]] = None, - context_id: Optional[str] = None, - metadata: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: + attachments: list[str] | None = None, + context_id: str | None = None, + metadata: dict[str, Any] | None = None, + ) -> dict[str, Any]: """Send a message to the remote agent and return task response.""" if not self._agent_card: await self.get_agent_card() @@ -131,7 +131,7 @@ async def send_message( _PRINTER.print(f"[A2A] Error sending message: {e}") raise - async def get_task(self, task_id: str) -> Dict[str, Any]: + async def get_task(self, task_id: str) -> dict[str, Any]: """Get the status and results of a task. Args: @@ -149,7 +149,7 @@ async def get_task(self, task_id: str) -> Dict[str, Any]: async def wait_for_completion( self, task_id: str, poll_interval: int = 2, max_wait: int = 300 - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Wait for a task to complete and return the final result. Args: diff --git a/backend/utils/file_browser.py b/backend/utils/file_browser.py index 828c580a..1ca4ebf3 100644 --- a/backend/utils/file_browser.py +++ b/backend/utils/file_browser.py @@ -4,7 +4,7 @@ import subprocess from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Tuple +from typing import Any from backend.utils import files from backend.utils.print_style import PrintStyle @@ -35,7 +35,7 @@ def _check_file_size(self, file) -> bool: size = file.tell() file.seek(0) return size <= self.MAX_FILE_SIZE - except (AttributeError, IOError): + except (OSError, AttributeError): return False def save_file_b64(self, current_path: str, filename: str, base64_content: str): @@ -54,7 +54,7 @@ def save_file_b64(self, current_path: str, filename: str, base64_content: str): PrintStyle.error(f"Error saving file {filename}: {e}") return False - def save_files(self, files: List, current_path: str = "") -> Tuple[List[str], List[str]]: + def save_files(self, files: list, current_path: str = "") -> tuple[list[str], list[str]]: """Save uploaded files and return successful and failed filenames""" successful = [] failed = [] @@ -199,10 +199,10 @@ def _get_file_extension(self, filename: str) -> str: def _get_files_via_ls( self, full_path: Path - ) -> Tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: + ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: """Get files and folders using ls command for better error handling""" - files: List[Dict[str, Any]] = [] - folders: List[Dict[str, Any]] = [] + files: list[dict[str, Any]] = [] + folders: list[dict[str, Any]] = [] try: # Use ls command to get directory listing @@ -256,7 +256,7 @@ def _get_files_via_ls( try: stat_info = entry_path.stat() - entry_data: Dict[str, Any] = { + entry_data: dict[str, Any] = { "name": filename, "path": str(entry_path.relative_to(self.base_dir)), "modified": datetime.fromtimestamp(stat_info.st_mtime).isoformat(), @@ -306,7 +306,7 @@ def _get_files_via_ls( return files, folders - def get_files(self, current_path: str = "") -> Dict: + def get_files(self, current_path: str = "") -> dict: try: # Resolve the full path while preventing directory traversal full_path = (self.base_dir / current_path).resolve() diff --git a/backend/utils/file_tree.py b/backend/utils/file_tree.py index ff28626b..36fb161f 100644 --- a/backend/utils/file_tree.py +++ b/backend/utils/file_tree.py @@ -2,9 +2,10 @@ import os from collections import deque +from collections.abc import Iterable, Sequence from dataclasses import dataclass -from datetime import datetime, timezone -from typing import Any, Callable, Iterable, Literal, Optional, Sequence +from datetime import UTC, datetime +from typing import Any, Literal from pathspec import PathSpec @@ -114,8 +115,8 @@ def file_tree( name=root_name, level=0, item_type="folder", - created=datetime.fromtimestamp(root_stat.st_ctime, tz=timezone.utc), - modified=datetime.fromtimestamp(root_stat.st_mtime, tz=timezone.utc), + created=datetime.fromtimestamp(root_stat.st_ctime, tz=UTC), + modified=datetime.fromtimestamp(root_stat.st_mtime, tz=UTC), parent=None, items=[], rel_path="", @@ -137,8 +138,8 @@ def make_entry( name=entry.name, level=level, item_type=item_type, - created=datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc), - modified=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc), + created=datetime.fromtimestamp(stat.st_ctime, tz=UTC), + modified=datetime.fromtimestamp(stat.st_mtime, tz=UTC), parent=parent, items=[] if item_type == "folder" else None, rel_path=rel_posix, @@ -269,8 +270,8 @@ class _TreeEntry: item_type: Literal["file", "folder", "comment"] created: datetime modified: datetime - parent: Optional["_TreeEntry"] = None - items: Optional[list["_TreeEntry"]] = None + parent: _TreeEntry | None = None + items: list[_TreeEntry] | None = None is_last: bool = False rel_path: str = "" text: str = "" @@ -400,8 +401,8 @@ def _create_folder_unprocessed_comment( folder_node: _TreeEntry, folder_path: str, abs_root: str, - ignore_spec: Optional[PathSpec], -) -> Optional[_TreeEntry]: + ignore_spec: PathSpec | None, +) -> _TreeEntry | None: try: folders, files = _list_directory_children( folder_path, @@ -421,8 +422,8 @@ def _create_folder_unprocessed_comment( name=entry.name, level=folder_node.level + 1, item_type="folder", - created=datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc), - modified=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc), + created=datetime.fromtimestamp(stat.st_ctime, tz=UTC), + modified=datetime.fromtimestamp(stat.st_mtime, tz=UTC), parent=folder_node, items=None, rel_path=os.path.join(folder_node.rel_path, entry.name), @@ -435,8 +436,8 @@ def _create_folder_unprocessed_comment( name=entry.name, level=folder_node.level + 1, item_type="file", - created=datetime.fromtimestamp(stat.st_ctime, tz=timezone.utc), - modified=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc), + created=datetime.fromtimestamp(stat.st_ctime, tz=UTC), + modified=datetime.fromtimestamp(stat.st_mtime, tz=UTC), parent=folder_node, items=None, rel_path=os.path.join(folder_node.rel_path, entry.name), @@ -477,7 +478,7 @@ def _refresh_render_metadata(node: _TreeEntry) -> None: _refresh_render_metadata(child) -def _resolve_ignore_patterns(ignore: str | None, root_abs_path: str) -> Optional[PathSpec]: +def _resolve_ignore_patterns(ignore: str | None, root_abs_path: str) -> PathSpec | None: if ignore is None: return None @@ -494,7 +495,7 @@ def _resolve_ignore_patterns(ignore: str | None, root_abs_path: str) -> Optional reference_path = os.path.join(root_abs_path, reference) try: - with open(reference_path, "r", encoding="utf-8") as handle: + with open(reference_path, encoding="utf-8") as handle: content = handle.read() except FileNotFoundError as exc: raise FileNotFoundError(f"Ignore file not found: {reference_path}") from exc @@ -516,7 +517,7 @@ def _resolve_ignore_patterns(ignore: str | None, root_abs_path: str) -> Optional def _list_directory_children( directory: str, root_abs_path: str, - ignore_spec: Optional[PathSpec], + ignore_spec: PathSpec | None, *, max_depth_remaining: int, cache: dict[str, bool], diff --git a/backend/utils/files.py b/backend/utils/files.py index e22726bf..45c47cca 100644 --- a/backend/utils/files.py +++ b/backend/utils/files.py @@ -9,7 +9,6 @@ import zipfile from abc import ABC, abstractmethod from fnmatch import fnmatch -from ntpath import isabs from typing import Any, Literal from simpleeval import simple_eval @@ -94,7 +93,7 @@ def parse_file(_filename: str, _directories: list[str] | None = None, _encoding= absolute_path = find_file_in_dirs(_filename, _directories) # Read the file content - with open(absolute_path, "r", encoding=_encoding) as f: + with open(absolute_path, encoding=_encoding) as f: # content = remove_code_fences(f.read()) content = f.read() @@ -135,7 +134,7 @@ def read_prompt_file( absolute_path = find_file_in_dirs(_file, _directories) # Read the file content - with open(absolute_path, "r", encoding=_encoding) as f: + with open(absolute_path, encoding=_encoding) as f: # content = remove_code_fences(f.read()) content = f.read() @@ -211,7 +210,7 @@ def read_file(relative_path: str, encoding="utf-8"): absolute_path = get_abs_path(relative_path) # Read the file content - with open(absolute_path, "r", encoding=encoding) as f: + with open(absolute_path, encoding=encoding) as f: return f.read() @@ -220,14 +219,14 @@ def read_file_json(relative_path: str, encoding="utf-8"): absolute_path = get_abs_path(relative_path) # Read the file content - with open(absolute_path, "r", encoding=encoding) as f: + with open(absolute_path, encoding=encoding) as f: return json.load(f) def read_file_yaml(relative_path: str, encoding="utf-8"): absolute_path = get_abs_path(relative_path) - with open(absolute_path, "r", encoding=encoding) as f: + with open(absolute_path, encoding=encoding) as f: return yaml.loads(f.read()) diff --git a/backend/utils/history.py b/backend/utils/history.py index e18bf8d8..bceba202 100644 --- a/backend/utils/history.py +++ b/backend/utils/history.py @@ -2,14 +2,12 @@ import json import math from abc import abstractmethod -from collections import OrderedDict from collections.abc import Mapping -from enum import Enum -from typing import Any, Coroutine, Dict, List, Literal, TypedDict, Union, cast +from typing import TypedDict, Union, cast -from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage -from backend.utils import call_llm, messages, settings, tokens +from backend.utils import messages, settings, tokens BULK_MERGE_COUNT = 3 TOPICS_MERGE_COUNT = 3 @@ -32,11 +30,11 @@ class RawMessage(TypedDict): MessageContent = Union[ - List["MessageContent"], - Dict[str, "MessageContent"], - List[Dict[str, "MessageContent"]], + list["MessageContent"], + dict[str, "MessageContent"], + list[dict[str, "MessageContent"]], str, - List[str], + list[str], RawMessage, ] @@ -280,7 +278,7 @@ def to_dict(self): def from_dict(data: dict, history: "History"): bulk = Bulk(history=history) bulk.summary = data["summary"] - cls = data["_cls"] + data["_cls"] bulk.records = [Record.from_dict(r, history=history) for r in data["records"]] return bulk @@ -563,8 +561,8 @@ def make_list(obj: MessageContent) -> list[MessageContent]: def _merge_properties( - a: Dict[str, MessageContent], b: Dict[str, MessageContent] -) -> Dict[str, MessageContent]: + a: dict[str, MessageContent], b: dict[str, MessageContent] +) -> dict[str, MessageContent]: result = a.copy() for k, v in b.items(): if k in result: diff --git a/backend/utils/job_loop.py b/backend/utils/job_loop.py index 4f44434d..a8d8bae5 100644 --- a/backend/utils/job_loop.py +++ b/backend/utils/job_loop.py @@ -1,6 +1,5 @@ import asyncio import time -from datetime import datetime from backend.utils import errors, runtime from backend.utils.print_style import PrintStyle diff --git a/backend/utils/kokoro_tts.py b/backend/utils/kokoro_tts.py index 169f17c6..f6cf7d2a 100644 --- a/backend/utils/kokoro_tts.py +++ b/backend/utils/kokoro_tts.py @@ -7,7 +7,6 @@ import soundfile as sf -from backend.utils import runtime from backend.utils.notification import NotificationManager, NotificationPriority, NotificationType from backend.utils.print_style import PrintStyle diff --git a/backend/utils/kvp.py b/backend/utils/kvp.py index 0360b76b..584d37a6 100644 --- a/backend/utils/kvp.py +++ b/backend/utils/kvp.py @@ -59,7 +59,7 @@ def get_persistent(key: str, default: Any = None) -> Any: path = _key_to_path(key) with _persistent_lock: try: - with open(path, "r", encoding="utf-8") as f: + with open(path, encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return default diff --git a/backend/utils/localization.py b/backend/utils/localization.py index 7f4744a3..039f9b0a 100644 --- a/backend/utils/localization.py +++ b/backend/utils/localization.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta from datetime import timezone as dt_timezone import pytz # type: ignore @@ -131,7 +131,7 @@ def localtime_str_to_utc_dt(self, localtime_str: str | None) -> datetime | None: ) # Convert to UTC - return local_datetime_obj.astimezone(dt_timezone.utc) + return local_datetime_obj.astimezone(UTC) except Exception as e: PrintStyle.error(f"Error converting localtime string to UTC: {e}") return None @@ -152,9 +152,9 @@ def utc_dt_to_localtime_str( try: # Ensure datetime is timezone aware in UTC if utc_dt.tzinfo is None: - utc_dt = utc_dt.replace(tzinfo=dt_timezone.utc) + utc_dt = utc_dt.replace(tzinfo=UTC) else: - utc_dt = utc_dt.astimezone(dt_timezone.utc) + utc_dt = utc_dt.astimezone(UTC) # Convert to local time using fixed offset local_tz = dt_timezone(timedelta(minutes=self._offset_minutes)) @@ -178,7 +178,7 @@ def serialize_datetime(self, dt: datetime | None) -> str | None: try: # Ensure datetime is timezone aware (if not, assume UTC) if dt.tzinfo is None: - dt = dt.replace(tzinfo=dt_timezone.utc) + dt = dt.replace(tzinfo=UTC) local_tz = dt_timezone(timedelta(minutes=self._offset_minutes)) local_dt = dt.astimezone(local_tz) diff --git a/backend/utils/log.py b/backend/utils/log.py index d847e090..606962ba 100644 --- a/backend/utils/log.py +++ b/backend/utils/log.py @@ -5,7 +5,7 @@ import uuid from collections import OrderedDict from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, cast +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from backend.utils.secrets import get_secrets_manager from backend.utils.strings import truncate_text_by_ratio @@ -149,9 +149,9 @@ class LogItem: type: Type heading: str = "" content: str = "" - update_progress: Optional[ProgressUpdate] = "persistent" - kvps: Optional[OrderedDict] = None # Use OrderedDict for kvps - id: Optional[str] = None # Add id field + update_progress: ProgressUpdate | None = "persistent" + kvps: OrderedDict | None = None # Use OrderedDict for kvps + id: str | None = None # Add id field guid: str = "" timestamp: float = 0.0 agentno: int = 0 @@ -219,7 +219,7 @@ class Log: def __init__(self): self._lock = threading.RLock() - self.context: "AgentContext|None" = None # set from outside + self.context: AgentContext|None = None # set from outside self.guid: str = str(uuid.uuid4()) self.updates: list[int] = [] self.logs: list[LogItem] = [] @@ -235,7 +235,7 @@ def log( content: str | None = None, kvps: dict | None = None, update_progress: ProgressUpdate | None = None, - id: Optional[str] = None, + id: str | None = None, **kwargs, ) -> LogItem: with self._lock: @@ -279,7 +279,7 @@ def _update_item( content: str | None = None, kvps: dict | None = None, update_progress: ProgressUpdate | None = None, - id: Optional[str] = None, + id: str | None = None, notify_state_monitor: bool = True, **kwargs, ): diff --git a/backend/utils/maintenance.py b/backend/utils/maintenance.py index cd28962a..19fa6e89 100644 --- a/backend/utils/maintenance.py +++ b/backend/utils/maintenance.py @@ -1,5 +1,4 @@ import os -import shutil import time # import psutil <-- Removed to avoid dependency diff --git a/backend/utils/mcp_handler.py b/backend/utils/mcp_handler.py index 0ebcd6cb..cf2383c6 100644 --- a/backend/utils/mcp_handler.py +++ b/backend/utils/mcp_handler.py @@ -3,22 +3,18 @@ import re import threading from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable from contextlib import AsyncExitStack from datetime import timedelta from shutil import which from typing import ( Annotated, Any, - Awaitable, - Callable, ClassVar, - Dict, - List, Literal, Optional, TextIO, TypeVar, - Union, cast, ) @@ -216,7 +212,7 @@ async def after_execution(self, response: Response, **kwargs: Any): class MCPServerRemote(BaseModel): name: str = Field(default_factory=str) - description: Optional[str] = Field(default="Remote SSE Server") + description: str | None = Field(default="Remote SSE Server") type: str = Field(default="sse", description="Server connection type") url: str = Field(default_factory=str) headers: dict[str, Any] | None = Field(default_factory=dict[str, Any]) @@ -241,7 +237,7 @@ def get_log(self) -> str: with self.__lock: return self.__client.get_log() # type: ignore - def get_tools(self) -> List[dict[str, Any]]: + def get_tools(self) -> list[dict[str, Any]]: """Get all tools from the server""" with self.__lock: return self.__client.tools # type: ignore @@ -251,7 +247,7 @@ def has_tool(self, tool_name: str) -> bool: with self.__lock: return self.__client.has_tool(tool_name) # type: ignore - async def call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult: + async def call_tool(self, tool_name: str, input_data: dict[str, Any]) -> CallToolResult: """Call a tool with the given input data""" with self.__lock: # We already run in an event loop, dont believe Pylance @@ -287,7 +283,7 @@ async def initialize(self) -> "MCPServerRemote": class MCPServerLocal(BaseModel): name: str = Field(default_factory=str) - description: Optional[str] = Field(default="Local StdIO Server") + description: str | None = Field(default="Local StdIO Server") type: str = Field(default="stdio", description="Server connection type") command: str = Field(default_factory=str) args: list[str] = Field(default_factory=list) @@ -315,7 +311,7 @@ def get_log(self) -> str: with self.__lock: return self.__client.get_log() # type: ignore - def get_tools(self) -> List[dict[str, Any]]: + def get_tools(self) -> list[dict[str, Any]]: """Get all tools from the server""" with self.__lock: return self.__client.tools # type: ignore @@ -325,7 +321,7 @@ def has_tool(self, tool_name: str) -> bool: with self.__lock: return self.__client.has_tool(tool_name) # type: ignore - async def call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult: + async def call_tool(self, tool_name: str, input_data: dict[str, Any]) -> CallToolResult: """Call a tool with the given input data""" with self.__lock: # We already run in an event loop, dont believe Pylance @@ -358,10 +354,7 @@ async def initialize(self) -> "MCPServerLocal": MCPServer = Annotated[ - Union[ - Annotated[MCPServerRemote, Tag("MCPServerRemote")], - Annotated[MCPServerLocal, Tag("MCPServerLocal")], - ], + Annotated[MCPServerRemote, Tag("MCPServerRemote")] | Annotated[MCPServerLocal, Tag("MCPServerLocal")], Discriminator(_determine_server_type), ] @@ -388,7 +381,7 @@ def wait_for_lock(cls): @classmethod def update(cls, config_str: str) -> Any: with cls.__lock: - servers_data: List[Dict[str, Any]] = [] # Default to empty list + servers_data: list[dict[str, Any]] = [] # Default to empty list if config_str and config_str.strip(): # Only parse if non-empty and not just whitespace try: @@ -453,9 +446,6 @@ def update(cls, config_str: str) -> Any: instance = cls.get_instance() # Directly update the servers attribute of the existing instance or re-initialize carefully # For simplicity and to ensure __init__ logic runs if needed for setup: - new_instance_data = { - "servers": servers_data - } # Prepare data for re-initialization or update # Option 1: Re-initialize the existing instance (if __init__ is idempotent for other fields) instance.__init__(servers_list=servers_data) @@ -499,7 +489,7 @@ def normalize_config(cls, servers: Any): normalized.append(servers) # single server? return normalized - def __init__(self, servers_list: List[Dict[str, Any]]): + def __init__(self, servers_list: list[dict[str, Any]]): from collections.abc import Iterable, Mapping # # DEBUG: Print the received servers_list @@ -691,7 +681,7 @@ def is_initialized(self) -> bool: with self.__lock: return self.__initialized - def get_tools(self) -> List[dict[str, dict[str, Any]]]: + def get_tools(self) -> list[dict[str, dict[str, Any]]]: """Get all tools from all servers""" with self.__lock: tools = [] @@ -772,7 +762,7 @@ def get_tool(self, agent: Any, tool_name: str) -> MCPTool | None: agent=agent, name=tool_name, method=None, args={}, message="", loop_data=None ) - async def call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult: + async def call_tool(self, tool_name: str, input_data: dict[str, Any]) -> CallToolResult: """Call a tool with the given input data""" if "." not in tool_name: raise ValueError(f"Tool {tool_name} not found") @@ -794,12 +784,12 @@ class MCPClientBase(ABC): __lock: ClassVar[threading.Lock] = threading.Lock() - def __init__(self, server: Union[MCPServerLocal, MCPServerRemote]): + def __init__(self, server: MCPServerLocal | MCPServerRemote): self.server = server - self.tools: List[dict[str, Any]] = [] # Tools are cached on the client instance + self.tools: list[dict[str, Any]] = [] # Tools are cached on the client instance self.error: str = "" - self.log: List[str] = [] - self.log_file: Optional[TextIO] = None + self.log: list[str] = [] + self.log_file: TextIO | None = None # Protected method @abstractmethod @@ -850,7 +840,7 @@ async def _execute_with_session( original_exception = e # Create a dummy exception to break out of the async block raise RuntimeError("Dummy exception to break out of async block") - except Exception as e: + except Exception: # Check if this is our dummy exception if original_exception is not None: e = original_exception @@ -915,12 +905,12 @@ def has_tool(self, tool_name: str) -> bool: return True return False - def get_tools(self) -> List[dict[str, Any]]: + def get_tools(self) -> list[dict[str, Any]]: """Get all tools from the server (uses cached tools)""" with self.__lock: return self.tools - async def call_tool(self, tool_name: str, input_data: Dict[str, Any]) -> CallToolResult: + async def call_tool(self, tool_name: str, input_data: dict[str, Any]) -> CallToolResult: # PrintStyle(font_color="cyan").print(f"MCPClientBase ({self.server.name}): Preparing for 'call_tool' operation for tool '{tool_name}'.") if not self.has_tool(tool_name): PrintStyle(font_color="orange").print( @@ -1050,10 +1040,10 @@ def __call__( class MCPClientRemote(MCPClientBase): - def __init__(self, server: Union[MCPServerLocal, MCPServerRemote]): + def __init__(self, server: MCPServerLocal | MCPServerRemote): super().__init__(server) - self.session_id: Optional[str] = None # Track session ID for streaming HTTP clients - self.session_id_callback: Optional[Callable[[], Optional[str]]] = None + self.session_id: str | None = None # Track session ID for streaming HTTP clients + self.session_id_callback: Callable[[], str | None] | None = None async def _create_stdio_transport(self, current_exit_stack: AsyncExitStack) -> tuple[ MemoryObjectReceiveStream[SessionMessage | Exception], @@ -1100,7 +1090,7 @@ async def _create_stdio_transport(self, current_exit_stack: AsyncExitStack) -> t ) return stdio_transport - def get_session_id(self) -> Optional[str]: + def get_session_id(self) -> str | None: """Get the current session ID if available (for streaming HTTP clients).""" if self.session_id_callback is not None: return self.session_id_callback() diff --git a/backend/utils/message_queue.py b/backend/utils/message_queue.py index eede354d..d88728db 100644 --- a/backend/utils/message_queue.py +++ b/backend/utils/message_queue.py @@ -1,5 +1,4 @@ import os -import uuid from typing import TYPE_CHECKING from backend.utils import guids diff --git a/backend/utils/notification.py b/backend/utils/notification.py index 55617570..61360479 100644 --- a/backend/utils/notification.py +++ b/backend/utils/notification.py @@ -1,7 +1,7 @@ import threading import uuid from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from enum import Enum @@ -108,7 +108,7 @@ def add_notification( title=title, message=message, detail=detail, - timestamp=datetime.now(timezone.utc), + timestamp=datetime.now(UTC), display_time=display_time, group=group, ) @@ -138,7 +138,7 @@ def _enforce_limit(self): self.updates = [no - to_remove for no in self.updates if no >= to_remove] def get_recent_notifications(self, seconds: int = 30) -> list[NotificationItem]: - cutoff = datetime.now(timezone.utc) - timedelta(seconds=seconds) + cutoff = datetime.now(UTC) - timedelta(seconds=seconds) with self._lock: return [n for n in self.notifications if n.timestamp >= cutoff] diff --git a/backend/utils/playwright.py b/backend/utils/playwright.py index 0c306332..9c4e967b 100644 --- a/backend/utils/playwright.py +++ b/backend/utils/playwright.py @@ -1,6 +1,5 @@ import os import subprocess -import sys from pathlib import Path from backend.utils import files diff --git a/backend/utils/plugins.py b/backend/utils/plugins.py index 135fe30f..16935976 100644 --- a/backend/utils/plugins.py +++ b/backend/utils/plugins.py @@ -3,15 +3,11 @@ import glob import json import re +from collections.abc import Iterator from pathlib import Path from typing import ( TYPE_CHECKING, - Any, - Dict, - Iterator, - List, Literal, - Optional, TypedDict, ) @@ -50,7 +46,7 @@ class PluginMetadata(BaseModel): title: str = "" description: str = "" version: str = "" - settings_sections: List[str] = Field(default_factory=list) + settings_sections: list[str] = Field(default_factory=list) per_project_config: bool = False per_agent_config: bool = False always_enabled: bool = False @@ -62,7 +58,7 @@ class PluginListItem(BaseModel): display_name: str = "" description: str = "" version: str = "" - settings_sections: List[str] = Field(default_factory=list) + settings_sections: list[str] = Field(default_factory=list) per_project_config: bool = False per_agent_config: bool = False always_enabled: bool = False @@ -79,7 +75,7 @@ def invalidate_plugin_cache(): cache.clear("*(plugins)*") -def get_plugin_roots(plugin_name: str = "") -> List[str]: +def get_plugin_roots(plugin_name: str = "") -> list[str]: """Plugin root directories, ordered by priority (user first).""" return [ files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name), @@ -103,7 +99,7 @@ def get_plugins_list(): return result -def get_enhanced_plugins_list(custom: bool = True, builtin: bool = True) -> List[PluginListItem]: +def get_enhanced_plugins_list(custom: bool = True, builtin: bool = True) -> list[PluginListItem]: """Discover plugins by directory convention. First root wins on ID conflict.""" results = [] @@ -191,15 +187,15 @@ def delete_plugin(plugin_name: str): files.delete_dir(plugin_dir) -def get_plugin_paths(*subpaths: str) -> List[str]: +def get_plugin_paths(*subpaths: str) -> list[str]: sub = "*/" + "/".join(subpaths) if subpaths else "*" - paths: List[str] = [] + paths: list[str] = [] for root in get_plugin_roots(): paths.extend(files.find_existing_paths_by_pattern(files.get_abs_path(root, sub))) return paths -def get_enabled_plugin_paths(agent: Agent | None, *subpaths: str) -> List[str]: +def get_enabled_plugin_paths(agent: Agent | None, *subpaths: str) -> list[str]: enabled = get_enabled_plugins(agent) paths: list[str] = [] @@ -278,7 +274,7 @@ def get_toggle_state(plugin_name: str) -> ToggleState: state = "enabled" if determined_toggle_from_paths(True, reversed(plugin_paths)) else "disabled" # global toggles - usr_toggles = [ + [ files.find_existing_paths_by_pattern( files.get_abs_path(files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN) ), @@ -340,9 +336,9 @@ def toggle_plugin( def get_webui_extensions( - agent: Agent | None, extension_point: str, filters: List[str] | None = None + agent: Agent | None, extension_point: str, filters: list[str] | None = None ): - entries: List[dict] = [] + entries: list[dict] = [] effective_filters = filters or ["*"] enabled = get_enabled_plugins(agent) diff --git a/backend/utils/print_catch.py b/backend/utils/print_catch.py index 2bb3fb1d..9de27a9b 100644 --- a/backend/utils/print_catch.py +++ b/backend/utils/print_catch.py @@ -1,12 +1,13 @@ import asyncio import io import sys -from typing import Any, Awaitable, Callable, Tuple +from collections.abc import Awaitable, Callable +from typing import Any def capture_prints_async( func: Callable[..., Awaitable[Any]], *args, **kwargs -) -> Tuple[Awaitable[Any], Callable[[], str]]: +) -> tuple[Awaitable[Any], Callable[[], str]]: # Create a StringIO object to capture the output captured_output = io.StringIO() original_stdout = sys.stdout diff --git a/backend/utils/projects.py b/backend/utils/projects.py index 1a6ecdf2..0f196d75 100644 --- a/backend/utils/projects.py +++ b/backend/utils/projects.py @@ -1,5 +1,5 @@ import os -from typing import TYPE_CHECKING, Literal, TypedDict, cast +from typing import TYPE_CHECKING, TypedDict, cast from backend.utils import dirty_json, file_tree, files, persist_chat from backend.utils.print_style import PrintStyle @@ -78,7 +78,7 @@ def delete_project(name: str): def create_project(name: str, data: BasicProjectData): - abs_path = files.create_dir_safe( + files.create_dir_safe( files.get_abs_path(PROJECTS_PARENT_DIR, name), rename_format="{name}_{number}" ) create_project_meta_folders(name) diff --git a/backend/utils/providers.py b/backend/utils/providers.py index 01d9a341..025ab392 100644 --- a/backend/utils/providers.py +++ b/backend/utils/providers.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Literal, Optional, TypedDict +from typing import Literal, TypedDict import yaml @@ -15,8 +15,8 @@ class FieldOption(TypedDict): class ProviderManager: _instance = None - _raw: Optional[Dict[str, List[Dict[str, str]]]] = None # full provider data - _options: Optional[Dict[str, List[FieldOption]]] = None # UI-friendly list + _raw: dict[str, list[dict[str, str]]] | None = None # full provider data + _options: dict[str, list[FieldOption]] | None = None # UI-friendly list @classmethod def get_instance(cls): @@ -32,7 +32,7 @@ def _load_providers(self): """Loads provider configurations from the YAML file and normalises them.""" try: config_path = files.get_abs_path("conf/model_providers.yaml") - with open(config_path, "r", encoding="utf-8") as f: + with open(config_path, encoding="utf-8") as f: raw_yaml = yaml.safe_load(f) or {} except (FileNotFoundError, yaml.YAMLError): raw_yaml = {} @@ -43,10 +43,10 @@ def _load_providers(self): # keeps existing callers unchanged while allowing the new nested # mapping format in the YAML (id -> { ... }). # ------------------------------------------------------------ - normalised: Dict[str, List[Dict[str, str]]] = {} + normalised: dict[str, list[dict[str, str]]] = {} for p_type, providers in (raw_yaml or {}).items(): - items: List[Dict[str, str]] = [] + items: list[dict[str, str]] = [] if isinstance(providers, dict): # New format: mapping of id -> config @@ -65,7 +65,7 @@ def _load_providers(self): # Build UI-friendly option list (value / label) self._options = {} for p_type, providers in normalised.items(): - opts: List[FieldOption] = [] + opts: list[FieldOption] = [] for p in providers: pid = (p.get("id") or p.get("value") or "").lower() name = p.get("name") or p.get("label") or pid @@ -73,17 +73,17 @@ def _load_providers(self): opts.append({"value": pid, "label": name}) self._options[p_type] = opts - def get_providers(self, provider_type: ModelType) -> List[FieldOption]: + def get_providers(self, provider_type: ModelType) -> list[FieldOption]: """Returns a list of providers for a given type (e.g., 'chat', 'embedding').""" return self._options.get(provider_type, []) if self._options else [] - def get_raw_providers(self, provider_type: ModelType) -> List[Dict[str, str]]: + def get_raw_providers(self, provider_type: ModelType) -> list[dict[str, str]]: """Return raw provider dictionaries for advanced use-cases.""" return self._raw.get(provider_type, []) if self._raw else [] def get_provider_config( self, provider_type: ModelType, provider_id: str - ) -> Optional[Dict[str, str]]: + ) -> dict[str, str] | None: """Return the metadata dict for a single provider id (case-insensitive).""" provider_id_low = provider_id.lower() for p in self.get_raw_providers(provider_type): @@ -92,16 +92,16 @@ def get_provider_config( return None -def get_providers(provider_type: ModelType) -> List[FieldOption]: +def get_providers(provider_type: ModelType) -> list[FieldOption]: """Convenience function to get providers of a specific type.""" return ProviderManager.get_instance().get_providers(provider_type) -def get_raw_providers(provider_type: ModelType) -> List[Dict[str, str]]: +def get_raw_providers(provider_type: ModelType) -> list[dict[str, str]]: """Return full metadata for providers of a given type.""" return ProviderManager.get_instance().get_raw_providers(provider_type) -def get_provider_config(provider_type: ModelType, provider_id: str) -> Optional[Dict[str, str]]: +def get_provider_config(provider_type: ModelType, provider_id: str) -> dict[str, str] | None: """Return metadata for a single provider (None if not found).""" return ProviderManager.get_instance().get_provider_config(provider_type, provider_id) diff --git a/backend/utils/rate_limiter.py b/backend/utils/rate_limiter.py index 73b59823..82bee747 100644 --- a/backend/utils/rate_limiter.py +++ b/backend/utils/rate_limiter.py @@ -1,6 +1,6 @@ import asyncio import time -from typing import Awaitable, Callable +from collections.abc import Awaitable, Callable class RateLimiter: @@ -16,7 +16,7 @@ def __init__(self, seconds: int = 60, **limits: int): def add(self, **kwargs: int): now = time.time() for key, value in kwargs.items(): - if not key in self.values: + if key not in self.values: self.values[key] = [] self.values[key].append((now, value)) @@ -29,7 +29,7 @@ async def cleanup(self): async def get_total(self, key: str) -> int: async with self._lock: - if not key in self.values: + if key not in self.values: return 0 return sum(value for _, value in self.values[key]) diff --git a/backend/utils/rfc.py b/backend/utils/rfc.py index b8355a59..6ef54d3d 100644 --- a/backend/utils/rfc.py +++ b/backend/utils/rfc.py @@ -5,7 +5,7 @@ import aiohttp -from backend.utils import crypto, dotenv +from backend.utils import crypto # Remote Function Call library # Call function via http request diff --git a/backend/utils/runtime.py b/backend/utils/runtime.py index 0b35700c..e142797f 100644 --- a/backend/utils/runtime.py +++ b/backend/utils/runtime.py @@ -5,8 +5,9 @@ import secrets import sys import threading +from collections.abc import Awaitable, Callable from pathlib import Path -from typing import Awaitable, Callable, TypeVar, Union, cast, overload +from typing import TypeVar, cast, overload import nest_asyncio @@ -94,7 +95,7 @@ async def call_development_function(func: Callable[..., T], *args, **kwargs) -> async def call_development_function( - func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs + func: Callable[..., T] | Callable[..., Awaitable[T]], *args, **kwargs ) -> T: if is_development(): url = _get_rfc_url() @@ -132,7 +133,7 @@ def _get_rfc_password() -> str: def _get_rfc_url() -> str: set = settings.get_settings() url = set["rfc_url"] - if not "://" in url: + if "://" not in url: url = "http://" + url if url.endswith("/"): url = url[:-1] @@ -142,7 +143,7 @@ def _get_rfc_url() -> str: def call_development_function_sync( - func: Union[Callable[..., T], Callable[..., Awaitable[T]]], *args, **kwargs + func: Callable[..., T] | Callable[..., Awaitable[T]], *args, **kwargs ) -> T: # run async function in sync manner result_queue = queue.Queue() diff --git a/backend/utils/secrets.py b/backend/utils/secrets.py index 35b7181b..12499e23 100644 --- a/backend/utils/secrets.py +++ b/backend/utils/secrets.py @@ -1,10 +1,9 @@ -import os import re import threading -import time +from collections.abc import Callable from dataclasses import dataclass from io import StringIO -from typing import TYPE_CHECKING, Callable, Dict, List, Literal, Optional, Set, Tuple +from typing import TYPE_CHECKING, Literal from dotenv.parser import parse_stream @@ -30,9 +29,9 @@ def alias_for_key(key: str, placeholder: str = "§§secret({key})") -> str: class EnvLine: raw: str type: Literal["pair", "comment", "blank", "other"] - key: Optional[str] = None - value: Optional[str] = None - inline_comment: Optional[str] = ( + key: str | None = None + value: str | None = None + inline_comment: str | None = ( None # preserves trailing inline comment including leading spaces and '#' ) @@ -46,16 +45,16 @@ class StreamingSecretsFilter: - On finalize(), any unresolved partial is masked with '***'. """ - def __init__(self, key_to_value: Dict[str, str], min_trigger: int = 3): + def __init__(self, key_to_value: dict[str, str], min_trigger: int = 3): self.min_trigger = max(1, int(min_trigger)) # Map value -> key for placeholder construction - self.value_to_key: Dict[str, str] = { + self.value_to_key: dict[str, str] = { v: k for k, v in key_to_value.items() if isinstance(v, str) and v } # Only keep non-empty values - self.secret_values: List[str] = [v for v in self.value_to_key.keys() if v] + self.secret_values: list[str] = [v for v in self.value_to_key.keys() if v] # Precompute all prefixes for quick suffix matching - self.prefixes: Set[str] = set() + self.prefixes: set[str] = set() for v in self.secret_values: for i in range(self.min_trigger, len(v) + 1): self.prefixes.add(v[:i]) @@ -128,9 +127,9 @@ class SecretsManager: PLACEHOLDER_PATTERN = ALIAS_PATTERN MASK_VALUE = "***" - _instances: Dict[Tuple[str, ...], "SecretsManager"] = {} - _secrets_cache: Optional[Dict[str, str]] = None - _last_raw_text: Optional[str] = None + _instances: dict[tuple[str, ...], "SecretsManager"] = {} + _secrets_cache: dict[str, str] | None = None + _last_raw_text: str | None = None @classmethod def get_instance(cls, *secrets_files: str) -> "SecretsManager": @@ -144,14 +143,14 @@ def get_instance(cls, *secrets_files: str) -> "SecretsManager": def __init__(self, *files: str): self._lock = threading.RLock() # instance-level list of secrets files - self._files: Tuple[str, ...] = tuple(files) if files else (DEFAULT_SECRETS_FILE,) - self._raw_snapshots: Dict[str, str] = {} + self._files: tuple[str, ...] = tuple(files) if files else (DEFAULT_SECRETS_FILE,) + self._raw_snapshots: dict[str, str] = {} self._secrets_cache = None self._last_raw_text = None def read_secrets_raw(self) -> str: """Read raw secrets file content from local filesystem (same system).""" - parts: List[str] = [] + parts: list[str] = [] self._raw_snapshots = {} for path in self._files: @@ -173,7 +172,7 @@ def _write_secrets_raw(self, content: str): raise RuntimeError("Saving secrets content is only supported for a single secrets file") files.write_file(self._files[0], content) - def load_secrets(self) -> Dict[str, str]: + def load_secrets(self) -> dict[str, str]: """Load secrets from file, return key-value dict""" with self._lock: if self._secrets_cache is not None: @@ -227,7 +226,7 @@ def save_secrets_with_merge(self, submitted_content: str): self._write_secrets_raw(merged_text) self._invalidate_all_caches() - def get_keys(self) -> List[str]: + def get_keys(self) -> list[str]: """Get list of secret keys""" secrets = self.load_secrets() return list(secrets.keys()) @@ -325,16 +324,16 @@ def get_masked_secrets(self) -> str: return self._serialize_env_lines(env_lines) - def parse_env_content(self, content: str) -> Dict[str, str]: + def parse_env_content(self, content: str) -> dict[str, str]: """Parse .env format content into key-value dict using python-dotenv. Keys are always uppercase.""" - env: Dict[str, str] = {} + env: dict[str, str] = {} for binding in parse_stream(StringIO(content)): if binding.key and not binding.error: env[binding.key.upper()] = binding.value or "" return env # Backward-compatible alias for callers using the old private method name - def _parse_env_content(self, content: str) -> Dict[str, str]: + def _parse_env_content(self, content: str) -> dict[str, str]: return self.parse_env_content(content) def clear_cache(self): @@ -351,11 +350,11 @@ def _invalidate_all_caches(cls): # ---------------- Internal helpers for parsing/merging ---------------- - def parse_env_lines(self, content: str) -> List[EnvLine]: + def parse_env_lines(self, content: str) -> list[EnvLine]: """Parse env file into EnvLine objects using python-dotenv, preserving comments and order. We reconstruct key_part and inline_comment based on the original string. """ - lines: List[EnvLine] = [] + lines: list[EnvLine] = [] for binding in parse_stream(StringIO(content)): orig = getattr(binding, "original", None) raw = getattr(orig, "string", "") if orig is not None else "" @@ -413,15 +412,15 @@ def parse_env_lines(self, content: str) -> List[EnvLine]: def _serialize_env_lines( self, - lines: List[EnvLine], + lines: list[EnvLine], with_values=True, with_comments=True, with_blank=True, with_other=True, key_delimiter="", - key_formatter: Optional[Callable[[str], str]] = None, + key_formatter: Callable[[str], str] | None = None, ) -> str: - out: List[str] = [] + out: list[str] = [] for ln in lines: if ln.type == "pair" and ln.key is not None: left_raw = ln.key @@ -444,7 +443,7 @@ def _serialize_env_lines( out.append(ln.raw) return "\n".join(out) - def _merge_env(self, existing_text: str, submitted_text: str) -> List[EnvLine]: + def _merge_env(self, existing_text: str, submitted_text: str) -> list[EnvLine]: """Merge using submitted content as the base to preserve its comments and structure. Behavior: - Iterate submitted lines in order and keep them (including comments/blanks/other). @@ -458,11 +457,11 @@ def _merge_env(self, existing_text: str, submitted_text: str) -> List[EnvLine]: existing_lines = self.parse_env_lines(existing_text) submitted_lines = self.parse_env_lines(submitted_text) - existing_pairs: Dict[str, EnvLine] = { + existing_pairs: dict[str, EnvLine] = { ln.key: ln for ln in existing_lines if ln.type == "pair" and ln.key is not None } - merged: List[EnvLine] = [] + merged: list[EnvLine] = [] for sub in submitted_lines: if sub.type != "pair" or sub.key is None: # Preserve submitted comments/blanks/other verbatim diff --git a/backend/utils/security.py b/backend/utils/security.py index 948c40ed..741a056e 100644 --- a/backend/utils/security.py +++ b/backend/utils/security.py @@ -1,7 +1,7 @@ import re import unicodedata from pathlib import Path -from typing import Final, Optional +from typing import Final # Forbidden characters: # Linux/Unix: / and NULL byte @@ -42,7 +42,7 @@ FILENAME_MAX_LENGTH: Final = 255 -def safe_filename(filename: str) -> Optional[str]: +def safe_filename(filename: str) -> str | None: # Normalize Unicode (NFC) filename = unicodedata.normalize("NFC", str(filename)) # Replace forbidden chars diff --git a/backend/utils/settings.py b/backend/utils/settings.py index 224b2aff..e17bf962 100644 --- a/backend/utils/settings.py +++ b/backend/utils/settings.py @@ -2,13 +2,12 @@ import hashlib import json import os -import re import subprocess from typing import Any, Literal, TypedDict, TypeVar, cast from backend.core import models from backend.infrastructure.system import git -from backend.utils import defer, dirty_json, runtime, subagents, whisper +from backend.utils import defer, runtime, subagents, whisper from backend.utils.notification import ( NotificationManager, NotificationPriority, @@ -638,7 +637,7 @@ def _apply_settings(previous: Settings | None): # reload whisper model if necessary if not previous or _settings["stt_model_size"] != previous["stt_model_size"]: - task = defer.DeferredTask().start_task( + defer.DeferredTask().start_task( whisper.preload, _settings["stt_model_size"] ) # TODO overkill, replace with background task @@ -704,7 +703,7 @@ async def update_mcp_settings(mcp_servers: str): group="settings-mcp", ) - task2 = defer.DeferredTask().start_task( + defer.DeferredTask().start_task( update_mcp_settings, config.mcp_servers ) # TODO overkill, replace with background task @@ -719,7 +718,7 @@ async def update_mcp_token(token: str): DynamicMcpProxy.get_instance().reconfigure(token=token) - task3 = defer.DeferredTask().start_task( + defer.DeferredTask().start_task( update_mcp_token, current_token ) # TODO overkill, replace with background task @@ -731,7 +730,7 @@ async def update_a2a_token(token: str): DynamicA2AProxy.get_instance().reconfigure(token=token) - task4 = defer.DeferredTask().start_task( + defer.DeferredTask().start_task( update_a2a_token, current_token ) # TODO overkill, replace with background task diff --git a/backend/utils/shell_local.py b/backend/utils/shell_local.py index b78be54b..93ae990a 100644 --- a/backend/utils/shell_local.py +++ b/backend/utils/shell_local.py @@ -1,9 +1,3 @@ -import platform -import select -import subprocess -import sys -import time -from typing import Optional, Tuple from backend.utils import runtime, tty_session from backend.utils.shell_ssh import clean_string @@ -33,7 +27,7 @@ async def send_command(self, command: str): async def read_output( self, timeout: float = 0, reset_full_output: bool = False - ) -> Tuple[str, Optional[str]]: + ) -> tuple[str, str | None]: if not self.session: raise Exception("Shell not connected") diff --git a/backend/utils/shell_ssh.py b/backend/utils/shell_ssh.py index 312a5bfe..b3414934 100644 --- a/backend/utils/shell_ssh.py +++ b/backend/utils/shell_ssh.py @@ -1,7 +1,6 @@ import asyncio import re import time -from typing import Tuple import paramiko @@ -116,14 +115,13 @@ async def send_command(self, command: str): async def read_output( self, timeout: float = 0, reset_full_output: bool = False - ) -> Tuple[str, str]: + ) -> tuple[str, str]: if not self.shell: raise Exception("Shell not connected") if reset_full_output: self.full_output = b"" partial_output = b"" - leftover = b"" start_time = time.time() while self.shell.recv_ready() and (timeout <= 0 or time.time() - start_time < timeout): diff --git a/backend/utils/skills.py b/backend/utils/skills.py index d0c21b73..0c0a411f 100644 --- a/backend/utils/skills.py +++ b/backend/utils/skills.py @@ -4,9 +4,9 @@ import re from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional, Tuple +from typing import TYPE_CHECKING, Any -from backend.utils import file_tree, files, projects, runtime, subagents +from backend.utils import file_tree, files, runtime, subagents if TYPE_CHECKING: from backend.core.agent import Agent @@ -25,16 +25,16 @@ class Skill: skill_md_path: Path version: str = "" author: str = "" - tags: List[str] = field(default_factory=list) - triggers: List[str] = field(default_factory=list) - allowed_tools: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) + triggers: list[str] = field(default_factory=list) + allowed_tools: list[str] = field(default_factory=list) license: str = "" compatibility: str = "" - metadata: Dict[str, Any] = field(default_factory=dict) + metadata: dict[str, Any] = field(default_factory=dict) # Optional heavy fields (only set when requested) content: str = "" # body content (markdown without frontmatter) - raw_frontmatter: Dict[str, Any] = field(default_factory=dict) + raw_frontmatter: dict[str, Any] = field(default_factory=dict) def get_skills_base_dir() -> Path: @@ -43,7 +43,7 @@ def get_skills_base_dir() -> Path: def get_skill_roots( agent: Agent | None = None, -) -> List[str]: +) -> list[str]: if agent: # skill roots available to agent @@ -83,7 +83,7 @@ def _is_hidden_path(path: Path) -> bool: return any(part.startswith(".") for part in path.parts) -def discover_skill_md_files(root: Path) -> List[Path]: +def discover_skill_md_files(root: Path) -> list[Path]: """ Recursively discover SKILL.md files under a root directory. Hidden folders/files are ignored. @@ -91,7 +91,7 @@ def discover_skill_md_files(root: Path) -> List[Path]: if not root.exists(): return [] - results: List[Path] = [] + results: list[Path] = [] for p in root.rglob("SKILL.md"): try: if not p.is_file(): @@ -107,7 +107,7 @@ def discover_skill_md_files(root: Path) -> List[Path]: return results -def _coerce_list(value: Any) -> List[str]: +def _coerce_list(value: Any) -> list[str]: if value is None: return [] if isinstance(value, list): @@ -132,12 +132,12 @@ def _read_text(path: Path) -> str: return path.read_text(encoding="utf-8", errors="replace") -def split_frontmatter(markdown: str) -> Tuple[Dict[str, Any], str, List[str]]: +def split_frontmatter(markdown: str) -> tuple[dict[str, Any], str, list[str]]: """ Splits a SKILL.md into (frontmatter_dict, body_text, errors). Enforces YAML frontmatter at the top for spec compatibility. """ - errors: List[str] = [] + errors: list[str] = [] text = markdown or "" lines = text.splitlines() @@ -172,10 +172,10 @@ def split_frontmatter(markdown: str) -> Tuple[Dict[str, Any], str, List[str]]: return fm, body, errors -def _parse_frontmatter_fallback(frontmatter_text: str) -> Dict[str, Any]: +def _parse_frontmatter_fallback(frontmatter_text: str) -> dict[str, Any]: # Minimal YAML subset: key: value, lists with "- item" - data: Dict[str, Any] = {} - current_key: Optional[str] = None + data: dict[str, Any] = {} + current_key: str | None = None for raw in frontmatter_text.splitlines(): line = raw.rstrip() if not line.strip() or line.strip().startswith("#"): @@ -210,12 +210,12 @@ def _parse_frontmatter_fallback(frontmatter_text: str) -> Dict[str, Any]: return data -def parse_frontmatter(frontmatter_text: str) -> Tuple[Dict[str, Any], List[str]]: +def parse_frontmatter(frontmatter_text: str) -> tuple[dict[str, Any], list[str]]: """ Parse YAML frontmatter with PyYAML when available, falling back to a minimal subset parser. """ - errors: List[str] = [] + errors: list[str] = [] if not frontmatter_text.strip(): return {}, errors @@ -241,7 +241,7 @@ def skill_from_markdown( *, include_content: bool = False, validate: bool = True, -) -> Optional[Skill]: +) -> Skill | None: try: text = _read_text(skill_md_path) except Exception: @@ -307,9 +307,9 @@ def skill_from_markdown( def list_skills( agent: Agent | None = None, include_content: bool = False, -) -> List[Skill]: +) -> list[Skill]: """List skills, optionally filtered by agent scope.""" - skills: List[Skill] = [] + skills: list[Skill] = [] roots = get_skill_roots(agent) @@ -324,7 +324,7 @@ def list_skills( return skills # Dedupe by normalized name, preserving root_order priority (earlier wins) - by_name: Dict[str, Skill] = {} + by_name: dict[str, Skill] = {} for s in skills: key = _normalize_name(s.name) or _normalize_name(s.path.name) if key and key not in by_name: @@ -360,7 +360,7 @@ def find_skill( skill_name: str, agent: Agent | None = None, include_content: bool = False, -) -> Optional[Skill]: +) -> Skill | None: target = _normalize_name(skill_name) if not target: return None @@ -455,7 +455,7 @@ def search_skills( query: str, limit: int = 25, agent: Agent | None = None, -) -> List[Skill]: +) -> list[Skill]: q = (query or "").strip().lower() if not q: return [] @@ -463,7 +463,7 @@ def search_skills( terms = [t for t in re.split(r"\s+", q) if t] candidates = list_skills(agent) - scored: List[Tuple[int, Skill]] = [] + scored: list[tuple[int, Skill]] = [] for s in candidates: name = s.name.lower() desc = (s.description or "").lower() @@ -488,8 +488,8 @@ def search_skills( _NAME_RE = re.compile(r"^[a-z0-9-]+$") -def validate_skill(skill: Skill) -> List[str]: - issues: List[str] = [] +def validate_skill(skill: Skill) -> list[str]: + issues: list[str] = [] name = (skill.name or "").strip() desc = (skill.description or "").strip() @@ -518,7 +518,7 @@ def validate_skill(skill: Skill) -> List[str]: return issues -def validate_skill_md(skill_md_path: Path) -> List[str]: +def validate_skill_md(skill_md_path: Path) -> list[str]: try: text = _read_text(skill_md_path) except Exception: diff --git a/backend/utils/skills_cli.py b/backend/utils/skills_cli.py index cd331abd..01413c3a 100644 --- a/backend/utils/skills_cli.py +++ b/backend/utils/skills_cli.py @@ -11,13 +11,10 @@ """ import argparse -import os import re import sys from dataclasses import dataclass, field -from datetime import datetime from pathlib import Path -from typing import Any, Dict, List, Optional import yaml @@ -36,12 +33,12 @@ class Skill: path: Path version: str = "1.0.0" author: str = "" - tags: List[str] = field(default_factory=list) - trigger_patterns: List[str] = field(default_factory=list) + tags: list[str] = field(default_factory=list) + trigger_patterns: list[str] = field(default_factory=list) content: str = "" -def get_skills_dirs() -> List[Path]: +def get_skills_dirs() -> list[Path]: """Get all skill directories""" base = Path(files.get_abs_path("usr", "skills")) return [ @@ -50,7 +47,7 @@ def get_skills_dirs() -> List[Path]: ] -def parse_skill_file(skill_path: Path) -> Optional[Skill]: +def parse_skill_file(skill_path: Path) -> Skill | None: """Parse a SKILL.md file and return a Skill object""" try: content = skill_path.read_text(encoding="utf-8") @@ -79,7 +76,7 @@ def parse_skill_file(skill_path: Path) -> Optional[Skill]: return None -def list_skills() -> List[Skill]: +def list_skills() -> list[Skill]: """List all available skills""" skills = [] for skills_dir in get_skills_dirs(): @@ -95,7 +92,7 @@ def list_skills() -> List[Skill]: return skills -def find_skill(name: str) -> Optional[Skill]: +def find_skill(name: str) -> Skill | None: """Find a skill by name""" for skill in list_skills(): if skill.name == name or skill.path.name == name: @@ -103,7 +100,7 @@ def find_skill(name: str) -> Optional[Skill]: return None -def search_skills(query: str) -> List[Skill]: +def search_skills(query: str) -> list[Skill]: """Search skills by name, description, or tags""" query = query.lower() results = [] @@ -118,7 +115,7 @@ def search_skills(query: str) -> List[Skill]: return results -def validate_skill(skill: Skill) -> List[str]: +def validate_skill(skill: Skill) -> list[str]: """Validate a skill and return list of issues""" issues = [] @@ -151,8 +148,8 @@ def validate_skill(skill: Skill) -> List[str]: # Check for associated files skill_dir = skill.path - has_scripts = (skill_dir / "scripts").exists() - has_docs = (skill_dir / "docs").exists() + (skill_dir / "scripts").exists() + (skill_dir / "docs").exists() return issues @@ -229,7 +226,7 @@ def create_skill(name: str, description: str = "", author: str = "") -> Path: return skill_dir -def print_skill_table(skills: List[Skill]): +def print_skill_table(skills: list[Skill]): """Print skills in a formatted table""" if not skills: print("No skills found.") @@ -309,7 +306,7 @@ def main(): try: skill_dir = create_skill(args.name, args.description, args.author) print(f"\n✅ Created skill at: {skill_dir}") - print(f"\nNext steps:") + print("\nNext steps:") print(f" 1. Edit {skill_dir / 'SKILL.md'} to add your instructions") print(f" 2. Add any helper scripts to {skill_dir / 'scripts'}/") print(f" 3. Run: python -m backend.utils.skills_cli validate {args.name}") @@ -330,9 +327,9 @@ def main(): print( f"Triggers: {', '.join(skill.trigger_patterns) if skill.trigger_patterns else 'None'}" ) - print(f"\nDescription:") + print("\nDescription:") print(f" {skill.description}") - print(f"\nContent Preview (first 500 chars):") + print("\nContent Preview (first 500 chars):") print("-" * 60) print(skill.content[:500]) if len(skill.content) > 500: diff --git a/backend/utils/skills_import.py b/backend/utils/skills_import.py index 35cd0af9..822fc8c0 100644 --- a/backend/utils/skills_import.py +++ b/backend/utils/skills_import.py @@ -1,13 +1,11 @@ from __future__ import annotations -import os import shutil -import tempfile import time import zipfile from dataclasses import dataclass from pathlib import Path -from typing import Iterable, List, Literal, Optional, Tuple +from typing import Literal from backend.utils import files from backend.utils.skills import discover_skill_md_files @@ -27,8 +25,8 @@ class ImportPlanItem: @dataclass(slots=True) class ImportResult: - imported: List[Path] - skipped: List[Path] + imported: list[Path] + skipped: list[Path] source_root: Path destination_root: Path namespace: str @@ -47,14 +45,14 @@ def _derive_namespace(source: Path) -> str: return (source.stem or source.name or "import").strip() -def _candidate_skill_roots(source_dir: Path) -> List[Path]: +def _candidate_skill_roots(source_dir: Path) -> list[Path]: """ Heuristics to find likely skill roots inside a repo/pack: - /skills - /plugins/*/skills (Claude Code style) - fallback: """ - candidates: List[Path] = [] + candidates: list[Path] = [] direct = source_dir / "skills" if direct.is_dir() and discover_skill_md_files(direct): @@ -70,7 +68,7 @@ def _candidate_skill_roots(source_dir: Path) -> List[Path]: candidates.append(skills_dir) # Deduplicate while preserving order - unique: List[Path] = [] + unique: list[Path] = [] seen = set() for c in candidates: key = str(c.resolve()) @@ -106,8 +104,8 @@ def build_import_plan( source: Path, dest_root: Path, *, - namespace: Optional[str] = None, -) -> Tuple[List[ImportPlanItem], Path]: + namespace: str | None = None, +) -> tuple[list[ImportPlanItem], Path]: """ Build a copy plan for importing skills from a source folder. @@ -115,7 +113,7 @@ def build_import_plan( """ source_dir = source roots = _candidate_skill_roots(source_dir) - plan: List[ImportPlanItem] = [] + plan: list[ImportPlanItem] = [] ns = (namespace or _derive_namespace(source)).strip() dest_ns_root = dest_root / ns @@ -137,7 +135,7 @@ def build_import_plan( # Deduplicate by destination path (keep first occurrence) seen_dest = set() - deduped: List[ImportPlanItem] = [] + deduped: list[ImportPlanItem] = [] for item in plan: key = str(item.dest_skill_dir.resolve()) if key in seen_dest: @@ -148,7 +146,7 @@ def build_import_plan( return deduped, roots[0] -def _resolve_conflict(dest: Path, policy: ConflictPolicy) -> Tuple[Path, bool]: +def _resolve_conflict(dest: Path, policy: ConflictPolicy) -> tuple[Path, bool]: """ Returns (final_dest_path, should_copy). """ @@ -189,8 +187,8 @@ def get_project_agent_profile_skills_folder(project_name: str, profile_name: str def resolve_skills_destination_root( - project_name: Optional[str], - agent_profile: Optional[str], + project_name: str | None, + agent_profile: str | None, ) -> Path: if project_name and agent_profile: return get_project_agent_profile_skills_folder(project_name, agent_profile) @@ -204,11 +202,11 @@ def resolve_skills_destination_root( def import_skills( source_path: str, *, - namespace: Optional[str] = None, + namespace: str | None = None, conflict: ConflictPolicy = "skip", dry_run: bool = False, - project_name: Optional[str] = None, - agent_profile: Optional[str] = None, + project_name: str | None = None, + agent_profile: str | None = None, ) -> ImportResult: """ Import external Skills into usr/skills//... @@ -227,7 +225,7 @@ def import_skills( dest_root = resolve_skills_destination_root(project_name, agent_profile) dest_root.mkdir(parents=True, exist_ok=True) - extracted_root: Optional[Path] = None + extracted_root: Path | None = None source_dir: Path if src.is_file() and src.suffix.lower() == ".zip": extracted_root = _unzip_to_temp_dir(src) @@ -242,8 +240,8 @@ def import_skills( ns = "import" plan, root_used = build_import_plan(source_dir, dest_root, namespace=ns) - imported: List[Path] = [] - skipped: List[Path] = [] + imported: list[Path] = [] + skipped: list[Path] = [] for item in plan: final_dest, should_copy = _resolve_conflict(item.dest_skill_dir, conflict) diff --git a/backend/utils/state_monitor.py b/backend/utils/state_monitor.py index dd81b506..aabc24f5 100644 --- a/backend/utils/state_monitor.py +++ b/backend/utils/state_monitor.py @@ -65,7 +65,7 @@ def __init__(self, debounce_seconds: float = 0.025) -> None: self._dispatcher_loop: asyncio.AbstractEventLoop | None = None self._dirty_wave_seq: int = 0 - def bind_manager(self, manager: "WebSocketManager", *, handler_id: str | None = None) -> None: + def bind_manager(self, manager: WebSocketManager, *, handler_id: str | None = None) -> None: with self._lock: self._manager = manager if handler_id: diff --git a/backend/utils/state_snapshot.py b/backend/utils/state_snapshot.py index a927834c..e9eb88dd 100644 --- a/backend/utils/state_snapshot.py +++ b/backend/utils/state_snapshot.py @@ -1,8 +1,9 @@ from __future__ import annotations import types +from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Mapping, TypedDict, Union, get_args, get_origin, get_type_hints +from typing import Any, TypedDict, Union, get_args, get_origin, get_type_hints import pytz # type: ignore[import-untyped] diff --git a/backend/utils/subagents.py b/backend/utils/subagents.py index ce098dc6..fb4e0938 100644 --- a/backend/utils/subagents.py +++ b/backend/utils/subagents.py @@ -1,6 +1,6 @@ import json import os -from typing import TYPE_CHECKING, Literal, TypedDict +from typing import TYPE_CHECKING, Literal from pydantic import BaseModel, model_validator diff --git a/backend/utils/task_scheduler.py b/backend/utils/task_scheduler.py index 31f65e0a..02f8554b 100644 --- a/backend/utils/task_scheduler.py +++ b/backend/utils/task_scheduler.py @@ -3,10 +3,11 @@ import random import threading import uuid -from datetime import datetime, timedelta, timezone +from collections.abc import Callable +from datetime import UTC, datetime, timedelta from enum import Enum from os.path import exists -from typing import Any, Callable, ClassVar, Dict, Literal, Optional, Type, TypeVar, Union, cast +from typing import Any, ClassVar, Literal, Optional, TypeVar, cast from urllib.parse import urlparse import nest_asyncio @@ -19,7 +20,7 @@ from crontab import CronTab from pydantic import BaseModel, Field, PrivateAttr -from backend.core.agent import Agent, AgentContext, UserMessage +from backend.core.agent import AgentContext, UserMessage from backend.utils import guids, projects from backend.utils.defer import DeferredTask from backend.utils.files import get_abs_path, make_dirs, read_file, write_file @@ -121,14 +122,14 @@ def should_launch(self) -> datetime | None: if next_launch_time is None: return None # return next launch time if current datetime utc is later than next launch time - if datetime.now(timezone.utc) > next_launch_time: + if datetime.now(UTC) > next_launch_time: return next_launch_time return None class BaseTask(BaseModel): uuid: str = Field(default_factory=lambda: guids.generate_id()) - context_id: Optional[str] = Field(default=None) + context_id: str | None = Field(default=None) state: TaskState = Field(default=TaskState.IDLE) name: str = Field() system_prompt: str @@ -136,8 +137,8 @@ class BaseTask(BaseModel): attachments: list[str] = Field(default_factory=list) project_name: str | None = Field(default=None) project_color: str | None = Field(default=None) - created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) - updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + created_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC)) last_run: datetime | None = None last_result: str | None = None @@ -162,32 +163,32 @@ def update( with self._lock: if name is not None: self.name = name - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) if state is not None: self.state = state - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) if system_prompt is not None: self.system_prompt = system_prompt - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) if prompt is not None: self.prompt = prompt - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) if attachments is not None: self.attachments = attachments - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) if last_run is not None: self.last_run = last_run - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) if last_result is not None: self.last_result = last_result - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) if context_id is not None: self.context_id = context_id - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) for key, value in kwargs.items(): if value is not None: setattr(self, key, value) - self.updated_at = datetime.now(timezone.utc) + self.updated_at = datetime.now(UTC) def check_schedule(self, frequency_seconds: float = 60.0) -> bool: return False @@ -202,7 +203,7 @@ def get_next_run_minutes(self) -> int | None: next_run = self.get_next_run() if next_run is None: return None - return int((next_run - datetime.now(timezone.utc)).total_seconds() / 60) + return int((next_run - datetime.now(UTC)).total_seconds() / 60) async def on_run(self): pass @@ -210,7 +211,7 @@ async def on_run(self): async def on_finish(self): # Ensure that updated_at is refreshed to reflect completion time # This helps track when the task actually finished, regardless of success/error - await TaskScheduler.get().update_task(self.uuid, updated_at=datetime.now(timezone.utc)) + await TaskScheduler.get().update_task(self.uuid, updated_at=datetime.now(UTC)) async def on_error(self, error: str): # Update task state to ERROR and set last result @@ -219,7 +220,7 @@ async def on_error(self, error: str): updated_task = await scheduler.update_task( self.uuid, state=TaskState.ERROR, - last_run=datetime.now(timezone.utc), + last_run=datetime.now(UTC), last_result=f"ERROR: {error}", ) if not updated_task: @@ -233,7 +234,7 @@ async def on_success(self, result: str): scheduler = TaskScheduler.get() await scheduler.reload() # Ensure we have the latest state updated_task = await scheduler.update_task( - self.uuid, state=TaskState.IDLE, last_run=datetime.now(timezone.utc), last_result=result + self.uuid, state=TaskState.IDLE, last_run=datetime.now(UTC), last_result=result ) if not updated_task: PrintStyle.error(f"Failed to update task {self.uuid} state to IDLE after success") @@ -366,11 +367,11 @@ def check_schedule(self, frequency_seconds: float = 60.0) -> bool: ) # Get reference time in task's timezone (by default now - frequency_seconds) - reference_time = datetime.now(timezone.utc) - timedelta(seconds=frequency_seconds) + reference_time = datetime.now(UTC) - timedelta(seconds=frequency_seconds) reference_time = reference_time.astimezone(task_timezone) # Get next run time as seconds until next execution - next_run_seconds: Optional[float] = crontab.next( # type: ignore + next_run_seconds: float | None = crontab.next( # type: ignore now=reference_time, return_datetime=False ) # type: ignore @@ -382,7 +383,7 @@ def check_schedule(self, frequency_seconds: float = 60.0) -> bool: def get_next_run(self) -> datetime | None: with self._lock: crontab = CronTab(crontab=self.schedule.to_crontab()) # type: ignore - return crontab.next(now=datetime.now(timezone.utc), return_datetime=True) # type: ignore + return crontab.next(now=datetime.now(UTC), return_datetime=True) # type: ignore class PlannedTask(BaseTask): @@ -485,7 +486,7 @@ async def on_error(self, error: str): class SchedulerTaskList(BaseModel): tasks: list[ - Annotated[Union[ScheduledTask, AdHocTask, PlannedTask], Field(discriminator="type")] + Annotated[ScheduledTask | AdHocTask | PlannedTask, Field(discriminator="type")] ] = Field(default_factory=list) # Singleton instance __instance: ClassVar[Optional["SchedulerTaskList"]] = PrivateAttr(default=None) @@ -519,7 +520,7 @@ async def reload(self) -> "SchedulerTaskList": return self async def add_task( - self, task: Union[ScheduledTask, AdHocTask, PlannedTask] + self, task: ScheduledTask | AdHocTask | PlannedTask ) -> "SchedulerTaskList": with self._lock: self.tasks.append(task) @@ -565,11 +566,11 @@ async def save(self) -> "SchedulerTaskList": async def update_task_by_uuid( self, task_uuid: str, - updater_func: Callable[[Union[ScheduledTask, AdHocTask, PlannedTask]], None], + updater_func: Callable[[ScheduledTask | AdHocTask | PlannedTask], None], verify_func: Callable[ - [Union[ScheduledTask, AdHocTask, PlannedTask]], bool + [ScheduledTask | AdHocTask | PlannedTask], bool ] = lambda task: True, - ) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None: + ) -> ScheduledTask | AdHocTask | PlannedTask | None: """ Atomically update a task by UUID using the provided updater function. @@ -597,13 +598,13 @@ async def update_task_by_uuid( return task - def get_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]: + def get_tasks(self) -> list[ScheduledTask | AdHocTask | PlannedTask]: with self._lock: return self.tasks def get_tasks_by_context_id( self, context_id: str, only_running: bool = False - ) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]: + ) -> list[ScheduledTask | AdHocTask | PlannedTask]: with self._lock: return [ task @@ -612,7 +613,7 @@ def get_tasks_by_context_id( and (not only_running or task.state == TaskState.RUNNING) ] - async def get_due_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]: + async def get_due_tasks(self) -> list[ScheduledTask | AdHocTask | PlannedTask]: with self._lock: await self.reload() return [ @@ -623,15 +624,15 @@ async def get_due_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTas def get_task_by_uuid( self, task_uuid: str - ) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None: + ) -> ScheduledTask | AdHocTask | PlannedTask | None: with self._lock: return next((task for task in self.tasks if task.uuid == task_uuid), None) - def get_task_by_name(self, name: str) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None: + def get_task_by_name(self, name: str) -> ScheduledTask | AdHocTask | PlannedTask | None: with self._lock: return next((task for task in self.tasks if task.name == name), None) - def find_task_by_name(self, name: str) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]: + def find_task_by_name(self, name: str) -> list[ScheduledTask | AdHocTask | PlannedTask]: with self._lock: return [task for task in self.tasks if name.lower() in task.name.lower()] @@ -653,7 +654,7 @@ class TaskScheduler: _tasks: SchedulerTaskList _printer: PrintStyle _instance = None - _running_deferred_tasks: Dict[str, DeferredTask] + _running_deferred_tasks: dict[str, DeferredTask] _running_tasks_lock: threading.RLock @classmethod @@ -702,17 +703,17 @@ def cancel_tasks_by_context(self, context_id: str, terminate_thread: bool = Fals async def reload(self): await self._tasks.reload() - def get_tasks(self) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]: + def get_tasks(self) -> list[ScheduledTask | AdHocTask | PlannedTask]: return self._tasks.get_tasks() def get_tasks_by_context_id( self, context_id: str, only_running: bool = False - ) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]: + ) -> list[ScheduledTask | AdHocTask | PlannedTask]: return self._tasks.get_tasks_by_context_id(context_id, only_running) - async def add_task(self, task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> "TaskScheduler": + async def add_task(self, task: ScheduledTask | AdHocTask | PlannedTask) -> "TaskScheduler": await self._tasks.add_task(task) - ctx = await self._get_chat_context(task) # invoke context creation + await self._get_chat_context(task) # invoke context creation from backend.utils.state_monitor_integration import mark_dirty_all mark_dirty_all(reason="task_scheduler.TaskScheduler.add_task") @@ -734,13 +735,13 @@ async def remove_task_by_name(self, name: str) -> "TaskScheduler": def get_task_by_uuid( self, task_uuid: str - ) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None: + ) -> ScheduledTask | AdHocTask | PlannedTask | None: return self._tasks.get_task_by_uuid(task_uuid) - def get_task_by_name(self, name: str) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None: + def get_task_by_name(self, name: str) -> ScheduledTask | AdHocTask | PlannedTask | None: return self._tasks.get_task_by_name(name) - def find_task_by_name(self, name: str) -> list[Union[ScheduledTask, AdHocTask, PlannedTask]]: + def find_task_by_name(self, name: str) -> list[ScheduledTask | AdHocTask | PlannedTask]: return self._tasks.find_task_by_name(name) async def tick(self): @@ -790,10 +791,10 @@ async def update_task_checked( self, task_uuid: str, verify_func: Callable[ - [Union[ScheduledTask, AdHocTask, PlannedTask]], bool + [ScheduledTask | AdHocTask | PlannedTask], bool ] = lambda task: True, **update_params, - ) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None: + ) -> ScheduledTask | AdHocTask | PlannedTask | None: """ Atomically update a task by UUID with the provided parameters. This prevents race conditions when multiple processes update tasks concurrently. @@ -813,11 +814,11 @@ def _update_task(task): async def update_task( self, task_uuid: str, **update_params - ) -> Union[ScheduledTask, AdHocTask, PlannedTask] | None: + ) -> ScheduledTask | AdHocTask | PlannedTask | None: return await self.update_task_checked(task_uuid, lambda task: True, **update_params) async def __new_context( - self, task: Union[ScheduledTask, AdHocTask, PlannedTask] + self, task: ScheduledTask | AdHocTask | PlannedTask ) -> AgentContext: if not task.context_id: raise ValueError(f"Task {task.name} has no context ID") @@ -837,7 +838,7 @@ async def __new_context( return context async def _get_chat_context( - self, task: Union[ScheduledTask, AdHocTask, PlannedTask] + self, task: ScheduledTask | AdHocTask | PlannedTask ) -> AgentContext: context = AgentContext.get(task.context_id) if task.context_id else None @@ -853,7 +854,7 @@ async def _get_chat_context( return await self.__new_context(task) async def _persist_chat( - self, task: Union[ScheduledTask, AdHocTask, PlannedTask], context: AgentContext + self, task: ScheduledTask | AdHocTask | PlannedTask, context: AgentContext ): if context.id != task.context_id: raise ValueError( @@ -862,13 +863,13 @@ async def _persist_chat( save_tmp_chat(context) async def _run_task( - self, task: Union[ScheduledTask, AdHocTask, PlannedTask], task_context: str | None = None + self, task: ScheduledTask | AdHocTask | PlannedTask, task_context: str | None = None ): async def _run_task_wrapper(task_uuid: str, task_context: str | None = None): # preflight checks with a snapshot of the task - task_snapshot: Union[ScheduledTask, AdHocTask, PlannedTask] | None = ( + task_snapshot: ScheduledTask | AdHocTask | PlannedTask | None = ( self.get_task_by_uuid(task_uuid) ) if task_snapshot is None: @@ -1036,13 +1037,13 @@ async def _run_task_wrapper(task_uuid: str, task_context: str | None = None): # without leaving stray pending tasks that trigger \"Task was destroyed\" warnings when the loop shuts down. await asyncio.sleep(0.1) - def serialize_all_tasks(self) -> list[Dict[str, Any]]: + def serialize_all_tasks(self) -> list[dict[str, Any]]: """ Serialize all tasks in the scheduler to a list of dictionaries. """ return serialize_tasks(self.get_tasks()) - def serialize_task(self, task_id: str) -> Optional[Dict[str, Any]]: + def serialize_task(self, task_id: str) -> dict[str, Any] | None: """ Serialize a specific task in the scheduler by UUID. Returns None if task is not found. @@ -1059,7 +1060,7 @@ def serialize_task(self, task_id: str) -> Optional[Dict[str, Any]]: # ---------------------- -def serialize_datetime(dt: Optional[datetime]) -> Optional[str]: +def serialize_datetime(dt: datetime | None) -> str | None: """ Serialize a datetime object to ISO format string in the user's timezone. @@ -1072,7 +1073,7 @@ def serialize_datetime(dt: Optional[datetime]) -> Optional[str]: return Localization.get().serialize_datetime(dt) -def parse_datetime(dt_str: Optional[str]) -> Optional[datetime]: +def parse_datetime(dt_str: str | None) -> datetime | None: """ Parse ISO format datetime string with timezone awareness. @@ -1091,7 +1092,7 @@ def parse_datetime(dt_str: Optional[str]) -> Optional[datetime]: raise ValueError(f"Invalid datetime format: {dt_str}. Expected ISO format. Error: {e}") -def serialize_task_schedule(schedule: TaskSchedule) -> Dict[str, str]: +def serialize_task_schedule(schedule: TaskSchedule) -> dict[str, str]: """Convert TaskSchedule to a standardized dictionary format.""" return { "minute": schedule.minute, @@ -1103,7 +1104,7 @@ def serialize_task_schedule(schedule: TaskSchedule) -> Dict[str, str]: } -def parse_task_schedule(schedule_data: Dict[str, str]) -> TaskSchedule: +def parse_task_schedule(schedule_data: dict[str, str]) -> TaskSchedule: """Parse dictionary into TaskSchedule with validation.""" try: return TaskSchedule( @@ -1118,7 +1119,7 @@ def parse_task_schedule(schedule_data: Dict[str, str]) -> TaskSchedule: raise ValueError(f"Invalid schedule format: {e}") from e -def serialize_task_plan(plan: TaskPlan) -> Dict[str, Any]: +def serialize_task_plan(plan: TaskPlan) -> dict[str, Any]: """Convert TaskPlan to a standardized dictionary format.""" return { "todo": [serialize_datetime(dt) for dt in plan.todo], @@ -1127,7 +1128,7 @@ def serialize_task_plan(plan: TaskPlan) -> Dict[str, Any]: } -def parse_task_plan(plan_data: Dict[str, Any]) -> TaskPlan: +def parse_task_plan(plan_data: dict[str, Any]) -> TaskPlan: """Parse dictionary into TaskPlan with validation.""" try: # Handle case where plan_data might be None or empty @@ -1142,7 +1143,7 @@ def parse_task_plan(plan_data: Dict[str, Any]) -> TaskPlan: if parsed_dt: # Ensure datetime is timezone-aware (use UTC if not specified) if parsed_dt.tzinfo is None: - parsed_dt = parsed_dt.replace(tzinfo=timezone.utc) + parsed_dt = parsed_dt.replace(tzinfo=UTC) todo_dates.append(parsed_dt) # Parse in_progress with validation @@ -1151,7 +1152,7 @@ def parse_task_plan(plan_data: Dict[str, Any]) -> TaskPlan: in_progress = parse_datetime(plan_data.get("in_progress")) # Ensure datetime is timezone-aware if in_progress and in_progress.tzinfo is None: - in_progress = in_progress.replace(tzinfo=timezone.utc) + in_progress = in_progress.replace(tzinfo=UTC) # Parse done items with validation done_dates = [] @@ -1161,7 +1162,7 @@ def parse_task_plan(plan_data: Dict[str, Any]) -> TaskPlan: if parsed_dt: # Ensure datetime is timezone-aware if parsed_dt.tzinfo is None: - parsed_dt = parsed_dt.replace(tzinfo=timezone.utc) + parsed_dt = parsed_dt.replace(tzinfo=UTC) done_dates.append(parsed_dt) # Sort dates for better usability @@ -1179,10 +1180,10 @@ def parse_task_plan(plan_data: Dict[str, Any]) -> TaskPlan: return TaskPlan(todo=[], in_progress=None, done=[]) -T = TypeVar("T", bound=Union[ScheduledTask, AdHocTask, PlannedTask]) +T = TypeVar("T", bound=ScheduledTask | AdHocTask | PlannedTask) -def serialize_task(task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> Dict[str, Any]: +def serialize_task(task: ScheduledTask | AdHocTask | PlannedTask) -> dict[str, Any]: """ Standardized serialization for task objects with proper handling of all complex types. """ @@ -1226,15 +1227,15 @@ def serialize_task(task: Union[ScheduledTask, AdHocTask, PlannedTask]) -> Dict[s def serialize_tasks( - tasks: list[Union[ScheduledTask, AdHocTask, PlannedTask]], -) -> list[Dict[str, Any]]: + tasks: list[ScheduledTask | AdHocTask | PlannedTask], +) -> list[dict[str, Any]]: """ Serialize a list of tasks to a list of dictionaries. """ return [serialize_task(task) for task in tasks] -def deserialize_task(task_data: Dict[str, Any], task_class: Optional[Type[T]] = None) -> T: +def deserialize_task(task_data: dict[str, Any], task_class: type[T] | None = None) -> T: """ Deserialize dictionary into appropriate task object with validation. If task_class is provided, uses that type. Otherwise determines type from data. @@ -1245,14 +1246,14 @@ def deserialize_task(task_data: Dict[str, Any], task_class: Optional[Type[T]] = if not task_class: # Determine task class from data if task_type_str == "scheduled": - determined_class = cast(Type[T], ScheduledTask) + determined_class = cast(type[T], ScheduledTask) elif task_type_str == "adhoc": - determined_class = cast(Type[T], AdHocTask) + determined_class = cast(type[T], AdHocTask) # Ensure token is a valid non-empty string if not task_data.get("token"): task_data["token"] = str(random.randint(1000000000000000000, 9999999999999999999)) elif task_type_str == "planned": - determined_class = cast(Type[T], PlannedTask) + determined_class = cast(type[T], PlannedTask) else: raise ValueError(f"Unknown task type: {task_type_str}") else: diff --git a/backend/utils/timed_input.py b/backend/utils/timed_input.py index 38f25011..5cb22055 100644 --- a/backend/utils/timed_input.py +++ b/backend/utils/timed_input.py @@ -6,7 +6,7 @@ def timeout_input(prompt, timeout=10): try: if sys.platform != "win32": - import readline + pass user_input = inputimeout(prompt=prompt, timeout=timeout) return user_input except TimeoutOccurred: diff --git a/backend/utils/tty_session.py b/backend/utils/tty_session.py index 19a85acf..0d8225d4 100644 --- a/backend/utils/tty_session.py +++ b/backend/utils/tty_session.py @@ -6,7 +6,6 @@ _IS_WIN = platform.system() == "Windows" if _IS_WIN: - import msvcrt import winpty # pip install pywinpty # type: ignore @@ -107,7 +106,7 @@ async def read(self, timeout=None): # Return any decoded text the child produced, or None on timeout try: return await asyncio.wait_for(self._buf.get(), timeout) - except asyncio.TimeoutError: + except TimeoutError: return None # backward-compat alias: diff --git a/backend/utils/vector_db.py b/backend/utils/vector_db.py index 24f29201..5985f044 100644 --- a/backend/utils/vector_db.py +++ b/backend/utils/vector_db.py @@ -1,4 +1,5 @@ -from typing import Any, List, Sequence +from collections.abc import Sequence +from typing import Any import faiss from langchain.embeddings import CacheBackedEmbeddings @@ -14,16 +15,16 @@ from backend.core.agent import Agent # faiss needs to be patched for python 3.12 on arm #TODO remove once not needed -from backend.utils import faiss_monkey_patch, guids +from backend.utils import guids class MyFaiss(FAISS): # override aget_by_ids - def get_by_ids(self, ids: Sequence[str], /) -> List[Document]: + def get_by_ids(self, ids: Sequence[str], /) -> list[Document]: # return all self.docstore._dict[id] in ids return [self.docstore._dict[id] for id in (ids if isinstance(ids, list) else [ids]) if id in self.docstore._dict] # type: ignore - async def aget_by_ids(self, ids: Sequence[str], /) -> List[Document]: + async def aget_by_ids(self, ids: Sequence[str], /) -> list[Document]: return self.get_by_ids(ids) def get_all_docs(self) -> dict[str, Document]: @@ -135,7 +136,7 @@ def comparator(data: dict[str, Any]): try: result = simple_eval(condition, names=data) return result - except Exception as e: + except Exception: # PrintStyle.error(f"Error evaluating condition: {e}") return False diff --git a/backend/utils/wait.py b/backend/utils/wait.py index a1b16780..f29d717e 100644 --- a/backend/utils/wait.py +++ b/backend/utils/wait.py @@ -1,5 +1,5 @@ import asyncio -from datetime import datetime, timezone +from datetime import UTC, datetime from backend.utils.print_style import PrintStyle @@ -41,10 +41,10 @@ def format_remaining_time(total_seconds: float) -> str: async def managed_wait(agent, target_time, is_duration_wait, log, get_heading_callback): - while datetime.now(timezone.utc) < target_time: - before_intervention = datetime.now(timezone.utc) + while datetime.now(UTC) < target_time: + before_intervention = datetime.now(UTC) await agent.handle_intervention() - after_intervention = datetime.now(timezone.utc) + after_intervention = datetime.now(UTC) if is_duration_wait: pause_duration = after_intervention - before_intervention @@ -56,7 +56,7 @@ async def managed_wait(agent, target_time, is_duration_wait, log, get_heading_ca f"Wait extended by {pause_duration.total_seconds():.1f}s to {target_time.isoformat()}...", ) - current_time = datetime.now(timezone.utc) + current_time = datetime.now(UTC) if current_time >= target_time: break diff --git a/backend/utils/whisper.py b/backend/utils/whisper.py index 6a7fff04..3f3b7476 100644 --- a/backend/utils/whisper.py +++ b/backend/utils/whisper.py @@ -5,7 +5,7 @@ import whisper -from backend.utils import files, rfc, runtime, settings +from backend.utils import files from backend.utils.notification import NotificationManager, NotificationPriority, NotificationType from backend.utils.print_style import PrintStyle diff --git a/docker/run/fs/exe/supervisor_event_listener.py b/docker/run/fs/exe/supervisor_event_listener.py index 143d589e..8b9a9136 100644 --- a/docker/run/fs/exe/supervisor_event_listener.py +++ b/docker/run/fs/exe/supervisor_event_listener.py @@ -1,11 +1,11 @@ #!/usr/bin/python -import sys -import os import logging +import os import subprocess +import sys import time -from supervisor.childutils import listener # type: ignore +from supervisor.childutils import listener # type: ignore def main(args): diff --git a/initialize.py b/initialize.py index aac54f85..a9a6de61 100644 --- a/initialize.py +++ b/initialize.py @@ -1,7 +1,6 @@ -from backend.core.agent import AgentConfig from backend.core import models -from backend.utils import runtime, settings, defer, extension -from backend.utils.print_style import PrintStyle +from backend.core.agent import AgentConfig +from backend.utils import defer, extension, runtime, settings @extension.extensible @@ -151,8 +150,8 @@ def initialize_job_loop(): @extension.extensible def initialize_preload(): - import sys import os + import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "scripts")) import preload @@ -162,7 +161,7 @@ def initialize_preload(): @extension.extensible def initialize_migration(): - from backend.utils import migration, dotenv + from backend.utils import dotenv, migration # run migration migration.startup_migration() diff --git a/plugins/chat_branching/api/branch_chat.py b/plugins/chat_branching/api/branch_chat.py index 78a91f2b..5211db2c 100644 --- a/plugins/chat_branching/api/branch_chat.py +++ b/plugins/chat_branching/api/branch_chat.py @@ -1,12 +1,12 @@ from datetime import datetime +from backend.core.agent import AgentContext from backend.utils.api import ApiHandler, Input, Output, Request, Response from backend.utils.persist_chat import ( - _serialize_context, _deserialize_context, + _serialize_context, save_tmp_chat, ) -from backend.core.agent import AgentContext class BranchChat(ApiHandler): @@ -72,4 +72,4 @@ async def process(self, input: Input, request: Request) -> Output: "ok": True, "ctxid": new_context.id, "message": "Chat branched successfully.", - } \ No newline at end of file + } diff --git a/plugins/error_retry/extensions/python/agent_Agent_handle_exception_end/_80_retry_critical_exception.py b/plugins/error_retry/extensions/python/agent_Agent_handle_exception_end/_80_retry_critical_exception.py index 69371b3a..fadb9504 100644 --- a/plugins/error_retry/extensions/python/agent_Agent_handle_exception_end/_80_retry_critical_exception.py +++ b/plugins/error_retry/extensions/python/agent_Agent_handle_exception_end/_80_retry_critical_exception.py @@ -1,13 +1,14 @@ import asyncio -from datetime import datetime, timezone -from backend.utils.extension import Extension -from backend.core.agent import LoopData -from backend.utils.localization import Localization -from backend.utils.errors import RepairableException, HandledException + +from plugins.error_retry.extensions.backend.agent_Agent_monologue_start._10_reset_critical_exception_counter import ( + DATA_NAME_COUNTER, +) + from backend.utils import errors +from backend.utils.errors import HandledException, RepairableException +from backend.utils.extension import Extension from backend.utils.print_style import PrintStyle -from plugins.error_retry.extensions.backend.agent_Agent_monologue_start._10_reset_critical_exception_counter import DATA_NAME_COUNTER class RetryCriticalException(Extension): async def execute(self, data: dict = {}, **kwargs): @@ -49,4 +50,4 @@ async def execute(self, data: dict = {}, **kwargs): data["exception"] = None - + diff --git a/plugins/error_retry/extensions/python/agent_Agent_monologue_start/_10_reset_critical_exception_counter.py b/plugins/error_retry/extensions/python/agent_Agent_monologue_start/_10_reset_critical_exception_counter.py index 89897709..ef632e50 100644 --- a/plugins/error_retry/extensions/python/agent_Agent_monologue_start/_10_reset_critical_exception_counter.py +++ b/plugins/error_retry/extensions/python/agent_Agent_monologue_start/_10_reset_critical_exception_counter.py @@ -1,10 +1,4 @@ -from datetime import datetime, timezone from backend.utils.extension import Extension -from backend.core.agent import LoopData -from backend.utils.localization import Localization -from backend.utils.errors import RepairableException -from backend.utils import errors -from backend.utils.print_style import PrintStyle DATA_NAME_COUNTER = "_plugin.error_retry.critical_exception_counter" @@ -12,7 +6,6 @@ class ResetCriticalExceptionCounter(Extension): async def execute(self, exception_data: dict = {}, **kwargs): if not self.agent: return - + self.agent.set_data(DATA_NAME_COUNTER, 0) - \ No newline at end of file diff --git a/plugins/plugin_scan/api/plugin_scan_queue.py b/plugins/plugin_scan/api/plugin_scan_queue.py index 11c57e23..53a6e86d 100644 --- a/plugins/plugin_scan/api/plugin_scan_queue.py +++ b/plugins/plugin_scan/api/plugin_scan_queue.py @@ -1,6 +1,6 @@ from backend.core.agent import AgentContext -from backend.utils.api import ApiHandler, Input, Output, Request, Response from backend.utils import message_queue as mq +from backend.utils.api import ApiHandler, Input, Output, Request, Response class PluginScanQueue(ApiHandler): diff --git a/plugins/text_editor/extensions/python/system_prompt/_15_text_editor_prompt.py b/plugins/text_editor/extensions/python/system_prompt/_15_text_editor_prompt.py index ae3c11b6..e83eb09b 100644 --- a/plugins/text_editor/extensions/python/system_prompt/_15_text_editor_prompt.py +++ b/plugins/text_editor/extensions/python/system_prompt/_15_text_editor_prompt.py @@ -1,6 +1,6 @@ -from backend.utils.extension import Extension +from backend.core.agent import LoopData from backend.utils import plugins -from backend.core.agent import Agent, LoopData +from backend.utils.extension import Extension class TextEditorPrompt(Extension): diff --git a/plugins/text_editor/helpers/file_ops.py b/plugins/text_editor/helpers/file_ops.py index 55cb37d4..9b8b1c22 100644 --- a/plugins/text_editor/helpers/file_ops.py +++ b/plugins/text_editor/helpers/file_ops.py @@ -102,7 +102,7 @@ def read_file( ) try: - with open(path, "r", encoding="utf-8", errors="replace") as f: + with open(path, encoding="utf-8", errors="replace") as f: all_lines = f.readlines() except OSError as exc: return ReadResult( @@ -278,7 +278,7 @@ def apply_patch(path: str, edits: list[dict]) -> int: fd, tmp_path = tempfile.mkstemp(dir=dir_name, suffix=".tmp") try: with ( - open(path, "r", encoding="utf-8", errors="replace") as src, + open(path, encoding="utf-8", errors="replace") as src, os.fdopen(fd, "w", encoding="utf-8") as dst, ): edit_idx = 0 diff --git a/plugins/text_editor/tools/text_editor.py b/plugins/text_editor/tools/text_editor.py index 2e2c1a49..f446b565 100644 --- a/plugins/text_editor/tools/text_editor.py +++ b/plugins/text_editor/tools/text_editor.py @@ -1,13 +1,13 @@ -from backend.utils.tool import Tool, Response -from backend.utils.extension import call_extensions from backend.utils import plugins, runtime +from backend.utils.extension import call_extensions +from backend.utils.tool import Response, Tool from plugins.text_editor.helpers.file_ops import ( FileInfo, - read_file, - write_file, - validate_edits, apply_patch, file_info, + read_file, + validate_edits, + write_file, ) # Key used in agent.data to store file state for patch validation diff --git a/prompts/agent.system.main.tips.py b/prompts/agent.system.main.tips.py index 11119bcd..086ff292 100644 --- a/prompts/agent.system.main.tips.py +++ b/prompts/agent.system.main.tips.py @@ -1,10 +1,9 @@ -from backend.utils.files import VariablesPlugin -from backend.utils import settings -from backend.utils import projects -from backend.utils import runtime -from backend.utils import files from typing import Any +from backend.utils import settings +from backend.utils.files import VariablesPlugin + + class WorkdirPath(VariablesPlugin): def get_variables( self, file: str, backup_dirs: list[str] | None = None, **kwargs @@ -21,4 +20,4 @@ def get_variables( set = settings.get_settings() return {"workdir_path": set["workdir_path"]} - + diff --git a/prompts/agent.system.tool.call_sub.py b/prompts/agent.system.tool.call_sub.py index a41b2f6c..3744e9f6 100644 --- a/prompts/agent.system.tool.call_sub.py +++ b/prompts/agent.system.tool.call_sub.py @@ -1,8 +1,7 @@ -import json -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any + +from backend.utils import projects, subagents from backend.utils.files import VariablesPlugin -from backend.utils import files, projects, subagents -from backend.utils.print_style import PrintStyle if TYPE_CHECKING: from backend.core.agent import Agent @@ -31,4 +30,4 @@ def get_variables( return {"agent_profiles": profiles} else: return {"agent_profiles": None} - + diff --git a/prompts/agent.system.tools.py b/prompts/agent.system.tools.py index 81c45a3d..b426b9d9 100644 --- a/prompts/agent.system.tools.py +++ b/prompts/agent.system.tools.py @@ -1,7 +1,8 @@ import os from typing import Any -from backend.utils.files import VariablesPlugin + from backend.utils import files +from backend.utils.files import VariablesPlugin from backend.utils.print_style import PrintStyle @@ -17,7 +18,7 @@ def get_variables(self, file: str, backup_dirs: list[str] | None = None, **kwarg # collect all tool instruction files prompt_files = files.get_unique_filenames_in_dirs(folders, "agent.system.tool.*.md") - + # load tool instructions tools = [] for prompt_file in prompt_files: diff --git a/pyproject.toml b/pyproject.toml index 0639b1e3..33b3fe56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -172,3 +172,16 @@ markers = [ "integration: marks tests as integration tests", "unit: marks tests as unit tests", ] + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "N", "B", "C4", "UP"] +ignore = ["E501", "B006", "B008", "E701", "E402", "C408", "B904", "B905", "B007", "UP047", "UP042", "UP007", "UP045", "E722", "B024", "N802", "C416", "W291", "C414", "B015", "UP031", "N818", "F811", "N812", "B023"] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["E402"] +"test_litellm2.py" = ["E402"] +"test_litellm.py" = ["E402"] diff --git a/run_ui.py b/run_ui.py index 9e8160b9..1c89ccef 100644 --- a/run_ui.py +++ b/run_ui.py @@ -1,36 +1,38 @@ -from datetime import timedelta +import asyncio + +# disable logging +import logging import os import secrets -import time import threading -import asyncio - +import time import urllib.request +from datetime import timedelta + +import socketio # type: ignore[import-untyped] import uvicorn -from flask import Flask, request, Response, session, redirect, url_for, render_template_string +from flask import Flask, Response, redirect, render_template_string, request, session, url_for +from socketio import ASGIApp, packet +from starlette.applications import Starlette +from starlette.routing import Mount +from uvicorn.middleware.wsgi import WSGIMiddleware from werkzeug.wrappers.request import Request as WerkzeugRequest import initialize -from backend.utils import files, settings as settings_helper, extension from backend.infrastructure.system import git, process -from backend.interfaces.mcp import server as mcp_server from backend.interfaces.a2a import server as fasta2a_server -from backend.utils.files import get_abs_path -from backend.utils import runtime, dotenv +from backend.interfaces.mcp import server as mcp_server from backend.interfaces.websockets.websocket import WebSocketHandler, validate_ws_origin -from backend.utils.api import register_api_route, requires_auth, csrf_protect -from backend.utils.print_style import PrintStyle -from backend.utils import login -import socketio # type: ignore[import-untyped] -from socketio import ASGIApp, packet -from starlette.applications import Starlette -from starlette.routing import Mount -from uvicorn.middleware.wsgi import WSGIMiddleware from backend.interfaces.websockets.websocket_manager import WebSocketManager -from backend.interfaces.websockets.websocket_namespace_discovery import discover_websocket_namespaces +from backend.interfaces.websockets.websocket_namespace_discovery import ( + discover_websocket_namespaces, +) +from backend.utils import dotenv, extension, files, login, runtime +from backend.utils import settings as settings_helper +from backend.utils.api import register_api_route, requires_auth +from backend.utils.files import get_abs_path +from backend.utils.print_style import PrintStyle -# disable logging -import logging logging.getLogger().setLevel(logging.WARNING) @@ -154,28 +156,29 @@ async def _serve_plugin_asset(plugin_name, asset_path): Serve static assets from plugin directories. Resolves using the plugin system (with overrides). """ - from backend.utils import plugins from flask import send_file - + + from backend.utils import plugins + # Use the new find_plugin helper plugin_dir = plugins.find_plugin_dir(plugin_name) if not plugin_dir: return Response("Plugin not found", 404) - + # Resolve the plugin asset path with security checks try: # Construct path using plugin root asset_file = files.get_abs_path(plugin_dir, asset_path) webui_dir = files.get_abs_path(plugin_dir, "webui") webui_extensions_dir = files.get_abs_path(plugin_dir, "extensions/webui") - + # Security: ensure the resolved path is within the plugin webui directory if not files.is_in_dir(str(asset_file), str(webui_dir)) and not files.is_in_dir(str(asset_file), str(webui_extensions_dir)): return Response("Access denied", 403) - + if not files.is_file(asset_file): return Response("Asset not found", 404) - + return send_file(str(asset_file)) except Exception as e: PrintStyle.error(f"Error serving plugin asset: {e}") diff --git a/scripts/maintenance_tool.py b/scripts/maintenance_tool.py index e3a36234..3a233088 100644 --- a/scripts/maintenance_tool.py +++ b/scripts/maintenance_tool.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 import os -import time -import sys import re +import sys +import time + def get_disk_usage(path='/'): try: @@ -17,13 +18,13 @@ def get_disk_usage(path='/'): def clean_disk_space(threshold=90, max_age_days=7, dry_run=False): usage = get_disk_usage() print(f"Current disk usage: {usage}% (Threshold: {threshold}%, Max Age: {max_age_days} days)") - + if usage < threshold: print("Disk usage is within limits. No cleaning needed.") return print(f"Disk usage {usage}% exceeds threshold {threshold}%. Starting cleanup...") - + # Base directories relative to current file location base_dir = os.path.dirname(os.path.abspath(__file__)) targets = [ @@ -32,7 +33,7 @@ def clean_disk_space(threshold=90, max_age_days=7, dry_run=False): os.path.join(base_dir, "usr", "temp"), os.path.join(base_dir, ".cache"), # Added .cache ] - + now = time.time() seconds_in_day = 86400 total_deleted = 0 @@ -41,23 +42,23 @@ def clean_disk_space(threshold=90, max_age_days=7, dry_run=False): for target_dir in targets: if not os.path.exists(target_dir): continue - + print(f"Cleaning directory: {target_dir}") for root, dirs, fnames in os.walk(target_dir): for name in fnames: if name == ".gitkeep": continue - + file_path = os.path.join(root, name) try: mtime = os.path.getmtime(file_path) age_days = (now - mtime) / seconds_in_day - + if age_days > max_age_days: size = os.path.getsize(file_path) total_size += size total_deleted += 1 - + if dry_run: print(f"[Dry Run] Would delete: {file_path} ({size} bytes, {age_days:.1f} days old)") else: @@ -74,13 +75,13 @@ def clean_disk_space(threshold=90, max_age_days=7, dry_run=False): def detect_language(text): if not text or not isinstance(text, str): return "unknown" - + sample = text[:1000] counts = { - 'latin': 0, 'cyrillic': 0, 'arabic': 0, 'hebrew': 0, + 'latin': 0, 'cyrillic': 0, 'arabic': 0, 'hebrew': 0, 'cjk': 0, 'greek': 0, 'other': 0 } - + for char in sample: cp = ord(char) if 0x0041 <= cp <= 0x005A or 0x0061 <= cp <= 0x007A: @@ -97,17 +98,17 @@ def detect_language(text): counts['greek'] += 1 elif not char.isspace() and not char.isdigit() and char not in '.,!?;:"\'()[]{}': counts['other'] += 1 - + total = sum(counts.values()) if total == 0: return "unknown" - + if counts['latin'] > total * 0.5: scores = {'en': 0, 'es': 0, 'fr': 0} - + en_words = {'the', 'is', 'and', 'of', 'to', 'in', 'that', 'it', 'with', 'for', 'was', 'are', 'have'} es_words = {'el', 'la', 'de', 'que', 'y', 'en', 'un', 'con', 'para', 'por', 'una', 'los', 'las', 'es'} fr_words = {'le', 'la', 'de', 'et', 'que', 'un', 'dans', 'est', 'une', 'pour', 'par', 'sur', 'avec'} - + text_lower = text.lower() for w in en_words: if re.search(r'\b' + re.escape(w) + r'\b', text_lower): scores['en'] += 1 @@ -115,20 +116,20 @@ def detect_language(text): if re.search(r'\b' + re.escape(w) + r'\b', text_lower): scores['es'] += 1 for w in fr_words: if re.search(r'\b' + re.escape(w) + r'\b', text_lower): scores['fr'] += 1 - + max_score = max(scores.values()) if max_score >= 2: # If scores are equal, prefer the one with most unique matches # Here we just pick the first one which is fine for a simple tool return [lang for lang, score in scores.items() if score == max_score][0] - + return "latin-family" if counts['cyrillic'] > total * 0.3: return "ru/cyrillic" if counts['cjk'] > total * 0.1: return "cjk" if counts['arabic'] > total * 0.3: return "ar" if counts['hebrew'] > total * 0.3: return "he" - + return "unknown" if __name__ == "__main__": diff --git a/scripts/preload.py b/scripts/preload.py index 51662f2c..324a71ef 100644 --- a/scripts/preload.py +++ b/scripts/preload.py @@ -1,8 +1,8 @@ import asyncio -from backend.utils import runtime, whisper, settings -from backend.utils.print_style import PrintStyle -from backend.utils import kokoro_tts + from backend.core import models +from backend.utils import kokoro_tts, runtime, settings, whisper +from backend.utils.print_style import PrintStyle async def preload(): diff --git a/scripts/prepare.py b/scripts/prepare.py index 698d4374..753e377f 100644 --- a/scripts/prepare.py +++ b/scripts/prepare.py @@ -1,8 +1,8 @@ -from backend.utils import dotenv, runtime, settings -import string import random -from backend.utils.print_style import PrintStyle +import string +from backend.utils import dotenv, runtime, settings +from backend.utils.print_style import PrintStyle PrintStyle.standard("Preparing environment...") diff --git a/scripts/run_tunnel.py b/scripts/run_tunnel.py index 50c11125..087c2ea8 100644 --- a/scripts/run_tunnel.py +++ b/scripts/run_tunnel.py @@ -1,11 +1,12 @@ import threading + +from backend.api.tunnel import Tunnel from flask import Flask, request + from backend.infrastructure.system import process -from backend.utils import runtime, dotenv +from backend.utils import dotenv, runtime from backend.utils.print_style import PrintStyle -from backend.api.tunnel import Tunnel - # initialize the internal Flask server app = Flask("app") app.config["JSON_SORT_KEYS"] = False # Disable key sorting in jsonify @@ -13,8 +14,7 @@ def run(): # Suppress only request logs but keep the startup messages - from werkzeug.serving import WSGIRequestHandler - from werkzeug.serving import make_server + from werkzeug.serving import WSGIRequestHandler, make_server PrintStyle().print("Starting tunnel server...") @@ -44,7 +44,7 @@ async def handle_request(): request_handler=NoRequestLoggingWSGIRequestHandler, threaded=True, ) - + process.set_server(server) # server.log_startup() server.serve_forever() diff --git a/scripts/update_reqs.py b/scripts/update_reqs.py index a334cc1c..62b12a63 100644 --- a/scripts/update_reqs.py +++ b/scripts/update_reqs.py @@ -1,6 +1,8 @@ -import pkg_resources import re +import pkg_resources + + def get_installed_version(package_name): try: return pkg_resources.get_distribution(package_name).version @@ -8,7 +10,7 @@ def get_installed_version(package_name): return None def update_requirements(): - with open('requirements.txt', 'r') as f: + with open('requirements.txt') as f: requirements = f.readlines() updated_requirements = [] @@ -17,7 +19,7 @@ def update_requirements(): if not req or req.startswith('#'): updated_requirements.append(req) continue - + # Extract package name match = re.match(r'^([^=<>]+)==', req) if match: diff --git a/test_litellm.py b/test_litellm.py index 69dd022d..101e2f9b 100644 --- a/test_litellm.py +++ b/test_litellm.py @@ -1,7 +1,7 @@ import asyncio -from backend.core.models import get_utility_model, ModelConfig, ModelType -from litellm import acompletion -import logging + +from backend.core.models import ModelConfig, ModelType, get_utility_model + async def main(): try: @@ -12,7 +12,7 @@ async def main(): ) model = get_utility_model("openrouter", "openai/gpt-3.5-turbo", config) print("Model created") - + response, reasoning = await model.unified_call( system_message="Hello", user_message="Are you there?" diff --git a/test_litellm2.py b/test_litellm2.py index dc9eb8b6..41d186eb 100644 --- a/test_litellm2.py +++ b/test_litellm2.py @@ -1,6 +1,9 @@ import sys + # Mock sentence_transformers to avoid import failure import sys.modules + + class MockModule: pass sys.modules['sentence_transformers'] = MockModule() @@ -8,13 +11,15 @@ class MockModule: import asyncio import os + from dotenv import load_dotenv load_dotenv() print("Env OPENROUTER_API_KEY:", os.environ.get("OPENROUTER_API_KEY", "None")[:5] + "...") print("Env API_KEY_OPENROUTER:", os.environ.get("API_KEY_OPENROUTER", "None")[:5] + "...") -from backend.core.models import get_utility_model, ModelConfig, ModelType +from backend.core.models import ModelConfig, ModelType, get_utility_model + async def main(): try: @@ -25,13 +30,13 @@ async def main(): ) model = get_utility_model("openrouter", "deepseek/deepseek-chat", config) print("Model created. Kwargs:", model.kwargs) - + response, reasoning = await model.unified_call( system_message="Hello", user_message="Are you there?" ) print("Success:", response) - except Exception as e: + except Exception: import traceback traceback.print_exc() diff --git a/tests/chunk_parser_test.py b/tests/chunk_parser_test.py index 8ecdd73b..7179b97c 100644 --- a/tests/chunk_parser_test.py +++ b/tests/chunk_parser_test.py @@ -1,4 +1,5 @@ -import sys, os +import os +import sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from backend.core import models diff --git a/tests/email_parser_test.py b/tests/email_parser_test.py index 6934214b..c4d91706 100644 --- a/tests/email_parser_test.py +++ b/tests/email_parser_test.py @@ -1,11 +1,14 @@ -import sys, os +import os +import sys sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import asyncio + import pytest -from backend.utils.email_client import read_messages + from backend.utils.dotenv import get_dotenv_value, load_dotenv +from backend.utils.email_client import read_messages @pytest.mark.skip(reason="This test is disabled as it has eternal dependencies and tests nothing automatically, please move it to a script or a manual test") diff --git a/tests/rate_limiter_test.py b/tests/rate_limiter_test.py index aa1c250b..fd62e455 100644 --- a/tests/rate_limiter_test.py +++ b/tests/rate_limiter_test.py @@ -1,5 +1,7 @@ -import sys, os +import os +import sys + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from backend.core import models @@ -28,4 +30,4 @@ async def run(): # import asyncio -# asyncio.run(run()) \ No newline at end of file +# asyncio.run(run()) diff --git a/tests/test_fasta2a_client.py b/tests/test_fasta2a_client.py index 583fc1f9..e2c6866c 100644 --- a/tests/test_fasta2a_client.py +++ b/tests/test_fasta2a_client.py @@ -3,12 +3,16 @@ Test script to verify FastA2A agent card routing and authentication. """ -import sys, os +import os +import sys + sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import asyncio + import pytest + from backend.utils import settings diff --git a/tests/test_file_tree_visualize.py b/tests/test_file_tree_visualize.py index c7ee1af4..fc1aa863 100644 --- a/tests/test_file_tree_visualize.py +++ b/tests/test_file_tree_visualize.py @@ -2,13 +2,13 @@ import argparse import os -from collections.abc import Iterable +import sys +import time +from collections.abc import Callable, Iterable from contextlib import contextmanager from dataclasses import dataclass, field from pathlib import Path -import sys -import time -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Optional try: import pytest # type: ignore @@ -36,14 +36,13 @@ ) from backend.utils.files import create_dir, delete_dir, get_abs_path, write_file - BASE_TEMP_ROOT = "tmp/tests/file_tree/visualize" @dataclass(slots=True) class Config: label: str - params: Dict[str, Any] + params: dict[str, Any] SetupHook = Optional[Callable[[str], None]] @@ -53,13 +52,13 @@ class Config: class Scenario: name: str description: str - structure: Dict[str, Any] - configs: List[Config] = field(default_factory=list) - ignore_content: Optional[str] = None + structure: dict[str, Any] + configs: list[Config] = field(default_factory=list) + ignore_content: str | None = None setup: SetupHook = None -def materialize_structure(base_rel: str, structure: Dict[str, Any]) -> None: +def materialize_structure(base_rel: str, structure: dict[str, Any]) -> None: for entry, value in structure.items(): rel = os.path.join(base_rel, entry) if isinstance(value, dict): @@ -79,7 +78,7 @@ def print_header(title: str, char: str = "=") -> None: print(char * 80) -def print_flat(items: List[Dict[str, Any]]) -> None: +def print_flat(items: list[dict[str, Any]]) -> None: print("level type name text") print("-" * 80) for item in items: @@ -90,10 +89,10 @@ def print_flat(items: List[Dict[str, Any]]) -> None: print(f"{level:<5} {item_type:<7} {name:<20} {text}") -def print_nested(items: List[Dict[str, Any]], root_label: str) -> None: +def print_nested(items: list[dict[str, Any]], root_label: str) -> None: print(root_label) - def recurse(nodes: List[Dict[str, Any]], prefix: str) -> None: + def recurse(nodes: list[dict[str, Any]], prefix: str) -> None: total = len(nodes) for index, node in enumerate(nodes): is_last = index == total - 1 @@ -125,20 +124,20 @@ def _set_entry_times(relative_path: str, timestamp: float) -> None: time.sleep(0.01) -def _apply_timestamps(base_rel: str, paths: List[str], base_ts: Optional[float] = None) -> None: +def _apply_timestamps(base_rel: str, paths: list[str], base_ts: float | None = None) -> None: if base_ts is None: base_ts = time.time() for offset, rel in enumerate(paths, start=1): _set_entry_times(os.path.join(base_rel, rel), base_ts + offset) -def list_scenarios(scenarios: List[Scenario]) -> None: +def list_scenarios(scenarios: list[Scenario]) -> None: print("Available scenarios:") for scenario in scenarios: print(f" - {scenario.name}: {scenario.description}") -def run_scenarios(selected: List[Scenario]) -> None: +def run_scenarios(selected: list[Scenario]) -> None: create_dir(BASE_TEMP_ROOT) for scenario in selected: print_header(f"Scenario: {scenario.name} — {scenario.description}") @@ -189,8 +188,8 @@ def run_scenarios(selected: List[Scenario]) -> None: print() -def build_scenarios() -> List[Scenario]: - scenarios: List[Scenario] = [] +def build_scenarios() -> list[Scenario]: + scenarios: list[Scenario] = [] scenarios.append( Scenario( diff --git a/tests/test_http_auth_csrf.py b/tests/test_http_auth_csrf.py index 2e9756ce..673c6eaf 100644 --- a/tests/test_http_auth_csrf.py +++ b/tests/test_http_auth_csrf.py @@ -2,8 +2,6 @@ from flask import Flask, Response -import pytest - from backend.utils import runtime diff --git a/tests/test_settings_developer_sections.py b/tests/test_settings_developer_sections.py index ced73c8a..34d7489e 100644 --- a/tests/test_settings_developer_sections.py +++ b/tests/test_settings_developer_sections.py @@ -1,7 +1,6 @@ import sys from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) diff --git a/tests/test_snapshot_parity.py b/tests/test_snapshot_parity.py index f94ac3c1..45095fb2 100644 --- a/tests/test_snapshot_parity.py +++ b/tests/test_snapshot_parity.py @@ -10,8 +10,8 @@ sys.path.insert(0, str(PROJECT_ROOT)) from backend.core.agent import AgentContext -from initialize import initialize_agent from backend.interfaces.api.routes.chat.poll import Poll +from initialize import initialize_agent @pytest.mark.asyncio diff --git a/tests/test_snapshot_schema_v1.py b/tests/test_snapshot_schema_v1.py index 9dbce402..e45d9b82 100644 --- a/tests/test_snapshot_schema_v1.py +++ b/tests/test_snapshot_schema_v1.py @@ -11,7 +11,6 @@ from backend.interfaces.api.routes.chat.poll import Poll - EXPECTED_SNAPSHOT_KEYS = { "deselect_chat", "context", diff --git a/tests/test_socketio_library_semantics.py b/tests/test_socketio_library_semantics.py index f5f5c8dc..f836f7a7 100644 --- a/tests/test_socketio_library_semantics.py +++ b/tests/test_socketio_library_semantics.py @@ -1,7 +1,8 @@ import asyncio import contextlib import socket -from typing import Any, AsyncIterator +from collections.abc import AsyncIterator +from typing import Any import pytest diff --git a/tests/test_socketio_unknown_namespace.py b/tests/test_socketio_unknown_namespace.py index 74840e98..d15a22da 100644 --- a/tests/test_socketio_unknown_namespace.py +++ b/tests/test_socketio_unknown_namespace.py @@ -1,7 +1,8 @@ import asyncio import contextlib import socket -from typing import Any, AsyncIterator +from collections.abc import AsyncIterator +from typing import Any import pytest diff --git a/tests/test_state_sync_handler.py b/tests/test_state_sync_handler.py index 30f1e01a..c249dc88 100644 --- a/tests/test_state_sync_handler.py +++ b/tests/test_state_sync_handler.py @@ -1,10 +1,10 @@ +import asyncio import sys import threading +import time from pathlib import Path import pytest -import asyncio -import time PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: diff --git a/tests/test_state_sync_welcome_screen.py b/tests/test_state_sync_welcome_screen.py index ab80707c..b6d96e4d 100644 --- a/tests/test_state_sync_welcome_screen.py +++ b/tests/test_state_sync_welcome_screen.py @@ -32,9 +32,9 @@ async def test_state_sync_handshake_and_initial_snapshot_work_with_no_selected_c is null. We must still handshake and receive an initial `state_push` quickly (no hang). """ - from backend.utils.state_snapshot import validate_snapshot_schema_v1 - from backend.utils.state_monitor import _reset_state_monitor_for_testing from backend.interfaces.websockets.state_sync_handler import StateSyncHandler + from backend.utils.state_monitor import _reset_state_monitor_for_testing + from backend.utils.state_snapshot import validate_snapshot_schema_v1 socketio = FakeSocketIOServer() manager = WebSocketManager(socketio, threading.RLock()) diff --git a/tests/test_websocket_client_api_surface.py b/tests/test_websocket_client_api_surface.py index 2d683bec..3f3be2a4 100644 --- a/tests/test_websocket_client_api_surface.py +++ b/tests/test_websocket_client_api_surface.py @@ -2,7 +2,6 @@ import sys from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parents[1] if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) diff --git a/tests/test_websocket_handlers.py b/tests/test_websocket_handlers.py index 5dae497a..7034d445 100644 --- a/tests/test_websocket_handlers.py +++ b/tests/test_websocket_handlers.py @@ -9,9 +9,9 @@ sys.path.insert(0, str(PROJECT_ROOT)) from backend.interfaces.websockets.websocket import ( + SingletonInstantiationError, WebSocketHandler, WebSocketResult, - SingletonInstantiationError, ) @@ -143,8 +143,8 @@ def test_get_instance_returns_singleton(): @pytest.mark.asyncio async def test_state_sync_handler_registers_and_routes_state_request(): - from backend.interfaces.websockets.websocket_manager import WebSocketManager from backend.interfaces.websockets.state_sync_handler import StateSyncHandler + from backend.interfaces.websockets.websocket_manager import WebSocketManager from backend.utils.state_monitor import _reset_state_monitor_for_testing _reset_state_monitor_for_testing() diff --git a/tests/test_websocket_harness.py b/tests/test_websocket_harness.py index 31418533..afb369b6 100644 --- a/tests/test_websocket_harness.py +++ b/tests/test_websocket_harness.py @@ -9,10 +9,10 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from backend.interfaces.websockets.websocket_manager import WebSocketManager from backend.interfaces.websockets.dev_websocket_test_handler import ( DevWebsocketTestHandler, ) +from backend.interfaces.websockets.websocket_manager import WebSocketManager NAMESPACE = "/dev_websocket_test" diff --git a/tests/test_websocket_manager.py b/tests/test_websocket_manager.py index f680f378..e30f3101 100644 --- a/tests/test_websocket_manager.py +++ b/tests/test_websocket_manager.py @@ -12,17 +12,19 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) +from datetime import UTC + from backend.interfaces.websockets.websocket import ( ConnectionNotFoundError, WebSocketHandler, WebSocketResult, ) from backend.interfaces.websockets.websocket_manager import ( - WebSocketManager, BUFFER_TTL, DIAGNOSTIC_EVENT, LIFECYCLE_CONNECT_EVENT, LIFECYCLE_DISCONNECT_EVENT, + WebSocketManager, ) NAMESPACE = "/test" @@ -380,9 +382,9 @@ async def test_expired_buffer_entries_are_discarded(monkeypatch): await manager.handle_connect(NAMESPACE, "sid-expired") await manager.handle_disconnect(NAMESPACE, "sid-expired") - from datetime import timedelta, timezone, datetime + from datetime import datetime, timedelta - past = datetime.now(timezone.utc) - (BUFFER_TTL + timedelta(seconds=5)) + past = datetime.now(UTC) - (BUFFER_TTL + timedelta(seconds=5)) future = past + BUFFER_TTL + timedelta(seconds=10) await manager.emit_to(NAMESPACE, "sid-expired", "event", {"a": 1}) diff --git a/tests/test_websocket_namespace_discovery.py b/tests/test_websocket_namespace_discovery.py index 282b784d..a69fdfd0 100644 --- a/tests/test_websocket_namespace_discovery.py +++ b/tests/test_websocket_namespace_discovery.py @@ -2,8 +2,9 @@ import contextlib import socket import sys +from collections.abc import AsyncIterator from pathlib import Path -from typing import Any, AsyncIterator +from typing import Any import pytest @@ -126,8 +127,8 @@ def test_discovery_folder_suffix_handler_stripped(tmp_path: Path) -> None: def test_discovery_empty_folder_warns_and_treats_namespace_unregistered( tmp_path: Path, monkeypatch ) -> None: - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket_manager import WebSocketManager from backend.interfaces.websockets.websocket_namespace_discovery import ( diff --git a/tests/test_websocket_namespace_security.py b/tests/test_websocket_namespace_security.py index a80038e8..8592dab1 100644 --- a/tests/test_websocket_namespace_security.py +++ b/tests/test_websocket_namespace_security.py @@ -3,8 +3,9 @@ import socket import sys import threading +from collections.abc import AsyncIterator from pathlib import Path -from typing import Any, AsyncIterator +from typing import Any import pytest @@ -60,8 +61,8 @@ def _make_session_cookie(app: Any, data: dict[str, Any]) -> str: async def test_connect_security_is_computed_per_namespace_and_enforced( monkeypatch, ) -> None: - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager @@ -194,8 +195,8 @@ async def process_event( async def test_unknown_namespace_rejected_with_deterministic_connect_error_payload() -> ( None ): - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager @@ -269,8 +270,8 @@ async def _on_connect_error(data: Any) -> None: async def test_secure_namespace_rejects_missing_auth_even_with_valid_csrf( monkeypatch, ) -> None: - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager @@ -341,8 +342,8 @@ async def process_event( @pytest.mark.asyncio async def test_secure_namespace_rejects_invalid_csrf_cookie(monkeypatch) -> None: - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager @@ -414,8 +415,8 @@ async def process_event( @pytest.mark.asyncio async def test_csrf_required_without_auth_is_enforced(monkeypatch) -> None: - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager diff --git a/tests/test_websocket_namespaces.py b/tests/test_websocket_namespaces.py index 0f13c128..05da1544 100644 --- a/tests/test_websocket_namespaces.py +++ b/tests/test_websocket_namespaces.py @@ -3,8 +3,9 @@ import socket import sys import threading +from collections.abc import AsyncIterator from pathlib import Path -from typing import Any, AsyncIterator +from typing import Any from unittest.mock import AsyncMock import pytest @@ -13,8 +14,8 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) -from backend.utils.state_monitor import StateMonitor from backend.interfaces.websockets.websocket_manager import WebSocketManager +from backend.utils.state_monitor import StateMonitor class FakeSocketIOServer: @@ -109,8 +110,8 @@ async def test_namespace_isolation_state_sync_vs_dev_websocket_test() -> None: Acceptance proof for `/state_sync` vs `/dev_websocket_test` namespaces. """ - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager diff --git a/tests/test_websocket_namespaces_integration.py b/tests/test_websocket_namespaces_integration.py index 9586a7a6..6cfdac33 100644 --- a/tests/test_websocket_namespaces_integration.py +++ b/tests/test_websocket_namespaces_integration.py @@ -1,7 +1,8 @@ import asyncio import contextlib import socket -from typing import Any, AsyncIterator +from collections.abc import AsyncIterator +from typing import Any import pytest @@ -50,8 +51,8 @@ async def test_unregistered_namespace_connection_fails_with_unknown_namespace_co connect_error payload (UNKNOWN_NAMESPACE), independent of python-socketio defaults. """ - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager diff --git a/tests/test_websocket_root_namespace.py b/tests/test_websocket_root_namespace.py index b668272b..0729beaf 100644 --- a/tests/test_websocket_root_namespace.py +++ b/tests/test_websocket_root_namespace.py @@ -1,7 +1,8 @@ import asyncio import contextlib import socket -from typing import Any, AsyncIterator +from collections.abc import AsyncIterator +from typing import Any import pytest @@ -48,8 +49,8 @@ async def test_root_namespace_request_style_calls_resolve_with_no_handlers() -> events by default, but request-style calls must not hang (NO_HANDLERS). """ - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager @@ -129,8 +130,8 @@ async def test_root_namespace_fire_and_forget_does_not_invoke_application_handle Fire-and-forget emits on `/` must not invoke any application handler. """ - from flask import Flask import socketio + from flask import Flask from backend.interfaces.websockets.websocket import WebSocketHandler from backend.interfaces.websockets.websocket_manager import WebSocketManager diff --git a/tests/test_webui_extension_surfaces.py b/tests/test_webui_extension_surfaces.py index 49095e17..32385b70 100644 --- a/tests/test_webui_extension_surfaces.py +++ b/tests/test_webui_extension_surfaces.py @@ -3,9 +3,9 @@ import sys import tempfile import threading +from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path -from typing import Iterator import pytest from flask import Flask @@ -18,7 +18,6 @@ LoadWebuiExtensions, ) - SURFACE_SCENARIOS: list[tuple[str, str]] = [ ("sidebar-start", "webui/components/sidebar/left-sidebar.html"), ("sidebar-end", "webui/components/sidebar/left-sidebar.html"), diff --git a/tests/websocket_namespace_test_utils.py b/tests/websocket_namespace_test_utils.py index f8252e97..63b180ec 100644 --- a/tests/websocket_namespace_test_utils.py +++ b/tests/websocket_namespace_test_utils.py @@ -4,7 +4,6 @@ from typing import Any from unittest.mock import AsyncMock - ConnectionIdentity = tuple[str, str] # (namespace, sid)