From 036bbb45e101d4765ae6b8b3ae78c92e6cca870f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D9=8A?= Date: Sun, 1 Mar 2026 21:59:12 +0530 Subject: [PATCH] Add strategy agent to daily planning committee --- .../intelligence/agents/agent_orchestrator.py | 13 +- backend/services/today_workflow_service.py | 28 +++- backend/test/test_today_workflow_service.py | 152 ++++++++++++++++++ 3 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 backend/test/test_today_workflow_service.py diff --git a/backend/services/intelligence/agents/agent_orchestrator.py b/backend/services/intelligence/agents/agent_orchestrator.py index 161a7ed1..fe04b58d 100644 --- a/backend/services/intelligence/agents/agent_orchestrator.py +++ b/backend/services/intelligence/agents/agent_orchestrator.py @@ -24,7 +24,11 @@ from services.intelligence.agents.core_agent_framework import ( BaseALwrityAgent, AgentAction, AgentPerformance, StrategyOrchestratorAgent ) from services.intelligence.agents.specialized_agents import ( - ContentStrategyAgent, CompetitorResponseAgent, SEOOptimizationAgent, SocialAmplificationAgent + ContentStrategyAgent, + CompetitorResponseAgent, + SEOOptimizationAgent, + SocialAmplificationAgent, + StrategyArchitectAgent, ) from services.intelligence.agents.trend_surfer_agent import TrendSurferAgent from services.intelligence.agents.market_signal_detector import ( @@ -157,6 +161,13 @@ class ALwrityAgentOrchestrator: self.social_agent = SocialAmplificationAgent(self.user_id, self.config.shared_llm, llm=self.llm) self.agents['social'] = self.social_agent + # Strategy Architect Agent + if enabled_by_key.get("strategy_architect", True): + from services.intelligence.txtai_service import TxtaiIntelligenceService + intel_service = TxtaiIntelligenceService(self.user_id) + self.strategy_agent = StrategyArchitectAgent(intel_service, self.user_id) + self.agents['strategy'] = self.strategy_agent + # Trend Surfer Agent if enabled_by_key.get("trend_surfer", True): # TrendSurferAgent needs TxtaiIntelligenceService, which we might need to get from SIF or initialize diff --git a/backend/services/today_workflow_service.py b/backend/services/today_workflow_service.py index d4ef0797..32d8037d 100644 --- a/backend/services/today_workflow_service.py +++ b/backend/services/today_workflow_service.py @@ -30,6 +30,19 @@ def _coerce_status(value: Any) -> str: return "pending" +def _proposal_priority_rank(priority: str) -> int: + return {"low": 0, "medium": 1, "high": 2}.get(str(priority or "").lower(), 1) + + +def _proposal_order_key(proposal: Any) -> tuple: + return ( + str(getattr(proposal, "source_agent", "") or "").lower(), + str(getattr(proposal, "title", "") or "").lower(), + str(getattr(proposal, "description", "") or "").lower(), + str(getattr(proposal, "action_url", "") or "").lower(), + ) + + def _fallback_tasks(date: str) -> List[Dict[str, Any]]: return [ { @@ -167,7 +180,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) -> orchestrator.agents.get('seo'), # SEOOptimizationAgent orchestrator.agents.get('social'), # SocialAmplificationAgent orchestrator.agents.get('competitor'), # CompetitorResponseAgent - # Add StrategyArchitect if available in orchestrator.agents + orchestrator.agents.get('strategy'), # StrategyArchitectAgent ] # Filter out None agents (disabled/failed init) @@ -198,7 +211,18 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) -> key = f"{p.pillar_id}:{p.title}" if key not in unique_map: unique_map[key] = p - elif p.priority == "high": # Overwrite with higher priority + continue + + existing = unique_map[key] + if _proposal_priority_rank(p.priority) > _proposal_priority_rank(existing.priority): + unique_map[key] = p + continue + + # Deterministic tie-breaker for equal priority proposals. + if ( + _proposal_priority_rank(p.priority) == _proposal_priority_rank(existing.priority) + and _proposal_order_key(p) < _proposal_order_key(existing) + ): unique_map[key] = p agent_tasks = list(unique_map.values()) diff --git a/backend/test/test_today_workflow_service.py b/backend/test/test_today_workflow_service.py new file mode 100644 index 00000000..6dd98436 --- /dev/null +++ b/backend/test/test_today_workflow_service.py @@ -0,0 +1,152 @@ +import sys +import types + +import pytest + +from services.intelligence.agents.core_agent_framework import TaskProposal + + +# Stub deep onboarding import chain before importing today_workflow_service. +module_names = [ + "api", + "api.content_planning", + "api.content_planning.services", + "api.content_planning.services.content_strategy", + "api.content_planning.services.content_strategy.onboarding", + "api.content_planning.services.content_strategy.onboarding.data_integration", +] +for name in module_names: + if name not in sys.modules: + sys.modules[name] = types.ModuleType(name) + + +class _StubOnboardingDataIntegrationService: + def get_integrated_data_sync(self, user_id, db): + return {} + + +sys.modules[ + "api.content_planning.services.content_strategy.onboarding.data_integration" +].OnboardingDataIntegrationService = _StubOnboardingDataIntegrationService + +from services import today_workflow_service as workflow + + +class _NoopActivityService: + def __init__(self, db, user_id): + self.db = db + self.user_id = user_id + + +class _NoopMemoryService: + def __init__(self, user_id, db): + self.user_id = user_id + self.db = db + + +class _StaticAgent: + def __init__(self, proposals): + self._proposals = proposals + + async def propose_daily_tasks(self, _grounding): + return self._proposals + + +class _FakeOrchestrationService: + def __init__(self, orchestrator): + self._orchestrator = orchestrator + + async def get_or_create_orchestrator(self, _user_id): + return self._orchestrator + + +@pytest.mark.asyncio +async def test_generate_agent_enhanced_plan_includes_strategy_agent_proposals(monkeypatch): + strategy_proposal = TaskProposal( + title="Define weekly content themes", + description="Create this week's strategic themes for planned posts.", + pillar_id="plan", + priority="high", + estimated_time=20, + source_agent="StrategyArchitectAgent", + reasoning="Strategy-driven planning", + action_type="navigate", + action_url="/content-planning-dashboard", + ) + + orchestrator = type( + "Orchestrator", + (), + {"agents": {"strategy": _StaticAgent([strategy_proposal])}}, + )() + + monkeypatch.setattr(workflow, "build_grounding_context", lambda db, user_id, date: {"date": date}) + monkeypatch.setattr(workflow, "AgentActivityService", _NoopActivityService) + monkeypatch.setattr(workflow, "TaskMemoryService", _NoopMemoryService) + monkeypatch.setattr(workflow, "orchestration_service", _FakeOrchestrationService(orchestrator)) + + result = await workflow.generate_agent_enhanced_plan(db=object(), user_id="u1", date="2026-01-01") + + assert len(result["tasks"]) == 1 + assert result["tasks"][0]["pillarId"] == "plan" + assert result["tasks"][0]["title"] == "Define weekly content themes" + assert result["tasks"][0]["metadata"]["source_agent"] == "StrategyArchitectAgent" + + +@pytest.mark.asyncio +async def test_generate_agent_enhanced_plan_dedupes_plan_tasks_with_priority_and_tiebreak(monkeypatch): + title = "Build next week content plan" + + content_medium = TaskProposal( + title=title, + description="Draft a medium-priority weekly plan.", + pillar_id="plan", + priority="medium", + estimated_time=15, + source_agent="ZetaAgent", + reasoning="baseline", + ) + strategy_high = TaskProposal( + title=title, + description="Draft a high-priority strategic plan.", + pillar_id="plan", + priority="high", + estimated_time=20, + source_agent="StrategyArchitectAgent", + reasoning="urgent update", + ) + competitor_high = TaskProposal( + title=title, + description="Alternative high-priority plan.", + pillar_id="plan", + priority="high", + estimated_time=25, + source_agent="BetaAgent", + reasoning="same priority tie", + ) + + orchestrator = type( + "Orchestrator", + (), + { + "agents": { + "content": _StaticAgent([content_medium]), + "strategy": _StaticAgent([strategy_high]), + "competitor": _StaticAgent([competitor_high]), + } + }, + )() + + monkeypatch.setattr(workflow, "build_grounding_context", lambda db, user_id, date: {"date": date}) + monkeypatch.setattr(workflow, "AgentActivityService", _NoopActivityService) + monkeypatch.setattr(workflow, "TaskMemoryService", _NoopMemoryService) + monkeypatch.setattr(workflow, "orchestration_service", _FakeOrchestrationService(orchestrator)) + + result = await workflow.generate_agent_enhanced_plan(db=object(), user_id="u1", date="2026-01-01") + + assert len(result["tasks"]) == 1 + task = result["tasks"][0] + assert task["title"] == title + assert task["priority"] == "high" + # Deterministic equal-priority tie-break keeps lexicographically earlier source agent. + assert task["metadata"]["source_agent"] == "BetaAgent"