diff --git a/alembic/versions/0034_automation_config.py b/alembic/versions/0034_automation_config.py new file mode 100644 index 0000000..f83f644 --- /dev/null +++ b/alembic/versions/0034_automation_config.py @@ -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") diff --git a/app/models/system_settings.py b/app/models/system_settings.py index ec858ac..83027ea 100644 --- a/app/models/system_settings.py +++ b/app/models/system_settings.py @@ -50,3 +50,6 @@ class SystemSettings(Base, TenantMixin): 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_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) diff --git a/app/plugins/builtins/automation/agent_routes.py b/app/plugins/builtins/automation/agent_routes.py index 7ce4cec..3220ee3 100644 --- a/app/plugins/builtins/automation/agent_routes.py +++ b/app/plugins/builtins/automation/agent_routes.py @@ -306,14 +306,38 @@ async def execute_agent( db.add(run) await db.flush() - # TODO: Execute agent asynchronously via Agent Runner - run.status = "completed" - run.completed_at = __import__("datetime").datetime.now(__import__("datetime").timezone.utc) - run.duration_seconds = 0.0 - run.result = "Executed successfully (stub)" - await db.flush() + # Execute agent via agent runner + from app.plugins.builtins.automation.agent_runner import run_agent - 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( diff --git a/app/plugins/builtins/automation/routes.py b/app/plugins/builtins/automation/routes.py index 38a55bd..96ddfc5 100644 --- a/app/plugins/builtins/automation/routes.py +++ b/app/plugins/builtins/automation/routes.py @@ -240,15 +240,28 @@ async def delete_miniapp( ) async def get_automation_settings( current_user: dict[str, Any] = Depends(get_current_user), + db: AsyncSession = Depends(get_db), ): - """Get automation default settings.""" - # Return default settings (could be stored in plugin config in the future) - return AutomationSettingsResponse( - default_llm_model="ollama/deepseek-v4-flash", - heartbeat_default_interval=300, - max_concurrent_agents=5, - log_level="INFO", + """Get automation settings (persisted in system_settings metadata).""" + tenant_id = uuid.UUID(current_user["tenant_id"]) + from app.models.system_settings import SystemSettings + from sqlalchemy import select + + result = await db.execute( + 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( @@ -259,17 +272,55 @@ async def get_automation_settings( async def update_automation_settings( data: AutomationSettingsUpdate, current_user: dict[str, Any] = Depends(get_current_user), + db: AsyncSession = Depends(get_db), ): - """Update automation settings (stored in plugin config).""" - # TODO: Persist settings to plugin config table - # For now, return the updated values as a preview - settings = AutomationSettingsResponse( - 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", + """Update automation settings (persisted in system_settings metadata).""" + tenant_id = uuid.UUID(current_user["tenant_id"]) + from app.models.system_settings import SystemSettings + from sqlalchemy import select + from sqlalchemy.dialects.postgresql import JSONB + + result = await db.execute( + 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( "/{automation_id}", dependencies=[Depends(require_permission("automation:read"))], @@ -380,14 +431,39 @@ async def execute_automation( db.add(run) await db.flush() - # TODO: Execute automation actions asynchronously - run.status = "completed" - run.completed_at = __import__("datetime").datetime.now(__import__("datetime").timezone.utc) - run.duration_seconds = 0.0 - run.result = "Executed successfully (stub)" - await db.flush() + # Execute automation via execution engine + import asyncio + from app.plugins.builtins.automation.execution_engine import run_automation - 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(