Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/prompts/nl2agent_en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ system_prompt: |-
### Resource Revisions
- In revision mode, search only for the newly requested capability. Do not search again for existing unrelated capabilities and do not replace or overwrite unrelated `bound_resources`.
- A request only to add or configure a resource does not confirm any Prompt update. If the user asks to update "related Prompts" without naming fields, first use `requirement_clarification` to ask which Prompt fields to synchronize.
- To add a Tool or Skill, apply the Two-Stage Resource Search only to that new capability. Use `suggested_resource_installation` when installation is needed, then search again and use `installed_resource_binding`. To reconfigure a specifically requested bound resource, search with its exact verified name or capability and use the installed binding card directly.
- To add a Tool or Skill, apply the Two-Stage Resource Search only to that new capability. Use `suggested_resource_installation` when installation is needed, then search again and use `installed_resource_binding`. To reconfigure a specifically requested bound resource, search with its exact verified name or capability and use the installed binding card directly. When installed search returns an already-bound Tool, keep it eligible for recommendation by its unchanged score; never discard it merely because it appears in `bound_resources`. The binding card restores its current configuration and updates the existing binding after confirmation.
- After a revision-mode `suggested_resource_installation` action, remain in revision mode. Preserve its results, search again only for the requested capability, and proceed to installed binding without revisiting unrelated capabilities.
- After a revision-mode `installed_resource_binding` `continue` action, remain in revision mode. If the user explicitly confirmed related Prompt updates, save only those confirmed fields; otherwise output the Revision Summary with the resource change and leave every Prompt unchanged. Never start at `duty_prompt` or enter the full Prompt generation chain merely because the card was confirmed.
- Conversational removal is unsupported. For removal, tell the user to use the Tools and Skills section of the form on the right. For replacement, the new resource may be added first, but tell the user to remove the old resource in that form.
Expand Down Expand Up @@ -63,7 +63,7 @@ system_prompt: |-
4. Decode every JSON text result with `json.loads` before indexing or forwarding it. A requirement is uncovered only when its ID occurs in both installed and installable-resource `uncovered_requirement_ids`. Use only the real candidates returned by those searches and never invent an unavailable source.
5. Select only the smallest uninstalled candidate set needed to close coverage gaps, call `{{ recommend_tool_name }}`, then call `{{ wrapper_name }}` with subtype `suggested_resource_installation`. When no installation is needed, select the smallest installed coverage set and proceed directly to `installed_resource_binding`.
6. After a `suggested_resource_installation` action, preserve its `installed` and `skipped` results unchanged. Search `{{ installed_tool_name }}` again with the same requirements and trust only newly returned real `tool_id`/`skill_id` values. If requirements remain uncovered, place every installed and skipped candidate ref in `exclude_refs` while searching alternatives. When no alternative exists, use a clarification card requiring the user to revise, explicitly abandon, or end; never generate an incomplete Agent silently.
7. After installed search succeeds, choose the smallest candidate set covering strong matches and keep the total at or below {{ max_results }}. Pass unchanged candidates to `{{ recommend_tool_name }}`, decode its result, then pass that unchanged dictionary and the same `agent_id` to `{{ wrapper_name }}` with subtype `installed_resource_binding`.
7. After installed search succeeds, choose the smallest candidate set covering strong matches and keep the total at or below {{ max_results }}. Already-bound Tools remain normal candidates: retain their search scores and include them when selected by the same coverage rules, so the binding card can show their current configuration for confirmation or revision. Pass unchanged candidates to `{{ recommend_tool_name }}`, decode its result, then pass that unchanged dictionary and the same `agent_id` to `{{ wrapper_name }}` with subtype `installed_resource_binding`.
8. After an `installed_resource_binding` action with `continue` or `retry_generation`, use only the newly injected `bound_resources` database facts and follow the Atomic Action Contract strictly, executing only one Prompt branch per model response.
9. After the final Prompt batch succeeds and `agent_generation_completed` is received, output the plain-text completion summary directly. Do not call another tool or wrapper. For tool errors, use only `code` and `retryable`; retry at most once.

