fix(audit): P0-P3 audit fixes — 838 ruff errors → 0, 30 F821 bugs fixed, 118 files changed

- P0: hooks.py 3-tuple fix, trigger_dispatcher Contract, contacts/plugin unregister_actions_by_owner
- P0: 5 test files — check_permission mocks removed, hardcoded DB credential → env var
- P1: attachment_service DmsFile via Contract helper, restore_registry/history_hooks dedup
- P1: mail/plugin restore unregister, mcp_client datetime.now(UTC), saved_views/filters patterns
- P1: ProtectedRoute fail-closed, 13 test assertion fixes (bcrypt, DB-URLs, SECRET_KEYs)
- P2: deprecated notifications → post_system_message (3 files), forgejo Base, report_generator lazy import
- P2: webhooks permissions, deps.py/roles.py plugin perms removed, import_export default
- P2: address/tags/entity_links patterns removed, worker.py Contract-Umgehungen fixed
- P2: 28 frontend TODOs (hardcoded constants, deprecated notification API)
- P3: dead code, duplicates, deprecated imports, private attr, __import__ inline
- P3: 8 frontend TODOs (LucideIcons, inline styles, XSS, i18n)
- ruff: 838 → 0 (612 auto-fix + 246 manual + 27 F821 regression fix)
- F821: 30 → 0 (AutomationDefinition, DmsFile, user_id, Path, Any, String)
- Contract-Umgehungen: 2 neue gefunden (worker.py:169, worker.py:280) und gefixt
This commit is contained in:
Agent Zero
2026-08-16 01:17:18 +02:00
parent 3d9b76cea4
commit abbe7a18fc
306 changed files with 5912 additions and 1827 deletions
@@ -12,8 +12,6 @@ import logging
import uuid
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from app.plugins.builtins.unified_search.query_understanding import llm_analyze_query
from app.plugins.builtins.unified_search.search_engine import hybrid_search
@@ -3,10 +3,11 @@
from __future__ import annotations
from app.plugins.builtins.contracts import get_contract_registry
from app.plugins.builtins.unified_search.embedding import generate_embedding
from app.plugins.builtins.unified_search.search_engine import hybrid_search, find_similar_all_types
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider
from app.plugins.builtins.unified_search.embedding import generate_embedding
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
from app.plugins.builtins.unified_search.query_understanding import llm_analyze_query
from app.plugins.builtins.unified_search.search_engine import find_similar_all_types, hybrid_search
class UnifiedSearchContract:
@@ -18,6 +19,7 @@ class UnifiedSearchContract:
hybrid_search = staticmethod(hybrid_search)
find_similar_all_types = staticmethod(find_similar_all_types)
get_search_registry = staticmethod(get_search_registry)
llm_analyze_query = staticmethod(llm_analyze_query)
BaseSearchProvider = BaseSearchProvider
@@ -19,10 +19,14 @@ from app.ai.llm_client import (
EMBEDDING_DIMENSIONS,
MAX_INPUT_CHARS,
OPENROUTER_EMBEDDING_MODEL,
build_model as _central_build_model,
get_api_credentials as _central_get_api_credentials,
llm_embed,
)
from app.ai.llm_client import (
build_model as _central_build_model,
)
from app.ai.llm_client import (
get_api_credentials as _central_get_api_credentials,
)
logger = logging.getLogger(__name__)
@@ -46,7 +50,7 @@ OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
async def _get_api_credentials(
db: "AsyncSession | None", tenant_id: "uuid.UUID | None"
db: AsyncSession | None, tenant_id: uuid.UUID | None
) -> tuple[str | None, str | None, str | None]:
"""Get API key, base_url and provider_type for embeddings.
@@ -68,8 +72,8 @@ def _build_model(model: str, provider_type: str | None) -> str:
async def generate_embedding(
text: str,
model: str | None = None,
db: "AsyncSession | None" = None,
tenant_id: "uuid.UUID | None" = None,
db: AsyncSession | None = None,
tenant_id: uuid.UUID | None = None,
) -> list[float]:
"""Generate a single embedding via LiteLLM.
@@ -98,8 +102,8 @@ async def generate_embedding(
async def generate_embeddings_batch(
texts: list[str],
model: str | None = None,
db: "AsyncSession | None" = None,
tenant_id: "uuid.UUID | None" = None,
db: AsyncSession | None = None,
tenant_id: uuid.UUID | None = None,
) -> list[list[float]]:
"""Generate embeddings for multiple texts in a single API call.
@@ -124,7 +128,7 @@ async def index_entity(
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
db: "AsyncSession",
db: AsyncSession,
) -> bool:
"""Generate and store embedding for a single entity.
@@ -134,6 +138,7 @@ async def index_entity(
Returns True on success, False on failure.
"""
from sqlalchemy import text as sql_text
from app.plugins.builtins.unified_search.models import SearchIndexLog
async def _log_index(action: str, status: str, error: str | None = None) -> None:
+11 -3
View File
@@ -59,8 +59,9 @@ async def index_mails(ctx: dict[str, Any], mail_ids: list[str]) -> None:
async def index_file(ctx: dict[str, Any], file_id: str) -> None:
"""Index a file: extract text, store content_text, generate embedding."""
from sqlalchemy import text
from app.plugins.builtins.unified_search.text_extraction import extract_text_from_file
from app.plugins.builtins.unified_search.embedding import generate_embedding
from app.plugins.builtins.unified_search.text_extraction import extract_text_from_file
factory = get_session_factory()
async with factory() as db:
@@ -119,6 +120,7 @@ async def index_file(ctx: dict[str, Any], file_id: str) -> None:
async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
"""Index a contact: generate and store embedding."""
from sqlalchemy import text
from app.plugins.builtins.unified_search.embedding import index_entity
factory = get_session_factory()
@@ -144,6 +146,7 @@ async def index_contact(ctx: dict[str, Any], contact_id: str) -> None:
async def index_event(ctx: dict[str, Any], event_id: str) -> None:
"""Index a calendar event: generate and store embedding."""
from sqlalchemy import text
from app.plugins.builtins.unified_search.embedding import index_entity
factory = get_session_factory()
@@ -173,6 +176,7 @@ async def reindex(ctx: dict[str, Any], entity_type: str) -> None:
and continues on individual failures.
"""
from sqlalchemy import text
from app.plugins.builtins.unified_search.embedding import index_entity
table = _TABLE_MAP.get(entity_type)
@@ -272,6 +276,7 @@ async def delete_entity_index(ctx: dict[str, Any], entity_type: str, entity_id:
Logs the action to SearchIndexLog on success.
"""
from sqlalchemy import text
from app.plugins.builtins.unified_search.models import SearchIndexLog
table = _TABLE_MAP.get(entity_type)
@@ -346,6 +351,7 @@ async def delete_file_chunks(ctx: dict[str, Any], file_id: str) -> None:
async def retry_failed_index(ctx: dict[str, Any], entity_type: str, entity_id: str) -> None:
"""Retry a failed index operation."""
from sqlalchemy import text
from app.plugins.builtins.unified_search.embedding import index_entity
table = _TABLE_MAP.get(entity_type)
@@ -377,6 +383,7 @@ async def retry_failed_index(ctx: dict[str, Any], entity_type: str, entity_id: s
async def embedding_batch(ctx: dict[str, Any]) -> None:
"""Periodic job: find entities without embeddings and index them."""
from sqlalchemy import text
from app.plugins.builtins.unified_search.embedding import index_entity
factory = get_session_factory()
@@ -414,9 +421,10 @@ async def embedding_batch(ctx: dict[str, Any]) -> None:
async def index_file_chunks(ctx: dict[str, Any], file_id: str) -> None:
"""Extract text from a file, chunk it, generate embeddings, and store in document_chunks."""
from sqlalchemy import text
from app.plugins.builtins.unified_search.text_extraction import extract_text_from_file
from app.plugins.builtins.unified_search.chunking import chunk_text
from app.plugins.builtins.unified_search.embedding import generate_embedding
from app.plugins.builtins.unified_search.text_extraction import extract_text_from_file
factory = get_session_factory()
async with factory() as db:
@@ -506,7 +514,7 @@ async def reindex_chunks(ctx: dict[str, Any], file_id: str) -> None:
# ── Register all job functions with the job registry ──────────────────────────
from app.core.job_registry import register_job
from app.core.job_registry import register_job # noqa: E402
register_job("index_mails", index_mails)
register_job("index_file", index_file)
@@ -33,7 +33,7 @@ def _resolve_entity(entity_type: str) -> tuple[str, str, str] | None:
async def remove_from_index(
db: "AsyncSession",
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
@@ -61,7 +61,7 @@ async def remove_from_index(
async def remove_chunks(
db: "AsyncSession",
db: AsyncSession,
file_id: uuid.UUID,
tenant_id: uuid.UUID,
) -> None:
@@ -78,7 +78,7 @@ async def remove_chunks(
async def rebuild_index(
db: "AsyncSession",
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
@@ -107,7 +107,7 @@ async def rebuild_index(
async def handle_entity_delete(
db: "AsyncSession",
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
@@ -119,7 +119,7 @@ async def handle_entity_delete(
async def handle_entity_restore(
db: "AsyncSession",
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
@@ -129,7 +129,7 @@ async def handle_entity_restore(
async def handle_entity_correction(
db: "AsyncSession",
db: AsyncSession,
entity_type: str,
entity_id: uuid.UUID,
tenant_id: uuid.UUID,
@@ -3,10 +3,10 @@
from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
from sqlalchemy import ForeignKey, Index, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
@@ -49,7 +49,7 @@ class SearchIndexLog(Base, TenantMixin):
status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
from pgvector.sqlalchemy import Vector
from pgvector.sqlalchemy import Vector # noqa: E402
class DocumentChunk(Base, TenantMixin):
+16 -9
View File
@@ -7,7 +7,7 @@ import uuid
from typing import Any
from app.plugins.base import BasePlugin
from app.plugins.manifest import PluginManifest, PluginRouteDef, FrontendPageRoute
from app.plugins.manifest import FrontendPageRoute, PluginManifest, PluginRouteDef
logger = logging.getLogger(__name__)
@@ -57,6 +57,11 @@ class UnifiedSearchPlugin(BasePlugin):
contract_version="1.0.0",
)
def get_job_modules(self) -> list[str]:
return [
"app.plugins.builtins.unified_search.jobs",
]
async def on_activate(self, db, service_container, event_bus) -> None:
"""Register search providers and AI tool on activation."""
await super().on_activate(db, service_container, event_bus)
@@ -69,12 +74,14 @@ class UnifiedSearchPlugin(BasePlugin):
except Exception:
logger.exception("Failed to auto-register search providers")
# Register the unified_search AI tool
# Register the unified_search AI tool via contract
try:
from app.plugins.builtins.ai_assistant.tool_registry import get_tool_registry
from app.plugins.builtins.unified_search.ai_tool import register_unified_search_tool
register_unified_search_tool(get_tool_registry())
logger.info("Unified Search AI tool registered")
from app.plugins.builtins.contracts import get_contract
ai_contract = get_contract("ai_assistant")
if ai_contract is not None:
from app.plugins.builtins.unified_search.ai_tool import register_unified_search_tool
register_unified_search_tool(ai_contract.get_tool_registry())
logger.info("Unified Search AI tool registered")
except Exception:
logger.exception("Failed to register unified_search AI tool")
@@ -172,8 +179,8 @@ class UnifiedSearchPlugin(BasePlugin):
async def on_entity_deleted(self, payload: dict[str, Any]) -> None:
"""Handle entity.deleted lifecycle event — remove from search index."""
from app.plugins.builtins.unified_search.lifecycle import handle_entity_delete
from app.core.db import get_session_factory
from app.plugins.builtins.unified_search.lifecycle import handle_entity_delete
entity_type = payload.get("entity_type")
entity_id = payload.get("entity_id")
@@ -188,8 +195,8 @@ class UnifiedSearchPlugin(BasePlugin):
async def on_entity_restored(self, payload: dict[str, Any]) -> None:
"""Handle entity.restored lifecycle event — rebuild search index."""
from app.plugins.builtins.unified_search.lifecycle import handle_entity_restore
from app.core.db import get_session_factory
from app.plugins.builtins.unified_search.lifecycle import handle_entity_restore
entity_type = payload.get("entity_type")
entity_id = payload.get("entity_id")
@@ -204,8 +211,8 @@ class UnifiedSearchPlugin(BasePlugin):
async def on_entity_corrected(self, payload: dict[str, Any]) -> None:
"""Handle entity.corrected lifecycle event — rebuild search index."""
from app.plugins.builtins.unified_search.lifecycle import handle_entity_correction
from app.core.db import get_session_factory
from app.plugins.builtins.unified_search.lifecycle import handle_entity_correction
entity_type = payload.get("entity_type")
entity_id = payload.get("entity_id")
@@ -5,10 +5,9 @@ from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Protocol, runtime_checkable
from sqlalchemy import select
if TYPE_CHECKING:
import uuid
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger(__name__)
@@ -121,47 +120,47 @@ async def auto_register_providers(db: AsyncSession) -> None:
Checks which plugins are active and registers corresponding providers.
"""
from app.plugins.builtins.unified_search.providers.contact_provider import (
ContactSearchProvider,
)
from app.plugins.builtins.unified_search.providers.mail_provider import (
MailSearchProvider,
)
from app.plugins.builtins.unified_search.providers.file_provider import (
FileSearchProvider,
)
from app.plugins.builtins.unified_search.providers.event_provider import (
EventSearchProvider,
)
from app.plugins.builtins.unified_search.providers.company_provider import (
CompanySearchProvider,
)
from app.plugins.builtins.unified_search.providers.task_provider import (
TaskSearchProvider,
)
from app.plugins.builtins.unified_search.providers.contactperson_provider import (
ContactPersonSearchProvider,
)
from app.plugins.builtins.unified_search.providers.tag_provider import (
TagSearchProvider,
)
from app.plugins.builtins.unified_search.providers.conversation_provider import (
ConversationSearchProvider,
)
from app.plugins.builtins.unified_search.providers.user_provider import (
UserSearchProvider,
)
from app.plugins.builtins.graph_rag.contracts import GraphRagContract
from app.plugins.builtins.unified_search.providers.agent_memory_provider import (
AgentMemorySearchProvider,
)
from app.plugins.builtins.unified_search.providers.ai_chat_provider import (
AIChatSearchProvider,
)
from app.plugins.builtins.unified_search.providers.company_provider import (
CompanySearchProvider,
)
from app.plugins.builtins.unified_search.providers.contact_provider import (
ContactSearchProvider,
)
from app.plugins.builtins.unified_search.providers.contactperson_provider import (
ContactPersonSearchProvider,
)
from app.plugins.builtins.unified_search.providers.conversation_provider import (
ConversationSearchProvider,
)
from app.plugins.builtins.unified_search.providers.event_provider import (
EventSearchProvider,
)
from app.plugins.builtins.unified_search.providers.file_provider import (
FileSearchProvider,
)
from app.plugins.builtins.unified_search.providers.mail_provider import (
MailSearchProvider,
)
from app.plugins.builtins.unified_search.providers.tag_provider import (
TagSearchProvider,
)
from app.plugins.builtins.unified_search.providers.task_provider import (
TaskSearchProvider,
)
from app.plugins.builtins.unified_search.providers.user_provider import (
UserSearchProvider,
)
from app.plugins.builtins.unified_search.providers.workflow_provider import (
WorkflowSearchProvider,
)
from app.plugins.builtins.graph_rag.contracts import GraphRagContract
GraphRAGSearchProvider = GraphRagContract.GraphRAGSearchProvider
graph_rag_search_provider = GraphRagContract.graph_rag_search_provider
registry = get_search_registry()
registry.clear()
@@ -181,7 +180,7 @@ async def auto_register_providers(db: AsyncSession) -> None:
AgentMemorySearchProvider,
AIChatSearchProvider,
WorkflowSearchProvider,
GraphRAGSearchProvider,
graph_rag_search_provider,
]:
try:
registry.register(provider_cls())
@@ -9,8 +9,8 @@ from typing import Any
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider
from app.config import settings
from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider
logger = logging.getLogger(__name__)
@@ -7,9 +7,10 @@ import logging
import uuid
from typing import Any
from app.ai.llm_client import llm_complete
from sqlalchemy.ext.asyncio import AsyncSession
from app.ai.llm_client import llm_complete
logger = logging.getLogger(__name__)
QUERY_ANALYZE_SYSTEM = (
+11 -12
View File
@@ -4,7 +4,8 @@ from __future__ import annotations
import logging
import uuid
from datetime import datetime, timezone
from datetime import UTC, datetime
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import text
@@ -12,7 +13,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.core.jobs import enqueue_job
from app.core.permissions import resolve_permissions, filter_fields_by_permission
from app.core.permissions import filter_fields_by_permission, resolve_permissions
from app.deps import get_current_user, require_permission
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
from app.plugins.builtins.unified_search.query_understanding import (
@@ -28,7 +29,6 @@ from app.plugins.builtins.unified_search.schemas import (
SearchResult,
SimilarRequest,
SimilarResponse,
SuggestRequest,
SuggestResponse,
)
from app.plugins.builtins.unified_search.search_engine import (
@@ -80,7 +80,7 @@ def _parse_date(value: str | None) -> datetime | None:
try:
dt = datetime.fromisoformat(value)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
dt = dt.replace(tzinfo=UTC)
return dt
except ValueError:
return None
@@ -109,7 +109,7 @@ def _apply_filters_and_sort(
except ValueError:
continue
if ts.tzinfo is None:
ts = ts.replace(tzinfo=timezone.utc)
ts = ts.replace(tzinfo=UTC)
if date_from and ts < date_from:
continue
if date_to and ts > date_to:
@@ -169,7 +169,7 @@ async def _do_search(
resolved_perms = await resolve_permissions(db, user_id, tenant_id)
# Map entity_type to module name for field-level permissions
_ENTITY_TO_MODULE = {
_entity_to_module = {
"contact": "contacts",
"mail": "mail",
"file": "dms",
@@ -189,7 +189,7 @@ async def _do_search(
data=filter_fields_by_permission(
r.get("data", {}),
resolved_perms,
_ENTITY_TO_MODULE.get(r.get("entity_type", ""), r.get("entity_type", "")),
_entity_to_module.get(r.get("entity_type", ""), r.get("entity_type", "")),
),
)
for r in results
@@ -307,7 +307,7 @@ async def find_similar(
try:
entity_id = uuid.UUID(req.entity_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid entity_id")
raise HTTPException(status_code=400, detail="Invalid entity_id") from None
similar = await find_similar_all_types(
db=db,
@@ -382,7 +382,7 @@ async def rebuild_entity_index(
try:
eid = uuid.UUID(entity_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid entity_id")
raise HTTPException(status_code=400, detail="Invalid entity_id") from None
from app.plugins.builtins.unified_search.lifecycle import rebuild_index
@@ -407,9 +407,9 @@ async def purge_entity_index(
try:
eid = uuid.UUID(entity_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid entity_id")
raise HTTPException(status_code=400, detail="Invalid entity_id") from None
from app.plugins.builtins.unified_search.lifecycle import remove_from_index, remove_chunks
from app.plugins.builtins.unified_search.lifecycle import remove_chunks, remove_from_index
await remove_from_index(db, entity_type, eid, tenant_id)
if entity_type == "file":
@@ -522,7 +522,6 @@ async def search_stats(
stats: dict = {}
tables = {
"contacts": "embedding",
"contacts": "embedding",
"mails": "embedding",
"files": "embedding",
@@ -6,7 +6,6 @@ from typing import Any
from pydantic import BaseModel, Field
# ─── Search ───
class SearchRequest(BaseModel):
@@ -3,7 +3,6 @@
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger(__name__)
@@ -122,6 +121,6 @@ async def _extract_text(file_path: str) -> str:
"""Read plain text file using aiofiles."""
import aiofiles
async with aiofiles.open(file_path, mode="r", encoding="utf-8", errors="replace") as f:
async with aiofiles.open(file_path, encoding="utf-8", errors="replace") as f:
content = await f.read()
return _truncate(content)