Skip to content

Commit 4e78841

Browse files
feat(ai): support Hermes Agent chat sessions in 1Panel (#12497)
* feat(ai): support Hermes Agent chat sessions in 1Panel * feat(ai): support Hermes Agent chat sessions in 1Panel * feat(ai): change docker command
1 parent 7f5bb58 commit 4e78841

File tree

26 files changed

+710
-25
lines changed

26 files changed

+710
-25
lines changed

agent/app/api/v2/agents.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,47 @@ func (b *BaseApi) GetAgentOverview(c *gin.Context) {
214214
helper.SuccessWithData(c, res)
215215
}
216216

217+
// @Tags AI
218+
// @Summary Get Hermes chat sessions
219+
// @Accept json
220+
// @Param request body dto.AgentIDReq true "request"
221+
// @Success 200 {array} dto.AgentHermesChatSessionItem
222+
// @Security ApiKeyAuth
223+
// @Security Timestamp
224+
// @Router /ai/agents/hermes/chat/sessions [post]
225+
func (b *BaseApi) GetHermesChatSessions(c *gin.Context) {
226+
var req dto.AgentIDReq
227+
if err := helper.CheckBindAndValidate(&req, c); err != nil {
228+
return
229+
}
230+
res, err := agentService.GetHermesChatSessions(req)
231+
if err != nil {
232+
helper.BadRequest(c, err)
233+
return
234+
}
235+
helper.SuccessWithData(c, res)
236+
}
237+
238+
// @Tags AI
239+
// @Summary Rename Hermes chat session
240+
// @Accept json
241+
// @Param request body dto.AgentHermesChatSessionRenameReq true "request"
242+
// @Success 200
243+
// @Security ApiKeyAuth
244+
// @Security Timestamp
245+
// @Router /ai/agents/hermes/chat/sessions/rename [post]
246+
func (b *BaseApi) RenameHermesChatSession(c *gin.Context) {
247+
var req dto.AgentHermesChatSessionRenameReq
248+
if err := helper.CheckBindAndValidate(&req, c); err != nil {
249+
return
250+
}
251+
if err := agentService.RenameHermesChatSession(req); err != nil {
252+
helper.BadRequest(c, err)
253+
return
254+
}
255+
helper.Success(c)
256+
}
257+
217258
// @Tags AI
218259
// @Summary Get Providers
219260
// @Success 200 {array} dto.ProviderInfo

agent/app/dto/agents.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ type AgentModelConfig struct {
9191
Fallbacks []string `json:"fallbacks"`
9292
}
9393

94+
type AgentHermesChatSessionItem struct {
95+
ID string `json:"id"`
96+
Title string `json:"title"`
97+
Model string `json:"model"`
98+
MessageCount int64 `json:"messageCount"`
99+
StartedAt string `json:"startedAt"`
100+
LastActive string `json:"lastActive"`
101+
}
102+
94103
type AgentOverviewReq struct {
95104
AgentID uint `json:"agentId" validate:"required"`
96105
}
@@ -99,6 +108,12 @@ type AgentIDReq struct {
99108
AgentID uint `json:"agentId" validate:"required"`
100109
}
101110

111+
type AgentHermesChatSessionRenameReq struct {
112+
AgentID uint `json:"agentId" validate:"required"`
113+
ID string `json:"id" validate:"required"`
114+
Title string `json:"title" validate:"required"`
115+
}
116+
102117
type AgentOverview struct {
103118
Snapshot AgentOverviewSnapshot `json:"snapshot"`
104119
}

agent/app/service/agents.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ type IAgentService interface {
3838
BindWebsite(req dto.AgentWebsiteBindReq) error
3939
GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error)
4040
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
41+
GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error)
42+
RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error
4143
GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error)
4244
GetProviders() ([]dto.ProviderInfo, error)
4345
GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error)