Expand Down
4 changes: 2 additions & 2 deletions backend/prompts/nl2agent_zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ system_prompt: |-
### 资源修订
- 修订模式只搜索用户新请求的能力,不得重新搜索已有无关能力,也不得替换或覆盖无关的 `bound_resources`。
- 仅要求新增或配置资源,不代表用户确认更新任何 Prompt。用户只说更新“相关 Prompt”但没有指定字段时,先使用 `requirement_clarification` 询问要同步哪些 Prompt 字段。
- 新增 Tool 或 Skill 时,只针对该新能力执行“两阶段资源搜索”。需要安装时使用 `suggested_resource_installation`,安装后重新搜索并进入 `installed_resource_binding`。重新配置用户明确指定的已绑定资源时,使用权威状态中的准确资源名称或能力进行搜索,并直接使用已安装资源绑定卡。
- 新增 Tool 或 Skill 时,只针对该新能力执行“两阶段资源搜索”。需要安装时使用 `suggested_resource_installation`,安装后重新搜索并进入 `installed_resource_binding`。重新配置用户明确指定的已绑定资源时,使用权威状态中的准确资源名称或能力进行搜索,并直接使用已安装资源绑定卡。已安装搜索返回已绑定 Tool 时,仍按其未改写的分数参与推荐,不得仅因它出现在 `bound_resources` 中就丢弃;绑定卡会恢复当前配置,并在用户确认后更新原绑定。
- 修订模式收到 `suggested_resource_installation` action 后,必须继续修订模式。原样保留 action 结果,只重新搜索用户请求的能力并进入已安装资源绑定,不得重新处理无关能力。
- 修订模式收到 `installed_resource_binding` 的 `continue` action 后,必须继续修订模式。仅当用户明确确认同步相关 Prompt 时,才保存这些确认字段;否则直接输出“修订总结”并说明资源变更,所有 Prompt 保持不变。不得因卡片已确认就从 `duty_prompt` 开始或进入完整 Prompt 生成链。
- 不支持通过对话移除资源。用户要求移除时,引导其在右侧表单的工具与技能区域操作。替换资源时可以先新增资源,但必须提示用户在该表单中移除旧资源。
Expand Down Expand Up @@ -63,7 +63,7 @@ system_prompt: |-
4. MCP JSON 文本在索引或继续传递前必须使用 `json.loads` 解码。只有同时出现在 installed 与 installable-resource 的 `uncovered_requirement_ids` 中的需求才算未覆盖;只能使用这些搜索真实返回的候选,不得编造不存在的来源。
5. 从未安装结果中只选择补齐缺口所必需的最小候选集,调用 `{{ recommend_tool_name }}` 后再调用 `{{ wrapper_name }}` 的 `suggested_resource_installation` subtype。若没有必要安装项,则从已安装结果选择最小覆盖集并直接进入 `installed_resource_binding`。
6. 收到 `suggested_resource_installation` action 后,原样保留 `installed` 与 `skipped` 结果。使用相同 requirements 重新调用 `{{ installed_tool_name }}`,只相信新返回的真实 `tool_id`/`skill_id`;仍未覆盖时把全部已安装和已跳过 candidate refs 放入 `exclude_refs` 搜索替代资源。没有替代资源时使用澄清卡要求用户明确修改需求、放弃需求或结束,禁止生成能力不完整的 Agent。
7. 已安装搜索成功后选择覆盖强匹配需求的最小候选集,总数不超过 {{ max_results }}。把未改写的候选传给 `{{ recommend_tool_name }}`,解码其结果,再将该字典和同一 `agent_id` 原样传给 `{{ wrapper_name }}` 的 `installed_resource_binding` subtype。
7. 已安装搜索成功后选择覆盖强匹配需求的最小候选集,总数不超过 {{ max_results }}。已绑定 Tool 仍是普通候选:保留搜索分数,并在相同覆盖规则选中它时继续推荐,让绑定卡展示当前配置供用户确认或修改。把未改写的候选传给 `{{ recommend_tool_name }}`,解码其结果,再将该字典和同一 `agent_id` 原样传给 `{{ wrapper_name }}` 的 `installed_resource_binding` subtype。
8. 收到 `installed_resource_binding` 的 `continue` 或 `retry_generation` action 后,只使用新注入的 `bound_resources` 数据库事实,并严格按“原子动作输出契约”每次只执行一个 Prompt 分支。
9. 最后一批 Prompt 保存成功并收到 `agent_generation_completed` 后,直接输出普通文本完成总结,不再调用任何 Tool 或 wrapper。Tool 出错时只依据 `code` 和 `retryable`,最多重试一次。

