Increase ruff coverage (#23644)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / Jetson Jetpack 6 (push) Waiting to run

* Pin ruff

* Add python upgrade fixes

This enables python upgrade checks in ruff to look for deprecated types and patterns. This namely fixes:
- usage of deprecated `Typing` which is now built in
- some specific exceptions which are caught and have new aliases

Some specific UP checks were also ignored as they are stylistic / unimportant and likely to cause bugs

* Remove async blocking calls

Use asyncio.to_thread on two remaining blocking calls to fix hanging event thread loop. Enable this specific rule to block it in the future.

* Use proper logging mechanism

* Correctly format logs

* Raise with context

When raising an exception include the from context to improve debugging

* Cleanup
This commit is contained in:
Nicolas Mowen
2026-07-06 12:28:02 -05:00
committed by GitHub
parent 455b8687e8
commit 4ee12e6237
169 changed files with 1053 additions and 1150 deletions
+11 -10
View File
@@ -6,7 +6,8 @@ import logging
import os
import re
import time
from typing import Any, AsyncGenerator, Callable, Optional
from collections.abc import AsyncGenerator, Callable
from typing import Any
import numpy as np
from pydantic import ValidationError
@@ -235,7 +236,7 @@ class GenAIClient:
camera_config: CameraConfig,
thumbnails: list[bytes],
event: Event,
) -> Optional[str]:
) -> str | None:
"""Generate a description for the frame."""
try:
prompt = build_object_description_prompt(camera_config, event)
@@ -254,9 +255,9 @@ class GenAIClient:
self,
prompt: str,
images: list[bytes],
response_format: Optional[dict] = None,
response_format: dict | None = None,
enable_thinking: bool = False,
) -> Optional[str]:
) -> str | None:
"""Submit a request to the provider.
``enable_thinking`` is honored only by providers that report
@@ -321,9 +322,9 @@ class GenAIClient:
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to LLM with optional tool definitions.
@@ -395,9 +396,9 @@ class GenAIClient:
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""Streaming counterpart to `chat_with_tools`.
+6 -6
View File
@@ -6,7 +6,7 @@ no chat feature is active) are never initialized.
"""
import logging
from typing import TYPE_CHECKING, Any, Optional
from typing import TYPE_CHECKING, Any
from frigate.config import FrigateConfig
from frigate.config.camera.genai import GenAIConfig, GenAIRoleEnum
@@ -23,7 +23,7 @@ class GenAIClientManager:
def __init__(self, config: FrigateConfig) -> None:
self._configs: dict[str, GenAIConfig] = {}
self._role_map: dict[GenAIRoleEnum, str] = {}
self._clients: dict[str, "GenAIClient"] = {}
self._clients: dict[str, GenAIClient] = {}
self.update_config(config)
def update_config(self, config: FrigateConfig) -> None:
@@ -59,7 +59,7 @@ class GenAIClientManager:
for role in genai_cfg.roles:
self._role_map[role] = name
def _get_client(self, name: str) -> "Optional[GenAIClient]":
def _get_client(self, name: str) -> "GenAIClient | None":
"""Return the client for *name*, creating it on first access."""
if name in self._clients:
client = self._clients[name]
@@ -93,19 +93,19 @@ class GenAIClientManager:
return client
@property
def chat_client(self) -> "Optional[GenAIClient]":
def chat_client(self) -> "GenAIClient | None":
"""Client configured for the chat role (e.g. chat with function calling)."""
name = self._role_map.get(GenAIRoleEnum.chat)
return self._get_client(name) if name else None
@property
def description_client(self) -> "Optional[GenAIClient]":
def description_client(self) -> "GenAIClient | None":
"""Client configured for the descriptions role (e.g. review descriptions, object descriptions)."""
name = self._role_map.get(GenAIRoleEnum.descriptions)
return self._get_client(name) if name else None
@property
def embeddings_client(self) -> "Optional[GenAIClient]":
def embeddings_client(self) -> "GenAIClient | None":
"""Client configured for the embeddings role."""
name = self._role_map.get(GenAIRoleEnum.embeddings)
return self._get_client(name) if name else None
+14 -13
View File
@@ -4,7 +4,8 @@ import base64
import binascii
import json
import logging
from typing import Any, AsyncGenerator, Optional
from collections.abc import AsyncGenerator
from typing import Any
from google import genai
from google.genai import errors, types
@@ -16,7 +17,7 @@ from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
def _decode_thought_signature(value: Any) -> Optional[bytes]:
def _decode_thought_signature(value: Any) -> bytes | None:
"""Decode a base64-encoded thought_signature carried across conversation turns."""
if not value:
return None
@@ -30,14 +31,14 @@ def _decode_thought_signature(value: Any) -> Optional[bytes]:
return None
def _encode_thought_signature(signature: Optional[bytes]) -> Optional[str]:
def _encode_thought_signature(signature: bytes | None) -> str | None:
"""Encode bytes thought_signature as base64 so it survives JSON-friendly transport."""
if not signature:
return None
return base64.b64encode(signature).decode("ascii")
def _stats_from_gemini_usage(usage: Any) -> Optional[dict[str, Any]]:
def _stats_from_gemini_usage(usage: Any) -> dict[str, Any] | None:
"""Build a stats dict from a Gemini usage_metadata object."""
prompt_tokens = getattr(usage, "prompt_token_count", None)
completion_tokens = getattr(usage, "candidates_token_count", None)
@@ -84,9 +85,9 @@ class GeminiClient(GenAIClient):
self,
prompt: str,
images: list[bytes],
response_format: Optional[dict] = None,
response_format: dict | None = None,
enable_thinking: bool = False,
) -> Optional[str]:
) -> str | None:
"""Submit a request to Gemini."""
contents = [prompt] + [
types.Part.from_bytes(data=img, mime_type="image/jpeg") for img in images
@@ -141,9 +142,9 @@ class GeminiClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to Gemini with optional tool definitions.
@@ -399,9 +400,9 @@ class GeminiClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""
Stream chat with tools; yields content deltas then final message.
@@ -554,7 +555,7 @@ class GeminiClient(GenAIClient):
reasoning_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
usage_stats: Optional[dict[str, Any]] = None
usage_stats: dict[str, Any] | None = None
stream = await self.provider.aio.models.generate_content_stream(
model=self.genai_config.model,
+15 -14
View File
@@ -4,7 +4,8 @@ import base64
import io
import json
import logging
from typing import Any, AsyncGenerator, Optional, cast
from collections.abc import AsyncGenerator
from typing import Any, cast
import httpx
import numpy as np
@@ -18,7 +19,7 @@ from frigate.genai.utils import parse_tool_calls_from_message
logger = logging.getLogger(__name__)
def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> Optional[dict[str, Any]]:
def _stats_from_llama_cpp_chunk(data: dict[str, Any]) -> dict[str, Any] | None:
"""Build a stats dict from a llama.cpp streaming chunk.
Final-chunk `usage` carries authoritative token counts. Per-chunk
@@ -311,9 +312,9 @@ class LlamaCppClient(GenAIClient):
self,
prompt: str,
images: list[bytes],
response_format: Optional[dict] = None,
response_format: dict | None = None,
enable_thinking: bool = False,
) -> Optional[str]:
) -> str | None:
"""Submit a request to llama.cpp server."""
if self.provider is None:
logger.warning(
@@ -521,10 +522,10 @@ class LlamaCppClient(GenAIClient):
def _build_payload(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]],
tool_choice: Optional[str],
tools: list[dict[str, Any]] | None,
tool_choice: str | None,
stream: bool = False,
enable_thinking: Optional[bool] = None,
enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""Build request payload for chat completions (sync or stream)."""
openai_tool_choice = None
@@ -588,7 +589,7 @@ class LlamaCppClient(GenAIClient):
@staticmethod
def _streamed_tool_calls_to_list(
tool_calls_by_index: dict[int, dict[str, Any]],
) -> Optional[list[dict[str, Any]]]:
) -> list[dict[str, Any]] | None:
"""Convert streamed tool_calls index map to list of {id, name, arguments}."""
if not tool_calls_by_index:
return None
@@ -758,9 +759,9 @@ class LlamaCppClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to llama.cpp server with optional tool definitions.
@@ -830,9 +831,9 @@ class LlamaCppClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""Stream chat with tools via OpenAI-compatible streaming API."""
if self.provider is None:
+16 -15
View File
@@ -4,7 +4,8 @@ import base64
import binascii
import json
import logging
from typing import Any, AsyncGenerator, Optional
from collections.abc import AsyncGenerator
from typing import Any
from httpx import RemoteProtocolError, TimeoutException
from ollama import AsyncClient as OllamaAsyncClient
@@ -18,7 +19,7 @@ from frigate.genai.utils import parse_tool_calls_from_message
logger = logging.getLogger(__name__)
def _extract_ollama_stats(response: Any) -> Optional[dict[str, Any]]:
def _extract_ollama_stats(response: Any) -> dict[str, Any] | None:
"""Build a stats dict from Ollama's response metadata.
Ollama reports eval_count/eval_duration (generation) and
@@ -51,7 +52,7 @@ def _extract_ollama_stats(response: Any) -> Optional[dict[str, Any]]:
def _normalize_multimodal_content(
content: Any,
) -> tuple[Optional[str], Optional[list[bytes]]]:
) -> tuple[str | None, list[bytes] | None]:
"""Convert OpenAI-style multimodal content to Ollama's (text, images) shape.
The chat API constructs user messages with content as a list of
@@ -98,7 +99,7 @@ class OllamaClient(GenAIClient):
provider: ApiClient | None
provider_options: dict[str, Any]
_supports_thinking_cache: Optional[bool] = None
_supports_thinking_cache: bool | None = None
@property
def supports_toggleable_thinking(self) -> bool:
@@ -193,9 +194,9 @@ class OllamaClient(GenAIClient):
self,
prompt: str,
images: list[bytes],
response_format: Optional[dict] = None,
response_format: dict | None = None,
enable_thinking: bool = False,
) -> Optional[str]:
) -> str | None:
"""Submit a request to Ollama"""
if self.provider is None:
logger.warning(
@@ -290,10 +291,10 @@ class OllamaClient(GenAIClient):
def _build_request_params(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]],
tool_choice: Optional[str],
tools: list[dict[str, Any]] | None,
tool_choice: str | None,
stream: bool = False,
enable_thinking: Optional[bool] = None,
enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""Build request_messages and params for chat (sync or stream)."""
request_messages = []
@@ -385,9 +386,9 @@ class OllamaClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> dict[str, Any]:
if self.provider is None:
logger.warning(
@@ -426,9 +427,9 @@ class OllamaClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""Stream chat with tools; yields content deltas then final message.
+13 -12
View File
@@ -3,7 +3,8 @@
import base64
import json
import logging
from typing import Any, AsyncGenerator, Optional
from collections.abc import AsyncGenerator
from typing import Any
from httpx import TimeoutException
from openai import OpenAI
@@ -14,7 +15,7 @@ from frigate.genai import GenAIClient, register_genai_provider
logger = logging.getLogger(__name__)
def _stats_from_openai_usage(usage: Any) -> Optional[dict[str, Any]]:
def _stats_from_openai_usage(usage: Any) -> dict[str, Any] | None:
"""Build a stats dict from an OpenAI-compatible usage object."""
if usage is None:
return None
@@ -35,7 +36,7 @@ class OpenAIClient(GenAIClient):
"""Generative AI client for Frigate using OpenAI."""
provider: OpenAI
context_size: Optional[int] = None
context_size: int | None = None
def _init_provider(self) -> OpenAI:
"""Initialize the client.
@@ -60,9 +61,9 @@ class OpenAIClient(GenAIClient):
self,
prompt: str,
images: list[bytes],
response_format: Optional[dict] = None,
response_format: dict | None = None,
enable_thinking: bool = False,
) -> Optional[str]:
) -> str | None:
"""Submit a request to OpenAI."""
encoded_images = [base64.b64encode(image).decode("utf-8") for image in images]
messages_content: list[dict] = [
@@ -186,9 +187,9 @@ class OpenAIClient(GenAIClient):
def chat_with_tools(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> dict[str, Any]:
"""
Send chat messages to OpenAI with optional tool definitions.
@@ -307,9 +308,9 @@ class OpenAIClient(GenAIClient):
async def chat_with_tools_stream(
self,
messages: list[dict[str, Any]],
tools: Optional[list[dict[str, Any]]] = None,
tool_choice: Optional[str] = "auto",
enable_thinking: Optional[bool] = None,
tools: list[dict[str, Any]] | None = None,
tool_choice: str | None = "auto",
enable_thinking: bool | None = None,
) -> AsyncGenerator[tuple[str, Any], None]:
"""
Stream chat with tools; yields content deltas then final message.
@@ -357,7 +358,7 @@ class OpenAIClient(GenAIClient):
reasoning_parts: list[str] = []
tool_calls_by_index: dict[int, dict[str, Any]] = {}
finish_reason = "stop"
usage_stats: Optional[dict[str, Any]] = None
usage_stats: dict[str, Any] | None = None
stream = self.provider.chat.completions.create(**request_params)
+9 -9
View File
@@ -6,7 +6,7 @@ transport.
"""
import datetime
from typing import Any, Dict, List, Optional
from typing import Any
from playhouse.shortcuts import model_to_dict
@@ -216,7 +216,7 @@ def build_object_description_prompt(
return template.format(**model_to_dict(event))
def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]]:
def get_attribute_classifications(config: FrigateConfig) -> list[dict[str, Any]]:
"""Return enabled custom classification models of `attribute` type.
Each entry: {"name": <model name>, "objects": [<object label>, ...]}.
@@ -224,7 +224,7 @@ def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]]
types, which can later be filtered via the search_objects `attribute`
field.
"""
result: List[Dict[str, Any]] = []
result: list[dict[str, Any]] = []
for model_key, model_config in config.classification.custom.items():
if not model_config.enabled or model_config.object_config is None:
@@ -248,8 +248,8 @@ def get_attribute_classifications(config: FrigateConfig) -> List[Dict[str, Any]]
def get_tool_definitions(
semantic_search_enabled: bool = False,
attribute_classifications: Optional[List[Dict[str, Any]]] = None,
) -> List[Dict[str, Any]]:
attribute_classifications: list[dict[str, Any]] | None = None,
) -> list[dict[str, Any]]:
"""
Get OpenAI-compatible tool definitions for Frigate.
@@ -260,7 +260,7 @@ def get_tool_definitions(
included. When attribute classification models are configured, an
`attribute` parameter is exposed for filtering by their labels.
"""
search_objects_properties: Dict[str, Any] = {
search_objects_properties: dict[str, Any] = {
"camera": {
"type": "string",
"description": "Camera name to filter by (optional).",
@@ -657,9 +657,9 @@ def get_tool_definitions(
def build_chat_system_prompt(
config: FrigateConfig,
allowed_cameras: List[str],
allowed_cameras: list[str],
semantic_search_enabled: bool,
attribute_classifications: List[Dict[str, Any]],
attribute_classifications: list[dict[str, Any]],
) -> str:
"""Build the system prompt for the chat completion endpoint.
@@ -671,7 +671,7 @@ def build_chat_system_prompt(
current_date_str = current_datetime.strftime("%Y-%m-%d")
current_time_str = current_datetime.strftime("%I:%M:%S %p")
cameras_info: List[str] = []
cameras_info: list[str] = []
has_speed_zone = False
for camera_id in allowed_cameras:
if camera_id not in config.cameras:
+3 -3
View File
@@ -2,14 +2,14 @@
import json
import logging
from typing import Any, List, Optional
from typing import Any
logger = logging.getLogger(__name__)
def parse_tool_calls_from_message(
message: dict[str, Any],
) -> Optional[list[dict[str, Any]]]:
) -> list[dict[str, Any]] | None:
"""
Parse tool_calls from an OpenAI-style message dict.
@@ -52,7 +52,7 @@ def parse_tool_calls_from_message(
def build_assistant_message_for_conversation(
content: Any,
tool_calls_raw: Optional[List[dict[str, Any]]],
tool_calls_raw: list[dict[str, Any]] | None,
) -> dict[str, Any]:
"""
Build the assistant message dict in OpenAI format for appending to a conversation.