Clawith/backend/app/api/relationships.py

312 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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")