Merge branch 'review/pr-359'
This commit is contained in:
@@ -24,7 +24,11 @@ from services.intelligence.agents.core_agent_framework import (
|
|||||||
BaseALwrityAgent, AgentAction, AgentPerformance, StrategyOrchestratorAgent
|
BaseALwrityAgent, AgentAction, AgentPerformance, StrategyOrchestratorAgent
|
||||||
)
|
)
|
||||||
from services.intelligence.agents.specialized_agents import (
|
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.trend_surfer_agent import TrendSurferAgent
|
||||||
from services.intelligence.agents.market_signal_detector import (
|
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.social_agent = SocialAmplificationAgent(self.user_id, self.config.shared_llm, llm=self.llm)
|
||||||
self.agents['social'] = self.social_agent
|
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
|
# Trend Surfer Agent
|
||||||
if enabled_by_key.get("trend_surfer", True):
|
if enabled_by_key.get("trend_surfer", True):
|
||||||
# TrendSurferAgent needs TxtaiIntelligenceService, which we might need to get from SIF or initialize
|
# TrendSurferAgent needs TxtaiIntelligenceService, which we might need to get from SIF or initialize
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ def _coerce_status(value: Any) -> str:
|
|||||||
return "pending"
|
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]]:
|
def _fallback_tasks(date: str) -> List[Dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -282,7 +295,7 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
|
|||||||
orchestrator.agents.get('seo'), # SEOOptimizationAgent
|
orchestrator.agents.get('seo'), # SEOOptimizationAgent
|
||||||
orchestrator.agents.get('social'), # SocialAmplificationAgent
|
orchestrator.agents.get('social'), # SocialAmplificationAgent
|
||||||
orchestrator.agents.get('competitor'), # CompetitorResponseAgent
|
orchestrator.agents.get('competitor'), # CompetitorResponseAgent
|
||||||
# Add StrategyArchitect if available in orchestrator.agents
|
orchestrator.agents.get('strategy'), # StrategyArchitectAgent
|
||||||
]
|
]
|
||||||
|
|
||||||
# Filter out None agents (disabled/failed init)
|
# Filter out None agents (disabled/failed init)
|
||||||
@@ -313,7 +326,18 @@ async def generate_agent_enhanced_plan(db: Session, user_id: str, date: str) ->
|
|||||||
key = f"{p.pillar_id}:{p.title}"
|
key = f"{p.pillar_id}:{p.title}"
|
||||||
if key not in unique_map:
|
if key not in unique_map:
|
||||||
unique_map[key] = p
|
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
|
unique_map[key] = p
|
||||||
|
|
||||||
agent_tasks = list(unique_map.values())
|
agent_tasks = list(unique_map.values())
|
||||||
|
|||||||
152
backend/test/test_today_workflow_service.py
Normal file
152
backend/test/test_today_workflow_service.py
Normal file
@@ -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"
|
||||||
Reference in New Issue
Block a user