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
20 changes: 20 additions & 0 deletions agent/app/api/v2/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -1171,6 +1171,26 @@ func (b *BaseApi) InstallAgentSkill(c *gin.Context) {
helper.Success(c)
}

// @Tags AI
// @Summary Uninstall Agent skill
// @Accept json
// @Param request body dto.AgentSkillUninstallReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/skills/uninstall [post]
func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
var req dto.AgentSkillUninstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UninstallSkill(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}

// @Tags AI
// @Summary Login Agent Weixin channel
// @Accept json
Expand Down
26 changes: 19 additions & 7 deletions agent/app/dto/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,25 +590,32 @@ type AgentConfigFile struct {

type AgentSkillSearchReq struct {
AgentID uint `json:"agentId" validate:"required"`
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub"`
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"`
Keyword string `json:"keyword" validate:"required"`
}

type AgentSkillItem struct {
Name string `json:"name"`
Description string `json:"description"`
Source string `json:"source"`
Bundled bool `json:"bundled"`
Disabled bool `json:"disabled"`
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
Tags []string `json:"tags"`
Source string `json:"source"`
Trust string `json:"trust"`
Identifier string `json:"identifier"`
Bundled bool `json:"bundled"`
Disabled bool `json:"disabled"`
Uninstallable bool `json:"uninstallable"`
}

type AgentSkillSearchItem struct {
Slug string `json:"slug"`
Identifier string `json:"identifier"`
Name string `json:"name"`
Description string `json:"description"`
Summary string `json:"summary"`
Version string `json:"version"`
Source string `json:"source"`
Trust string `json:"trust"`
Score string `json:"score"`
}

Expand All @@ -620,7 +627,12 @@ type AgentSkillUpdateReq struct {

type AgentSkillInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub"`
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"`
Slug string `json:"slug" validate:"required"`
TaskID string `json:"taskID" validate:"required"`
}

type AgentSkillUninstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Name string `json:"name" validate:"required"`
}
7 changes: 4 additions & 3 deletions agent/app/service/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type IAgentService interface {
SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error)
UpdateSkill(req dto.AgentSkillUpdateReq) error
InstallSkill(req dto.AgentSkillInstallReq) error
UninstallSkill(req dto.AgentSkillUninstallReq) error

CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error)
DeleteRole(req dto.AgentRoleDeleteReq) error
Expand Down Expand Up @@ -455,9 +456,9 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
if err != nil {
return nil, err
}
model := resolveHermesConfiguredModelID(account, accountModels, cfg.Model.Default)
if model == "" {
model = agent.Model
model, err := resolveHermesConfiguredModelIDStrict(account, accountModels, cfg.Model.Default)
if err != nil {
return nil, err
Comment thread
zhengkunwang223 marked this conversation as resolved.
}
return &dto.AgentModelConfig{
AccountID: agent.AccountID,
Expand Down
21 changes: 21 additions & 0 deletions agent/app/service/agents_hermes.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
Expand All @@ -19,6 +20,7 @@ import (
)

const hermesWorkspaceDir = "/opt/data/workspace"
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"

type hermesConfig struct {
Model hermesModelConfig `yaml:"model"`
Expand Down Expand Up @@ -52,6 +54,17 @@ func buildHermesDockerExecArgs(containerName string, hermesArgs ...string) []str
return buildHermesDockerExecCommandArgs(containerName, "hermes", hermesArgs...)
}

func buildHermesSkillUninstallArgs(containerName, skillName string) []string {
return buildHermesDockerExecCommandArgs(
containerName,
"sh",
"-lc",
fmt.Sprintf(`printf 'y\n' | %s skills uninstall "$1"`, hermesExecutablePath),
"sh",
skillName,
)
}

func writeHermesConfig(confDir string, account *model.AgentAccount, modelName string, timezone string) error {
if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required")
Expand Down Expand Up @@ -386,6 +399,14 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
return ""
}

func resolveHermesConfiguredModelIDStrict(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
modelID := resolveHermesConfiguredModelID(account, accountModels, configuredModel)
if modelID == "" {
return "", buserr.New("ErrAgentModelNotInAccount")
}
return modelID, nil
}

func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
if account == nil {
return nil
Expand Down
11 changes: 7 additions & 4 deletions agent/app/service/agents_hermes_channels.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
}
groupAllowFrom := extractStringList(extra["group_allow_from"])

return &dto.AgentQQBotConfig{
result := &dto.AgentQQBotConfig{
Enabled: extractBoolValue(platform["enabled"], false) && appID != "" && clientSecret != "",
DmPolicy: dmPolicy,
AllowFrom: allowFrom,
GroupPolicy: groupPolicy,
GroupAllowFrom: groupAllowFrom,
Bots: []dto.AgentQQBotBot{
}
if appID != "" || clientSecret != "" {
result.Bots = []dto.AgentQQBotBot{
{
AgentChannelBotBase: dto.AgentChannelBotBase{
AccountID: "default",
Expand All @@ -74,8 +76,9 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
AppID: appID,
ClientSecret: clientSecret,
},
},
}, nil
}
}
return result, nil
}

func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
Expand Down
215 changes: 215 additions & 0 deletions agent/app/service/agents_hermes_skills.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package service

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
)

type hermesSkillsListEntry struct {
Name string `json:"name"`
Description string `json:"description"`
Category string `json:"category"`
}

type hermesSkillsListPayload struct {
Skills []hermesSkillsListEntry `json:"skills"`
}

func listHermesSkills(containerName string) ([]dto.AgentSkillItem, error) {
output, err := runHermesSkillsCommandWithStdout(2*time.Minute, containerName, "list", "--source", "all")
if err != nil {
return nil, err
}
items, err := parseHermesSkillsListOutput(output)
if err != nil {
return nil, err
}
metadata, err := readHermesSkillsListMetadata(containerName)
if err != nil {
return nil, err
}
return mergeHermesSkillsWithMetadata(items, metadata), nil
}

func searchHermesSkills(containerName, source, keyword string) ([]dto.AgentSkillSearchItem, error) {
if source != "official" && source != "skills-sh" {
return nil, fmt.Errorf("unsupported hermes skill source: %s", source)
}
output, err := runHermesSkillsCommandWithStdout(
2*time.Minute,
containerName,
"search",
keyword,
"--source",
source,
"--limit",
"20",
)
if err != nil {
return nil, err
}
return parseHermesSkillSearchOutput(output)
}

func runHermesSkillsCommandWithStdout(timeout time.Duration, containerName string, hermesArgs ...string) (string, error) {
args := []string{"exec", "-e", "COLUMNS=240", "-u", "hermes", containerName, "hermes", "skills"}
args = append(args, hermesArgs...)
return cmd.NewCommandMgr(cmd.WithTimeout(timeout)).RunWithStdout("docker", args...)
}

func readHermesSkillsListMetadata(containerName string) (map[string]hermesSkillsListEntry, error) {
output, err := cmd.NewCommandMgr(cmd.WithTimeout(2*time.Minute)).RunWithStdout(
"docker",
"exec",
"-u",
"hermes",
containerName,
"python",
"-c",
"import sys; sys.path.insert(0, '/opt/hermes'); from tools.skills_tool import skills_list; print(skills_list())",
)
if err != nil {
return nil, err
}
return parseHermesSkillsListMetadataOutput(output)
}

func parseHermesSkillsListOutput(output string) ([]dto.AgentSkillItem, error) {
headers, rows, err := parseHermesTableOutput(output)
if err != nil {
return nil, err
}
items := make([]dto.AgentSkillItem, 0, len(rows))
for _, row := range rows {
item := dto.AgentSkillItem{
Name: row[headers["Name"]],
Category: row[headers["Category"]],
Source: row[headers["Source"]],
Trust: row[headers["Trust"]],
Uninstallable: row[headers["Source"]] != "builtin" && row[headers["Source"]] != "local",
}
items = append(items, item)
}
return items, nil
}

func parseHermesSkillsListMetadataOutput(output string) (map[string]hermesSkillsListEntry, error) {
if strings.TrimSpace(output) == "" {
return map[string]hermesSkillsListEntry{}, nil
}
var payload hermesSkillsListPayload
if err := json.Unmarshal([]byte(output), &payload); err != nil {
return nil, err
Comment thread
zhengkunwang223 marked this conversation as resolved.
}
metadata := make(map[string]hermesSkillsListEntry, len(payload.Skills))
for _, skill := range payload.Skills {
if skill.Name == "" {
continue
}
metadata[skill.Name] = skill
}
return metadata, nil
}

func mergeHermesSkillsWithMetadata(items []dto.AgentSkillItem, metadata map[string]hermesSkillsListEntry) []dto.AgentSkillItem {
for i := range items {
entry, ok := metadata[items[i].Name]
if !ok {
continue
}
items[i].Description = entry.Description
if items[i].Category == "" && entry.Category != "" {
items[i].Category = entry.Category
}
}
return items
}

func parseHermesSkillSearchOutput(output string) ([]dto.AgentSkillSearchItem, error) {
if strings.Contains(output, "No skills found matching your query.") {
return []dto.AgentSkillSearchItem{}, nil
}
headers, rows, err := parseHermesTableOutput(output)
if err != nil {
return nil, err
}
items := make([]dto.AgentSkillSearchItem, 0, len(rows))
for _, row := range rows {
identifier := row[headers["Identifier"]]
items = append(items, dto.AgentSkillSearchItem{
Slug: identifier,
Identifier: identifier,
Name: row[headers["Name"]],
Description: row[headers["Description"]],
Source: row[headers["Source"]],
Trust: row[headers["Trust"]],
})
}
return items, nil
}

func parseHermesTableOutput(output string) (map[string]int, [][]string, error) {
lines := strings.Split(strings.TrimSpace(ansiEscapePattern.ReplaceAllString(output, "")), "\n")
var headers []string
rows := make([][]string, 0)
var current []string

for _, rawLine := range lines {
line := strings.TrimSpace(rawLine)
if (!strings.HasPrefix(line, "│") || !strings.HasSuffix(line, "│")) &&
(!strings.HasPrefix(line, "┃") || !strings.HasSuffix(line, "┃")) {
continue
}
line = strings.ReplaceAll(line, "┃", "│")
parts := strings.Split(line, "│")
if len(parts) < 3 {
continue
}
cols := make([]string, 0, len(parts)-2)
for _, part := range parts[1 : len(parts)-1] {
cols = append(cols, strings.TrimSpace(part))
}
if len(headers) == 0 {
headers = cols
continue
}
if cols[0] != "" {
if current != nil {
rows = append(rows, current)
}
current = cols
continue
}
if current == nil {
continue
}
for i := range cols {
if cols[i] == "" {
continue
}
if current[i] == "" {
current[i] = cols[i]
continue
}
current[i] = current[i] + " " + cols[i]
}
}

if current != nil {
rows = append(rows, current)
}
if len(headers) == 0 {
return nil, nil, fmt.Errorf("hermes skills table not found")
}

headerIndex := make(map[string]int, len(headers))
for i, header := range headers {
headerIndex[header] = i
}
return headerIndex, rows, nil
}
Loading
Loading