"""Agent relationship management API — human + agent-to-agent.""" import json import uuid from pathlib import Path from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy import select, delete from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from app.config import get_settings from app.core.security import get_current_user from app.core.permissions import check_agent_access from app.database import get_db from app.models.org import AgentRelationship, AgentAgentRelationship, OrgMember from app.models.user import User settings = get_settings() router = APIRouter(prefix="/agents/{agent_id}/relationships", tags=["relationships"]) RELATION_LABELS = { "direct_leader": "直属上级", "collaborator": "协作伙伴", "stakeholder": "利益相关者", "team_member": "团队成员", "subordinate": "下属", "mentor": "导师", "other": "其他", } AGENT_RELATION_LABELS = { "peer": "同级协作", "supervisor": "上级数字员工", "assistant": "助手", "collaborator": "协作伙伴", "other": "其他", } # ─── Schemas ─────────────────────────────────────────── class RelationshipIn(BaseModel): member_id: str relation: str = "collaborator" description: str = "" class RelationshipBatchIn(BaseModel): relationships: list[RelationshipIn] class AgentRelationshipIn(BaseModel): target_agent_id: str relation: str = "collaborator" description: str = "" class AgentRelationshipBatchIn(BaseModel): relationships: list[AgentRelationshipIn] # ─── Human Relationships (existing) ─────────────────── @router.get("/") async def get_relationships( agent_id: uuid.UUID, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get all human relationships for this agent.""" from app.models.identity import IdentityProvider result = await db.execute( select(AgentRelationship, IdentityProvider.name.label("provider_name")) .outerjoin(OrgMember, AgentRelationship.member_id == OrgMember.id) .outerjoin(IdentityProvider, OrgMember.provider_id == IdentityProvider.id) .where(AgentRelationship.agent_id == agent_id) .options(selectinload(AgentRelationship.member)) ) rows = result.all() return [ { "id": str(r.id), "member_id": str(r.member_id), "relation": r.relation, "relation_label": RELATION_LABELS.get(r.relation, r.relation), "description": r.description, "member": { "name": r.member.name, "title": r.member.title, "department_path": r.member.department_path, "avatar_url": r.member.avatar_url, "email": r.member.email, "provider_name": provider_name, } if r.member else None, } for r, provider_name in rows ] @router.put("/") async def save_relationships( agent_id: uuid.UUID, data: RelationshipBatchIn, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Replace all human relationships for this agent.""" await check_agent_access(db, current_user, agent_id) await db.execute( delete(AgentRelationship).where(AgentRelationship.agent_id == agent_id) ) for r in data.relationships: db.add(AgentRelationship( agent_id=agent_id, member_id=uuid.UUID(r.member_id), relation=r.relation, description=r.description, )) await db.flush() # Regenerate file with both types await _regenerate_relationships_file(db, agent_id) await db.commit() return {"status": "ok"} @router.delete("/{rel_id}") async def delete_relationship( agent_id: uuid.UUID, rel_id: uuid.UUID, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Delete a single human relationship.""" await check_agent_access(db, current_user, agent_id) result = await db.execute( select(AgentRelationship).where(AgentRelationship.id == rel_id, AgentRelationship.agent_id == agent_id) ) rel = result.scalar_one_or_none() if rel: await db.delete(rel) await db.flush() await _regenerate_relationships_file(db, agent_id) await db.commit() return {"status": "ok"} # ─── Agent-to-Agent Relationships (new) ─────────────── @router.get("/agents") async def get_agent_relationships( agent_id: uuid.UUID, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Get all agent-to-agent relationships.""" await check_agent_access(db, current_user, agent_id) result = await db.execute( select(AgentAgentRelationship) .where(AgentAgentRelationship.agent_id == agent_id) .options(selectinload(AgentAgentRelationship.target_agent)) ) rels = result.scalars().all() return [ { "id": str(r.id), "target_agent_id": str(r.target_agent_id), "relation": r.relation, "relation_label": AGENT_RELATION_LABELS.get(r.relation, r.relation), "description": r.description, "target_agent": { "id": str(r.target_agent.id), "name": r.target_agent.name, "role_description": r.target_agent.role_description or "", "avatar_url": r.target_agent.avatar_url or "", } if r.target_agent else None, } for r in rels ] @router.put("/agents") async def save_agent_relationships( agent_id: uuid.UUID, data: AgentRelationshipBatchIn, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Replace all agent-to-agent relationships.""" await check_agent_access(db, current_user, agent_id) await db.execute( delete(AgentAgentRelationship).where(AgentAgentRelationship.agent_id == agent_id) ) for r in data.relationships: target_id = uuid.UUID(r.target_agent_id) if target_id == agent_id: continue # skip self-reference db.add(AgentAgentRelationship( agent_id=agent_id, target_agent_id=target_id, relation=r.relation, description=r.description, )) await db.flush() await _regenerate_relationships_file(db, agent_id) await db.commit() return {"status": "ok"} @router.delete("/agents/{rel_id}") async def delete_agent_relationship( agent_id: uuid.UUID, rel_id: uuid.UUID, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """Delete a single agent-to-agent relationship.""" await check_agent_access(db, current_user, agent_id) result = await db.execute( select(AgentAgentRelationship).where( AgentAgentRelationship.id == rel_id, AgentAgentRelationship.agent_id == agent_id, ) ) rel = result.scalar_one_or_none() if rel: await db.delete(rel) await db.flush() await _regenerate_relationships_file(db, agent_id) await db.commit() return {"status": "ok"} # ─── relationships.md Generation ────────────────────── async def _regenerate_relationships_file(db: AsyncSession, agent_id: uuid.UUID): """Regenerate relationships.md with both human and agent relationships.""" from app.models.identity import IdentityProvider # Load human relationships with provider name h_result = await db.execute( select(AgentRelationship, IdentityProvider.name.label("provider_name")) .outerjoin(OrgMember, AgentRelationship.member_id == OrgMember.id) .outerjoin(IdentityProvider, OrgMember.provider_id == IdentityProvider.id) .where(AgentRelationship.agent_id == agent_id) .options(selectinload(AgentRelationship.member)) ) human_rows = h_result.all() # Load agent relationships a_result = await db.execute( select(AgentAgentRelationship) .where(AgentAgentRelationship.agent_id == agent_id) .options(selectinload(AgentAgentRelationship.target_agent)) ) agent_rels = a_result.scalars().all() ws = Path(settings.AGENT_DATA_DIR) / str(agent_id) ws.mkdir(parents=True, exist_ok=True) if not human_rows and not agent_rels: (ws / "relationships.md").write_text("# 关系网络\n\n_暂无配置的关系。_\n", encoding="utf-8") return lines = ["# 关系网络\n"] # Human relationships if human_rows: lines.append("## 👤 人类同事\n") for r, provider_name in human_rows: m = r.member if not m: continue label = RELATION_LABELS.get(r.relation, r.relation) source = f"(通过 {provider_name} 同步)" if provider_name else "" lines.append(f"### {m.name} — {m.title or '未设置职位'}{source}") lines.append(f"- 部门:{m.department_path or '未设置'}") lines.append(f"- 关系:{label}") if m.open_id: lines.append(f"- OpenID:{m.open_id}") if m.email: lines.append(f"- 邮箱:{m.email}") if r.description: lines.append(f"- {r.description}") lines.append("") # Agent relationships if agent_rels: lines.append("## 🤖 数字员工同事\n") for r in agent_rels: a = r.target_agent if not a: continue label = AGENT_RELATION_LABELS.get(r.relation, r.relation) lines.append(f"### {a.name} — {a.role_description or '数字员工'}") lines.append(f"- 关系:{label}") lines.append(f"- 可以用 send_message_to_agent 工具给 {a.name} 发消息协作") if r.description: lines.append(f"- {r.description}") lines.append("") (ws / "relationships.md").write_text("\n".join(lines), encoding="utf-8")