349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""
|
||
全局 Agent 状态监控脚本
|
||
列出三个 agent 所有会话的实时状态,一眼看出哪些在跑、哪些卡住了。
|
||
|
||
用法:
|
||
python agent_status.py # 查看所有 agent 的所有会话
|
||
python agent_status.py --watch # 实时刷新模式(每 3 秒刷新一次)
|
||
python agent_status.py -w -i 2 # 实时刷新,间隔 2 秒
|
||
python agent_status.py --agent my_agent # 只看指定 agent
|
||
python agent_status.py --limit 20 # 每个 agent 最多显示 20 条
|
||
python agent_status.py -s my_session # 查看特定会话的详细状态
|
||
"""
|
||
import argparse
|
||
import json
|
||
import os
|
||
import sys
|
||
import time
|
||
from datetime import datetime
|
||
|
||
import httpx
|
||
|
||
|
||
# 三个 agent 的 API 地址
|
||
AGENTS = {
|
||
"my_agent": "http://127.0.0.1:8001",
|
||
"luna_agent": "http://127.0.0.1:8002",
|
||
"qwen_agent": "http://127.0.0.1:8003",
|
||
}
|
||
|
||
USER_ID = "codebuddy"
|
||
|
||
|
||
def check_api_alive(url: str) -> bool:
|
||
"""检查 API Server 是否存活"""
|
||
try:
|
||
resp = httpx.get(f"{url}/health", timeout=2.0)
|
||
return resp.status_code == 200
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def list_sessions(api_url: str, app_name: str, user_id: str) -> list[dict]:
|
||
"""获取所有会话列表"""
|
||
try:
|
||
resp = httpx.get(
|
||
f"{api_url}/apps/{app_name}/users/{user_id}/sessions",
|
||
timeout=5.0,
|
||
)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
# 响应可能是 list 或 {sessions: [...]}
|
||
if isinstance(data, list):
|
||
return data
|
||
return data.get("sessions", [])
|
||
return []
|
||
except Exception:
|
||
return []
|
||
|
||
|
||
def get_session_detail(api_url: str, app_name: str, user_id: str, session_id: str) -> dict | None:
|
||
"""获取会话详情"""
|
||
try:
|
||
resp = httpx.get(
|
||
f"{api_url}/apps/{app_name}/users/{user_id}/sessions/{session_id}",
|
||
timeout=5.0,
|
||
)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
return None
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def format_time(timestamp: float) -> str:
|
||
"""格式化时间戳"""
|
||
if not timestamp:
|
||
return "?"
|
||
try:
|
||
return datetime.fromtimestamp(timestamp).strftime("%m-%d %H:%M:%S")
|
||
except Exception:
|
||
return str(timestamp)[:19]
|
||
|
||
|
||
def get_last_event_summary(events: list[dict]) -> tuple[str, str, str, str]:
|
||
"""
|
||
从事件列表提取最后一条事件的摘要信息。
|
||
返回: (角色, 作者, 状态描述, 内容摘要)
|
||
"""
|
||
if not events:
|
||
return ("-", "-", "(空会话)", "")
|
||
|
||
last = events[-1]
|
||
content = last.get("content", {})
|
||
role = content.get("role", "?")
|
||
author = last.get("author", "")
|
||
parts = content.get("parts", [])
|
||
|
||
# 判断状态
|
||
status = ""
|
||
summary = ""
|
||
|
||
role_cn = {
|
||
"user": "用户输入",
|
||
"model": "模型回复",
|
||
"function": "工具调用",
|
||
}.get(role, role)
|
||
|
||
for part in parts:
|
||
if "thought" in part and part.get("thought"):
|
||
status = "💭 思考中"
|
||
text = part.get("text", "")
|
||
summary = text[:60].replace("\n", " ")
|
||
break
|
||
elif "functionCall" in part:
|
||
call = part["functionCall"]
|
||
status = f"📞 调用中: {call.get('name', '?')}"
|
||
args = call.get("args", {})
|
||
# 显示关键参数
|
||
if "path" in args:
|
||
summary = f"path: {args['path'][:50]}"
|
||
elif "command" in args:
|
||
summary = f"cmd: {args['command'][:50]}"
|
||
else:
|
||
args_str = json.dumps(args, ensure_ascii=False)[:60]
|
||
summary = args_str
|
||
break
|
||
elif "functionResponse" in part:
|
||
resp = part["functionResponse"]
|
||
status = f"✅ 工具返回: {resp.get('name', '?')}"
|
||
resp_content = resp.get("content", [])
|
||
text = ""
|
||
for c in resp_content:
|
||
if isinstance(c, dict) and c.get("type") == "text":
|
||
text += c.get("text", "")
|
||
elif isinstance(c, str):
|
||
text += c
|
||
summary = text[:80].replace("\n", " ")
|
||
if resp.get("isError"):
|
||
status = f"❌ 工具错误: {resp.get('name', '?')}"
|
||
break
|
||
elif "text" in part:
|
||
status = f"💬 {role_cn}"
|
||
summary = part["text"][:80].replace("\n", " ")
|
||
break
|
||
|
||
if not status:
|
||
status = f"📨 {role_cn}"
|
||
|
||
return (role, author, status, summary)
|
||
|
||
|
||
def print_status_table(agent_names: list[str], limit: int):
|
||
"""打印所有 agent 的会话状态表格"""
|
||
total_sessions = 0
|
||
active_count = 0
|
||
|
||
for agent_name in agent_names:
|
||
api_url = AGENTS[agent_name]
|
||
alive = check_api_alive(api_url)
|
||
|
||
print(f"\n{'═' * 80}")
|
||
status_icon = "🟢" if alive else "🔴"
|
||
print(f"{status_icon} {agent_name} ({api_url})")
|
||
print(f"{'─' * 80}")
|
||
|
||
if not alive:
|
||
print(" ⚠️ API Server 未启动或无法连接")
|
||
continue
|
||
|
||
sessions = list_sessions(api_url, agent_name, USER_ID)
|
||
total_sessions += len(sessions)
|
||
|
||
if not sessions:
|
||
print(" (暂无会话)")
|
||
continue
|
||
|
||
# 按更新时间倒序(字段名可能是 lastUpdateTime 或 last_update_time)
|
||
sessions.sort(
|
||
key=lambda s: s.get("lastUpdateTime") or s.get("last_update_time", 0),
|
||
reverse=True,
|
||
)
|
||
|
||
# 只显示 limit 条
|
||
shown = sessions[:limit]
|
||
hidden_count = len(sessions) - limit
|
||
|
||
print(f" {'#':>3s} {'最后更新时间':<18s} {'事件数':>5s} {'状态'}")
|
||
print(f" {'─' * 76}")
|
||
|
||
for idx, sess in enumerate(shown, 1):
|
||
sid = sess.get("id", "?")
|
||
last_time = sess.get("lastUpdateTime") or sess.get("last_update_time", 0)
|
||
time_str = format_time(last_time)
|
||
|
||
# 取详情获取最后事件
|
||
detail = get_session_detail(api_url, agent_name, USER_ID, sid)
|
||
events = detail.get("events", []) if detail else []
|
||
event_count = len(events)
|
||
_, _, status, _ = get_last_event_summary(events)
|
||
|
||
# 判断是否活跃(5分钟内有更新)
|
||
now_ts = time.time()
|
||
is_active = (now_ts - last_time) < 300
|
||
if is_active and event_count > 0:
|
||
active_count += 1
|
||
active_icon = "⚡"
|
||
else:
|
||
active_icon = " "
|
||
|
||
sid_short = sid if len(sid) <= 12 else sid[:10] + ".."
|
||
print(f" {active_icon}{idx:>2d}. {time_str} {event_count:>5d} {status[:50]}")
|
||
print(f" id: {sid}")
|
||
|
||
if hidden_count > 0:
|
||
print(f"\n ... 还有 {hidden_count} 个会话未显示(共 {len(sessions)} 个)")
|
||
|
||
print(f"\n{'═' * 80}")
|
||
print(f" 总计: {total_sessions} 个会话 | 活跃中(5分钟内有更新): {active_count}")
|
||
print(f"{'═' * 80}\n")
|
||
|
||
|
||
def watch_mode(agent_names: list[str], interval: float, limit: int):
|
||
"""实时刷新模式"""
|
||
print(f"\n🔄 实时监控模式(每 {interval} 秒刷新,Ctrl+C 退出)\n")
|
||
try:
|
||
while True:
|
||
# 清屏
|
||
if os.name == "nt":
|
||
os.system("cls")
|
||
else:
|
||
os.system("clear")
|
||
print_status_table(agent_names, limit)
|
||
print(f" 最后刷新: {datetime.now().strftime('%H:%M:%S')} | Ctrl+C 退出")
|
||
time.sleep(interval)
|
||
except KeyboardInterrupt:
|
||
print("\n👋 已退出监控。")
|
||
|
||
|
||
def show_session_detail(agent_name: str, session_id: str):
|
||
"""查看特定会话的详细状态"""
|
||
api_url = AGENTS.get(agent_name, "")
|
||
if not api_url:
|
||
print(f"未知 agent: {agent_name}")
|
||
return
|
||
|
||
alive = check_api_alive(api_url)
|
||
if not alive:
|
||
print(f"⚠️ {agent_name} API Server 未启动({api_url})")
|
||
return
|
||
|
||
detail = get_session_detail(api_url, agent_name, USER_ID, session_id)
|
||
if not detail:
|
||
print(f"会话 [{session_id}] 不存在")
|
||
return
|
||
|
||
events = detail.get("events", [])
|
||
event_count = len(events)
|
||
last_time = detail.get("lastUpdateTime") or detail.get("last_update_time", 0)
|
||
# 会话详情里没有 create_time,从第一个事件的时间戳估算
|
||
create_time = detail.get("create_time", 0)
|
||
if not create_time and events:
|
||
create_time = events[0].get("timestamp", 0)
|
||
|
||
print(f"\n{'═' * 80}")
|
||
print(f"📋 会话详情")
|
||
print(f"{'═' * 80}")
|
||
print(f" Agent: {agent_name}")
|
||
print(f" Session: {session_id}")
|
||
print(f" 创建时间: {format_time(create_time)}")
|
||
print(f" 更新时间: {format_time(last_time)}")
|
||
print(f" 事件数: {event_count}")
|
||
|
||
if events:
|
||
duration = last_time - create_time if create_time and last_time else 0
|
||
if duration > 0:
|
||
mins = int(duration // 60)
|
||
secs = int(duration % 60)
|
||
print(f" 运行时长: {mins}分{secs}秒")
|
||
|
||
# 最后 5 条事件
|
||
print(f"\n{'─' * 80}")
|
||
print(f" 最后 5 条事件:")
|
||
print(f"{'─' * 80}")
|
||
|
||
for i, event in enumerate(events[-5:], max(1, event_count - 4)):
|
||
role, author, status, summary = get_last_event_summary([event])
|
||
ts = event.get("timestamp", 0)
|
||
t_str = format_time(ts).split()[-1] if " " in format_time(ts) else format_time(ts)
|
||
print(f"\n #{i} [{t_str}] {status}")
|
||
if summary:
|
||
print(f" {summary[:100]}")
|
||
|
||
print(f"\n{'═' * 80}\n")
|
||
|
||
|
||
def main():
|
||
global USER_ID
|
||
parser = argparse.ArgumentParser(description="全局 Agent 状态监控工具")
|
||
parser.add_argument("--agent", "-a", default=None,
|
||
help="只查看指定 agent(默认查看所有)")
|
||
parser.add_argument("--watch", "-w", action="store_true",
|
||
help="实时刷新模式")
|
||
parser.add_argument("--interval", "-i", type=float, default=3.0,
|
||
help="刷新间隔秒数(默认 3.0)")
|
||
parser.add_argument("--limit", "-l", type=int, default=10,
|
||
help="每个 agent 最多显示的会话数(默认 10)")
|
||
parser.add_argument("--session", "-s", default=None,
|
||
help="查看特定会话的详细状态")
|
||
parser.add_argument("--user", "-u", default="codebuddy",
|
||
help="用户 ID(默认 codebuddy)")
|
||
args = parser.parse_args()
|
||
|
||
global USER_ID
|
||
USER_ID = args.user
|
||
|
||
# 确定要查看的 agent 列表
|
||
if args.agent:
|
||
agent_name = args.agent
|
||
# 支持别名
|
||
aliases = {
|
||
"my": "my_agent", "default": "my_agent", "aq": "my_agent",
|
||
"luna": "luna_agent", "gpt": "luna_agent",
|
||
"qwen": "qwen_agent", "astron": "qwen_agent",
|
||
}
|
||
if agent_name in aliases:
|
||
agent_name = aliases[agent_name]
|
||
if agent_name not in AGENTS:
|
||
print(f"未知 agent: {args.agent}")
|
||
print(f"可用: {list(AGENTS.keys())}")
|
||
sys.exit(1)
|
||
agent_names = [agent_name]
|
||
else:
|
||
agent_names = list(AGENTS.keys())
|
||
|
||
# 查看单个会话详情
|
||
if args.session:
|
||
show_session_detail(agent_names[0], args.session)
|
||
return
|
||
|
||
# 实时刷新模式
|
||
if args.watch:
|
||
watch_mode(agent_names, args.interval, args.limit)
|
||
else:
|
||
print_status_table(agent_names, args.limit)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|