agent/app/service/agents_channels.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,10 @@ func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveRe
505505
return err
506506
}
507507
if agent.AgentType == constant.AppHermesAgent {
508-
output, err := cmd.RunDefaultWithStdoutBashC(buildHermesPairingApproveCommand(install.ContainerName, req.Type, req.PairingCode))
508+
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
509+
"docker",
510+
buildHermesDockerExecArgs(install.ContainerName, "pairing", "approve", req.Type, req.PairingCode)...,
511+
)
509512
if err != nil {
510513
return err
511514
}

agent/app/service/agents_hermes.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -555,15 +555,6 @@ func extractHermesEnvBool(envMap map[string]string, key string, defaultValue boo
555555
return strings.EqualFold(value, "true")
556556
}
557557

558-
func buildHermesPairingApproveCommand(containerName string, channel string, pairingCode string) string {
559-
return fmt.Sprintf(
560-
"docker exec -u hermes -e HOME=/opt/data/home -e HERMES_HOME=/opt/data %s /opt/hermes/.venv/bin/hermes pairing approve %s %q",
561-
containerName,
562-
channel,
563-
pairingCode,
564-
)
565-
}
566-
567558
func validateHermesPairingApproveOutput(output string) error {
568559
text := strings.TrimSpace(output)
569560
if text == "" {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package service
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"math"
7+
"path/filepath"
8+
"strings"
9+
"time"
10+
11+
"github.com/1Panel-dev/1Panel/agent/app/dto"
12+
"github.com/1Panel-dev/1Panel/agent/constant"
13+
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
14+
"github.com/1Panel-dev/1Panel/agent/utils/files"
15+
)
16+
17+
const hermesSessionListLimit = 100
18+
19+
func (a AgentService) GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error) {
20+
agent, install, err := a.loadAgentAndInstall(req.AgentID)
21+
if err != nil {
22+
return nil, err
23+
}
24+
if agent.AgentType != constant.AppHermesAgent {
25+
return nil, fmt.Errorf("%s does not support", agent.AgentType)
26+
}
27+
return listHermesChatSessionsFromStateDB(filepath.Join(install.GetPath(), "data", "state.db"))
28+
}
29+
30+
func (a AgentService) RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error {
31+
agent, install, err := a.loadAgentAndInstall(req.AgentID)
32+
if err != nil {
33+
return err
34+
}
35+
if agent.AgentType != constant.AppHermesAgent {
36+
return fmt.Errorf("%s does not support", agent.AgentType)
37+
}
38+
39+
_, err = cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
40+
"docker",
41+
buildHermesDockerExecArgs(install.ContainerName, "sessions", "rename", req.ID, req.Title)...,
42+
)
43+
return err
44+
}
45+
46+
func listHermesChatSessionsFromStateDB(stateDBPath string) ([]dto.AgentHermesChatSessionItem, error) {
47+
if !files.NewFileOp().Stat(stateDBPath) {
48+
return []dto.AgentHermesChatSessionItem{}, nil
49+
}
50+
51+
db, err := sql.Open("sqlite", stateDBPath)
52+
if err != nil {
53+
return nil, err
54+
}
55+
defer db.Close()
56+
57+
rows, err := db.Query(`
58+
SELECT
59+
s.id,
60+
COALESCE(NULLIF(TRIM(s.title), ''), s.id) AS title,
61+
COALESCE(s.model, '') AS model,
62+
COALESCE(s.message_count, 0) AS message_count,
63+
s.started_at,
64+
COALESCE(MAX(m.timestamp), s.started_at) AS last_active
65+
FROM sessions s
66+
LEFT JOIN messages m ON m.session_id = s.id
67+
WHERE s.source = 'cli'
68+
GROUP BY s.id, title, model, s.message_count, s.started_at
69+
ORDER BY last_active DESC
70+
LIMIT ?
71+
`, hermesSessionListLimit)
72+
if err != nil {
73+
return nil, err
74+
}
75+
defer rows.Close()
76+
77+
items := make([]dto.AgentHermesChatSessionItem, 0, 8)
78+
for rows.Next() {
79+
var item dto.AgentHermesChatSessionItem
80+
var title sql.NullString
81+
var model sql.NullString
82+
var startedAt sql.NullFloat64
83+
var lastActive sql.NullFloat64
84+
if err := rows.Scan(&item.ID, &title, &model, &item.MessageCount, &startedAt, &lastActive); err != nil {
85+
return nil, err
86+
}
87+
item.Title = strings.TrimSpace(title.String)
88+
if item.Title == "" {
89+
item.Title = item.ID
90+
}
91+
item.Model = strings.TrimSpace(model.String)
92+
item.StartedAt = formatHermesSessionTimestamp(startedAt)
93+
item.LastActive = formatHermesSessionTimestamp(lastActive)
94+
items = append(items, item)
95+
}
96+
if err := rows.Err(); err != nil {
97+
return nil, err
98+
}
99+
100+
return items, nil
101+
}
102+
103+
func formatHermesSessionTimestamp(value sql.NullFloat64) string {
104+
if !value.Valid || value.Float64 <= 0 {
105+
return ""
106+
}
107+
108+
seconds, fraction := math.Modf(value.Float64)
109+
return time.Unix(int64(seconds), int64(fraction*float64(time.Second))).UTC().Format(time.RFC3339)
110+
}
111+
112+
func buildHermesDockerExecArgs(containerName string, hermesArgs ...string) []string {
113+
args := []string{"exec", "-u", "hermes", containerName, "hermes"}
114+
return append(args, hermesArgs...)
115+
}

