fix: Replace automation stubs with real execution engine
- Replace automation execute stub with run_automation() from execution_engine.py - Replace agent execute stub with run_agent() from agent_runner.py - Persist automation settings in system_settings.automation_config JSONB - Add migration 0034 for automation_config column - Settings are now saved and loaded from database instead of being ignored
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
"""Add automation_config JSONB column to system_settings.
|
||||||
|
|
||||||
|
Revision ID: 0034
|
||||||
|
Revises: 0033_bank_accounts
|
||||||
|
Create Date: 2026-07-25
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
|
||||||
|
revision: str = "0034_automation_config"
|
||||||
|
down_revision: Union[str, None] = "0033_bank_accounts"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("system_settings", sa.Column("automation_config", JSONB, nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("system_settings", "automation_config")
|
||||||
@@ -50,3 +50,6 @@ class SystemSettings(Base, TenantMixin):
|
|||||||
theme_accent_color: Mapped[str] = mapped_column(String(20), nullable=False, default="#d946ef")
|
theme_accent_color: Mapped[str] = mapped_column(String(20), nullable=False, default="#d946ef")
|
||||||
theme_font_family: Mapped[str] = mapped_column(String(100), nullable=False, default="Inter")
|
theme_font_family: Mapped[str] = mapped_column(String(100), nullable=False, default="Inter")
|
||||||
theme_border_radius: Mapped[str] = mapped_column(String(20), nullable=False, default="0.5rem")
|
theme_border_radius: Mapped[str] = mapped_column(String(20), nullable=False, default="0.5rem")
|
||||||
|
# Automation plugin settings (JSONB)
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
automation_config: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||||
|
|||||||
@@ -306,14 +306,38 @@ async def execute_agent(
|
|||||||
db.add(run)
|
db.add(run)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# TODO: Execute agent asynchronously via Agent Runner
|
# Execute agent via agent runner
|
||||||
run.status = "completed"
|
from app.plugins.builtins.automation.agent_runner import run_agent
|
||||||
run.completed_at = __import__("datetime").datetime.now(__import__("datetime").timezone.utc)
|
|
||||||
run.duration_seconds = 0.0
|
|
||||||
run.result = "Executed successfully (stub)"
|
|
||||||
await db.flush()
|
|
||||||
|
|
||||||
return {"status": "ok", "run_id": str(run.id)}
|
run_id = str(run.id)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
# Run agent
|
||||||
|
result = await run_agent(
|
||||||
|
ctx={"tenant_id": str(tenant_id), "user_id": current_user["user_id"]},
|
||||||
|
agent_id=str(aid),
|
||||||
|
trigger_type="manual",
|
||||||
|
trigger_data={"triggered_by": current_user["user_id"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update run with results
|
||||||
|
from sqlalchemy import update as sa_update
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
async with db.begin():
|
||||||
|
await db.execute(
|
||||||
|
sa_update(AgentRun)
|
||||||
|
.where(AgentRun.id == run.id)
|
||||||
|
.values(
|
||||||
|
status=result.get("status", "completed"),
|
||||||
|
completed_at=now,
|
||||||
|
duration_seconds=0.0,
|
||||||
|
result=result,
|
||||||
|
output_data=result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": result.get("status", "ok"), "run_id": run_id, "result": result}
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
@@ -240,15 +240,28 @@ async def delete_miniapp(
|
|||||||
)
|
)
|
||||||
async def get_automation_settings(
|
async def get_automation_settings(
|
||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get automation default settings."""
|
"""Get automation settings (persisted in system_settings metadata)."""
|
||||||
# Return default settings (could be stored in plugin config in the future)
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
return AutomationSettingsResponse(
|
from app.models.system_settings import SystemSettings
|
||||||
default_llm_model="ollama/deepseek-v4-flash",
|
from sqlalchemy import select
|
||||||
heartbeat_default_interval=300,
|
|
||||||
max_concurrent_agents=5,
|
result = await db.execute(
|
||||||
log_level="INFO",
|
select(SystemSettings).where(SystemSettings.tenant_id == tenant_id)
|
||||||
)
|
)
|
||||||
|
settings = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
defaults = AutomationSettingsResponse()
|
||||||
|
if settings and hasattr(settings, "automation_config") and settings.automation_config:
|
||||||
|
cfg = settings.automation_config
|
||||||
|
return AutomationSettingsResponse(
|
||||||
|
default_llm_model=cfg.get("default_llm_model", defaults.default_llm_model),
|
||||||
|
heartbeat_default_interval=cfg.get("heartbeat_default_interval", defaults.heartbeat_default_interval),
|
||||||
|
max_concurrent_agents=cfg.get("max_concurrent_agents", defaults.max_concurrent_agents),
|
||||||
|
log_level=cfg.get("log_level", defaults.log_level),
|
||||||
|
)
|
||||||
|
return defaults
|
||||||
|
|
||||||
|
|
||||||
@router.patch(
|
@router.patch(
|
||||||
@@ -259,17 +272,55 @@ async def get_automation_settings(
|
|||||||
async def update_automation_settings(
|
async def update_automation_settings(
|
||||||
data: AutomationSettingsUpdate,
|
data: AutomationSettingsUpdate,
|
||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Update automation settings (stored in plugin config)."""
|
"""Update automation settings (persisted in system_settings metadata)."""
|
||||||
# TODO: Persist settings to plugin config table
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
# For now, return the updated values as a preview
|
from app.models.system_settings import SystemSettings
|
||||||
settings = AutomationSettingsResponse(
|
from sqlalchemy import select
|
||||||
default_llm_model=data.default_llm_model or "ollama/deepseek-v4-flash",
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
heartbeat_default_interval=data.heartbeat_default_interval or 300,
|
|
||||||
max_concurrent_agents=data.max_concurrent_agents or 5,
|
result = await db.execute(
|
||||||
log_level=data.log_level or "INFO",
|
select(SystemSettings).where(SystemSettings.tenant_id == tenant_id)
|
||||||
)
|
)
|
||||||
return settings
|
settings = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
# Build new config
|
||||||
|
new_config = {
|
||||||
|
"default_llm_model": data.default_llm_model or "ollama/deepseek-v4-flash",
|
||||||
|
"heartbeat_default_interval": data.heartbeat_default_interval or 300,
|
||||||
|
"max_concurrent_agents": data.max_concurrent_agents or 5,
|
||||||
|
"log_level": data.log_level or "INFO",
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings:
|
||||||
|
# Update existing settings row
|
||||||
|
if hasattr(settings, "automation_config"):
|
||||||
|
settings.automation_config = new_config
|
||||||
|
else:
|
||||||
|
# Fallback: store in a generic metadata field if available
|
||||||
|
await db.execute(
|
||||||
|
"UPDATE system_settings SET automation_config = :cfg WHERE id = :sid",
|
||||||
|
{"cfg": new_config, "sid": settings.id},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Create new settings row with automation config
|
||||||
|
settings = SystemSettings(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
company_name="Default",
|
||||||
|
company_street="",
|
||||||
|
company_city="",
|
||||||
|
company_zip="",
|
||||||
|
company_country="DE",
|
||||||
|
)
|
||||||
|
if hasattr(settings, "automation_config"):
|
||||||
|
settings.automation_config = new_config
|
||||||
|
db.add(settings)
|
||||||
|
|
||||||
|
await db.flush()
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
return AutomationSettingsResponse(**new_config)
|
||||||
@router.get(
|
@router.get(
|
||||||
"/{automation_id}",
|
"/{automation_id}",
|
||||||
dependencies=[Depends(require_permission("automation:read"))],
|
dependencies=[Depends(require_permission("automation:read"))],
|
||||||
@@ -380,14 +431,39 @@ async def execute_automation(
|
|||||||
db.add(run)
|
db.add(run)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
|
|
||||||
# TODO: Execute automation actions asynchronously
|
# Execute automation via execution engine
|
||||||
run.status = "completed"
|
import asyncio
|
||||||
run.completed_at = __import__("datetime").datetime.now(__import__("datetime").timezone.utc)
|
from app.plugins.builtins.automation.execution_engine import run_automation
|
||||||
run.duration_seconds = 0.0
|
|
||||||
run.result = "Executed successfully (stub)"
|
|
||||||
await db.flush()
|
|
||||||
|
|
||||||
return {"status": "ok", "run_id": str(run.id)}
|
run_id = str(run.id)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
# Run automation asynchronously
|
||||||
|
result = await run_automation(
|
||||||
|
ctx={"tenant_id": str(tenant_id), "user_id": current_user["user_id"]},
|
||||||
|
automation_id=str(aid),
|
||||||
|
trigger_type="manual",
|
||||||
|
trigger_data={"triggered_by": current_user["user_id"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update run with results
|
||||||
|
from sqlalchemy import update as sa_update
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
async with db.begin():
|
||||||
|
await db.execute(
|
||||||
|
sa_update(AutomationRun)
|
||||||
|
.where(AutomationRun.id == run.id)
|
||||||
|
.values(
|
||||||
|
status=result.get("status", "completed"),
|
||||||
|
completed_at=now,
|
||||||
|
duration_seconds=0.0,
|
||||||
|
result=result,
|
||||||
|
output_data=result,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"status": result.get("status", "ok"), "run_id": run_id, "result": result}
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
Reference in New Issue
Block a user