修复了一些已知问题
This commit is contained in:
@@ -15,10 +15,19 @@ class HandoffTool(FunctionTool, Generic[TContext]):
|
||||
**kwargs,
|
||||
):
|
||||
self.agent = agent
|
||||
|
||||
# Avoid passing duplicate `description` to the FunctionTool dataclass.
|
||||
# Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs
|
||||
# to override what the main agent sees, while we also compute a default
|
||||
# description here.
|
||||
description = kwargs.pop(
|
||||
"description",
|
||||
agent.instructions or self.default_description(agent.name),
|
||||
)
|
||||
super().__init__(
|
||||
name=f"transfer_to_{agent.name}",
|
||||
parameters=parameters or self.default_parameters(),
|
||||
description=agent.instructions or self.default_description(agent.name),
|
||||
description=description,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ class SubAgentConfig:
|
||||
"""Runtime representation of a configured subagent."""
|
||||
|
||||
name: str
|
||||
# Instructions are used as the subagent's system prompt.
|
||||
instructions: str
|
||||
# Public description is what the main LLM sees for transfer_to_* tool description.
|
||||
public_description: str
|
||||
tools: list[str]
|
||||
enabled: bool = True
|
||||
|
||||
@@ -63,7 +66,8 @@ class SubAgentOrchestrator:
|
||||
if not name:
|
||||
continue
|
||||
|
||||
instructions = str(item.get("description", "")).strip()
|
||||
instructions = str(item.get("system_prompt", "")).strip()
|
||||
public_description = str(item.get("public_description", "")).strip()
|
||||
tools = item.get("tools", [])
|
||||
if not isinstance(tools, list):
|
||||
tools = []
|
||||
@@ -74,7 +78,9 @@ class SubAgentOrchestrator:
|
||||
instructions=instructions,
|
||||
tools=tools,
|
||||
)
|
||||
handoff = HandoffTool(agent=agent)
|
||||
# The tool description should be a short description for the main LLM,
|
||||
# while the subagent system prompt can be longer/more specific.
|
||||
handoff = HandoffTool(agent=agent, description=public_description or None)
|
||||
|
||||
# Mark as dynamic so we can replace/remove later.
|
||||
handoff.handler_module_path = "core.subagent_orchestrator"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import traceback
|
||||
|
||||
from quart import request
|
||||
from quart import jsonify
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||
@@ -16,11 +17,13 @@ class SubAgentRoute(Route):
|
||||
) -> None:
|
||||
super().__init__(context)
|
||||
self.core_lifecycle = core_lifecycle
|
||||
self.routes = {
|
||||
"/subagent/config": ("GET", self.get_config),
|
||||
"/subagent/config": ("POST", self.update_config),
|
||||
"/subagent/available-tools": ("GET", self.get_available_tools),
|
||||
}
|
||||
# NOTE: dict cannot hold duplicate keys; use list form to register multiple
|
||||
# methods for the same path.
|
||||
self.routes = [
|
||||
("/subagent/config", ("GET", self.get_config)),
|
||||
("/subagent/config", ("POST", self.update_config)),
|
||||
("/subagent/available-tools", ("GET", self.get_available_tools)),
|
||||
]
|
||||
self.register_routes()
|
||||
|
||||
async def get_config(self):
|
||||
@@ -45,16 +48,16 @@ class SubAgentRoute(Route):
|
||||
data.setdefault("main_enable", False)
|
||||
data.setdefault("main_tools_policy", "handoff_only")
|
||||
data.setdefault("agents", [])
|
||||
return Response().ok(data=data).__dict__
|
||||
return jsonify(Response().ok(data=data).__dict__)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"获取 subagent 配置失败: {e!s}").__dict__
|
||||
return jsonify(Response().error(f"获取 subagent 配置失败: {e!s}").__dict__)
|
||||
|
||||
async def update_config(self):
|
||||
try:
|
||||
data = await request.json
|
||||
if not isinstance(data, dict):
|
||||
return Response().error("配置必须为 JSON 对象").__dict__
|
||||
return jsonify(Response().error("配置必须为 JSON 对象").__dict__)
|
||||
|
||||
cfg = self.core_lifecycle.astrbot_config
|
||||
provider_settings = cfg.get("provider_settings", {})
|
||||
@@ -62,17 +65,18 @@ class SubAgentRoute(Route):
|
||||
cfg["provider_settings"] = provider_settings
|
||||
|
||||
# Persist to cmd_config.json
|
||||
self.core_lifecycle.astrbot_config_mgr.save(cfg)
|
||||
# AstrBotConfigManager does not expose a `save()` method; persist via AstrBotConfig.
|
||||
cfg.save_config()
|
||||
|
||||
# Reload dynamic handoff tools if orchestrator exists
|
||||
orch = getattr(self.core_lifecycle, "subagent_orchestrator", None)
|
||||
if orch is not None:
|
||||
orch.reload_from_config(provider_settings)
|
||||
|
||||
return Response().ok(message="保存成功").__dict__
|
||||
return jsonify(Response().ok(message="保存成功").__dict__)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"保存 subagent 配置失败: {e!s}").__dict__
|
||||
return jsonify(Response().error(f"保存 subagent 配置失败: {e!s}").__dict__)
|
||||
|
||||
async def get_available_tools(self):
|
||||
"""Return all registered tools (name/description/parameters/active/origin).
|
||||
@@ -92,7 +96,7 @@ class SubAgentRoute(Route):
|
||||
"handler_module_path": tool.handler_module_path,
|
||||
}
|
||||
)
|
||||
return Response().ok(data=tools_dict).__dict__
|
||||
return jsonify(Response().ok(data=tools_dict).__dict__)
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
return Response().error(f"获取可用工具失败: {e!s}").__dict__
|
||||
return jsonify(Response().error(f"获取可用工具失败: {e!s}").__dict__)
|
||||
|
||||
@@ -132,7 +132,8 @@ class AstrBotDashboard:
|
||||
r = jsonify(Response().error("未授权").__dict__)
|
||||
r.status_code = 401
|
||||
return r
|
||||
token = token.removeprefix("Bearer ")
|
||||
# Be tolerant of different header casing / formatting.
|
||||
token = token.strip().removeprefix("Bearer ").strip()
|
||||
try:
|
||||
payload = jwt.decode(token, self._jwt_secret, algorithms=["HS256"])
|
||||
g.username = payload["username"]
|
||||
|
||||
@@ -61,6 +61,20 @@ axios.interceptors.request.use((config) => {
|
||||
return config;
|
||||
});
|
||||
|
||||
// Keep fetch() calls consistent with axios by automatically attaching the JWT.
|
||||
// Some parts of the UI use fetch directly; without this, those requests will 401.
|
||||
const _origFetch = window.fetch.bind(window);
|
||||
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return _origFetch(input, init);
|
||||
|
||||
const headers = new Headers(init?.headers || (typeof input !== 'string' && 'headers' in input ? (input as Request).headers : undefined));
|
||||
if (!headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
return _origFetch(input, { ...init, headers });
|
||||
};
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs',
|
||||
|
||||
@@ -116,9 +116,12 @@
|
||||
label="分配工具(多选)"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
class="subagent-tools"
|
||||
multiple
|
||||
chips
|
||||
closable-chips
|
||||
:menu-props="{ maxHeight: 320 }"
|
||||
:max-chips="8"
|
||||
:loading="toolsLoading"
|
||||
:disabled="toolsLoading"
|
||||
clearable
|
||||
@@ -127,15 +130,26 @@
|
||||
</v-row>
|
||||
|
||||
<v-textarea
|
||||
v-model="agent.description"
|
||||
label="SubAgent 描述 / 指令"
|
||||
v-model="agent.public_description"
|
||||
label="对主 LLM 的描述(用于决定是否 handoff)"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
auto-grow
|
||||
hint="主 LLM 主要通过这里的描述来决定是否 handoff 到该 SubAgent。"
|
||||
hint="这段会作为 transfer_to_* 工具的描述给主 LLM 看,建议简短明确。"
|
||||
persistent-hint
|
||||
/>
|
||||
|
||||
<v-textarea
|
||||
v-model="agent.system_prompt"
|
||||
label="SubAgent System Prompt(该 SubAgent 自己的指令)"
|
||||
variant="outlined"
|
||||
rows="4"
|
||||
auto-grow
|
||||
hint="这段只给该 SubAgent 自己作为 system prompt 使用,可以更长、更严格。"
|
||||
persistent-hint
|
||||
class="mt-3"
|
||||
/>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="text-caption text-medium-emphasis">预览:主 LLM 将看到的 handoff 工具</div>
|
||||
<div class="d-flex align-center" style="gap: 8px; flex-wrap: wrap;">
|
||||
@@ -175,7 +189,8 @@ type ToolOption = { title: string; value: string }
|
||||
type SubAgentItem = {
|
||||
__key: string
|
||||
name: string
|
||||
description: string
|
||||
public_description: string
|
||||
system_prompt: string
|
||||
tools: string[]
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -221,14 +236,16 @@ function normalizeConfig(raw: any): SubAgentConfig {
|
||||
|
||||
const agents: SubAgentItem[] = agentsRaw.map((a: any, i: number) => {
|
||||
const name = (a?.name ?? '').toString()
|
||||
const description = (a?.description ?? '').toString()
|
||||
const public_description = (a?.public_description ?? '').toString()
|
||||
const system_prompt = (a?.system_prompt ?? '').toString()
|
||||
const tools = Array.isArray(a?.tools) ? a.tools.map((x: any) => String(x)) : []
|
||||
const enabled = a?.enabled !== false
|
||||
|
||||
return {
|
||||
__key: `${Date.now()}_${i}_${Math.random().toString(16).slice(2)}`,
|
||||
name,
|
||||
description,
|
||||
public_description,
|
||||
system_prompt,
|
||||
tools,
|
||||
enabled
|
||||
}
|
||||
@@ -296,7 +313,8 @@ function addAgent() {
|
||||
cfg.value.agents.push({
|
||||
__key: `${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
||||
name: '',
|
||||
description: '',
|
||||
public_description: '',
|
||||
system_prompt: '',
|
||||
tools: [],
|
||||
enabled: true
|
||||
})
|
||||
@@ -316,7 +334,8 @@ async function save() {
|
||||
main_tools_policy: 'handoff_only',
|
||||
agents: cfg.value.agents.map(a => ({
|
||||
name: a.name,
|
||||
description: a.description,
|
||||
public_description: a.public_description,
|
||||
system_prompt: a.system_prompt,
|
||||
tools: a.tools,
|
||||
enabled: a.enabled
|
||||
}))
|
||||
@@ -351,3 +370,25 @@ onMounted(() => {
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/*
|
||||
Vuetify renders selected chips inside the input control and will grow the
|
||||
field height as chips wrap. For subagent tool assignment this quickly becomes
|
||||
unwieldy, so we cap the chip area height and allow scrolling.
|
||||
|
||||
Note: this must be a non-scoped style so it can reach Vuetify's internal
|
||||
elements.
|
||||
*/
|
||||
.subagent-tools .v-field__input {
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
/* Small breathing room so the scrollbar doesn't overlap chip close icons. */
|
||||
.subagent-tools .v-field__input {
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user