"""内容段执行智能体的人工覆盖,以及重新生成时的版本与连续性边界。""" from __future__ import annotations from fastapi import HTTPException from sqlmodel import Session, select from .access import can_access, can_use_canvas from .models import Agent, AgentSession, AppUser, Campaign, CampaignRun, Canvas, KnowledgeUsage, Segment, now def available_agents(session: Session, campaign: Campaign, user: AppUser | None) -> list[Agent]: canvas_agent_ids = set() if user: for canvas in session.exec(select(Canvas).where(Canvas.organization_id.in_([0, user.organization_id]))).all(): if can_use_canvas(session, canvas, user, 'use'): canvas_agent_ids.update(int(node['agent_id']) for node in canvas.nodes or [] if node.get('enabled', True) and str(node.get('agent_id', '')).isdigit()) return [agent for agent in session.exec(select(Agent).where( Agent.status == 'active', Agent.role == 'production', Agent.organization_id.in_([0, campaign.organization_id])).order_by(Agent.id)).all() if can_access(agent, user, 'use') or agent.id in canvas_agent_ids] def edit_blocker(session: Session, campaign: Campaign) -> str: if campaign.status == 'done': return '任务已交付,不能更换执行智能体,请创建新版本任务' if campaign.status in {'running', 'parsing', 'composing', 'qc'}: return '任务正在生成、拆解或组合,请等待结束后再更换智能体' if session.exec(select(CampaignRun.id).where( CampaignRun.campaign_id == campaign.id, CampaignRun.status.in_(['queued', 'running', 'paused']))).first(): return '任务运行中或已暂停,请先停止本次运行,再更换智能体' if session.exec(select(AgentSession.id).where( AgentSession.campaign_id == campaign.id, AgentSession.status.in_(['running', 'waiting_approval']))).first(): return '智能体或后台质检仍在执行,请等待结束或停止运行后再更换' if session.exec(select(Segment.id).where(Segment.campaign_id == campaign.id, Segment.status.in_(['running', 'qc']))).first(): return '内容段正在生成或质检,请等待结束后再更换智能体' return '' def has_generation(segment: Segment) -> bool: return bool(segment.artifact_path or segment.anchor_card or segment.qc_reports or segment.status in {'human_review', 'approved', 'rework'}) def affected_segments(session: Session, segment: Segment) -> list[Segment]: # 当前逐段工作台采用内容段 ID 顺序;全部后续非跳过段都依赖前面已确认的正文。 return session.exec(select(Segment).where( Segment.campaign_id == segment.campaign_id, Segment.id >= segment.id, Segment.status != 'skipped').order_by(Segment.id)).all() def change_agent(session: Session, campaign: Campaign, segment: Segment, agent: Agent, *, reset_generated: bool = False) -> list[int]: frozen_agent_id = (segment.runtime_snapshot or {}).get('primary', {}).get('id') if segment.agent_id == agent.id and (not frozen_agent_id or int(frozen_agent_id) == agent.id): segment.agent_assignment_mode = 'manual' session.add(segment) return [] blocker = edit_blocker(session, campaign) if blocker: raise HTTPException(409, blocker) if segment.status == 'skipped': raise HTTPException(409, '该内容段已跳过,请新增需要生成的内容段') affected = affected_segments(session, segment) generated = [item for item in affected if has_generation(item)] if generated and not reset_generated: names = '、'.join(item.title or f'内容段#{item.id}' for item in generated[:4]) raise HTTPException(409, f'更换后需重新生成并确认:{names}。请确认保留旧稿并重新生成') old_agent = session.get(Agent, segment.agent_id) change_note = f'执行智能体由「{old_agent.name if old_agent else segment.agent_id}」更换为「{agent.name}」' for item in affected: if has_generation(item) or item.runtime_snapshot: previous_agent = session.get(Agent, item.agent_id) item.generation_history = list(item.generation_history or []) + [{ 'agent_id': item.agent_id, 'agent_name': previous_agent.name if previous_agent else '', 'round': item.round, 'status': item.status, 'artifact_path': item.artifact_path, 'anchor_card': item.anchor_card, 'references': item.references, 'qc_reports': item.qc_reports, 'runtime_snapshot': item.runtime_snapshot, 'archived_at': now(), 'reason': change_note if item.id == segment.id else f'前段「{segment.title}」更换了智能体,重新衔接', }] item.round += 1 # 新稿使用新的文件名,不覆盖历史正文。 item.runtime_snapshot = {} item.artifact_path = '' item.anchor_card = {} item.qc_reports = [] item.references = [] item.failure = {'type': 'agent_reassigned' if item.id == segment.id else 'upstream_reassigned', 'detail': change_note, 'source_segment_id': segment.id} item.status = 'pending' item.updated_at = now() session.add(item) # 未写入稿件的旧预留素材不应被新智能体的生成占住;已消费历史保留。 for usage in session.exec(select(KnowledgeUsage).where( KnowledgeUsage.campaign_id == campaign.id, KnowledgeUsage.segment_id == item.id, KnowledgeUsage.status == 'reserved')).all(): usage.status = 'available'; usage.released_at = now(); session.add(usage) segment.agent_id = agent.id segment.agent_assignment_mode = 'manual' segment.fusion_agent_ids = list(agent.fusion_agent_ids or []) segment.kb_category_ids = [] segment.kb_category_recommendation = {} campaign.status = 'parsed' campaign.has_failure = False for run in session.exec(select(CampaignRun).where(CampaignRun.campaign_id == campaign.id)).all(): # 历史快照与调用记录保留;不能再从旧会话续跑出被替换的内容。 run.checkpoint = {**(run.checkpoint or {}), 'segment_assignment_changed': True, 'assignment_changed_at': now()} session.add(run) session.add(segment); session.add(campaign) return [int(item.id) for item in generated]