fix(e7): CI-Gate-Vorbereitung — ruff über app/ von 105 auf 0 Findings bereinigt; 8 echte F821-NameError-Produktionsbugs behoben (external_api stream_chat-Call-Signatur an stream_chat_comm angepasst, agent_runner uuid vor lokalem Import, automation/plugin UserTenant-Import, tasks delete-audit user_id, workflows/engine timedelta, unified_search/contracts Any); py311-kompatibles StepHandler-Alias statt type-Statement; E402/F841 bereinigt; Verifikation 85/89 grün (4 Failures = bekannter Vorbestand BUG-099)
Check Cross-Plugin Imports / check (push) Has been cancelled

This commit is contained in:
Agent Zero
2026-08-24 13:36:17 +02:00
parent 3934aea6ef
commit 197b0d3bab
54 changed files with 151 additions and 110 deletions
+1 -1
View File
@@ -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
-2
View File
@@ -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
-2
View File
@@ -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__)
-1
View File
@@ -6,7 +6,6 @@ import uuid
from dataclasses import dataclass, field
from typing import Any
LOW_CONFIDENCE_THRESHOLD = 0.6
-1
View File
@@ -7,7 +7,6 @@ from typing import Any
from app.ai.knowledge_sources import get_source_config
EXTRACTION_TRIGGERS = {
"mail.received",
"dms.file_uploaded",
+1 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import Any
+6 -2
View File
@@ -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
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -2
View File
@@ -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.
-1
View File
@@ -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):
@@ -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):
@@ -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:
@@ -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()
@@ -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}"
@@ -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
@@ -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,
)
+15 -9
View File
@@ -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:
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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,
+2 -3
View File
@@ -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):
+1 -2
View File
@@ -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
+4 -4
View File
@@ -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):
+2 -1
View File
@@ -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
+7 -2
View File
@@ -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"
+4 -2
View File
@@ -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:
+4 -1
View File
@@ -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
+3 -3
View File
@@ -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) ---
-1
View File
@@ -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
@@ -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
@@ -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__)
+4 -4
View File
@@ -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):
+1 -2
View File
@@ -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
+2 -2
View File
@@ -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
-1
View File
@@ -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)$"
+4 -6
View File
@@ -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")
@@ -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
@@ -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):
+5 -1
View File
@@ -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:
+11 -4
View File
@@ -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"])
+2 -1
View File
@@ -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)
+4 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -2
View File
@@ -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,
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -2
View File
@@ -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
+11 -7
View File
@@ -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(
+2 -3
View File
@@ -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()
+3 -2
View File
@@ -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: