diff --git a/PROGRESS.md b/PROGRESS.md index 62a00a1..ff91186 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -85,7 +85,9 @@ | D5-b | BUG-077 trace_plugins 27 issues: Scanner erwartete manifest.py, Projekt-Konvention ist Inline-Manifest in plugin.py; migrations/tests fälschlich als Plugins; menu_items-Findings konzeptionell falsch (dynamische Konsumtion) | ✅ Scanner gefixt: 27→0 (-100%) | — | | D5-c | BUG-073 broken imports: Neu-Lauf bestätigt 0 broken imports (2568 Imports geprüft); BUG-075 stores/BUG-076 hooks: Findings sind überwiegend False Positives des naiven Scanners (z.B. 'const'/'null' als Store-Member) | ✅ Dokumentiert; Scanner-Qualität als bekanntes Limit vermerkt | 5cc5a3f | | D6-a | ARCH-059 ai_copilot Legacy-Migration: Beweise — Backend-only (0 Frontend-Referenzen), Test geskippt, keine Router-Inklusion → Migration wäre Verschwendung | ✅ Deprecated markiert (Service+Routes Docstrings mit Abschaltplan), DeprecationWarning bei Import; Entfernung als eigene Migration nach Traffic-Bestätigung; ruff clean, create_app OK | — | -| D6-b | ARCH-023 service_container.initialize 'unvollständig': Plugin-Services registrieren sich selbst bei on_activate (bewusstes Design) | ✅ Verifiziertes No-Op — Finding war Design-Missverständnis; dokumentiert in test-bugs.md | — | +| D6-b | ARCH-023 service_container.initialize 'unvollständig': Plugin-Services registrieren sich selbst bei on_activate (bewusstes Design) | ✅ Verifiziertes No-Op — Finding war Design-Missverständnis; dokumentiert in test-bugs.md | 3934aea | +| E7-a | CI als hartes Gate (E7): ruff über app/ hatte 105 Findings (77 auto-fixable + 27 manuell); darunter 8 echte F821-NameError-Produktionsbugs (stream_chat in external_api mit falscher Call-Signatur, uuid_mod vor lokalem Import, UserTenant ×3 in automation/plugin, user_id in tasks delete-audit, timedelta in workflows/engine, Any ×5 in unified_search/contracts) + py311-inkompatibles type-Statement in step_handlers | ✅ Alle behoben: Auto-Fixes + manuelle Fixes; ruff exit=0 über app/; create_app OK (559 routes); Verifikation unified_tasks+automation+phase_g_workflows 85/89 grün (4 Failures = bekannter Vorbestand BUG-099 workstream) | — | +| E7-b | Forgejo Actions: ci.yml existiert (.forgejo/workflows/ci.yml, trigger push/PR main), aber 0 Läufe bisher (total_count=0) — Runner-Konfiguration auf Server-Seite zu prüfen; Branch-Protection 'Merge nur bei grün' ist Forgejo-Server-Einstellung | ⏳ Dokumentiert für Server-Admin: Actions-Runner aktivieren + Branch-Protection setzen; Pipeline-Inhalt ist vollständig (15 Checks) | — | **Block D ABGESCHLOSSEN** (D1–D6) — D1: alle 9 Ziel-Suites grün; D2: DateTime/SQLITE-001; D3: ARCH-051/055/056/057 + systemischer Permission-Resolver-Bug + conftest-pgvector; D4: Security-Triage (ARCH-027 verifiziert, BUG-019 = 0 echte Secrets, BUG-020 kein fixbares Finding); D5: Scanner-Triage (api_contracts -75%, plugins -100%, 371 Fehlalarme eliminiert); D6: ai_copilot deprecated + ARCH-023 No-Op. Offene Follow-ups dokumentiert (~12 echte API-Bugs aus D5, IMAP-Mocking für Mail-Tests). Nächster Block: E (Production-Härtung). diff --git a/app/ai/agent_permissions.py b/app/ai/agent_permissions.py index 62cfb31..05804d0 100644 --- a/app/ai/agent_permissions.py +++ b/app/ai/agent_permissions.py @@ -17,7 +17,7 @@ from __future__ import annotations import logging import uuid -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any from sqlalchemy import select diff --git a/app/ai/context_builder.py b/app/ai/context_builder.py index 2ba80ff..7119d8b 100644 --- a/app/ai/context_builder.py +++ b/app/ai/context_builder.py @@ -25,8 +25,6 @@ import logging import uuid from typing import TYPE_CHECKING, Any -from app.core.sensitive_data import sanitize_dict - if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession diff --git a/app/ai/data_policy.py b/app/ai/data_policy.py index 46ed95e..1003bfd 100644 --- a/app/ai/data_policy.py +++ b/app/ai/data_policy.py @@ -20,8 +20,6 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.ai.ai_use_case import AIUseCaseMetadata from app.core.sensitive_data import ( SENSITIVE_FIELDS, - filter_for_llm_context, - get_data_class_for_field, ) logger = logging.getLogger(__name__) diff --git a/app/ai/knowledge_extraction.py b/app/ai/knowledge_extraction.py index 0dfb4e8..2dcf460 100644 --- a/app/ai/knowledge_extraction.py +++ b/app/ai/knowledge_extraction.py @@ -6,7 +6,6 @@ import uuid from dataclasses import dataclass, field from typing import Any - LOW_CONFIDENCE_THRESHOLD = 0.6 diff --git a/app/ai/knowledge_lifecycle.py b/app/ai/knowledge_lifecycle.py index 843c1df..f805f22 100644 --- a/app/ai/knowledge_lifecycle.py +++ b/app/ai/knowledge_lifecycle.py @@ -7,7 +7,6 @@ from typing import Any from app.ai.knowledge_sources import get_source_config - EXTRACTION_TRIGGERS = { "mail.received", "dms.file_uploaded", diff --git a/app/ai/knowledge_sources.py b/app/ai/knowledge_sources.py index 89ca2bf..01caa80 100644 --- a/app/ai/knowledge_sources.py +++ b/app/ai/knowledge_sources.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from typing import Any diff --git a/app/core/worker.py b/app/core/worker.py index 0e15ba5..285f06e 100644 --- a/app/core/worker.py +++ b/app/core/worker.py @@ -345,9 +345,11 @@ async def cleanup_audit_log_job(ctx: dict[str, Any]) -> None: Runs daily to prevent the audit_log table from growing indefinitely. Iterates per-tenant for RLS compliance. """ - from sqlalchemy import text as sa_text, delete as sa_delete from datetime import UTC, datetime, timedelta + from sqlalchemy import delete as sa_delete + from sqlalchemy import text as sa_text + from app.core.db import get_worker_session_factory from app.models.audit import AuditLog @@ -388,9 +390,11 @@ async def cleanup_trash_job(ctx: dict[str, Any]) -> None: Runs daily to clean up the trash. Iterates per-tenant for RLS compliance. Default retention: 90 days in trash before permanent deletion. """ - from sqlalchemy import text as sa_text, delete as sa_delete from datetime import UTC, datetime, timedelta + from sqlalchemy import delete as sa_delete + from sqlalchemy import text as sa_text + from app.core.db import get_worker_session_factory from app.models.contact import Contact from app.models.entity_attachment import EntityAttachment diff --git a/app/main.py b/app/main.py index 1ec9f6c..fffa3d4 100644 --- a/app/main.py +++ b/app/main.py @@ -36,13 +36,14 @@ from app.routes import ( # noqa: E402 attachments, audit, auth, - compliance, backups, bank_accounts, + compliance, currencies, custom_field_definitions, custom_fields, dashboard, + delegations, entity_history, entity_permissions, errors, @@ -56,7 +57,6 @@ from app.routes import ( # noqa: E402 owner_transfer, permission_templates, plugins, - delegations, policies, roles, saved_filters, diff --git a/app/models/__init__.py b/app/models/__init__.py index 0295ea7..cc1687e 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -6,8 +6,8 @@ from app.models.audit import AuditLog from app.models.auth import ApiToken, PasswordResetToken from app.models.backup import Backup from app.models.bank_account import BankAccount -from app.models.consumer_inbox import ConsumerInbox from app.models.compliance import ComplianceIncident +from app.models.consumer_inbox import ConsumerInbox from app.models.contact import Contact, ContactPerson from app.models.contact_folder import ContactFolder from app.models.contact_merge import ContactMergeHistory diff --git a/app/models/audit.py b/app/models/audit.py index 8bea96f..914f049 100644 --- a/app/models/audit.py +++ b/app/models/audit.py @@ -11,13 +11,12 @@ from datetime import datetime from typing import Any from sqlalchemy import DateTime, ForeignKey, String, func -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin -from sqlalchemy.dialects.postgresql import TSVECTOR # Re-export EntityHistory as DeletionLog for backward compatibility. # Tests import DeletionLog from app.models.audit and use entity_snapshot attribute. diff --git a/app/models/user.py b/app/models/user.py index bfa3868..13756bb 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -12,7 +12,6 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, SoftDeleteMixin, TimestampMixin -from app.models.owned_mixin import OwnedMixin class User(Base, TimestampMixin, SoftDeleteMixin): diff --git a/app/plugins/builtins/agent_memory/models.py b/app/plugins/builtins/agent_memory/models.py index 1705253..7864f12 100644 --- a/app/plugins/builtins/agent_memory/models.py +++ b/app/plugins/builtins/agent_memory/models.py @@ -12,7 +12,6 @@ from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin -from pgvector.sqlalchemy import Vector class AgentMemory(Base, TenantMixin, OwnedMixin): diff --git a/app/plugins/builtins/ai_assistant/external_api.py b/app/plugins/builtins/ai_assistant/external_api.py index 0808bf0..c435130 100644 --- a/app/plugins/builtins/ai_assistant/external_api.py +++ b/app/plugins/builtins/ai_assistant/external_api.py @@ -22,6 +22,7 @@ from app.plugins.builtins.ai_assistant.schemas import ( ExternalAgentRequest, ExternalAgentResponse, ) +from app.plugins.builtins.ai_assistant.services import stream_chat_comm as stream_chat logger = logging.getLogger(__name__) @@ -120,13 +121,18 @@ async def run_agent_external( } # Run the agent via streaming chat (non-streaming mode) - from app.plugins.builtins.ai_assistant.services import stream_chat_comm full_response = "" async with get_db() as stream_db: await set_tenant_context(stream_db, tenant_id) async for chunk in stream_chat( - stream_db, session, agent, data.message, user_context, tenant_id + stream_db, + session.id, + agent, + data.message, + user_context, + tenant_id, + uuid.UUID(current_user["user_id"]), ): if chunk.startswith("data: ") and chunk != "data: [DONE]\n\n": try: diff --git a/app/plugins/builtins/ai_proactive/services.py b/app/plugins/builtins/ai_proactive/services.py index 26b010c..486a964 100644 --- a/app/plugins/builtins/ai_proactive/services.py +++ b/app/plugins/builtins/ai_proactive/services.py @@ -69,10 +69,12 @@ async def push_suggestion(user_id: str, suggestion: dict[str, Any]) -> None: # Post suggestion to Communication (I-WORK-PROACTIVE) try: import uuid as uuid_mod + + from sqlalchemy import select as sa_select + + from app.core.db import get_worker_session_factory from app.plugins.builtins.contracts import get_contract_registry from app.plugins.builtins.kommunikation.models import CommConversation - from sqlalchemy import select as sa_select - from app.core.db import get_worker_session_factory komm = get_contract_registry().get("kommunikation") if komm: factory = get_worker_session_factory() diff --git a/app/plugins/builtins/automation/agent_comm.py b/app/plugins/builtins/automation/agent_comm.py index fdd6722..9221cc6 100644 --- a/app/plugins/builtins/automation/agent_comm.py +++ b/app/plugins/builtins/automation/agent_comm.py @@ -55,7 +55,8 @@ async def send_agent_message( # 2. Create a kommunikation message in a dedicated agent room try: - from app.plugins.builtins.kommunikation.contracts import CommConversation as Room, CommMessage as Message + from app.plugins.builtins.kommunikation.contracts import CommConversation as Room + from app.plugins.builtins.kommunikation.contracts import CommMessage as Message # Find or create the agent-to-agent room room_name = f"agent:{from_agent_id}:{target_agent.id}" diff --git a/app/plugins/builtins/automation/agent_routes.py b/app/plugins/builtins/automation/agent_routes.py index 5df6c1b..2f90c1c 100644 --- a/app/plugins/builtins/automation/agent_routes.py +++ b/app/plugins/builtins/automation/agent_routes.py @@ -619,6 +619,7 @@ async def stream_agent_run( in real-time as the agent processes. """ from fastapi.responses import StreamingResponse + from app.ai.agent_stream import stream_react_loop from app.plugins.builtins.ai_assistant.contracts import get_tool_registry diff --git a/app/plugins/builtins/automation/agent_runner.py b/app/plugins/builtins/automation/agent_runner.py index a603ae9..6ccd4d6 100644 --- a/app/plugins/builtins/automation/agent_runner.py +++ b/app/plugins/builtins/automation/agent_runner.py @@ -11,6 +11,7 @@ Safety features: from __future__ import annotations import logging +import uuid from datetime import UTC, datetime from typing import Any @@ -37,12 +38,12 @@ async def run_agent( 3. Infinite loop: same tool 5x consecutively (handled in ReAct loop) 4. Budget limit: cumulative cost_usd """ + from app.plugins.builtins.ai_assistant.contracts import get_tool_registry from app.plugins.builtins.automation.models import ( AgentDefinition, AgentRun, AgentRunStep, ) - from app.plugins.builtins.ai_assistant.contracts import get_tool_registry factory = get_session_factory() @@ -167,7 +168,7 @@ async def run_agent( perm_ctx = await resolve_agent_permissions( db=db, tenant_id=agent.tenant_id, - user_id=agent.created_by or uuid_mod.uuid4(), + user_id=agent.created_by or uuid.uuid4(), agent_definition=agent, ) diff --git a/app/plugins/builtins/automation/plugin.py b/app/plugins/builtins/automation/plugin.py index fff33d2..78b60fe 100644 --- a/app/plugins/builtins/automation/plugin.py +++ b/app/plugins/builtins/automation/plugin.py @@ -248,17 +248,23 @@ class AutomationPlugin(BasePlugin): logger.exception("Failed to register own cron jobs") # Register pre-built agents in DB (if not already present) try: - from app.plugins.builtins.automation.models import AgentDefinition - from app.plugins.builtins.automation.prebuilt.email_triage_agent import create_email_triage_agent - from app.plugins.builtins.automation.prebuilt.contact_enrichment_agent import create_contact_enrichment_agent - from app.plugins.builtins.automation.prebuilt.follow_up_agent import create_follow_up_agent - from app.plugins.builtins.automation.prebuilt.report_agent import create_report_agent from sqlalchemy import select as sa_select # Get system tenant + admin user for seeding (ARCH-043: # deterministic slug lookup instead of arbitrary first row) from app.core.db import get_system_tenant - from app.models.user import User + from app.models.user import User, UserTenant + from app.plugins.builtins.automation.models import AgentDefinition + from app.plugins.builtins.automation.prebuilt.contact_enrichment_agent import ( + create_contact_enrichment_agent, + ) + from app.plugins.builtins.automation.prebuilt.email_triage_agent import ( + create_email_triage_agent, + ) + from app.plugins.builtins.automation.prebuilt.follow_up_agent import ( + create_follow_up_agent, + ) + from app.plugins.builtins.automation.prebuilt.report_agent import create_report_agent tenant = await get_system_tenant(db) if tenant: @@ -298,15 +304,14 @@ class AutomationPlugin(BasePlugin): def _register_workflow_agent_tools(self) -> None: """Register I-AW agent tools for starting and inspecting workflows.""" import uuid - from typing import Any from app.ai.tool_registry import get_tool_registry registry = get_tool_registry() async def _start_workflow_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: """Start a workflow by ID.""" - from app.services.workflow_service import create_instance from app.core.db import get_worker_session_factory + from app.services.workflow_service import create_instance workflow_id = arguments.get("workflow_id", "") tenant_id = context.get("tenant_id") user_id = context.get("user_id") @@ -342,8 +347,9 @@ class AutomationPlugin(BasePlugin): async def _check_workflow_status_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: """Check the status of a workflow instance.""" from sqlalchemy import select - from app.models.workflow import WorkflowInstance + from app.core.db import get_worker_session_factory + from app.models.workflow import WorkflowInstance instance_id = arguments.get("instance_id", "") tenant_id = context.get("tenant_id") if not instance_id or not tenant_id: diff --git a/app/plugins/builtins/automation/prebuilt/contact_enrichment_agent.py b/app/plugins/builtins/automation/prebuilt/contact_enrichment_agent.py index 4127b34..6906ae5 100644 --- a/app/plugins/builtins/automation/prebuilt/contact_enrichment_agent.py +++ b/app/plugins/builtins/automation/prebuilt/contact_enrichment_agent.py @@ -3,7 +3,9 @@ Enriches contact data by searching for related information. """ from __future__ import annotations + import uuid + from app.plugins.builtins.automation.models import AgentDefinition CONTACT_ENRICHMENT_SYSTEM_PROMPT = """You are a Contact Enrichment Agent for a CRM system. diff --git a/app/plugins/builtins/automation/prebuilt/email_triage_agent.py b/app/plugins/builtins/automation/prebuilt/email_triage_agent.py index 21539c5..66fc9a0 100644 --- a/app/plugins/builtins/automation/prebuilt/email_triage_agent.py +++ b/app/plugins/builtins/automation/prebuilt/email_triage_agent.py @@ -3,7 +3,9 @@ Sorts and prioritizes incoming emails automatically. """ from __future__ import annotations + import uuid + from app.plugins.builtins.automation.models import AgentDefinition EMAIL_TRIAGE_SYSTEM_PROMPT = """You are an E-Mail Triage Agent for a CRM system. diff --git a/app/plugins/builtins/automation/prebuilt/follow_up_agent.py b/app/plugins/builtins/automation/prebuilt/follow_up_agent.py index 1f1512b..ef6f521 100644 --- a/app/plugins/builtins/automation/prebuilt/follow_up_agent.py +++ b/app/plugins/builtins/automation/prebuilt/follow_up_agent.py @@ -3,7 +3,9 @@ Reminds about and creates follow-up tasks for contacts. """ from __future__ import annotations + import uuid + from app.plugins.builtins.automation.models import AgentDefinition FOLLOW_UP_SYSTEM_PROMPT = """You are a Follow-up Agent for a CRM system. diff --git a/app/plugins/builtins/automation/prebuilt/report_agent.py b/app/plugins/builtins/automation/prebuilt/report_agent.py index f60c9dc..b3e878e 100644 --- a/app/plugins/builtins/automation/prebuilt/report_agent.py +++ b/app/plugins/builtins/automation/prebuilt/report_agent.py @@ -3,7 +3,9 @@ Generates reports from CRM data using search and API tools. """ from __future__ import annotations + import uuid + from app.plugins.builtins.automation.models import AgentDefinition REPORT_SYSTEM_PROMPT = """You are a Report Agent for a CRM system. diff --git a/app/plugins/builtins/automation/tests/test_automation.py b/app/plugins/builtins/automation/tests/test_automation.py index b118d16..77bf940 100644 --- a/app/plugins/builtins/automation/tests/test_automation.py +++ b/app/plugins/builtins/automation/tests/test_automation.py @@ -7,7 +7,12 @@ PGUUID/JSONB column types. from __future__ import annotations +# Register ALL plugin models so create_all can resolve cross-plugin FKs +# (e.g. entity_attachments.dms_file_id -> files) — same pattern as +# scripts/sync_plugin_schema.py. +import importlib import os +import pkgutil import uuid from collections.abc import AsyncGenerator from datetime import UTC, datetime, timedelta @@ -17,17 +22,10 @@ import pytest_asyncio from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine -from app.core.db import Base import app.models # noqa: F401 — registers core models import app.models.outbox # noqa: F401 — event_outbox is NOT re-exported by app.models - -# Register ALL plugin models so create_all can resolve cross-plugin FKs -# (e.g. entity_attachments.dms_file_id -> files) — same pattern as -# scripts/sync_plugin_schema.py. -import importlib -import pkgutil - import app.plugins.builtins as _builtins_pkg +from app.core.db import Base for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__): if not _ispkg: @@ -39,11 +37,11 @@ for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__): except Exception: # pragma: no cover - defensive pass -from app.plugins.builtins.automation.models import ( +from app.plugins.builtins.automation.models import ( # noqa: E402 — after dynamic plugin-model discovery AgentRun, AutomationRun, ) -from app.plugins.builtins.automation.services import ( +from app.plugins.builtins.automation.services import ( # noqa: E402 — after dynamic plugin-model discovery AgentService, AutomationService, CronJobService, diff --git a/app/plugins/builtins/calendar/models.py b/app/plugins/builtins/calendar/models.py index 9a5e151..4d61ecc 100644 --- a/app/plugins/builtins/calendar/models.py +++ b/app/plugins/builtins/calendar/models.py @@ -6,6 +6,7 @@ import uuid from datetime import datetime from typing import Any +from pgvector.sqlalchemy import Vector from sqlalchemy import ( Boolean, DateTime, @@ -13,14 +14,12 @@ from sqlalchemy import ( Index, String, ) -from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin -from sqlalchemy.dialects.postgresql import TSVECTOR -from pgvector.sqlalchemy import Vector class Calendar(Base, TenantMixin, OwnedMixin): diff --git a/app/plugins/builtins/calendar/routes.py b/app/plugins/builtins/calendar/routes.py index b3b3fab..c7e699a 100644 --- a/app/plugins/builtins/calendar/routes.py +++ b/app/plugins/builtins/calendar/routes.py @@ -21,6 +21,7 @@ from fastapi.responses import StreamingResponse from sqlalchemy import select, update from sqlalchemy.ext.asyncio import AsyncSession +from app.core.audit import log_audit from app.core.db import get_db from app.deps import get_current_user, require_admin, require_permission from app.plugins.builtins.calendar.ics_utils import ( @@ -1023,5 +1024,3 @@ async def book_resource( "start_at": booking.start_at.isoformat(), "end_at": booking.end_at.isoformat(), } - -from app.core.audit import log_audit \ No newline at end of file diff --git a/app/plugins/builtins/dms/models.py b/app/plugins/builtins/dms/models.py index 66697d4..94d19e8 100644 --- a/app/plugins/builtins/dms/models.py +++ b/app/plugins/builtins/dms/models.py @@ -4,16 +4,16 @@ from __future__ import annotations import uuid from datetime import datetime +from typing import Any -from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, UniqueConstraint, Text +from pgvector.sqlalchemy import Vector +from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, Text, UniqueConstraint +from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin -from pgvector.sqlalchemy import Vector -from sqlalchemy.dialects.postgresql import TSVECTOR -from typing import Any class Folder(Base, TenantMixin, OwnedMixin): diff --git a/app/plugins/builtins/knowledge/jobs.py b/app/plugins/builtins/knowledge/jobs.py index 04a8d06..c307c1a 100644 --- a/app/plugins/builtins/knowledge/jobs.py +++ b/app/plugins/builtins/knowledge/jobs.py @@ -25,7 +25,8 @@ async def cleanup_knowledge_job(ctx: dict[str, Any]) -> None: Runs daily. Keeps approved extractions indefinitely. Iterates per-tenant for RLS compliance. """ - from sqlalchemy import text as sa_text, delete as sa_delete + from sqlalchemy import delete as sa_delete + from sqlalchemy import text as sa_text from app.core.db import get_worker_session_factory diff --git a/app/plugins/builtins/knowledge/models.py b/app/plugins/builtins/knowledge/models.py index 57c6163..1facc3f 100644 --- a/app/plugins/builtins/knowledge/models.py +++ b/app/plugins/builtins/knowledge/models.py @@ -1,12 +1,17 @@ """Knowledge extraction models — tracks LLM extractions and review queue.""" from __future__ import annotations + import uuid from datetime import datetime -from sqlalchemy import DateTime, Float, ForeignKey, Index, Integer, String, Text, func -from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID + +from sqlalchemy import DateTime, Float, ForeignKey, Index, String, Text, func +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 + class KnowledgeExtraction(Base, TenantMixin): """Tracks a single knowledge extraction run from a source (wiki, dms, mail, comm).""" __tablename__ = "knowledge_extractions" diff --git a/app/plugins/builtins/knowledge/plugin.py b/app/plugins/builtins/knowledge/plugin.py index 02d2195..7b31c37 100644 --- a/app/plugins/builtins/knowledge/plugin.py +++ b/app/plugins/builtins/knowledge/plugin.py @@ -1,8 +1,10 @@ """Knowledge plugin — LLM-based entity/relationship extraction, ask-knowledge, review queue.""" from __future__ import annotations + import logging import uuid from typing import Any + from app.plugins.base import BasePlugin from app.plugins.manifest import PluginManifest, PluginRouteDef @@ -78,8 +80,8 @@ class KnowledgePlugin(BasePlugin): async def _ask_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: """Ask a knowledge question.""" - from app.plugins.builtins.knowledge.services import ask_knowledge from app.core.db import get_worker_session_factory + from app.plugins.builtins.knowledge.services import ask_knowledge question = arguments.get("question", "") tenant_id = context.get("tenant_id") if not question or not tenant_id: @@ -107,8 +109,8 @@ class KnowledgePlugin(BasePlugin): async def _search_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]: """Search wiki articles via unified search.""" - from app.plugins.builtins.unified_search.provider_registry import get_search_registry from app.core.db import get_worker_session_factory + from app.plugins.builtins.unified_search.provider_registry import get_search_registry query = arguments.get("query", "") tenant_id = context.get("tenant_id") if not query or not tenant_id: diff --git a/app/plugins/builtins/knowledge/services.py b/app/plugins/builtins/knowledge/services.py index c40dff5..4ea5034 100644 --- a/app/plugins/builtins/knowledge/services.py +++ b/app/plugins/builtins/knowledge/services.py @@ -1,10 +1,13 @@ """Knowledge extraction services — LLM-based entity/relationship extraction.""" from __future__ import annotations + import logging import uuid from typing import Any -from sqlalchemy import select, update + +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession + from app.ai.llm_client import llm_complete from app.plugins.builtins.knowledge.models import KnowledgeExtraction diff --git a/app/plugins/builtins/mail/models.py b/app/plugins/builtins/mail/models.py index 8b89980..b565538 100644 --- a/app/plugins/builtins/mail/models.py +++ b/app/plugins/builtins/mail/models.py @@ -4,7 +4,9 @@ from __future__ import annotations import uuid from datetime import UTC, datetime +from typing import Any +from pgvector.sqlalchemy import Vector from sqlalchemy import ( JSON, Boolean, @@ -16,14 +18,12 @@ from sqlalchemy import ( Text, UniqueConstraint, ) +from sqlalchemy.dialects.postgresql import TSVECTOR from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin -from sqlalchemy.dialects.postgresql import TSVECTOR -from pgvector.sqlalchemy import Vector -from typing import Any # --- Mail Accounts (F-MAIL-14, F-MAIL-18) --- diff --git a/app/plugins/builtins/mail/routes.py b/app/plugins/builtins/mail/routes.py index aa6daba..dfcde98 100644 --- a/app/plugins/builtins/mail/routes.py +++ b/app/plugins/builtins/mail/routes.py @@ -1862,4 +1862,3 @@ async def get_mail( ) return mail_to_response(mail, attachments=list(attachments), labels=list(label_assignments)) -from app.core.audit import log_audit \ No newline at end of file diff --git a/app/plugins/builtins/self_improvement/models.py b/app/plugins/builtins/self_improvement/models.py index 0bb55b6..1ae5346 100644 --- a/app/plugins/builtins/self_improvement/models.py +++ b/app/plugins/builtins/self_improvement/models.py @@ -18,7 +18,8 @@ from sqlalchemy import ( Text, func, ) -from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID +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 diff --git a/app/plugins/builtins/self_improvement/services.py b/app/plugins/builtins/self_improvement/services.py index 5d4d7d0..3e01cd3 100644 --- a/app/plugins/builtins/self_improvement/services.py +++ b/app/plugins/builtins/self_improvement/services.py @@ -21,11 +21,10 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.ai.llm_client import llm_complete from app.plugins.builtins.self_improvement.models import ( + ImpactMeasurement, ImprovementPattern, ImprovementProposal, ImprovementSignal, - ImpactMeasurement, - PROPOSAL_STATUSES, ) logger = logging.getLogger(__name__) diff --git a/app/plugins/builtins/tags/models.py b/app/plugins/builtins/tags/models.py index 9a28104..92c2f5a 100644 --- a/app/plugins/builtins/tags/models.py +++ b/app/plugins/builtins/tags/models.py @@ -3,16 +3,16 @@ from __future__ import annotations import uuid +from typing import Any +from pgvector.sqlalchemy import Vector from sqlalchemy import ForeignKey, Index, String, UniqueConstraint -from sqlalchemy.dialects.postgresql import UUID as PGUUID, JSONB +from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR +from sqlalchemy.dialects.postgresql import UUID as PGUUID from sqlalchemy.orm import Mapped, mapped_column from app.core.db import Base, TenantMixin from app.models.owned_mixin import OwnedMixin -from sqlalchemy.dialects.postgresql import TSVECTOR -from pgvector.sqlalchemy import Vector -from typing import Any class Tag(Base, TenantMixin, OwnedMixin): diff --git a/app/plugins/builtins/tags/routes.py b/app/plugins/builtins/tags/routes.py index 7ad7ab2..ed8e52f 100644 --- a/app/plugins/builtins/tags/routes.py +++ b/app/plugins/builtins/tags/routes.py @@ -8,6 +8,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Response, status from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession +from app.core.audit import log_audit from app.core.db import get_db from app.core.visibility import apply_visibility_filter from app.deps import get_current_user, require_permission @@ -389,5 +390,3 @@ async def list_tag_entities( } for a in assignments ] - -from app.core.audit import log_audit \ No newline at end of file diff --git a/app/plugins/builtins/tasks/routes.py b/app/plugins/builtins/tasks/routes.py index 122ea97..31f09cf 100644 --- a/app/plugins/builtins/tasks/routes.py +++ b/app/plugins/builtins/tasks/routes.py @@ -7,6 +7,7 @@ import uuid from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status from sqlalchemy.ext.asyncio import AsyncSession +from app.core.audit import log_audit from app.core.db import get_db from app.deps import get_current_user, require_permission from app.plugins.builtins.tasks import services @@ -126,6 +127,7 @@ async def delete_task( ): """Delete a task (soft-delete).""" tenant_id = uuid.UUID(current_user["tenant_id"]) + user_id = uuid.UUID(current_user["user_id"]) tid = _parse_uuid(task_id, "task_id") deleted = await services.delete_task(db, tenant_id, tid) if not deleted: @@ -261,5 +263,3 @@ async def decompose_goal( if result is None: raise HTTPException(404, detail={"detail": "Goal not found", "code": "not_found"}) return result - -from app.core.audit import log_audit \ No newline at end of file diff --git a/app/plugins/builtins/tasks/schemas.py b/app/plugins/builtins/tasks/schemas.py index 388e87a..cc5af43 100644 --- a/app/plugins/builtins/tasks/schemas.py +++ b/app/plugins/builtins/tasks/schemas.py @@ -7,7 +7,6 @@ from typing import Any from pydantic import BaseModel, Field - TASK_STATUSES = "^(open|in_progress|review|blocked|done|cancelled)$" TASK_TYPES = "^(todo|approval|follow_up|review|goal|milestone|agent_subtask)$" ASSIGNEE_TYPES = "^(user|agent|group)$" diff --git a/app/plugins/builtins/tasks/services.py b/app/plugins/builtins/tasks/services.py index 5e7fce9..7e0a110 100644 --- a/app/plugins/builtins/tasks/services.py +++ b/app/plugins/builtins/tasks/services.py @@ -16,12 +16,11 @@ def _to_uuid(val: str | UUID | None) -> UUID | None: return val return uuid.UUID(str(val)) -from sqlalchemy import func, select -from sqlalchemy.ext.asyncio import AsyncSession - -from app.core.visibility import apply_visibility_filter -from app.plugins.builtins.tasks.models import Task +from sqlalchemy import func, select # noqa: E402 — after helper defs by design +from sqlalchemy.ext.asyncio import AsyncSession # noqa: E402 +from app.core.visibility import apply_visibility_filter # noqa: E402 +from app.plugins.builtins.tasks.models import Task # noqa: E402 # Lifecycle statuses in display order (Kanban columns) STATUS_ORDER = ["open", "in_progress", "review", "blocked", "done", "cancelled"] @@ -307,7 +306,6 @@ async def create_task( entity_id = contact_id # Don't store contact_id in FK column if it's just a polymorphic entity link # — contact_id FK requires a real Contact row. Use entity_id instead. - contact_id_for_fk = _to_uuid(contact_id) if contact_id else None # Resolve polymorphic creator. creator_type = data.get("creator_type", "user") diff --git a/app/plugins/builtins/unified_search/contracts.py b/app/plugins/builtins/unified_search/contracts.py index ab00070..26a0066 100644 --- a/app/plugins/builtins/unified_search/contracts.py +++ b/app/plugins/builtins/unified_search/contracts.py @@ -2,6 +2,8 @@ from __future__ import annotations +from typing import Any + from app.plugins.builtins.contracts import get_contract_registry from app.plugins.builtins.unified_search.base_provider import BaseSearchProvider from app.plugins.builtins.unified_search.embedding import generate_embedding diff --git a/app/plugins/builtins/unified_search/models.py b/app/plugins/builtins/unified_search/models.py index af9beaa..67256c6 100644 --- a/app/plugins/builtins/unified_search/models.py +++ b/app/plugins/builtins/unified_search/models.py @@ -50,8 +50,7 @@ class SearchIndexLog(Base, TenantMixin, OwnedMixin): 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 # noqa: E402 -from app.models.owned_mixin import OwnedMixin +from pgvector.sqlalchemy import Vector # noqa: E402 — optional dependency import after model defs class DocumentChunk(Base, TenantMixin, OwnedMixin): diff --git a/app/plugins/builtins/wiki/plugin.py b/app/plugins/builtins/wiki/plugin.py index 0278cc0..20575e2 100644 --- a/app/plugins/builtins/wiki/plugin.py +++ b/app/plugins/builtins/wiki/plugin.py @@ -1,6 +1,8 @@ """Wiki plugin - knowledge articles, categories, versioning.""" from __future__ import annotations + import logging + from app.plugins.base import BasePlugin from app.plugins.manifest import FrontendMenuItem, FrontendPageRoute, PluginManifest, PluginRouteDef @@ -28,7 +30,9 @@ class WikiPlugin(BasePlugin): from app.plugins.builtins.contracts import get_contract search_contract = get_contract("unified_search") if search_contract and hasattr(search_contract, "get_search_registry"): - from app.plugins.builtins.unified_search.providers.wiki_provider import WikiSearchProvider + from app.plugins.builtins.unified_search.providers.wiki_provider import ( + WikiSearchProvider, + ) search_contract.get_search_registry().register(WikiSearchProvider()) logger.info("Registered WikiSearchProvider via contract") else: diff --git a/app/plugins/builtins/wiki/routes.py b/app/plugins/builtins/wiki/routes.py index 760322e..05bb7fb 100644 --- a/app/plugins/builtins/wiki/routes.py +++ b/app/plugins/builtins/wiki/routes.py @@ -1,13 +1,20 @@ """Wiki plugin routes — articles CRUD, categories, versioning.""" from __future__ import annotations + import uuid + from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.ext.asyncio import AsyncSession -from app.core.db import get_db -from app.deps import get_current_user, require_permission -from app.plugins.builtins.wiki import services -from app.plugins.builtins.wiki.schemas import ArticleCreate, ArticleUpdate, CategoryCreate, CategoryUpdate + from app.core.audit import log_audit +from app.core.db import get_db +from app.deps import require_permission +from app.plugins.builtins.wiki import services +from app.plugins.builtins.wiki.schemas import ( + ArticleCreate, + ArticleUpdate, + CategoryCreate, +) router = APIRouter(prefix="/api/v1/wiki", tags=["wiki"]) diff --git a/app/plugins/builtins/wiki/schemas.py b/app/plugins/builtins/wiki/schemas.py index 3f381b5..17f5923 100644 --- a/app/plugins/builtins/wiki/schemas.py +++ b/app/plugins/builtins/wiki/schemas.py @@ -1,8 +1,9 @@ """Pydantic schemas for the Wiki plugin.""" from __future__ import annotations -from datetime import datetime + from pydantic import BaseModel, Field + class CategoryCreate(BaseModel): name: str = Field(..., min_length=1, max_length=200) slug: str = Field(..., min_length=1, max_length=200) diff --git a/app/plugins/builtins/wiki/services.py b/app/plugins/builtins/wiki/services.py index daac237..76fb478 100644 --- a/app/plugins/builtins/wiki/services.py +++ b/app/plugins/builtins/wiki/services.py @@ -1,11 +1,13 @@ """Wiki plugin services — CRUD for articles, categories, versioning.""" from __future__ import annotations + import uuid from datetime import UTC, datetime from typing import Any -from sqlalchemy import select, func + +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from app.core.visibility import apply_visibility_filter + from app.plugins.builtins.wiki.models import WikiArticle, WikiArticleVersion, WikiCategory diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 032f36c..7ea611a 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -4,9 +4,9 @@ from app.routes import ( addresses, # noqa: F401 attachments, # noqa: F401 audit, # noqa: F401 - compliance, # noqa: F401 auth, # noqa: F401 bank_accounts, # noqa: F401 + compliance, # noqa: F401 currencies, # noqa: F401 dashboard, # noqa: F401 entity_history, # noqa: F401 diff --git a/app/routes/approvals.py b/app/routes/approvals.py index f20b5f9..00b9e78 100644 --- a/app/routes/approvals.py +++ b/app/routes/approvals.py @@ -7,7 +7,7 @@ from __future__ import annotations import logging import uuid -from datetime import UTC, datetime +from datetime import datetime from typing import Any from fastapi import APIRouter, Depends, HTTPException, Query @@ -16,7 +16,6 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.core.approval import ( - APPROVAL_STATUSES, ApprovalRequest, create_approval_request, expire_approval_request, diff --git a/app/routes/audit.py b/app/routes/audit.py index 3c38ae0..737a903 100644 --- a/app/routes/audit.py +++ b/app/routes/audit.py @@ -10,7 +10,7 @@ from datetime import UTC, datetime, timedelta from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse -from sqlalchemy import func, select, delete +from sqlalchemy import delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db diff --git a/app/routes/compliance.py b/app/routes/compliance.py index 9eef822..4e12847 100644 --- a/app/routes/compliance.py +++ b/app/routes/compliance.py @@ -11,7 +11,7 @@ from typing import Any from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field -from sqlalchemy import select, func +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.ai.ai_use_case import AIUseCaseMetadata, validate_ai_use_case diff --git a/app/routes/plugins.py b/app/routes/plugins.py index 380146f..97adf7b 100644 --- a/app/routes/plugins.py +++ b/app/routes/plugins.py @@ -10,8 +10,7 @@ from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_db -from app.deps import require_admin, require_permission -from app.deps import get_current_user +from app.deps import get_current_user, require_admin, require_permission from app.plugins.migration_runner import MigrationValidationError from app.services.plugin_service import get_plugin_service diff --git a/app/routes/workflows.py b/app/routes/workflows.py index cdbd805..51d0e63 100644 --- a/app/routes/workflows.py +++ b/app/routes/workflows.py @@ -173,7 +173,6 @@ async def create_instance( """Create a new workflow instance.""" tenant_id = uuid.UUID(current_user["tenant_id"]) user_id = uuid.UUID(current_user["user_id"]) - is_admin = current_user.get("is_system_admin", False) try: result = await workflow_service.create_instance( @@ -304,10 +303,11 @@ async def resume_instance( """Resume a waiting workflow instance (e.g. after wait timer expired).""" tenant_id = uuid.UUID(current_user["tenant_id"]) - from app.workflows.engine import WorkflowEngine - from app.models.workflow import WorkflowInstance from sqlalchemy import select + from app.models.workflow import WorkflowInstance + from app.workflows.engine import WorkflowEngine + result = await db.execute( select(WorkflowInstance).where( WorkflowInstance.id == uuid.UUID(instance_id), @@ -375,9 +375,10 @@ async def webhook_trigger( db: AsyncSession = Depends(get_db), ): """Incoming webhook trigger — starts a workflow via secure token.""" - from app.models.webhook import Webhook from sqlalchemy import select + from app.models.webhook import Webhook + result = await db.execute( select(Webhook).where( Webhook.token == token, @@ -423,9 +424,10 @@ async def get_instance_history( """Get step history for a workflow instance (execution log).""" tenant_id = uuid.UUID(current_user["tenant_id"]) - from app.models.workflow import WorkflowStepHistory from sqlalchemy import select + from app.models.workflow import WorkflowStepHistory + result = await db.execute( select(WorkflowStepHistory) .where( @@ -472,9 +474,10 @@ async def approve_workflow_step( body = {} comment = body.get("comment", "") + from sqlalchemy import select + from app.core.approval import create_approval_request, resolve_approval_request from app.models.workflow import WorkflowInstance - from sqlalchemy import select result = await db.execute( select(WorkflowInstance).where( @@ -535,9 +538,10 @@ async def reject_workflow_step( body = {} comment = body.get("comment", "") + from sqlalchemy import select + from app.core.approval import create_approval_request, resolve_approval_request from app.models.workflow import WorkflowInstance - from sqlalchemy import select result = await db.execute( select(WorkflowInstance).where( diff --git a/app/workflows/engine.py b/app/workflows/engine.py index e9e088b..217ba3b 100644 --- a/app/workflows/engine.py +++ b/app/workflows/engine.py @@ -15,7 +15,7 @@ from __future__ import annotations import logging import time import uuid -from datetime import UTC, datetime +from datetime import UTC, datetime, timedelta from typing import Any from sqlalchemy import select @@ -313,7 +313,6 @@ class WorkflowEngine: return _instance_to_dict(instance) else: # Retry: stay on same step, set resume_at with backoff - import asyncio backoff = min(2 ** instance.retry_count, 60) from datetime import timedelta instance.resume_at = datetime.now(UTC) + timedelta(seconds=backoff) @@ -572,8 +571,8 @@ class WorkflowEngine: Prevents two workers from processing the same instance simultaneously. """ + from app.core.redis import get_redis - import redis.asyncio as aioredis try: r = await get_redis() diff --git a/app/workflows/step_handlers.py b/app/workflows/step_handlers.py index 35153fd..c536a93 100644 --- a/app/workflows/step_handlers.py +++ b/app/workflows/step_handlers.py @@ -14,7 +14,6 @@ import uuid from datetime import UTC, datetime, timedelta from typing import Any -from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.workflow import WorkflowInstance @@ -45,7 +44,8 @@ class StepResult: self.abort = abort -type StepHandler = Any # Callable[..., Awaitable[StepResult]] +# Callable[..., Awaitable[StepResult]] — classic alias for py3.11 compat +StepHandler = Any # ─── Registry ──────────────────────────────────────────────────────────────── @@ -534,6 +534,7 @@ async def _handle_webhook( return StepResult(error=f"URL blocked by SSRF protection: {url}", abort=True) import json + import httpx try: