package vip.mate.agent;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import vip.mate.agent.binding.service.AgentBindingService;
import vip.mate.agent.context.ChatOrigin;
import vip.mate.agent.model.AgentEntity;
import vip.mate.exception.MateClawException;
import vip.mate.skill.model.SkillEntity;
import vip.mate.skill.repository.SkillMapper;
import vip.mate.tool.model.AvailableToolDTO;
import vip.mate.tool.service.AvailableToolService;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Agent-callable employee authoring tool.
*
*
Lets an agent design and persist a new specialized employee (Agent)
* from a plain-language role spec, then bind a focused capability set to
* it. Pairs with the workflow drafting tool so a single chat turn can plan
* a team of employees and chain them into a workflow:
* design roles → {@link #create_employee} for each → workflow drafting tool
* referencing the just-created employees.
*
*
Workspace is taken from {@link ChatOrigin} on the active
* {@link ToolContext}; the LLM can never write into a foreign workspace
* even if its prompt tried to forge one. Mirrors the create-then-bind
* sequence used when applying an agent template.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AgentAuthoringTool {
private final AgentService agentService;
private final AgentBindingService agentBindingService;
private final SkillMapper skillMapper;
private final AvailableToolService availableToolService;
private final ObjectMapper objectMapper;
/** Cap on names listed per catalog section so the tool result stays small. */
private static final int CATALOG_MAX_PER_SECTION = 200;
@Tool(description = """
Create a new specialized employee (Agent) in the current workspace from a role spec, \
and optionally bind a focused set of skills and tools to it. \
Use this when a task needs a role that does not exist yet — design the role, then create it. \
Returns the new agentId (string) and a short summary. \
Leave skillNames/toolNames empty to make a generalist that inherits all globally-enabled capabilities. \
Call list_capability_catalog first to learn the exact skill and tool names you can assign. \
The created employee is enabled immediately and can be referenced by the workflow drafting tool.""")
public String create_employee(
@ToolParam(description = "Employee name, unique within the workspace, e.g. \"market-research-analyst\".")
String name,
@ToolParam(description = "One-line description of the employee's role and responsibility. Shown in pickers and used by the workflow planner to route work.")
String description,
@ToolParam(description = "System prompt that defines the employee's persona, expertise, and working style. Be specific about its specialty.")
String systemPrompt,
@ToolParam(description = "Agent type: \"react\" (single-loop reasoning, default) or \"plan_execute\" (decompose then execute). Leave empty for react.", required = false)
String agentType,
@ToolParam(description = "Optional model name override (must match an enabled model). Leave empty to use the workspace default model.", required = false)
String modelName,
@ToolParam(description = "Skills to bind, as a JSON array of skill names or a comma-separated list, e.g. [\"sql_query\",\"make_plan\"]. Empty = inherit all globally-enabled skills. Names must come from list_capability_catalog.", required = false)
String skillNames,
@ToolParam(description = "Tools to bind, as a JSON array of tool names or a comma-separated list, e.g. [\"web_search\",\"read_file\"]. Empty = inherit all globally-enabled tools. Names must come from list_capability_catalog.", required = false)
String toolNames,
@Nullable ToolContext ctx) {
ChatOrigin origin = ChatOrigin.from(ctx);
Long workspaceId = origin.workspaceId();
if (workspaceId == null || workspaceId <= 0) {
return "[error] Cannot determine the current workspace; invoke this tool within a workspace context.";
}
if (name == null || name.isBlank()) {
return "[error] Employee name is required.";
}
AgentEntity agent = new AgentEntity();
agent.setName(name.trim());
agent.setDescription(blankToNull(description));
if (systemPrompt != null && !systemPrompt.isBlank()) {
agent.setSystemPrompt(systemPrompt);
}
agent.setAgentType(normalizeAgentType(agentType));
agent.setModelName(blankToNull(modelName));
agent.setWorkspaceId(workspaceId);
agent.setCreatorUserId(parseUserId(origin.requesterId()));
AgentEntity created;
try {
created = agentService.createAgent(agent);
} catch (MateClawException e) {
// Duplicate name / blank name surface here as a friendly message
// so the planner can rename and retry instead of aborting.
return "[error] Failed to create employee: " + e.getMessage();
}
List requestedSkills = parseNameList(skillNames);
List requestedTools = parseNameList(toolNames);
List boundSkills = bindSkills(created, workspaceId, requestedSkills);
List boundTools = bindTools(created, requestedTools);
Map result = new LinkedHashMap<>();
result.put("agentId", String.valueOf(created.getId()));
result.put("name", created.getName());
result.put("agentType", created.getAgentType());
result.put("skillsBound", boundSkills.isEmpty() ? "(inherits global defaults)" : boundSkills);
result.put("toolsBound", boundTools.isEmpty() ? "(inherits global defaults)" : boundTools);
result.put("note", "Employee created and enabled. Reference it by name in the workflow drafting tool to chain it into a workflow.");
try {
return objectMapper.writeValueAsString(result);
} catch (Exception e) {
return "Employee created: id=" + created.getId() + " name=" + created.getName();
}
}
@Tool(description = """
List the capabilities you can assign when creating an employee: the enabled skill names \
and the bindable tool names in the current workspace. \
Call this before create_employee so you assign real, resolvable names rather than guessing.""")
public String list_capability_catalog(@Nullable ToolContext ctx) {
ChatOrigin origin = ChatOrigin.from(ctx);
Long workspaceId = origin.workspaceId();
// Skills: builtin (global) + skills owned by this workspace, enabled only.
List skills = skillMapper.selectList(new LambdaQueryWrapper()
.eq(SkillEntity::getEnabled, true)
.eq(SkillEntity::getDeleted, 0)
.orderByAsc(SkillEntity::getName));
long effectiveWs = workspaceId == null ? 1L : workspaceId;
List