ADK-gateway/backend/app/repository/agent_repo.py
2026-08-05 16:33:27 +08:00

216 lines
8.8 KiB
Python
Raw 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 池 Redis 存储层。"""
import time
from typing import Any
import redis.asyncio as aioredis
from app import constants as C
from app.constants import AgentStatus
from app.models.schemas import AgentInfo
class AgentRepo:
def __init__(self, redis: aioredis.Redis):
self.redis = redis
@staticmethod
def _info_key(agent_id: str) -> str:
return C.AGENT_INFO_KEY.format(agent_id=agent_id)
@staticmethod
def _tag_key(tag: str) -> str:
return C.AGENT_TAG_KEY.format(tag=tag)
# ---------- 写入 ----------
async def upsert(self, agent: AgentInfo, *, heartbeat: bool = False) -> bool:
"""写入 Agent 信息,返回是否新建。非心跳时为全量注册。"""
key = self._info_key(agent.agent_id)
existed = await self.redis.exists(key)
if not existed:
await self.redis.sadd(C.AGENT_ALL_SET, agent.agent_id)
# 建立标签索引
for tag in agent.agent_tags:
await self.redis.sadd(self._tag_key(tag), agent.agent_id)
mapping: dict[str, Any] = {
"agent_id": agent.agent_id,
"endpoint": agent.endpoint,
"agent_tags": ",".join(agent.agent_tags),
"max_concurrent": str(agent.max_concurrent),
"current_load": str(agent.current_load),
"priority": str(agent.priority),
"last_heartbeat": str(agent.last_heartbeat),
"status": agent.status.value,
"create_time": str(agent.create_time),
}
await self.redis.hset(key, mapping=mapping)
# 心跳 ZSet 记录时间戳
await self.redis.zadd(C.AGENT_HEARTBEAT_ZSET, {agent.agent_id: agent.last_heartbeat})
return not existed
async def update_status(self, agent_id: str, status: AgentStatus) -> bool:
key = self._info_key(agent_id)
if not await self.redis.exists(key):
return False
await self.redis.hset(key, "status", status.value)
# 离线(心跳失联)与不可用均退出心跳 ZSet避免被误判/干扰
if status in (AgentStatus.OFFLINE, AgentStatus.UNAVAILABLE):
await self.redis.zrem(C.AGENT_HEARTBEAT_ZSET, agent_id)
return True
async def set_unavailable(self, agent_id: str) -> bool:
"""后台手动置为不可用unavailable心跳不会复活。"""
key = self._info_key(agent_id)
if not await self.redis.exists(key):
return False
await self.redis.hset(key, "status", AgentStatus.UNAVAILABLE.value)
await self.redis.zrem(C.AGENT_HEARTBEAT_ZSET, agent_id)
return True
async def set_available(self, agent_id: str) -> bool:
"""后台手动恢复可用ready清除不可用状态。"""
key = self._info_key(agent_id)
if not await self.redis.exists(key):
return False
await self.redis.hset(key, "status", AgentStatus.READY.value)
# 恢复就绪时补齐心跳时间戳,避免被误判为超时
await self.redis.zadd(C.AGENT_HEARTBEAT_ZSET, {agent_id: time.time()})
return True
async def mark_idle(self, agent_id: str) -> bool:
"""任务结束/取消后标记 Agent 空闲。
若 agent 已被手动置为不可用unavailable保持不可用、不复活
否则置回 ready。
"""
key = self._info_key(agent_id)
if not await self.redis.exists(key):
return False
cur = await self.get(agent_id)
if cur is None:
return False
if cur.status == AgentStatus.UNAVAILABLE:
return True
await self.redis.hset(key, "status", AgentStatus.READY.value)
return True
async def beat(self, agent_id: str, current_load: int) -> bool:
"""更新心跳时间与负载,按状态机规则流转状态。
- 不可用unavailable保持不可用不因心跳复活。
- 离线offline心跳失联恢复 ready。
- ready/processing/stopping保持原状态。
"""
key = self._info_key(agent_id)
if not await self.redis.exists(key):
return False
now = time.time()
cur = await self.get(agent_id)
if cur is None:
return False
if cur.status == AgentStatus.UNAVAILABLE:
status = AgentStatus.UNAVAILABLE
elif cur.status == AgentStatus.OFFLINE:
status = AgentStatus.READY
else:
status = cur.status
await self.redis.hset(
key,
mapping={
"last_heartbeat": str(now),
"current_load": str(current_load),
"status": status.value,
},
)
await self.redis.zadd(C.AGENT_HEARTBEAT_ZSET, {agent_id: now})
return True
async def update_priority(self, agent_id: str, priority: int) -> bool:
"""更新 Agent 调度优先级。"""
key = self._info_key(agent_id)
if not await self.redis.exists(key):
return False
await self.redis.hset(key, "priority", str(priority))
return True
async def adjust_load(self, agent_id: str, delta: int) -> bool:
"""原子调整 Agent 负载delta 可为正/负),负载下限为 0。"""
key = self._info_key(agent_id)
if not await self.redis.exists(key):
return False
# hincrby 为原子增量操作,兼容 fakeredis不支持 Lua eval
new = await self.redis.hincrby(key, "current_load", delta)
if new < 0:
await self.redis.hset(key, "current_load", "0")
return True
# ---------- 读取 ----------
async def get(self, agent_id: str) -> AgentInfo | None:
raw = await self.redis.hgetall(self._info_key(agent_id))
if not raw:
return None
return AgentInfo(
agent_id=raw.get("agent_id", ""),
endpoint=raw.get("endpoint", ""),
agent_tags=[t for t in raw.get("agent_tags", "").split(",") if t],
max_concurrent=int(raw.get("max_concurrent", 1) or 1),
current_load=int(raw.get("current_load", 0) or 0),
priority=int(raw.get("priority", 3) or 3),
last_heartbeat=float(raw.get("last_heartbeat", 0) or 0),
status=AgentStatus(raw.get("status", AgentStatus.READY.value)),
create_time=float(raw.get("create_time", 0) or 0),
)
async def online(self) -> list[AgentInfo]:
# 可参与调度的状态ready空闲与 processing忙碌但仍有并发容量
# offline心跳失联、unavailable手动禁用与 stopping正在停止不参与调度。
return [a for a in await self.all() if a.status in (AgentStatus.READY, AgentStatus.PROCESSING)]
async def all(self) -> list[AgentInfo]:
ids = list(await self.redis.smembers(C.AGENT_ALL_SET))
out = []
for aid in ids:
a = await self.get(aid)
if a:
out.append(a)
return out
async def by_tags(self, tags: list[str]) -> list[AgentInfo]:
"""按标签索引取候选 Agent 池(取所有标签的交集,无标签则返回全部可调度)。
只返回可调度的 agentready/processingoffline/unavailable/stopping 被排除。
"""
if not tags:
return await self.online()
keys = [self._tag_key(t) for t in tags]
if len(keys) == 1:
ids = list(await self.redis.smembers(keys[0]))
else:
await self.redis.sinterstore("agent:tmp:intersect", keys)
ids = list(await self.redis.smembers("agent:tmp:intersect"))
await self.redis.delete("agent:tmp:intersect")
out = []
for aid in ids:
a = await self.get(aid)
if a and a.status in (AgentStatus.READY, AgentStatus.PROCESSING):
out.append(a)
return out
async def remove(self, agent_id: str) -> None:
"""注销:删除信息、心跳 ZSet、标签索引、全量索引。"""
a = await self.get(agent_id)
if a:
for tag in a.agent_tags:
await self.redis.srem(self._tag_key(tag), agent_id)
await self.redis.delete(self._info_key(agent_id))
await self.redis.zrem(C.AGENT_HEARTBEAT_ZSET, agent_id)
await self.redis.srem(C.AGENT_ALL_SET, agent_id)
async def stale_agents(self, timeout: float) -> list[str]:
"""返回超过 timeout 秒未心跳的 Agent ID基于 ZSet score"""
cutoff = time.time() - timeout
scored = await self.redis.zrangebyscore(C.AGENT_HEARTBEAT_ZSET, 0, cutoff)
return list(scored)
async def heartbeat_map(self) -> dict[str, float]:
"""返回 {agent_id: last_heartbeat}。"""
return {k: float(v) for k, v in await self.redis.zrange(C.AGENT_HEARTBEAT_ZSET, 0, -1, withscores=True)}