agent/app/service/app.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"strconv"
1414
"strings"
1515
"sync"
16+
"time"
1617

1718
"github.com/gin-gonic/gin"
1819

@@ -576,7 +577,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
576577
_ = appInstallRepo.Save(context.Background(), appInstall)
577578
}
578579

579-
installTask.AddSubTask(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus)
580+
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, time.Hour)
580581

581582
go func() {
582583
if taskErr := installTask.Execute(); taskErr != nil {

agent/router/ro_ai.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
4949
aiToolsRouter.POST("/agents/website/bind", baseApi.BindAgentWebsite)
5050
aiToolsRouter.POST("/agents/model/get", baseApi.GetAgentModelConfig)
5151
aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig)
52+
aiToolsRouter.POST("/agents/hermes/chat/sessions", baseApi.GetHermesChatSessions)
53+
aiToolsRouter.POST("/agents/hermes/chat/sessions/rename", baseApi.RenameHermesChatSession)
5254
aiToolsRouter.POST("/agents/overview", baseApi.GetAgentOverview)
5355
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
5456
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)

frontend/src/api/interface/ai.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,21 @@ export namespace AI {
327327
fallbacks: string[];
328328
}
329329

330+
export interface AgentHermesChatSessionItem {
331+
id: string;
332+
title: string;
333+
model: string;
334+
messageCount: number;
335+
startedAt: string;
336+
lastActive: string;
337+
}
338+
339+
export interface AgentHermesChatSessionRenameReq {
340+
agentId: number;
341+
id: string;
342+
title: string;
343+
}
344+
330345
export interface AgentOverviewReq {
331346
agentId: number;
332347
}

frontend/src/api/modules/ai.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ export const updateAgentModelConfig = (req: AI.AgentModelConfigUpdateReq) => {
130130
return http.post(`/ai/agents/model/update`, req);
131131
};
132132

133+
export const getAgentHermesChatSessions = (req: AI.AgentIDReq) => {
134+
return http.post<AI.AgentHermesChatSessionItem[]>(`/ai/agents/hermes/chat/sessions`, req);
135+
};
136+
137+
export const renameAgentHermesChatSession = (req: AI.AgentHermesChatSessionRenameReq) => {
138+
return http.post(`/ai/agents/hermes/chat/sessions/rename`, req);
139+
};
140+
133141
export const getAgentOverview = (req: AI.AgentOverviewReq) => {
134142
return http.post<AI.AgentOverview>(`/ai/agents/overview`, req, TimeoutEnum.T_5M);
135143
};

0 commit comments

Comments
 (0)