Expand Down
16 changes: 15 additions & 1 deletion backend/services/nl2agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,6 @@ async def _load_installed_resource_catalog(
if (
source not in {ToolSourceEnum.LOCAL.value, ToolSourceEnum.MCP.value}
or tool.get("is_available") is not True
or tool.get("is_user_selectable") is False
or name in internal_names
):
continue
Expand Down Expand Up @@ -989,6 +988,7 @@ def _recommended_resource(
actual: dict[str, Any],
supplied: ResourceCandidate,
recommended_refs: set[str],
is_bound: bool = False,
) -> RecommendedResource:
return RecommendedResource(
candidate=_verified_resource_candidate(actual, supplied),
Expand All @@ -997,6 +997,7 @@ def _recommended_resource(
if supplied.candidate_ref in recommended_refs
else "optional"
),
is_bound=is_bound,
form_kind=actual.get("form_kind") or (
"TOOL_CONFIG"
if actual["resource_type"] == "tool"
Expand Down Expand Up @@ -1038,6 +1039,7 @@ async def recommend_uninstalled_resources_impl(

async def recommend_installed_resources_impl(
*,
agent_id: int,
candidates: list[ResourceCandidate],
recommended_refs: list[str],
tenant_id: str,
Expand All @@ -1050,6 +1052,15 @@ async def recommend_installed_resources_impl(
user_id=user_id,
)
by_ref = {item["candidate_ref"]: item for item in catalog}
bound_tool_refs = {
f"tool:{instance['tool_id']}"
for instance in query_all_enabled_tool_instances(
agent_id=agent_id,
tenant_id=tenant_id,
version_no=0,
)
if isinstance(instance.get("tool_id"), int)
}
recommended = set(recommended_refs)
resources: list[RecommendedResource] = []
for supplied in candidates:
Expand All @@ -1060,12 +1071,14 @@ async def recommend_installed_resources_impl(
actual=actual,
supplied=supplied,
recommended_refs=recommended,
is_bound=supplied.candidate_ref in bound_tool_refs,
))
return RecommendResourcesOutput(resources=resources)


async def recommend_resources_impl(
*,
agent_id: int,
candidates: list[ResourceCandidate],
recommended_refs: list[str],
tenant_id: str,
Expand All @@ -1076,6 +1089,7 @@ async def recommend_resources_impl(
sources = {candidate.source for candidate in candidates}
if sources and sources.issubset(INSTALLED_RESOURCE_SOURCES):
return await recommend_installed_resources_impl(
agent_id=agent_id,
candidates=candidates,
recommended_refs=recommended_refs,
tenant_id=tenant_id,
Expand Down
3 changes: 3 additions & 0 deletions backend/tool_collection/mcp/nl2agent_mcp_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class RecommendedResource(BaseModel):
model_config = ConfigDict(extra="forbid")
candidate: ResourceCandidate
recommendation: Literal["recommended", "optional"]
is_bound: bool = False
form_kind: Literal[
"TOOL_CONFIG",
"SKILL_CONFIG",
Expand Down Expand Up @@ -943,6 +944,7 @@ async def recommend_resources(
user_id=user_id,
)
result = await recommend_resources_impl(
agent_id=resolved_agent_id,
candidates=payload.candidates,
recommended_refs=payload.recommended_refs,
tenant_id=tenant_id,
Expand Down Expand Up @@ -1017,6 +1019,7 @@ async def nl2a_wrapper(
if not sources or not sources.issubset(required_sources):
raise ValueError(f"invalid resources for {subtype}")
verified = await recommend_resources_impl(
agent_id=resolved_agent_id,
candidates=[resource.candidate for resource in supplied.resources],
recommended_refs=[
resource.candidate.candidate_ref
Expand Down
11 changes: 9 additions & 2 deletions frontend/app/[locale]/agents/agent-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
const displayInfoSectionRef = useRef<HTMLDivElement>(null);
const roleModelSectionRef = useRef<HTMLDivElement>(null);
const toolsSkillsSectionRef = useRef<HTMLDivElement>(null);
const knowledgeBaseSectionRef = useRef<HTMLDivElement>(null);
const conversationGuideSectionRef = useRef<HTMLDivElement>(null);
const lastScrolledRequestRef = useRef<string | null>(null);
const { configFocusRequest } = useNl2AgentFlow();
Expand Down Expand Up @@ -181,7 +182,10 @@

const { requestId, target } = configFocusRequest;
setActiveConfigTab(
target.section === "conversation_guide" ? "advanced" : "basic"
target.section === "conversation_guide" ||
target.section === "knowledge_base"
? "advanced"
: "basic"
);
setOpenSections((current) =>
current[target.section] ? current : { ...current, [target.section]: true }
Expand All @@ -198,7 +202,9 @@
? roleModelSectionRef.current
: target.section === "tools_skills"
? toolsSkillsSectionRef.current
: conversationGuideSectionRef.current;
: target.section === "knowledge_base"
? knowledgeBaseSectionRef.current
: conversationGuideSectionRef.current;

Check warning on line 207 in frontend/app/[locale]/agents/agent-config.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AaA8zAXjnaLCuzE29eGM&open=AaA8zAXjnaLCuzE29eGM&pullRequest=3783
if (!sectionElement) return;

const prefersReducedMotion = window.matchMedia(
Expand Down Expand Up @@ -410,6 +416,7 @@
onOpenChange={(open) =>
handleSectionOpenChange("knowledge_base", open)
}
containerRef={knowledgeBaseSectionRef}
headerActions={<KnowledgeBaseConfigActions />}
>
<KnowledgeBaseConfig />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { Tool, ToolParam } from "@/types/agentConfig";
import { TOOL_SOURCE_TYPES } from "@/const/agentConfig";
import { isManagedKnowledgeTool } from "@/lib/managedKnowledgeTools";
import ToolConfigModal from "./tool/ToolConfigModal";
import { useMergedToolParams } from "./tool/useMergedToolParams";
import {
TOOLS_REQUIRING_EMBEDDING,
TOOLS_REQUIRING_IMAGE_UNDERSTANDING,
Expand All @@ -21,7 +22,6 @@ import {
getToolLabels,
mergeCanonicalTool,
} from "./tool/utils";
import log from "@/lib/logger";

// --- Local tool helpers (not in utils) ---

Expand Down Expand Up @@ -122,42 +122,7 @@ export default function ToolManagement({
// --- Group by source → category ---
const grouped = groupToolsBySource(visibleSelectedTools);

const mergeParams = useCallback(
async (tool: Tool, forceFetch?: boolean): Promise<ToolParam[]> => {
const params = tool.initParams || [];
// If tool already has stored params in the agent config store, the user's
// unsaved modifications are already reflected in those params — skip the
// API call to avoid overwriting them with stale server data.
const hasStoredParams = params.some(
(p) => p.value !== undefined && p.value !== null && p.value !== ""
);
if (!forceFetch && hasStoredParams) {
return params;
}
if (!currentAgentId) return params;
try {
const { searchToolConfig } =
await import("@/services/agentConfigService");
const instance = await searchToolConfig(
parseInt(tool.id),
currentAgentId
);
if (instance.success && instance.data) {
return params.map((p) => ({
...p,
value:
instance.data?.params?.[p.name] !== undefined
? instance.data.params[p.name]
: p.value,
}));
}
} catch (err) {
log.error("mergeParams:", err);
}
return params;
},
[currentAgentId]
);
const mergeParams = useMergedToolParams(currentAgentId);

const openConfig = useCallback(
async (tool: Tool) => {
Expand All @@ -172,6 +137,7 @@ export default function ToolManagement({
: tool;
const toolToUse = mergeCanonicalTool(configuredTool, availableTools);
const merged = await mergeParams(toolToUse);
if (!merged) return;
setConfigTool(toolToUse);
setConfigParams(merged);
setModalOpen(true);
Expand Down
Loading
Loading