312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""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")
|