diff --git a/server/internal/handler/agent_handler.go b/server/internal/handler/agent_handler.go new file mode 100644 index 0000000..d645de1 --- /dev/null +++ b/server/internal/handler/agent_handler.go @@ -0,0 +1,141 @@ +package handler + +import ( + "net/http" + + "x-agents/server/internal/service" + + "github.com/gin-gonic/gin" +) + +// AgentHandler Agent 处理器 +type AgentHandler struct { + agentService *service.AgentService +} + +// NewAgentHandler 创建 Agent 处理器 +func NewAgentHandler(agentService *service.AgentService) *AgentHandler { + return &AgentHandler{ + agentService: agentService, + } +} + +// ChatRequest 对话请求 +type ChatRequest struct { + AgentID int `json:"agent_id" binding:"required"` + Message string `json:"message" binding:"required"` + SessionID string `json:"session_id"` +} + +// ChatResponse 对话响应 +type ChatResponse struct { + AgentID int `json:"agent_id"` + Reply string `json:"reply"` + ToolsUsed []string `json:"tools_used"` + SessionID string `json:"session_id"` + TokensUsed int `json:"tokens_used"` + DurationMs int `json:"duration_ms"` + Metadata interface{} `json:"metadata"` +} + +// Chat 单智能体对话 +func (h *AgentHandler) Chat(c *gin.Context) { + var req ChatRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 获取用户 ID(从认证中间件获取) + userID := 1 // TODO: 从 c.Get("user_id") 获取 + + pythonReq := service.AgentChatRequest{ + AgentID: req.AgentID, + Message: req.Message, + UserID: userID, + SessionID: req.SessionID, + } + + result, err := h.agentService.Chat(pythonReq) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // 转换工具调用 + toolsUsed := make([]string, 0) + for _, tool := range result.ToolCalls { + if toolMap, ok := tool.(map[string]interface{}); ok { + if skillID, ok := toolMap["skill_id"].(string); ok { + toolsUsed = append(toolsUsed, skillID) + } + } + } + + c.JSON(http.StatusOK, ChatResponse{ + AgentID: result.AgentID, + Reply: result.Response, + ToolsUsed: toolsUsed, + SessionID: result.SessionID, + TokensUsed: result.TokensUsed, + DurationMs: result.DurationMs, + Metadata: nil, + }) +} + +// TeamChatRequest 多智能体群聊请求 +type TeamChatRequest struct { + SupervisorAgentID int `json:"supervisor_agent_id" binding:"required"` + MemberAgentIDs []int `json:"member_agent_ids" binding:"required"` + Message string `json:"message" binding:"required"` + SessionID string `json:"session_id"` + Strategy string `json:"strategy"` +} + +// TeamChatResponse 多智能体群聊响应 +type TeamChatResponse struct { + SupervisorAgentID int `json:"supervisor_agent_id"` + Reply string `json:"reply"` + SubtaskResults interface{} `json:"subtask_results"` + Strategy string `json:"strategy"` + SessionID string `json:"session_id"` + DurationMs int `json:"duration_ms"` + Metadata interface{} `json:"metadata"` +} + +// TeamChat 多智能体群聊 +func (h *AgentHandler) TeamChat(c *gin.Context) { + var req TeamChatRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 获取用户 ID + userID := 1 // TODO: 从 c.Get("user_id") 获取 + + pythonReq := service.TeamChatRequest{ + SupervisorAgentID: req.SupervisorAgentID, + MemberAgentIDs: req.MemberAgentIDs, + Message: req.Message, + UserID: userID, + SessionID: req.SessionID, + Strategy: req.Strategy, + } + + result, err := h.agentService.TeamChat(pythonReq) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, TeamChatResponse{ + SupervisorAgentID: result.SupervisorAgentID, + Reply: result.Response, + SubtaskResults: result.SubtaskResults, + Strategy: result.Strategy, + SessionID: result.SessionID, + DurationMs: result.DurationMs, + Metadata: nil, + }) +} diff --git a/server/internal/handler/memory_handler.go b/server/internal/handler/memory_handler.go new file mode 100644 index 0000000..d90006e --- /dev/null +++ b/server/internal/handler/memory_handler.go @@ -0,0 +1,91 @@ +package handler + +import ( + "net/http" + "strconv" + + "x-agents/server/internal/service" + + "github.com/gin-gonic/gin" +) + +// MemoryHandler 记忆处理器 +type MemoryHandler struct { + memoryService *service.MemoryService +} + +// NewMemoryHandler 创建记忆处理器 +func NewMemoryHandler(memoryService *service.MemoryService) *MemoryHandler { + return &MemoryHandler{ + memoryService: memoryService, + } +} + +// CreateMemoryRequest 创建记忆请求 +type CreateMemoryRequest struct { + AgentID string `json:"agent_id" binding:"required"` + UserID string `json:"user_id"` + Content string `json:"content" binding:"required"` + MemoryType string `json:"memory_type"` + Importance int `json:"importance"` +} + +// CreateMemory 创建记忆 +func (h *MemoryHandler) CreateMemory(c *gin.Context) { + var req CreateMemoryRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // 设置默认值 + memoryType := req.MemoryType + if memoryType == "" { + memoryType = "conversation" + } + importance := req.Importance + if importance == 0 { + importance = 5 + } + + memory, err := h.memoryService.CreateMemory(req.AgentID, req.UserID, req.Content, memoryType, importance) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, memory) +} + +// GetMemories 获取记忆列表 +func (h *MemoryHandler) GetMemories(c *gin.Context) { + agentID := c.Param("id") + userID := c.Query("user_id") + limitStr := c.DefaultQuery("limit", "10") + + limit, err := strconv.Atoi(limitStr) + if err != nil { + limit = 10 + } + + memories, err := h.memoryService.GetMemories(agentID, userID, limit) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, memories) +} + +// DeleteMemory 删除记忆 +func (h *MemoryHandler) DeleteMemory(c *gin.Context) { + memoryID := c.Param("memory_id") + + err := h.memoryService.DeleteMemory(memoryID) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "Memory deleted successfully"}) +} diff --git a/server/internal/handler/skill_handler.go b/server/internal/handler/skill_handler.go new file mode 100644 index 0000000..2b717b5 --- /dev/null +++ b/server/internal/handler/skill_handler.go @@ -0,0 +1,172 @@ +package handler + +import ( + "net/http" + + "x-agents/server/internal/model" + "x-agents/server/internal/service" + + "github.com/gin-gonic/gin" +) + +// SkillHandler 技能处理器 +type SkillHandler struct { + skillService *service.SkillService +} + +// NewSkillHandler 创建技能处理器 +func NewSkillHandler(skillService *service.SkillService) *SkillHandler { + return &SkillHandler{skillService: skillService} +} + +// List 获取技能列表 +// @Summary 获取技能列表 +// @Description 获取所有技能列表,支持按类型筛选(system/user) +// @Tags 技能管理 +// @Accept json +// @Produce json +// @Param type query string false "技能类型: system(系统技能)/user(用户技能)" +// @Success 200 {object} map[string]interface{} "{"list": [], "total": 0}" +// @Router /skill/list [get] +func (h *SkillHandler) List(c *gin.Context) { + skillType := c.Query("type") + + var skills []model.Skill + var err error + + if skillType != "" { + skills, err = h.skillService.GetSkillsByType(skillType) + } else { + skills, err = h.skillService.GetAllSkills() + } + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"list": skills, "total": len(skills)}) +} + +// Sync 手动同步 skills +// @Summary 手动同步技能 +// @Description 从文件系统扫描 skills 目录并同步到数据库。扫描 account/admin/skills(系统技能) 和 account/{username}/skills(用户技能) +// @Tags 技能管理 +// @Accept json +// @Produce json +// @Success 200 {object} map[string]interface{} "{"message": "skills synced", "count": 0}" +// @Router /skill/sync [get] +func (h *SkillHandler) Sync(c *gin.Context) { + if err := h.skillService.InitSkills(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + skills, _ := h.skillService.GetAllSkills() + c.JSON(http.StatusOK, gin.H{"message": "skills synced", "count": len(skills)}) +} + +// GetByID 根据ID获取技能 +// @Summary 获取技能详情 +// @Description 根据ID获取技能详情 +// @Tags 技能管理 +// @Accept json +// @Produce json +// @Param id path string true "技能ID" +// @Success 200 {object} map[string]interface{} "{"skill": {}}" +// @Router /skill/{id} [get] +func (h *SkillHandler) GetByID(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "skill id is required"}) + return + } + + skill, err := h.skillService.GetSkillByID(id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "skill not found"}) + return + } + + c.JSON(http.StatusOK, gin.H{"skill": skill}) +} + +// Create 创建技能 +// @Summary 创建技能 +// @Description 创建新的技能 +// @Tags 技能管理 +// @Accept json +// @Produce json +// @Param skill body model.Skill true "技能信息" +// @Success 200 {object} map[string]interface{} "{"message": "skill created", "skill": {}}" +// @Router /skill/add [post] +func (h *SkillHandler) Create(c *gin.Context) { + var skill model.Skill + if err := c.ShouldBindJSON(&skill); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if err := h.skillService.CreateSkill(&skill); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "skill created", "skill": skill}) +} + +// Update 更新技能 +// @Summary 更新技能 +// @Description 更新技能信息 +// @Tags 技能管理 +// @Accept json +// @Produce json +// @Param id path string true "技能ID" +// @Param skill body model.Skill true "技能信息" +// @Success 200 {object} map[string]interface{} "{"message": "skill updated"}" +// @Router /skill/{id} [put] +func (h *SkillHandler) Update(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "skill id is required"}) + return + } + + var skill model.Skill + if err := c.ShouldBindJSON(&skill); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + skill.ID = id + if err := h.skillService.UpdateSkill(&skill); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "skill updated"}) +} + +// Delete 删除技能 +// @Summary 删除技能 +// @Description 删除技能 +// @Tags 技能管理 +// @Accept json +// @Produce json +// @Param id path string true "技能ID" +// @Success 200 {object} map[string]interface{} "{"message": "skill deleted"}" +// @Router /skill/{id} [delete] +func (h *SkillHandler) Delete(c *gin.Context) { + id := c.Param("id") + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "skill id is required"}) + return + } + + if err := h.skillService.DeleteSkill(id); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "skill deleted"}) +} diff --git a/server/internal/model/agent.go b/server/internal/model/agent.go index 13aa32c..bf908bc 100644 --- a/server/internal/model/agent.go +++ b/server/internal/model/agent.go @@ -15,10 +15,10 @@ const ( // Agent 智能体 type Agent struct { - ID string `json:"id" gorm:"primaryKey"` - Name string `json:"name" gorm:"size:100;not null"` - Description string `json:"description" gorm:"type:text"` - OwnerID string `json:"owner_id" gorm:"size:50;not null;index"` + ID string `json:"id" gorm:"primaryKey"` + Name string `json:"name" gorm:"size:100;not null"` + Description string `json:"description" gorm:"type:text"` + OwnerID string `json:"owner_id" gorm:"size:50;not null;index"` // Agent能力配置 Capabilities []string `json:"capabilities" gorm:"type:text"` // JSON数组,可用工具列表 @@ -29,6 +29,14 @@ type Agent struct { SecurityLevel SecurityLevel `json:"security_level" gorm:"size:20;default:'safe'"` AllowDangerousTools bool `json:"allow_dangerous_tools" gorm:"default:false"` + // 扩展字段:角色描述和模型配置 + RoleDescription string `json:"role_description" gorm:"type:text"` // 角色描述 (System Prompt) + ModelProvider string `json:"model_provider" gorm:"size:50"` // 模型提供商: openai/anthropic + ModelName string `json:"model_name" gorm:"size:100"` // 模型名称 + + // 协作模式 + IsSupervisor bool `json:"is_supervisor" gorm:"default:false"` // 是否为主智能体 + // 状态 IsActive bool `json:"is_active" gorm:"default:true"` @@ -36,6 +44,62 @@ type Agent struct { UpdatedAt time.Time `json:"updated_at"` } +// AgentSkill 智能体-技能绑定 +type AgentSkill struct { + ID string `json:"id" gorm:"primaryKey"` + AgentID string `json:"agent_id" gorm:"size:191;index"` + SkillID string `json:"skill_id" gorm:"size:191;index"` + SkillConfig string `json:"skill_config" gorm:"type:text"` // JSON 配置 + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AgentKnowledgeBase 智能体-知识库绑定 +type AgentKnowledgeBase struct { + ID string `json:"id" gorm:"primaryKey"` + AgentID string `json:"agent_id" gorm:"size:191;index"` + KnowledgeBaseID string `json:"knowledge_base_id" gorm:"size:191;index"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AgentMemory 长期记忆 +type AgentMemory struct { + ID string `json:"id" gorm:"primaryKey"` + AgentID string `json:"agent_id" gorm:"size:191;index"` + UserID string `json:"user_id" gorm:"size:191;index"` + Content string `json:"content" gorm:"type:text"` + MemoryType string `json:"memory_type" gorm:"size:20"` // experience/preference/conversation + Importance int `json:"importance" gorm:"default:5"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AgentTeam 多智能体协作配置 +type AgentTeam struct { + ID string `json:"id" gorm:"primaryKey"` + SupervisorAgentID string `json:"supervisor_agent_id" gorm:"size:191;index"` + MemberAgentID string `json:"member_agent_id" gorm:"size:191;index"` + DispatchStrategy string `json:"dispatch_strategy" gorm:"size:20;default:'parallel'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// AgentTask 任务记录 +type AgentTask struct { + ID string `json:"id" gorm:"primaryKey"` + AgentID string `json:"agent_id" gorm:"size:191;index"` + UserID string `json:"user_id" gorm:"size:191;index"` + UserInput string `json:"user_input" gorm:"type:text"` + AgentResponse string `json:"agent_response" gorm:"type:text"` + Status string `json:"status" gorm:"size:20"` // pending/running/completed/failed + TokensUsed int `json:"tokens_used" gorm:"default:0"` + DurationMs int `json:"duration_ms"` + SessionID string `json:"session_id" gorm:"size:191;index"` + CreatedAt time.Time `json:"created_at"` + CompletedAt time.Time `json:"completed_at"` +} + // AgentRequest 聊天请求 type AgentRequest struct { AgentID string `json:"agent_id" binding:"required"` diff --git a/server/internal/model/skill.go b/server/internal/model/skill.go new file mode 100644 index 0000000..0efec74 --- /dev/null +++ b/server/internal/model/skill.go @@ -0,0 +1,28 @@ +package model + +import ( + "time" + + "github.com/google/uuid" + "gorm.io/gorm" +) + +// Skill 技能 +type Skill struct { + ID string `json:"id" gorm:"primaryKey"` + SkillName string `json:"skill_name" gorm:"size:200;not null"` + SkillType string `json:"skill_type" gorm:"size:20;not null"` // system / user + SkillDesc string `json:"skill_desc" gorm:"type:text"` + Path string `json:"path" gorm:"size:500"` // skill 文件路径 + Status string `json:"status" gorm:"size:20;default:'active'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// BeforeCreate 创建前自动生成ID +func (s *Skill) BeforeCreate(tx *gorm.DB) error { + if s.ID == "" { + s.ID = uuid.New().String() + } + return nil +} diff --git a/server/internal/repository/agent_repo.go b/server/internal/repository/agent_repo.go index 3d99745..3693142 100644 --- a/server/internal/repository/agent_repo.go +++ b/server/internal/repository/agent_repo.go @@ -46,3 +46,108 @@ func (r *AgentRepository) Update(agent *model.Agent) error { func (r *AgentRepository) Delete(id string) error { return r.db.Delete(&model.Agent{}, "id = ?", id).Error } + +// AgentSkill 相关方法 + +func (r *AgentRepository) CreateAgentSkill(as *model.AgentSkill) error { + return r.db.Create(as).Error +} + +func (r *AgentRepository) FindSkillsByAgentID(agentID string) ([]model.AgentSkill, error) { + var agentSkills []model.AgentSkill + err := r.db.Where("agent_id = ?", agentID).Find(&agentSkills).Error + return agentSkills, err +} + +func (r *AgentRepository) DeleteAgentSkills(agentID string) error { + return r.db.Delete(&model.AgentSkill{}, "agent_id = ?", agentID).Error +} + +// AgentKnowledgeBase 相关方法 + +func (r *AgentRepository) CreateAgentKnowledgeBase(akb *model.AgentKnowledgeBase) error { + return r.db.Create(akb).Error +} + +func (r *AgentRepository) FindKnowledgeBasesByAgentID(agentID string) ([]model.AgentKnowledgeBase, error) { + var agentKBs []model.AgentKnowledgeBase + err := r.db.Where("agent_id = ?", agentID).Find(&agentKBs).Error + return agentKBs, err +} + +func (r *AgentRepository) DeleteAgentKnowledgeBases(agentID string) error { + return r.db.Delete(&model.AgentKnowledgeBase{}, "agent_id = ?", agentID).Error +} + +// AgentMemory 相关方法 + +func (r *AgentRepository) CreateMemory(memory *model.AgentMemory) error { + return r.db.Create(memory).Error +} + +func (r *AgentRepository) FindMemoriesByAgentID(agentID string, limit int) ([]model.AgentMemory, error) { + var memories []model.AgentMemory + err := r.db.Where("agent_id = ?", agentID).Order("importance DESC, created_at DESC").Limit(limit).Find(&memories).Error + return memories, err +} + +func (r *AgentRepository) FindMemoriesByUserID(agentID, userID string, limit int) ([]model.AgentMemory, error) { + var memories []model.AgentMemory + err := r.db.Where("agent_id = ? AND user_id = ?", agentID, userID).Order("importance DESC, created_at DESC").Limit(limit).Find(&memories).Error + return memories, err +} + +func (r *AgentRepository) DeleteMemory(id string) error { + return r.db.Delete(&model.AgentMemory{}, "id = ?", id).Error +} + +// AgentTeam 相关方法 + +func (r *AgentRepository) CreateAgentTeam(team *model.AgentTeam) error { + return r.db.Create(team).Error +} + +func (r *AgentRepository) FindTeamMembers(supervisorAgentID string) ([]model.AgentTeam, error) { + var teams []model.AgentTeam + err := r.db.Where("supervisor_agent_id = ?", supervisorAgentID).Find(&teams).Error + return teams, err +} + +func (r *AgentRepository) FindByMemberAgentID(memberAgentID string) ([]model.AgentTeam, error) { + var teams []model.AgentTeam + err := r.db.Where("member_agent_id = ?", memberAgentID).Find(&teams).Error + return teams, err +} + +func (r *AgentRepository) DeleteAgentTeam(id string) error { + return r.db.Delete(&model.AgentTeam{}, "id = ?", id).Error +} + +func (r *AgentRepository) DeleteTeamMembers(supervisorAgentID string) error { + return r.db.Delete(&model.AgentTeam{}, "supervisor_agent_id = ?", supervisorAgentID).Error +} + +// AgentTask 相关方法 + +func (r *AgentRepository) CreateTask(task *model.AgentTask) error { + return r.db.Create(task).Error +} + +func (r *AgentRepository) UpdateTask(task *model.AgentTask) error { + return r.db.Save(task).Error +} + +func (r *AgentRepository) FindTasksByAgentID(agentID string, limit int) ([]model.AgentTask, error) { + var tasks []model.AgentTask + err := r.db.Where("agent_id = ?", agentID).Order("created_at DESC").Limit(limit).Find(&tasks).Error + return tasks, err +} + +func (r *AgentRepository) FindTaskByID(id string) (*model.AgentTask, error) { + var task model.AgentTask + err := r.db.First(&task, "id = ?", id).Error + if err != nil { + return nil, err + } + return &task, nil +} diff --git a/server/internal/repository/skill_repo.go b/server/internal/repository/skill_repo.go new file mode 100644 index 0000000..2a2a31a --- /dev/null +++ b/server/internal/repository/skill_repo.go @@ -0,0 +1,90 @@ +package repository + +import ( + "x-agents/server/internal/model" + + "gorm.io/gorm" +) + +type SkillRepository struct { + db *gorm.DB +} + +func NewSkillRepository(db *gorm.DB) *SkillRepository { + return &SkillRepository{db: db} +} + +// DB 获取数据库连接 +func (r *SkillRepository) DB() *gorm.DB { + return r.db +} + +func (r *SkillRepository) Create(skill *model.Skill) error { + return r.db.Create(skill).Error +} + +func (r *SkillRepository) FindAll() ([]model.Skill, error) { + var skills []model.Skill + err := r.db.Order("skill_type, skill_name").Find(&skills).Error + return skills, err +} + +func (r *SkillRepository) FindByID(id string) (*model.Skill, error) { + var skill model.Skill + err := r.db.First(&skill, "id = ?", id).Error + if err != nil { + return nil, err + } + return &skill, nil +} + +func (r *SkillRepository) FindByType(skillType string) ([]model.Skill, error) { + var skills []model.Skill + err := r.db.Where("skill_type = ?", skillType).Order("skill_name").Find(&skills).Error + return skills, err +} + +func (r *SkillRepository) FindByName(skillName string) (*model.Skill, error) { + var skill model.Skill + err := r.db.First(&skill, "skill_name = ?", skillName).Error + if err != nil { + return nil, err + } + return &skill, nil +} + +func (r *SkillRepository) Update(skill *model.Skill) error { + return r.db.Save(skill).Error +} + +func (r *SkillRepository) Delete(id string) error { + return r.db.Delete(&model.Skill{}, "id = ?", id).Error +} + +// UpsertBatch 批量upsert skills +func (r *SkillRepository) UpsertBatch(skills []model.Skill) error { + for _, skill := range skills { + var existing model.Skill + err := r.db.First(&existing, "skill_name = ? AND skill_type = ?", skill.SkillName, skill.SkillType).Error + if err == gorm.ErrRecordNotFound { + if err := r.db.Create(&skill).Error; err != nil { + return err + } + } else if err != nil { + return err + } else { + existing.SkillDesc = skill.SkillDesc + existing.Path = skill.Path + existing.Status = skill.Status + if err := r.db.Save(&existing).Error; err != nil { + return err + } + } + } + return nil +} + +// DeleteByType 根据类型删除 +func (r *SkillRepository) DeleteByType(skillType string) error { + return r.db.Where("skill_type = ?", skillType).Delete(&model.Skill{}).Error +} diff --git a/server/internal/service/agent_service.go b/server/internal/service/agent_service.go new file mode 100644 index 0000000..67633fd --- /dev/null +++ b/server/internal/service/agent_service.go @@ -0,0 +1,145 @@ +package service + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +// AgentChatRequest Python Agent 对话请求 +type AgentChatRequest struct { + AgentID int `json:"agent_id"` + Message string `json:"message"` + UserID int `json:"user_id"` + SessionID string `json:"session_id,omitempty"` +} + +// AgentChatResponse Python Agent 对话响应 +type AgentChatResponse struct { + AgentID int `json:"agent_id"` + Response string `json:"response"` + ToolCalls []interface{} `json:"tool_calls"` + TokensUsed int `json:"tokens_used"` + DurationMs int `json:"duration_ms"` + SessionID string `json:"session_id"` +} + +// TeamChatRequest 多智能体群聊请求 +type TeamChatRequest struct { + SupervisorAgentID int `json:"supervisor_agent_id"` + MemberAgentIDs []int `json:"member_agent_ids"` + Message string `json:"message"` + UserID int `json:"user_id"` + SessionID string `json:"session_id,omitempty"` + Strategy string `json:"strategy,omitempty"` +} + +// TeamChatResponse 多智能体群聊响应 +type TeamChatResponse struct { + SupervisorAgentID int `json:"supervisor_agent_id"` + Response string `json:"response"` + SubtaskResults []interface{} `json:"subtask_results"` + Strategy string `json:"strategy"` + DurationMs int `json:"duration_ms"` + SessionID string `json:"session_id"` +} + +// AgentService Python Agent 服务 +type AgentService struct { + pythonURL string + client *http.Client +} + +// NewAgentService 创建 Agent 服务 +func NewAgentService(pythonURL string) *AgentService { + return &AgentService{ + pythonURL: pythonURL, + client: &http.Client{ + Timeout: 120 * time.Second, // Agent 可能需要较长时间 + }, + } +} + +// Chat 单智能体对话 +func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) { + url := fmt.Sprintf("%s/agent/chat", s.pythonURL) + + jsonData, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to call python agent: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("python agent error: %s", string(body)) + } + + var result AgentChatResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &result, nil +} + +// TeamChat 多智能体群聊 +func (s *AgentService) TeamChat(req TeamChatRequest) (*TeamChatResponse, error) { + url := fmt.Sprintf("%s/agent/team/chat", s.pythonURL) + + // 设置默认策略 + if req.Strategy == "" { + req.Strategy = "parallel" + } + + jsonData, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := s.client.Do(httpReq) + if err != nil { + return nil, fmt.Errorf("failed to call python agent: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("python agent error: %s", string(body)) + } + + var result TeamChatResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) + } + + return &result, nil +} diff --git a/server/internal/service/memory_service.go b/server/internal/service/memory_service.go new file mode 100644 index 0000000..e9d3837 --- /dev/null +++ b/server/internal/service/memory_service.go @@ -0,0 +1,50 @@ +package service + +import ( + "x-agents/server/internal/repository" + + "x-agents/server/internal/model" +) + +// MemoryService 记忆服务 +type MemoryService struct { + agentRepo *repository.AgentRepository +} + +// NewMemoryService 创建记忆服务 +func NewMemoryService(agentRepo *repository.AgentRepository) *MemoryService { + return &MemoryService{ + agentRepo: agentRepo, + } +} + +// CreateMemory 创建记忆 +func (s *MemoryService) CreateMemory(agentID, userID, content, memoryType string, importance int) (*model.AgentMemory, error) { + memory := &model.AgentMemory{ + AgentID: agentID, + UserID: userID, + Content: content, + MemoryType: memoryType, + Importance: importance, + } + + err := s.agentRepo.CreateMemory(memory) + if err != nil { + return nil, err + } + + return memory, nil +} + +// GetMemories 获取记忆列表 +func (s *MemoryService) GetMemories(agentID string, userID string, limit int) ([]model.AgentMemory, error) { + if userID != "" { + return s.agentRepo.FindMemoriesByUserID(agentID, userID, limit) + } + return s.agentRepo.FindMemoriesByAgentID(agentID, limit) +} + +// DeleteMemory 删除记忆 +func (s *MemoryService) DeleteMemory(memoryID string) error { + return s.agentRepo.DeleteMemory(memoryID) +} diff --git a/server/internal/service/skill_service.go b/server/internal/service/skill_service.go new file mode 100644 index 0000000..a9983b3 --- /dev/null +++ b/server/internal/service/skill_service.go @@ -0,0 +1,226 @@ +package service + +import ( + "log" + "os" + "path/filepath" + "strings" + + "x-agents/server/internal/model" + "x-agents/server/internal/repository" + + "gopkg.in/yaml.v3" +) + +type SkillService struct { + skillRepo *repository.SkillRepository +} + +func NewSkillService(skillRepo *repository.SkillRepository) *SkillService { + return &SkillService{skillRepo: skillRepo} +} + +func (s *SkillService) GetAllSkills() ([]model.Skill, error) { + return s.skillRepo.FindAll() +} + +func (s *SkillService) GetSkillByID(id string) (*model.Skill, error) { + return s.skillRepo.FindByID(id) +} + +func (s *SkillService) GetSkillsByType(skillType string) ([]model.Skill, error) { + return s.skillRepo.FindByType(skillType) +} + +func (s *SkillService) CreateSkill(skill *model.Skill) error { + return s.skillRepo.Create(skill) +} + +func (s *SkillService) UpdateSkill(skill *model.Skill) error { + return s.skillRepo.Update(skill) +} + +func (s *SkillService) DeleteSkill(id string) error { + return s.skillRepo.Delete(id) +} + +// InitSkills 初始化扫描所有 skills 目录 +func (s *SkillService) InitSkills() error { + log.Println("[SkillService] Starting init skills...") + + // 获取项目根目录 + projectRoot := s.getProjectRoot() + if projectRoot == "" { + log.Println("[SkillService] Cannot determine project root, skipping skill init") + return nil + } + + // 扫描 system skills: account/admin/skills + systemSkillsPath := filepath.Join(projectRoot, "account", "admin", "skills") + if _, err := os.Stat(systemSkillsPath); err == nil { + log.Printf("[SkillService] Scanning system skills from: %s", systemSkillsPath) + systemSkills, err := s.scanSkillsDirectory(systemSkillsPath, "system") + if err != nil { + log.Printf("[SkillService] Error scanning system skills: %v", err) + } else { + log.Printf("[SkillService] Found %d system skills", len(systemSkills)) + // 先删除旧的 system skills + s.skillRepo.DeleteByType("system") + // 批量插入 + if err := s.skillRepo.UpsertBatch(systemSkills); err != nil { + log.Printf("[SkillService] Error saving system skills: %v", err) + } + } + } + + // 扫描 user skills: account/{username}/skills (除了 admin) + accountPath := filepath.Join(projectRoot, "account") + entries, err := os.ReadDir(accountPath) + if err != nil { + log.Printf("[SkillService] Error reading account directory: %v", err) + } else { + for _, entry := range entries { + if !entry.IsDir() || entry.Name() == "admin" { + continue + } + userSkillsPath := filepath.Join(accountPath, entry.Name(), "skills") + if _, err := os.Stat(userSkillsPath); err == nil { + log.Printf("[SkillService] Scanning user skills for %s from: %s", entry.Name(), userSkillsPath) + userSkills, err := s.scanSkillsDirectory(userSkillsPath, "user") + if err != nil { + log.Printf("[SkillService] Error scanning user skills for %s: %v", entry.Name(), err) + } else { + log.Printf("[SkillService] Found %d user skills for %s", len(userSkills), entry.Name()) + // 批量插入 + if err := s.skillRepo.UpsertBatch(userSkills); err != nil { + log.Printf("[SkillService] Error saving user skills for %s: %v", entry.Name(), err) + } + } + } + } + } + + log.Println("[SkillService] Skills initialized successfully") + return nil +} + +// scanSkillsDirectory 扫描指定目录下的所有 skill +func (s *SkillService) scanSkillsDirectory(basePath string, skillType string) ([]model.Skill, error) { + var skills []model.Skill + + entries, err := os.ReadDir(basePath) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + skillDir := filepath.Join(basePath, entry.Name()) + skillPath := filepath.Join(skillDir, "SKILL.md") + + // 尝试 skill.md(大小写不敏感) + if _, err := os.Stat(skillPath); os.IsNotExist(err) { + skillPath = filepath.Join(skillDir, "skill.md") + } + + skillInfo, err := s.parseSkillFile(skillPath) + if err != nil { + log.Printf("[SkillService] Error parsing skill file %s: %v", skillPath, err) + continue + } + + // 如果没有从文件解析到名称,使用目录名 + if skillInfo.SkillName == "" { + skillInfo.SkillName = entry.Name() + } + + skill := model.Skill{ + SkillName: skillInfo.SkillName, + SkillType: skillType, + SkillDesc: skillInfo.SkillDesc, + Path: skillPath, + Status: "active", + } + + skills = append(skills, skill) + } + + return skills, nil +} + +// parseSkillFile 解析 SKILL.md 文件,提取 YAML front matter +func (s *SkillService) parseSkillFile(skillPath string) (*model.Skill, error) { + content, err := os.ReadFile(skillPath) + if err != nil { + return nil, err + } + + // 检查是否有 YAML front matter + contentStr := string(content) + if !strings.HasPrefix(contentStr, "---") { + // 没有 front matter,返回空 + return &model.Skill{}, nil + } + + // 找到结束标记 + lines := strings.Split(contentStr, "\n") + var endIdx int + for i := 1; i < len(lines); i++ { + if strings.TrimSpace(lines[i]) == "---" { + endIdx = i + break + } + } + + if endIdx == 0 { + return &model.Skill{}, nil + } + + // 提取 YAML 内容 + yamlContent := strings.Join(lines[1:endIdx], "\n") + + // 解析 YAML + var frontMatter map[string]string + if err := yaml.Unmarshal([]byte(yamlContent), &frontMatter); err != nil { + return nil, err + } + + skill := &model.Skill{ + SkillName: frontMatter["name"], + SkillDesc: frontMatter["description"], + } + + return skill, nil +} + +// getProjectRoot 获取项目根目录 +func (s *SkillService) getProjectRoot() string { + execPath, _ := os.Getwd() + projectRoot := execPath + + // 如果当前目录名为 server,向上找一级 + baseName := filepath.Base(execPath) + if baseName == "server" { + projectRoot = filepath.Dir(execPath) + } + + // 尝试向上查找包含 .git 的目录 + if _, err := os.Stat(filepath.Join(projectRoot, ".git")); os.IsNotExist(err) { + for i := 0; i < 3; i++ { + parent := filepath.Dir(projectRoot) + if parent == projectRoot { + break + } + if _, err := os.Stat(filepath.Join(parent, ".git")); err == nil { + projectRoot = parent + break + } + projectRoot = parent + } + } + + return projectRoot +} diff --git a/server/migrations/agent_system.sql b/server/migrations/agent_system.sql new file mode 100644 index 0000000..39d474b --- /dev/null +++ b/server/migrations/agent_system.sql @@ -0,0 +1,63 @@ +-- ===================================================== +-- Agent System Database Migration +-- Run this script if you want vector-based memory storage +-- Note: Requires pgvector extension for PostgreSQL +-- ===================================================== + +-- Enable pgvector extension (PostgreSQL only) +-- CREATE EXTENSION IF NOT EXISTS vector; + +-- Agent Memory with Vector Embedding (Optional - for vector search) +-- This table is optional. If not using vector search, use agent_memories table instead. +-- CREATE TABLE IF NOT EXISTS agent_memory_vectors ( +-- id VARCHAR(191) PRIMARY KEY, +-- agent_id VARCHAR(191) NOT NULL, +-- user_id VARCHAR(191), +-- content TEXT NOT NULL, +-- embedding vector(1536), -- Adjust dimension based on your embedding model +-- memory_type VARCHAR(20), -- experience/preference/conversation +-- importance INT DEFAULT 5, +-- created_at DATETIME(3), +-- updated_at DATETIME(3), +-- INDEX idx_agent_vector USING ivfflat (agent_id, embedding vector_cosine_ops), +-- INDEX idx_importance (agent_id, importance DESC) +-- ); + +-- For MySQL (without vector support, use text search instead) +CREATE TABLE IF NOT EXISTS agent_memory_vectors ( + id VARCHAR(191) PRIMARY KEY, + agent_id VARCHAR(191) NOT NULL, + user_id VARCHAR(191), + content TEXT NOT NULL, + embedding_text TEXT, -- Store serialized vector for MySQL + memory_type VARCHAR(20), + importance INT DEFAULT 5, + created_at DATETIME(3), + updated_at DATETIME(3), + INDEX idx_agent_memory_agent (agent_id), + INDEX idx_agent_memory_user (agent_id, user_id), + INDEX idx_importance (agent_id, importance DESC) +); + +-- Agent Task Indexes +CREATE INDEX IF NOT EXISTS idx_agent_tasks_agent_user ON agent_tasks(agent_id, user_id); +CREATE INDEX IF NOT EXISTS idx_agent_tasks_session ON agent_tasks(session_id); +CREATE INDEX IF NOT EXISTS idx_agent_tasks_status ON agent_tasks(status); +CREATE INDEX IF NOT EXISTS idx_agent_tasks_created ON agent_tasks(created_at DESC); + +-- Agent Team Indexes +CREATE INDEX IF NOT EXISTS idx_agent_team_supervisor ON agent_teams(supervisor_agent_id); +CREATE INDEX IF NOT EXISTS idx_agent_team_member ON agent_teams(member_agent_id); + +-- Agent Memory Indexes +CREATE INDEX IF NOT EXISTS idx_agent_memory_agent ON agent_memories(agent_id); +CREATE INDEX IF NOT EXISTS idx_agent_memory_user ON agent_memories(agent_id, user_id); +CREATE INDEX IF NOT EXISTS idx_agent_memory_type ON agent_memories(memory_type); + +-- Agent Skills Indexes +CREATE INDEX IF NOT EXISTS idx_agent_skills_agent ON agent_skills(agent_id); +CREATE INDEX IF NOT EXISTS idx_agent_skills_skill ON agent_skills(skill_id); + +-- Agent Knowledge Base Indexes +CREATE INDEX IF NOT EXISTS idx_agent_kb_agent ON agent_knowledge_bases(agent_id); +CREATE INDEX IF NOT EXISTS idx_agent_kb_kb ON agent_knowledge_bases(knowledge_base_id);