feat: 新增 Agent、Memory、Skill 模块
- handler: agent_handler, memory_handler, skill_handler - model: agent.go, skill.go - repository: agent_repo, skill_repo - service: agent_service, memory_service, skill_service - 新增 migrations 目录 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
145
server/internal/service/agent_service.go
Normal file
145
server/internal/service/agent_service.go
Normal file
@@ -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
|
||||
}
|
||||
50
server/internal/service/memory_service.go
Normal file
50
server/internal/service/memory_service.go
Normal file
@@ -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)
|
||||
}
|
||||
226
server/internal/service/skill_service.go
Normal file
226
server/internal/service/skill_service.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user