Skip to content

Commit 620a0f2

Browse files
feat: support skill management for hermes-agent
1 parent 254ed95 commit 620a0f2

25 files changed

Lines changed: 1259 additions & 474 deletions

File tree

agent/app/api/v2/agents.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,26 @@ func (b *BaseApi) InstallAgentSkill(c *gin.Context) {
11711171
helper.Success(c)
11721172
}
11731173

1174+
// @Tags AI
1175+
// @Summary Uninstall Agent skill
1176+
// @Accept json
1177+
// @Param request body dto.AgentSkillUninstallReq true "request"
1178+
// @Success 200
1179+
// @Security ApiKeyAuth
1180+
// @Security Timestamp
1181+
// @Router /ai/agents/skills/uninstall [post]
1182+
func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
1183+
var req dto.AgentSkillUninstallReq
1184+
if err := helper.CheckBindAndValidate(&req, c); err != nil {
1185+
return
1186+
}
1187+
if err := agentService.UninstallSkill(req); err != nil {
1188+
helper.BadRequest(c, err)
1189+
return
1190+
}
1191+
helper.Success(c)
1192+
}
1193+
11741194
// @Tags AI
11751195
// @Summary Login Agent Weixin channel
11761196
// @Accept json

agent/app/dto/agents.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -590,25 +590,32 @@ type AgentConfigFile struct {
590590

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

597597
type AgentSkillItem struct {
598-
Name string `json:"name"`
599-
Description string `json:"description"`
600-
Source string `json:"source"`
601-
Bundled bool `json:"bundled"`
602-
Disabled bool `json:"disabled"`
598+
Name string `json:"name"`
599+
Description string `json:"description"`
600+
Category string `json:"category"`
601+
Tags []string `json:"tags"`
602+
Source string `json:"source"`
603+
Trust string `json:"trust"`
604+
Identifier string `json:"identifier"`
605+
Bundled bool `json:"bundled"`
606+
Disabled bool `json:"disabled"`
607+
Uninstallable bool `json:"uninstallable"`
603608
}
604609

605610
type AgentSkillSearchItem struct {
606611
Slug string `json:"slug"`
612+
Identifier string `json:"identifier"`
607613
Name string `json:"name"`
608614
Description string `json:"description"`
609615
Summary string `json:"summary"`
610616
Version string `json:"version"`
611617
Source string `json:"source"`
618+
Trust string `json:"trust"`
612619
Score string `json:"score"`
613620
}
614621

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

621628
type AgentSkillInstallReq struct {
622629
AgentID uint `json:"agentId" validate:"required"`
623-
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub"`
630+
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"`
624631
Slug string `json:"slug" validate:"required"`
625632
TaskID string `json:"taskID" validate:"required"`
626633
}
634+
635+
type AgentSkillUninstallReq struct {
636+
AgentID uint `json:"agentId" validate:"required"`
637+
Name string `json:"name" validate:"required"`
638+
}

agent/app/service/agents.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ type IAgentService interface {
5353
SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error)
5454
UpdateSkill(req dto.AgentSkillUpdateReq) error
5555
InstallSkill(req dto.AgentSkillInstallReq) error
56+
UninstallSkill(req dto.AgentSkillUninstallReq) error
5657

5758
CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error)
5859
DeleteRole(req dto.AgentRoleDeleteReq) error
@@ -455,9 +456,9 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
455456
if err != nil {
456457
return nil, err
457458
}
458-
model := resolveHermesConfiguredModelID(account, accountModels, cfg.Model.Default)
459-
if model == "" {
460-
model = agent.Model
459+
model, err := resolveHermesConfiguredModelIDStrict(account, accountModels, cfg.Model.Default)
460+
if err != nil {
461+
return nil, err
461462
}
462463
return &dto.AgentModelConfig{
463464
AccountID: agent.AccountID,

agent/app/service/agents_hermes.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/1Panel-dev/1Panel/agent/app/dto"
1111
"github.com/1Panel-dev/1Panel/agent/app/model"
1212
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
13+
"github.com/1Panel-dev/1Panel/agent/buserr"
1314
"github.com/1Panel-dev/1Panel/agent/constant"
1415
"github.com/1Panel-dev/1Panel/agent/utils/common"
1516
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
@@ -19,6 +20,7 @@ import (
1920
)
2021

2122
const hermesWorkspaceDir = "/opt/data/workspace"
23+
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
2224

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

57+
func buildHermesSkillUninstallArgs(containerName, skillName string) []string {
58+
return buildHermesDockerExecCommandArgs(
59+
containerName,
60+
"sh",
61+
"-lc",
62+
fmt.Sprintf(`printf 'y\n' | %s skills uninstall "$1"`, hermesExecutablePath),
63+
"sh",
64+
skillName,
65+
)
66+
}
67+
5568
func writeHermesConfig(confDir string, account *model.AgentAccount, modelName string, timezone string) error {
5669
if strings.TrimSpace(confDir) == "" {
5770
return fmt.Errorf("config dir is required")
@@ -386,6 +399,14 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
386399
return ""
387400
}
388401

402+
func resolveHermesConfiguredModelIDStrict(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
403+
modelID := resolveHermesConfiguredModelID(account, accountModels, configuredModel)
404+
if modelID == "" {
405+
return "", buserr.New("ErrAgentModelNotInAccount")
406+
}
407+
return modelID, nil
408+
}
409+
389410
func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
390411
if account == nil {
391412
return nil

agent/app/service/agents_hermes_channels.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,15 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
5757
}
5858
groupAllowFrom := extractStringList(extra["group_allow_from"])
5959

60-
return &dto.AgentQQBotConfig{
60+
result := &dto.AgentQQBotConfig{
6161
Enabled: extractBoolValue(platform["enabled"], false) && appID != "" && clientSecret != "",
6262
DmPolicy: dmPolicy,
6363
AllowFrom: allowFrom,
6464
GroupPolicy: groupPolicy,
6565
GroupAllowFrom: groupAllowFrom,
66-
Bots: []dto.AgentQQBotBot{
66+
}
67+
if appID != "" || clientSecret != "" {
68+
result.Bots = []dto.AgentQQBotBot{
6769
{
6870
AgentChannelBotBase: dto.AgentChannelBotBase{
6971
AccountID: "default",
@@ -74,8 +76,9 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
7476
AppID: appID,
7577
ClientSecret: clientSecret,
7678
},
77-
},
78-
}, nil
79+
}
80+
}
81+
return result, nil
7982
}
8083

8184
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package service
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
"github.com/1Panel-dev/1Panel/agent/app/dto"
10+
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
11+
)
12+
13+
type hermesSkillsListEntry struct {
14+
Name string `json:"name"`
15+
Description string `json:"description"`
16+
Category string `json:"category"`
17+
}
18+
19+
type hermesSkillsListPayload struct {
20+
Skills []hermesSkillsListEntry `json:"skills"`
21+
}
22+
23+
func listHermesSkills(containerName string) ([]dto.AgentSkillItem, error) {
24+
output, err := runHermesSkillsCommandWithStdout(2*time.Minute, containerName, "list", "--source", "all")
25+
if err != nil {
26+
return nil, err
27+
}
28+
items, err := parseHermesSkillsListOutput(output)
29+
if err != nil {
30+
return nil, err
31+
}
32+
metadata, err := readHermesSkillsListMetadata(containerName)
33+
if err != nil {
34+
return nil, err
35+
}
36+
return mergeHermesSkillsWithMetadata(items, metadata), nil
37+
}
38+
39+
func searchHermesSkills(containerName, source, keyword string) ([]dto.AgentSkillSearchItem, error) {
40+
if source != "official" && source != "skills-sh" {
41+
return nil, fmt.Errorf("unsupported hermes skill source: %s", source)
42+
}
43+
output, err := runHermesSkillsCommandWithStdout(
44+
2*time.Minute,
45+
containerName,
46+
"search",
47+
keyword,
48+
"--source",
49+
source,
50+
"--limit",
51+
"20",
52+
)
53+
if err != nil {
54+
return nil, err
55+
}
56+
return parseHermesSkillSearchOutput(output)
57+
}
58+
59+
func runHermesSkillsCommandWithStdout(timeout time.Duration, containerName string, hermesArgs ...string) (string, error) {
60+
args := []string{"exec", "-e", "COLUMNS=240", "-u", "hermes", containerName, "hermes", "skills"}
61+
args = append(args, hermesArgs...)
62+
return cmd.NewCommandMgr(cmd.WithTimeout(timeout)).RunWithStdout("docker", args...)
63+
}
64+
65+
func readHermesSkillsListMetadata(containerName string) (map[string]hermesSkillsListEntry, error) {
66+
output, err := cmd.NewCommandMgr(cmd.WithTimeout(2*time.Minute)).RunWithStdout(
67+
"docker",
68+
"exec",
69+
"-u",
70+
"hermes",
71+
containerName,
72+
"python",
73+
"-c",
74+
"import sys; sys.path.insert(0, '/opt/hermes'); from tools.skills_tool import skills_list; print(skills_list())",
75+
)
76+
if err != nil {
77+
return nil, err
78+
}
79+
return parseHermesSkillsListMetadataOutput(output)
80+
}
81+
82+
func parseHermesSkillsListOutput(output string) ([]dto.AgentSkillItem, error) {
83+
headers, rows, err := parseHermesTableOutput(output)
84+
if err != nil {
85+
return nil, err
86+
}
87+
items := make([]dto.AgentSkillItem, 0, len(rows))
88+
for _, row := range rows {
89+
item := dto.AgentSkillItem{
90+
Name: row[headers["Name"]],
91+
Category: row[headers["Category"]],
92+
Source: row[headers["Source"]],
93+
Trust: row[headers["Trust"]],
94+
Uninstallable: row[headers["Source"]] != "builtin" && row[headers["Source"]] != "local",
95+
}
96+
items = append(items, item)
97+
}
98+
return items, nil
99+
}
100+
101+
func parseHermesSkillsListMetadataOutput(output string) (map[string]hermesSkillsListEntry, error) {
102+
if strings.TrimSpace(output) == "" {
103+
return map[string]hermesSkillsListEntry{}, nil
104+
}
105+
var payload hermesSkillsListPayload
106+
if err := json.Unmarshal([]byte(output), &payload); err != nil {
107+
return nil, err
108+
}
109+
metadata := make(map[string]hermesSkillsListEntry, len(payload.Skills))
110+
for _, skill := range payload.Skills {
111+
if skill.Name == "" {
112+
continue
113+
}
114+
metadata[skill.Name] = skill
115+
}
116+
return metadata, nil
117+
}
118+
119+
func mergeHermesSkillsWithMetadata(items []dto.AgentSkillItem, metadata map[string]hermesSkillsListEntry) []dto.AgentSkillItem {
120+
for i := range items {
121+
entry, ok := metadata[items[i].Name]
122+
if !ok {
123+
continue
124+
}
125+
items[i].Description = entry.Description
126+
if items[i].Category == "" && entry.Category != "" {
127+
items[i].Category = entry.Category
128+
}
129+
}
130+
return items
131+
}
132+
133+
func parseHermesSkillSearchOutput(output string) ([]dto.AgentSkillSearchItem, error) {
134+
if strings.Contains(output, "No skills found matching your query.") {
135+
return []dto.AgentSkillSearchItem{}, nil
136+
}
137+
headers, rows, err := parseHermesTableOutput(output)
138+
if err != nil {
139+
return nil, err
140+
}
141+
items := make([]dto.AgentSkillSearchItem, 0, len(rows))
142+
for _, row := range rows {
143+
identifier := row[headers["Identifier"]]
144+
items = append(items, dto.AgentSkillSearchItem{
145+
Slug: identifier,
146+
Identifier: identifier,
147+
Name: row[headers["Name"]],
148+
Description: row[headers["Description"]],
149+
Source: row[headers["Source"]],
150+
Trust: row[headers["Trust"]],
151+
})
152+
}
153+
return items, nil
154+
}
155+
156+
func parseHermesTableOutput(output string) (map[string]int, [][]string, error) {
157+
lines := strings.Split(strings.TrimSpace(ansiEscapePattern.ReplaceAllString(output, "")), "\n")
158+
var headers []string
159+
rows := make([][]string, 0)
160+
var current []string
161+
162+
for _, rawLine := range lines {
163+
line := strings.TrimSpace(rawLine)
164+
if (!strings.HasPrefix(line, "│") || !strings.HasSuffix(line, "│")) &&
165+
(!strings.HasPrefix(line, "┃") || !strings.HasSuffix(line, "┃")) {
166+
continue
167+
}
168+
line = strings.ReplaceAll(line, "┃", "│")
169+
parts := strings.Split(line, "│")
170+
if len(parts) < 3 {
171+
continue
172+
}
173+
cols := make([]string, 0, len(parts)-2)
174+
for _, part := range parts[1 : len(parts)-1] {
175+
cols = append(cols, strings.TrimSpace(part))
176+
}
177+
if len(headers) == 0 {
178+
headers = cols
179+
continue
180+
}
181+
if cols[0] != "" {
182+
if current != nil {
183+
rows = append(rows, current)
184+
}
185+
current = cols
186+
continue
187+
}
188+
if current == nil {
189+
continue
190+
}
191+
for i := range cols {
192+
if cols[i] == "" {
193+
continue
194+
}
195+
if current[i] == "" {
196+
current[i] = cols[i]
197+
continue
198+
}
199+
current[i] = current[i] + " " + cols[i]
200+
}
201+
}
202+
203+
if current != nil {
204+
rows = append(rows, current)
205+
}
206+
if len(headers) == 0 {
207+
return nil, nil, fmt.Errorf("hermes skills table not found")
208+
}
209+
210+
headerIndex := make(map[string]int, len(headers))
211+
for i, header := range headers {
212+
headerIndex[header] = i
213+
}
214+
return headerIndex, rows, nil
215+
}

0 commit comments

Comments
 (0)