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
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
+3
-1
@@ -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-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 |
|
| 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-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).
|
**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).
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ import logging
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from app.core.sensitive_data import sanitize_dict
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.ai.ai_use_case import AIUseCaseMetadata
|
from app.ai.ai_use_case import AIUseCaseMetadata
|
||||||
from app.core.sensitive_data import (
|
from app.core.sensitive_data import (
|
||||||
SENSITIVE_FIELDS,
|
SENSITIVE_FIELDS,
|
||||||
filter_for_llm_context,
|
|
||||||
get_data_class_for_field,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import uuid
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
LOW_CONFIDENCE_THRESHOLD = 0.6
|
LOW_CONFIDENCE_THRESHOLD = 0.6
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from typing import Any
|
|||||||
|
|
||||||
from app.ai.knowledge_sources import get_source_config
|
from app.ai.knowledge_sources import get_source_config
|
||||||
|
|
||||||
|
|
||||||
EXTRACTION_TRIGGERS = {
|
EXTRACTION_TRIGGERS = {
|
||||||
"mail.received",
|
"mail.received",
|
||||||
"dms.file_uploaded",
|
"dms.file_uploaded",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -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.
|
Runs daily to prevent the audit_log table from growing indefinitely.
|
||||||
Iterates per-tenant for RLS compliance.
|
Iterates per-tenant for RLS compliance.
|
||||||
"""
|
"""
|
||||||
from sqlalchemy import text as sa_text, delete as sa_delete
|
|
||||||
from datetime import UTC, datetime, timedelta
|
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.core.db import get_worker_session_factory
|
||||||
from app.models.audit import AuditLog
|
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.
|
Runs daily to clean up the trash. Iterates per-tenant for RLS compliance.
|
||||||
Default retention: 90 days in trash before permanent deletion.
|
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 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.core.db import get_worker_session_factory
|
||||||
from app.models.contact import Contact
|
from app.models.contact import Contact
|
||||||
from app.models.entity_attachment import EntityAttachment
|
from app.models.entity_attachment import EntityAttachment
|
||||||
|
|||||||
+2
-2
@@ -36,13 +36,14 @@ from app.routes import ( # noqa: E402
|
|||||||
attachments,
|
attachments,
|
||||||
audit,
|
audit,
|
||||||
auth,
|
auth,
|
||||||
compliance,
|
|
||||||
backups,
|
backups,
|
||||||
bank_accounts,
|
bank_accounts,
|
||||||
|
compliance,
|
||||||
currencies,
|
currencies,
|
||||||
custom_field_definitions,
|
custom_field_definitions,
|
||||||
custom_fields,
|
custom_fields,
|
||||||
dashboard,
|
dashboard,
|
||||||
|
delegations,
|
||||||
entity_history,
|
entity_history,
|
||||||
entity_permissions,
|
entity_permissions,
|
||||||
errors,
|
errors,
|
||||||
@@ -56,7 +57,6 @@ from app.routes import ( # noqa: E402
|
|||||||
owner_transfer,
|
owner_transfer,
|
||||||
permission_templates,
|
permission_templates,
|
||||||
plugins,
|
plugins,
|
||||||
delegations,
|
|
||||||
policies,
|
policies,
|
||||||
roles,
|
roles,
|
||||||
saved_filters,
|
saved_filters,
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from app.models.audit import AuditLog
|
|||||||
from app.models.auth import ApiToken, PasswordResetToken
|
from app.models.auth import ApiToken, PasswordResetToken
|
||||||
from app.models.backup import Backup
|
from app.models.backup import Backup
|
||||||
from app.models.bank_account import BankAccount
|
from app.models.bank_account import BankAccount
|
||||||
from app.models.consumer_inbox import ConsumerInbox
|
|
||||||
from app.models.compliance import ComplianceIncident
|
from app.models.compliance import ComplianceIncident
|
||||||
|
from app.models.consumer_inbox import ConsumerInbox
|
||||||
from app.models.contact import Contact, ContactPerson
|
from app.models.contact import Contact, ContactPerson
|
||||||
from app.models.contact_folder import ContactFolder
|
from app.models.contact_folder import ContactFolder
|
||||||
from app.models.contact_merge import ContactMergeHistory
|
from app.models.contact_merge import ContactMergeHistory
|
||||||
|
|||||||
+1
-2
@@ -11,13 +11,12 @@ from datetime import datetime
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import DateTime, ForeignKey, String, func
|
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.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, TenantMixin
|
from app.core.db import Base, TenantMixin
|
||||||
from app.models.owned_mixin import OwnedMixin
|
from app.models.owned_mixin import OwnedMixin
|
||||||
from sqlalchemy.dialects.postgresql import TSVECTOR
|
|
||||||
|
|
||||||
# Re-export EntityHistory as DeletionLog for backward compatibility.
|
# Re-export EntityHistory as DeletionLog for backward compatibility.
|
||||||
# Tests import DeletionLog from app.models.audit and use entity_snapshot attribute.
|
# Tests import DeletionLog from app.models.audit and use entity_snapshot attribute.
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
|||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, SoftDeleteMixin, TimestampMixin
|
from app.core.db import Base, SoftDeleteMixin, TimestampMixin
|
||||||
from app.models.owned_mixin import OwnedMixin
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base, TimestampMixin, SoftDeleteMixin):
|
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.core.db import Base, TenantMixin
|
||||||
from app.models.owned_mixin import OwnedMixin
|
from app.models.owned_mixin import OwnedMixin
|
||||||
from pgvector.sqlalchemy import Vector
|
|
||||||
|
|
||||||
|
|
||||||
class AgentMemory(Base, TenantMixin, OwnedMixin):
|
class AgentMemory(Base, TenantMixin, OwnedMixin):
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from app.plugins.builtins.ai_assistant.schemas import (
|
|||||||
ExternalAgentRequest,
|
ExternalAgentRequest,
|
||||||
ExternalAgentResponse,
|
ExternalAgentResponse,
|
||||||
)
|
)
|
||||||
|
from app.plugins.builtins.ai_assistant.services import stream_chat_comm as stream_chat
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -120,13 +121,18 @@ async def run_agent_external(
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Run the agent via streaming chat (non-streaming mode)
|
# Run the agent via streaming chat (non-streaming mode)
|
||||||
from app.plugins.builtins.ai_assistant.services import stream_chat_comm
|
|
||||||
|
|
||||||
full_response = ""
|
full_response = ""
|
||||||
async with get_db() as stream_db:
|
async with get_db() as stream_db:
|
||||||
await set_tenant_context(stream_db, tenant_id)
|
await set_tenant_context(stream_db, tenant_id)
|
||||||
async for chunk in stream_chat(
|
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":
|
if chunk.startswith("data: ") and chunk != "data: [DONE]\n\n":
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -69,10 +69,12 @@ async def push_suggestion(user_id: str, suggestion: dict[str, Any]) -> None:
|
|||||||
# Post suggestion to Communication (I-WORK-PROACTIVE)
|
# Post suggestion to Communication (I-WORK-PROACTIVE)
|
||||||
try:
|
try:
|
||||||
import uuid as uuid_mod
|
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.contracts import get_contract_registry
|
||||||
from app.plugins.builtins.kommunikation.models import CommConversation
|
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")
|
komm = get_contract_registry().get("kommunikation")
|
||||||
if komm:
|
if komm:
|
||||||
factory = get_worker_session_factory()
|
factory = get_worker_session_factory()
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ async def send_agent_message(
|
|||||||
|
|
||||||
# 2. Create a kommunikation message in a dedicated agent room
|
# 2. Create a kommunikation message in a dedicated agent room
|
||||||
try:
|
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
|
# Find or create the agent-to-agent room
|
||||||
room_name = f"agent:{from_agent_id}:{target_agent.id}"
|
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.
|
in real-time as the agent processes.
|
||||||
"""
|
"""
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from app.ai.agent_stream import stream_react_loop
|
from app.ai.agent_stream import stream_react_loop
|
||||||
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
|
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Safety features:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -37,12 +38,12 @@ async def run_agent(
|
|||||||
3. Infinite loop: same tool 5x consecutively (handled in ReAct loop)
|
3. Infinite loop: same tool 5x consecutively (handled in ReAct loop)
|
||||||
4. Budget limit: cumulative cost_usd
|
4. Budget limit: cumulative cost_usd
|
||||||
"""
|
"""
|
||||||
|
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
|
||||||
from app.plugins.builtins.automation.models import (
|
from app.plugins.builtins.automation.models import (
|
||||||
AgentDefinition,
|
AgentDefinition,
|
||||||
AgentRun,
|
AgentRun,
|
||||||
AgentRunStep,
|
AgentRunStep,
|
||||||
)
|
)
|
||||||
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
|
|
||||||
|
|
||||||
factory = get_session_factory()
|
factory = get_session_factory()
|
||||||
|
|
||||||
@@ -167,7 +168,7 @@ async def run_agent(
|
|||||||
perm_ctx = await resolve_agent_permissions(
|
perm_ctx = await resolve_agent_permissions(
|
||||||
db=db,
|
db=db,
|
||||||
tenant_id=agent.tenant_id,
|
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,
|
agent_definition=agent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -248,17 +248,23 @@ class AutomationPlugin(BasePlugin):
|
|||||||
logger.exception("Failed to register own cron jobs")
|
logger.exception("Failed to register own cron jobs")
|
||||||
# Register pre-built agents in DB (if not already present)
|
# Register pre-built agents in DB (if not already present)
|
||||||
try:
|
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
|
from sqlalchemy import select as sa_select
|
||||||
|
|
||||||
# Get system tenant + admin user for seeding (ARCH-043:
|
# Get system tenant + admin user for seeding (ARCH-043:
|
||||||
# deterministic slug lookup instead of arbitrary first row)
|
# deterministic slug lookup instead of arbitrary first row)
|
||||||
from app.core.db import get_system_tenant
|
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)
|
tenant = await get_system_tenant(db)
|
||||||
if tenant:
|
if tenant:
|
||||||
@@ -298,15 +304,14 @@ class AutomationPlugin(BasePlugin):
|
|||||||
def _register_workflow_agent_tools(self) -> None:
|
def _register_workflow_agent_tools(self) -> None:
|
||||||
"""Register I-AW agent tools for starting and inspecting workflows."""
|
"""Register I-AW agent tools for starting and inspecting workflows."""
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from app.ai.tool_registry import get_tool_registry
|
from app.ai.tool_registry import get_tool_registry
|
||||||
registry = get_tool_registry()
|
registry = get_tool_registry()
|
||||||
|
|
||||||
async def _start_workflow_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
async def _start_workflow_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Start a workflow by ID."""
|
"""Start a workflow by ID."""
|
||||||
from app.services.workflow_service import create_instance
|
|
||||||
from app.core.db import get_worker_session_factory
|
from app.core.db import get_worker_session_factory
|
||||||
|
from app.services.workflow_service import create_instance
|
||||||
workflow_id = arguments.get("workflow_id", "")
|
workflow_id = arguments.get("workflow_id", "")
|
||||||
tenant_id = context.get("tenant_id")
|
tenant_id = context.get("tenant_id")
|
||||||
user_id = context.get("user_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]:
|
async def _check_workflow_status_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Check the status of a workflow instance."""
|
"""Check the status of a workflow instance."""
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from app.models.workflow import WorkflowInstance
|
|
||||||
from app.core.db import get_worker_session_factory
|
from app.core.db import get_worker_session_factory
|
||||||
|
from app.models.workflow import WorkflowInstance
|
||||||
instance_id = arguments.get("instance_id", "")
|
instance_id = arguments.get("instance_id", "")
|
||||||
tenant_id = context.get("tenant_id")
|
tenant_id = context.get("tenant_id")
|
||||||
if not instance_id or not tenant_id:
|
if not instance_id or not tenant_id:
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
Enriches contact data by searching for related information.
|
Enriches contact data by searching for related information.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from app.plugins.builtins.automation.models import AgentDefinition
|
from app.plugins.builtins.automation.models import AgentDefinition
|
||||||
|
|
||||||
CONTACT_ENRICHMENT_SYSTEM_PROMPT = """You are a Contact Enrichment Agent for a CRM system.
|
CONTACT_ENRICHMENT_SYSTEM_PROMPT = """You are a Contact Enrichment Agent for a CRM system.
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
Sorts and prioritizes incoming emails automatically.
|
Sorts and prioritizes incoming emails automatically.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from app.plugins.builtins.automation.models import AgentDefinition
|
from app.plugins.builtins.automation.models import AgentDefinition
|
||||||
|
|
||||||
EMAIL_TRIAGE_SYSTEM_PROMPT = """You are an E-Mail Triage Agent for a CRM system.
|
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.
|
Reminds about and creates follow-up tasks for contacts.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from app.plugins.builtins.automation.models import AgentDefinition
|
from app.plugins.builtins.automation.models import AgentDefinition
|
||||||
|
|
||||||
FOLLOW_UP_SYSTEM_PROMPT = """You are a Follow-up Agent for a CRM system.
|
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.
|
Generates reports from CRM data using search and API tools.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from app.plugins.builtins.automation.models import AgentDefinition
|
from app.plugins.builtins.automation.models import AgentDefinition
|
||||||
|
|
||||||
REPORT_SYSTEM_PROMPT = """You are a Report Agent for a CRM system.
|
REPORT_SYSTEM_PROMPT = """You are a Report Agent for a CRM system.
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ PGUUID/JSONB column types.
|
|||||||
|
|
||||||
from __future__ import annotations
|
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 os
|
||||||
|
import pkgutil
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
@@ -17,17 +22,10 @@ import pytest_asyncio
|
|||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
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 # noqa: F401 — registers core models
|
||||||
import app.models.outbox # noqa: F401 — event_outbox is NOT re-exported by app.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
|
import app.plugins.builtins as _builtins_pkg
|
||||||
|
from app.core.db import Base
|
||||||
|
|
||||||
for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__):
|
for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__):
|
||||||
if not _ispkg:
|
if not _ispkg:
|
||||||
@@ -39,11 +37,11 @@ for _importer, _modname, _ispkg in pkgutil.iter_modules(_builtins_pkg.__path__):
|
|||||||
except Exception: # pragma: no cover - defensive
|
except Exception: # pragma: no cover - defensive
|
||||||
pass
|
pass
|
||||||
|
|
||||||
from app.plugins.builtins.automation.models import (
|
from app.plugins.builtins.automation.models import ( # noqa: E402 — after dynamic plugin-model discovery
|
||||||
AgentRun,
|
AgentRun,
|
||||||
AutomationRun,
|
AutomationRun,
|
||||||
)
|
)
|
||||||
from app.plugins.builtins.automation.services import (
|
from app.plugins.builtins.automation.services import ( # noqa: E402 — after dynamic plugin-model discovery
|
||||||
AgentService,
|
AgentService,
|
||||||
AutomationService,
|
AutomationService,
|
||||||
CronJobService,
|
CronJobService,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import uuid
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from pgvector.sqlalchemy import Vector
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
Boolean,
|
Boolean,
|
||||||
DateTime,
|
DateTime,
|
||||||
@@ -13,14 +14,12 @@ from sqlalchemy import (
|
|||||||
Index,
|
Index,
|
||||||
String,
|
String,
|
||||||
)
|
)
|
||||||
from sqlalchemy.dialects.postgresql import JSONB
|
from sqlalchemy.dialects.postgresql import JSONB, TSVECTOR
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, TenantMixin
|
from app.core.db import Base, TenantMixin
|
||||||
from app.models.owned_mixin import OwnedMixin
|
from app.models.owned_mixin import OwnedMixin
|
||||||
from sqlalchemy.dialects.postgresql import TSVECTOR
|
|
||||||
from pgvector.sqlalchemy import Vector
|
|
||||||
|
|
||||||
|
|
||||||
class Calendar(Base, TenantMixin, OwnedMixin):
|
class Calendar(Base, TenantMixin, OwnedMixin):
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ from fastapi.responses import StreamingResponse
|
|||||||
from sqlalchemy import select, update
|
from sqlalchemy import select, update
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.audit import log_audit
|
||||||
from app.core.db import get_db
|
from app.core.db import get_db
|
||||||
from app.deps import get_current_user, require_admin, require_permission
|
from app.deps import get_current_user, require_admin, require_permission
|
||||||
from app.plugins.builtins.calendar.ics_utils import (
|
from app.plugins.builtins.calendar.ics_utils import (
|
||||||
@@ -1023,5 +1024,3 @@ async def book_resource(
|
|||||||
"start_at": booking.start_at.isoformat(),
|
"start_at": booking.start_at.isoformat(),
|
||||||
"end_at": booking.end_at.isoformat(),
|
"end_at": booking.end_at.isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
from app.core.audit import log_audit
|
|
||||||
@@ -4,16 +4,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
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.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, TenantMixin
|
from app.core.db import Base, TenantMixin
|
||||||
from app.models.owned_mixin import OwnedMixin
|
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):
|
class Folder(Base, TenantMixin, OwnedMixin):
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ async def cleanup_knowledge_job(ctx: dict[str, Any]) -> None:
|
|||||||
Runs daily. Keeps approved extractions indefinitely.
|
Runs daily. Keeps approved extractions indefinitely.
|
||||||
Iterates per-tenant for RLS compliance.
|
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
|
from app.core.db import get_worker_session_factory
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
"""Knowledge extraction models — tracks LLM extractions and review queue."""
|
"""Knowledge extraction models — tracks LLM extractions and review queue."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
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 sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, TenantMixin
|
from app.core.db import Base, TenantMixin
|
||||||
|
|
||||||
|
|
||||||
class KnowledgeExtraction(Base, TenantMixin):
|
class KnowledgeExtraction(Base, TenantMixin):
|
||||||
"""Tracks a single knowledge extraction run from a source (wiki, dms, mail, comm)."""
|
"""Tracks a single knowledge extraction run from a source (wiki, dms, mail, comm)."""
|
||||||
__tablename__ = "knowledge_extractions"
|
__tablename__ = "knowledge_extractions"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"""Knowledge plugin — LLM-based entity/relationship extraction, ask-knowledge, review queue."""
|
"""Knowledge plugin — LLM-based entity/relationship extraction, ask-knowledge, review queue."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from app.plugins.base import BasePlugin
|
from app.plugins.base import BasePlugin
|
||||||
from app.plugins.manifest import PluginManifest, PluginRouteDef
|
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]:
|
async def _ask_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Ask a knowledge question."""
|
"""Ask a knowledge question."""
|
||||||
from app.plugins.builtins.knowledge.services import ask_knowledge
|
|
||||||
from app.core.db import get_worker_session_factory
|
from app.core.db import get_worker_session_factory
|
||||||
|
from app.plugins.builtins.knowledge.services import ask_knowledge
|
||||||
question = arguments.get("question", "")
|
question = arguments.get("question", "")
|
||||||
tenant_id = context.get("tenant_id")
|
tenant_id = context.get("tenant_id")
|
||||||
if not question or not 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]:
|
async def _search_knowledge_handler(arguments: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Search wiki articles via unified search."""
|
"""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.core.db import get_worker_session_factory
|
||||||
|
from app.plugins.builtins.unified_search.provider_registry import get_search_registry
|
||||||
query = arguments.get("query", "")
|
query = arguments.get("query", "")
|
||||||
tenant_id = context.get("tenant_id")
|
tenant_id = context.get("tenant_id")
|
||||||
if not query or not tenant_id:
|
if not query or not tenant_id:
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"""Knowledge extraction services — LLM-based entity/relationship extraction."""
|
"""Knowledge extraction services — LLM-based entity/relationship extraction."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from sqlalchemy import select, update
|
|
||||||
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.ai.llm_client import llm_complete
|
from app.ai.llm_client import llm_complete
|
||||||
from app.plugins.builtins.knowledge.models import KnowledgeExtraction
|
from app.plugins.builtins.knowledge.models import KnowledgeExtraction
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pgvector.sqlalchemy import Vector
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
JSON,
|
JSON,
|
||||||
Boolean,
|
Boolean,
|
||||||
@@ -16,14 +18,12 @@ from sqlalchemy import (
|
|||||||
Text,
|
Text,
|
||||||
UniqueConstraint,
|
UniqueConstraint,
|
||||||
)
|
)
|
||||||
|
from sqlalchemy.dialects.postgresql import TSVECTOR
|
||||||
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, TenantMixin
|
from app.core.db import Base, TenantMixin
|
||||||
from app.models.owned_mixin import OwnedMixin
|
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) ---
|
# --- Mail Accounts (F-MAIL-14, F-MAIL-18) ---
|
||||||
|
|
||||||
|
|||||||
@@ -1862,4 +1862,3 @@ async def get_mail(
|
|||||||
)
|
)
|
||||||
return mail_to_response(mail, attachments=list(attachments), labels=list(label_assignments))
|
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,
|
Text,
|
||||||
func,
|
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 sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, TenantMixin
|
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.ai.llm_client import llm_complete
|
||||||
from app.plugins.builtins.self_improvement.models import (
|
from app.plugins.builtins.self_improvement.models import (
|
||||||
|
ImpactMeasurement,
|
||||||
ImprovementPattern,
|
ImprovementPattern,
|
||||||
ImprovementProposal,
|
ImprovementProposal,
|
||||||
ImprovementSignal,
|
ImprovementSignal,
|
||||||
ImpactMeasurement,
|
|
||||||
PROPOSAL_STATUSES,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -3,16 +3,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pgvector.sqlalchemy import Vector
|
||||||
from sqlalchemy import ForeignKey, Index, String, UniqueConstraint
|
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 sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from app.core.db import Base, TenantMixin
|
from app.core.db import Base, TenantMixin
|
||||||
from app.models.owned_mixin import OwnedMixin
|
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):
|
class Tag(Base, TenantMixin, OwnedMixin):
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Response, status
|
|||||||
from sqlalchemy import delete, func, select
|
from sqlalchemy import delete, func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.audit import log_audit
|
||||||
from app.core.db import get_db
|
from app.core.db import get_db
|
||||||
from app.core.visibility import apply_visibility_filter
|
from app.core.visibility import apply_visibility_filter
|
||||||
from app.deps import get_current_user, require_permission
|
from app.deps import get_current_user, require_permission
|
||||||
@@ -389,5 +390,3 @@ async def list_tag_entities(
|
|||||||
}
|
}
|
||||||
for a in assignments
|
for a in assignments
|
||||||
]
|
]
|
||||||
|
|
||||||
from app.core.audit import log_audit
|
|
||||||
@@ -7,6 +7,7 @@ import uuid
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.audit import log_audit
|
||||||
from app.core.db import get_db
|
from app.core.db import get_db
|
||||||
from app.deps import get_current_user, require_permission
|
from app.deps import get_current_user, require_permission
|
||||||
from app.plugins.builtins.tasks import services
|
from app.plugins.builtins.tasks import services
|
||||||
@@ -126,6 +127,7 @@ async def delete_task(
|
|||||||
):
|
):
|
||||||
"""Delete a task (soft-delete)."""
|
"""Delete a task (soft-delete)."""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
tid = _parse_uuid(task_id, "task_id")
|
tid = _parse_uuid(task_id, "task_id")
|
||||||
deleted = await services.delete_task(db, tenant_id, tid)
|
deleted = await services.delete_task(db, tenant_id, tid)
|
||||||
if not deleted:
|
if not deleted:
|
||||||
@@ -261,5 +263,3 @@ async def decompose_goal(
|
|||||||
if result is None:
|
if result is None:
|
||||||
raise HTTPException(404, detail={"detail": "Goal not found", "code": "not_found"})
|
raise HTTPException(404, detail={"detail": "Goal not found", "code": "not_found"})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
from app.core.audit import log_audit
|
|
||||||
@@ -7,7 +7,6 @@ from typing import Any
|
|||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
TASK_STATUSES = "^(open|in_progress|review|blocked|done|cancelled)$"
|
TASK_STATUSES = "^(open|in_progress|review|blocked|done|cancelled)$"
|
||||||
TASK_TYPES = "^(todo|approval|follow_up|review|goal|milestone|agent_subtask)$"
|
TASK_TYPES = "^(todo|approval|follow_up|review|goal|milestone|agent_subtask)$"
|
||||||
ASSIGNEE_TYPES = "^(user|agent|group)$"
|
ASSIGNEE_TYPES = "^(user|agent|group)$"
|
||||||
|
|||||||
@@ -16,12 +16,11 @@ def _to_uuid(val: str | UUID | None) -> UUID | None:
|
|||||||
return val
|
return val
|
||||||
return uuid.UUID(str(val))
|
return uuid.UUID(str(val))
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select # noqa: E402 — after helper defs by design
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession # noqa: E402
|
||||||
|
|
||||||
from app.core.visibility import apply_visibility_filter
|
|
||||||
from app.plugins.builtins.tasks.models import Task
|
|
||||||
|
|
||||||
|
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)
|
# Lifecycle statuses in display order (Kanban columns)
|
||||||
STATUS_ORDER = ["open", "in_progress", "review", "blocked", "done", "cancelled"]
|
STATUS_ORDER = ["open", "in_progress", "review", "blocked", "done", "cancelled"]
|
||||||
@@ -307,7 +306,6 @@ async def create_task(
|
|||||||
entity_id = contact_id
|
entity_id = contact_id
|
||||||
# Don't store contact_id in FK column if it's just a polymorphic entity link
|
# 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 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.
|
# Resolve polymorphic creator.
|
||||||
creator_type = data.get("creator_type", "user")
|
creator_type = data.get("creator_type", "user")
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from app.plugins.builtins.contracts import get_contract_registry
|
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.base_provider import BaseSearchProvider
|
||||||
from app.plugins.builtins.unified_search.embedding import generate_embedding
|
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")
|
status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending")
|
||||||
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|
||||||
from pgvector.sqlalchemy import Vector # noqa: E402
|
from pgvector.sqlalchemy import Vector # noqa: E402 — optional dependency import after model defs
|
||||||
from app.models.owned_mixin import OwnedMixin
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentChunk(Base, TenantMixin, OwnedMixin):
|
class DocumentChunk(Base, TenantMixin, OwnedMixin):
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""Wiki plugin - knowledge articles, categories, versioning."""
|
"""Wiki plugin - knowledge articles, categories, versioning."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from app.plugins.base import BasePlugin
|
from app.plugins.base import BasePlugin
|
||||||
from app.plugins.manifest import FrontendMenuItem, FrontendPageRoute, PluginManifest, PluginRouteDef
|
from app.plugins.manifest import FrontendMenuItem, FrontendPageRoute, PluginManifest, PluginRouteDef
|
||||||
|
|
||||||
@@ -28,7 +30,9 @@ class WikiPlugin(BasePlugin):
|
|||||||
from app.plugins.builtins.contracts import get_contract
|
from app.plugins.builtins.contracts import get_contract
|
||||||
search_contract = get_contract("unified_search")
|
search_contract = get_contract("unified_search")
|
||||||
if search_contract and hasattr(search_contract, "get_search_registry"):
|
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())
|
search_contract.get_search_registry().register(WikiSearchProvider())
|
||||||
logger.info("Registered WikiSearchProvider via contract")
|
logger.info("Registered WikiSearchProvider via contract")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
"""Wiki plugin routes — articles CRUD, categories, versioning."""
|
"""Wiki plugin routes — articles CRUD, categories, versioning."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
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.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"])
|
router = APIRouter(prefix="/api/v1/wiki", tags=["wiki"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""Pydantic schemas for the Wiki plugin."""
|
"""Pydantic schemas for the Wiki plugin."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from datetime import datetime
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class CategoryCreate(BaseModel):
|
class CategoryCreate(BaseModel):
|
||||||
name: str = Field(..., min_length=1, max_length=200)
|
name: str = Field(..., min_length=1, max_length=200)
|
||||||
slug: str = Field(..., min_length=1, max_length=200)
|
slug: str = Field(..., min_length=1, max_length=200)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
"""Wiki plugin services — CRUD for articles, categories, versioning."""
|
"""Wiki plugin services — CRUD for articles, categories, versioning."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from sqlalchemy import select, func
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.core.visibility import apply_visibility_filter
|
|
||||||
from app.plugins.builtins.wiki.models import WikiArticle, WikiArticleVersion, WikiCategory
|
from app.plugins.builtins.wiki.models import WikiArticle, WikiArticleVersion, WikiCategory
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ from app.routes import (
|
|||||||
addresses, # noqa: F401
|
addresses, # noqa: F401
|
||||||
attachments, # noqa: F401
|
attachments, # noqa: F401
|
||||||
audit, # noqa: F401
|
audit, # noqa: F401
|
||||||
compliance, # noqa: F401
|
|
||||||
auth, # noqa: F401
|
auth, # noqa: F401
|
||||||
bank_accounts, # noqa: F401
|
bank_accounts, # noqa: F401
|
||||||
|
compliance, # noqa: F401
|
||||||
currencies, # noqa: F401
|
currencies, # noqa: F401
|
||||||
dashboard, # noqa: F401
|
dashboard, # noqa: F401
|
||||||
entity_history, # noqa: F401
|
entity_history, # noqa: F401
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
@@ -16,7 +16,6 @@ from sqlalchemy import select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.approval import (
|
from app.core.approval import (
|
||||||
APPROVAL_STATUSES,
|
|
||||||
ApprovalRequest,
|
ApprovalRequest,
|
||||||
create_approval_request,
|
create_approval_request,
|
||||||
expire_approval_request,
|
expire_approval_request,
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ from datetime import UTC, datetime, timedelta
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from sqlalchemy import func, select, delete
|
from sqlalchemy import delete, func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.db import get_db
|
from app.core.db import get_db
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from typing import Any
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.ai.ai_use_case import AIUseCaseMetadata, validate_ai_use_case
|
from app.ai.ai_use_case import AIUseCaseMetadata, validate_ai_use_case
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ from pydantic import BaseModel
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.db import get_db
|
from app.core.db import get_db
|
||||||
from app.deps import require_admin, require_permission
|
from app.deps import get_current_user, require_admin, require_permission
|
||||||
from app.deps import get_current_user
|
|
||||||
from app.plugins.migration_runner import MigrationValidationError
|
from app.plugins.migration_runner import MigrationValidationError
|
||||||
from app.services.plugin_service import get_plugin_service
|
from app.services.plugin_service import get_plugin_service
|
||||||
|
|
||||||
|
|||||||
+11
-7
@@ -173,7 +173,6 @@ async def create_instance(
|
|||||||
"""Create a new workflow instance."""
|
"""Create a new workflow instance."""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
user_id = uuid.UUID(current_user["user_id"])
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
is_admin = current_user.get("is_system_admin", False)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = await workflow_service.create_instance(
|
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)."""
|
"""Resume a waiting workflow instance (e.g. after wait timer expired)."""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
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 sqlalchemy import select
|
||||||
|
|
||||||
|
from app.models.workflow import WorkflowInstance
|
||||||
|
from app.workflows.engine import WorkflowEngine
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(WorkflowInstance).where(
|
select(WorkflowInstance).where(
|
||||||
WorkflowInstance.id == uuid.UUID(instance_id),
|
WorkflowInstance.id == uuid.UUID(instance_id),
|
||||||
@@ -375,9 +375,10 @@ async def webhook_trigger(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Incoming webhook trigger — starts a workflow via secure token."""
|
"""Incoming webhook trigger — starts a workflow via secure token."""
|
||||||
from app.models.webhook import Webhook
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.models.webhook import Webhook
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Webhook).where(
|
select(Webhook).where(
|
||||||
Webhook.token == token,
|
Webhook.token == token,
|
||||||
@@ -423,9 +424,10 @@ async def get_instance_history(
|
|||||||
"""Get step history for a workflow instance (execution log)."""
|
"""Get step history for a workflow instance (execution log)."""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
|
||||||
from app.models.workflow import WorkflowStepHistory
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.models.workflow import WorkflowStepHistory
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(WorkflowStepHistory)
|
select(WorkflowStepHistory)
|
||||||
.where(
|
.where(
|
||||||
@@ -472,9 +474,10 @@ async def approve_workflow_step(
|
|||||||
body = {}
|
body = {}
|
||||||
comment = body.get("comment", "")
|
comment = body.get("comment", "")
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.core.approval import create_approval_request, resolve_approval_request
|
from app.core.approval import create_approval_request, resolve_approval_request
|
||||||
from app.models.workflow import WorkflowInstance
|
from app.models.workflow import WorkflowInstance
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(WorkflowInstance).where(
|
select(WorkflowInstance).where(
|
||||||
@@ -535,9 +538,10 @@ async def reject_workflow_step(
|
|||||||
body = {}
|
body = {}
|
||||||
comment = body.get("comment", "")
|
comment = body.get("comment", "")
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.core.approval import create_approval_request, resolve_approval_request
|
from app.core.approval import create_approval_request, resolve_approval_request
|
||||||
from app.models.workflow import WorkflowInstance
|
from app.models.workflow import WorkflowInstance
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(WorkflowInstance).where(
|
select(WorkflowInstance).where(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
@@ -313,7 +313,6 @@ class WorkflowEngine:
|
|||||||
return _instance_to_dict(instance)
|
return _instance_to_dict(instance)
|
||||||
else:
|
else:
|
||||||
# Retry: stay on same step, set resume_at with backoff
|
# Retry: stay on same step, set resume_at with backoff
|
||||||
import asyncio
|
|
||||||
backoff = min(2 ** instance.retry_count, 60)
|
backoff = min(2 ** instance.retry_count, 60)
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
instance.resume_at = datetime.now(UTC) + timedelta(seconds=backoff)
|
instance.resume_at = datetime.now(UTC) + timedelta(seconds=backoff)
|
||||||
@@ -572,8 +571,8 @@ class WorkflowEngine:
|
|||||||
|
|
||||||
Prevents two workers from processing the same instance simultaneously.
|
Prevents two workers from processing the same instance simultaneously.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from app.core.redis import get_redis
|
from app.core.redis import get_redis
|
||||||
import redis.asyncio as aioredis
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
r = await get_redis()
|
r = await get_redis()
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import uuid
|
|||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.models.workflow import WorkflowInstance
|
from app.models.workflow import WorkflowInstance
|
||||||
@@ -45,7 +44,8 @@ class StepResult:
|
|||||||
self.abort = abort
|
self.abort = abort
|
||||||
|
|
||||||
|
|
||||||
type StepHandler = Any # Callable[..., Awaitable[StepResult]]
|
# Callable[..., Awaitable[StepResult]] — classic alias for py3.11 compat
|
||||||
|
StepHandler = Any
|
||||||
|
|
||||||
|
|
||||||
# ─── Registry ────────────────────────────────────────────────────────────────
|
# ─── Registry ────────────────────────────────────────────────────────────────
|
||||||
@@ -534,6 +534,7 @@ async def _handle_webhook(
|
|||||||
return StepResult(error=f"URL blocked by SSRF protection: {url}", abort=True)
|
return StepResult(error=f"URL blocked by SSRF protection: {url}", abort=True)
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user