259 lines
7.0 KiB
Go
259 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/yourname/cyrene-ai/ai-core/internal/context"
|
|
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
|
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
|
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
|
"github.com/yourname/cyrene-ai/ai-core/internal/orchestrator"
|
|
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
|
)
|
|
|
|
func main() {
|
|
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
log.Println("🧠 AI-Core 服务启动中...")
|
|
|
|
// 加载配置
|
|
cfg := loadConfig()
|
|
|
|
// 初始化人格加载器
|
|
personaDir := cfg.PersonaDir
|
|
if personaDir == "" {
|
|
personaDir = "./internal/persona"
|
|
}
|
|
personaLoader, err := persona.NewLoader(personaDir)
|
|
if err != nil {
|
|
log.Fatalf("加载人格配置失败: %v", err)
|
|
}
|
|
log.Printf("已加载 %d 个人格: %v", len(personaLoader.List()), personaLoader.List())
|
|
|
|
// 初始化LLM适配器
|
|
llmProvider := llm.NewOpenAIProvider(llm.OpenAIConfig{
|
|
BaseURL: cfg.LLMBaseURL,
|
|
APIKey: cfg.LLMAPIKey,
|
|
Model: cfg.LLMModel,
|
|
FallbackModel: cfg.LLMFallbackModel,
|
|
Timeout: 120 * time.Second,
|
|
})
|
|
llmAdapter := llm.NewAdapter(llmProvider)
|
|
log.Printf("LLM适配器已就绪: 模型=%s", llmAdapter.ModelName())
|
|
|
|
// 初始化记忆系统
|
|
var memStore *memory.Store
|
|
var memRetriever *memory.Retriever
|
|
var memExtractor *memory.Extractor
|
|
|
|
if cfg.DatabaseURL != "" {
|
|
memStore, err = memory.NewStore(cfg.DatabaseURL)
|
|
if err != nil {
|
|
log.Printf("⚠ 记忆存储初始化失败 (将跳过记忆功能): %v", err)
|
|
} else {
|
|
defer memStore.Close()
|
|
log.Println("记忆存储已就绪")
|
|
|
|
memRetriever = memory.NewRetriever(memStore, nil)
|
|
|
|
// 记忆提取器使用LLM
|
|
memExtractor = memory.NewExtractor(memStore, func(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error) {
|
|
return llmAdapter.Chat(ctx, messages)
|
|
})
|
|
log.Println("记忆提取器已就绪")
|
|
}
|
|
}
|
|
|
|
// 初始化上下文构建器
|
|
ctxBuilder := &context.Builder{}
|
|
|
|
// 手动注入 Injector 到 orchestrator(临时方案,后续会用依赖注入框架)
|
|
personaInjector := &persona.Injector{}
|
|
|
|
// 健康检查与对话API的HTTP mux
|
|
mux := http.NewServeMux()
|
|
|
|
// 手动构建 orchestrator 用于处理(因为现有orchestrator结构体已定义但未导出构造函数)
|
|
orch := &orchestrator.Orchestrator{}
|
|
|
|
// 注册对话API端点
|
|
mux.HandleFunc("/api/v1/chat", func(w http.ResponseWriter, r *http.Request) {
|
|
handleChat(w, r, orch, ctxBuilder, llmAdapter, personaLoader, personaInjector, memRetriever, memExtractor)
|
|
})
|
|
|
|
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write([]byte(`{"status":"ok","service":"ai-core","model":"` + llmAdapter.ModelName() + `"}`))
|
|
})
|
|
|
|
// 启动HTTP服务
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.Port,
|
|
Handler: mux,
|
|
}
|
|
|
|
go func() {
|
|
log.Printf("🚀 AI-Core 服务已启动在端口 %s", cfg.Port)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("服务启动失败: %v", err)
|
|
}
|
|
}()
|
|
|
|
// 优雅关闭
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
log.Println("正在关闭 AI-Core 服务...")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
srv.Shutdown(ctx)
|
|
log.Println("AI-Core 服务已关闭")
|
|
}
|
|
|
|
// Config AI-Core配置
|
|
type Config struct {
|
|
Port string
|
|
PersonaDir string
|
|
LLMBaseURL string
|
|
LLMAPIKey string
|
|
LLMModel string
|
|
LLMFallbackModel string
|
|
DatabaseURL string
|
|
}
|
|
|
|
func loadConfig() Config {
|
|
return Config{
|
|
Port: getEnv("AI_CORE_PORT", "8081"),
|
|
PersonaDir: getEnv("PERSONA_DIR", "./internal/persona"),
|
|
LLMBaseURL: getEnv("LLM_API_URL", "https://api.openai.com/v1"),
|
|
LLMAPIKey: getEnv("LLM_API_KEY", ""),
|
|
LLMModel: getEnv("LLM_MODEL", "gpt-4o"),
|
|
LLMFallbackModel: getEnv("LLM_FALLBACK_MODEL", "gpt-4o-mini"),
|
|
DatabaseURL: buildDatabaseURL(),
|
|
}
|
|
}
|
|
|
|
func buildDatabaseURL() string {
|
|
host := getEnv("POSTGRES_HOST", "localhost")
|
|
port := getEnv("POSTGRES_PORT", "5432")
|
|
user := getEnv("POSTGRES_USER", "cyrene")
|
|
password := getEnv("POSTGRES_PASSWORD", "change_me")
|
|
dbname := getEnv("POSTGRES_DB", "cyrene_ai")
|
|
sslmode := getEnv("POSTGRES_SSLMODE", "disable")
|
|
|
|
return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s",
|
|
user, password, host, port, dbname, sslmode)
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// handleChat 处理对话请求
|
|
func handleChat(
|
|
w http.ResponseWriter,
|
|
r *http.Request,
|
|
_ *orchestrator.Orchestrator,
|
|
ctxBuilder *context.Builder,
|
|
llmAdapter *llm.Adapter,
|
|
personaLoader *persona.Loader,
|
|
personaInjector *persona.Injector,
|
|
memRetriever *memory.Retriever,
|
|
memExtractor *memory.Extractor,
|
|
) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// 解析请求
|
|
var req struct {
|
|
UserID string `json:"user_id"`
|
|
SessionID string `json:"session_id"`
|
|
Message string `json:"message"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "无效的请求体", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.Mode == "" {
|
|
req.Mode = "text"
|
|
}
|
|
|
|
ctx := r.Context()
|
|
|
|
// 1. 检索相关记忆
|
|
var memories []memory.MemoryEntry
|
|
if memRetriever != nil {
|
|
var err error
|
|
memories, err = memRetriever.Retrieve(ctx, req.UserID, req.Message)
|
|
if err != nil {
|
|
log.Printf("[chat] 记忆检索失败: %v", err)
|
|
}
|
|
}
|
|
|
|
// 2. 加载人格配置
|
|
personaConfig, err := personaLoader.Get("cyrene")
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("加载人格失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// 3. 构建对话上下文
|
|
llmMessages, err := ctxBuilder.Build(ctx, context.BuildParams{
|
|
UserID: req.UserID,
|
|
SessionID: req.SessionID,
|
|
UserMessage: req.Message,
|
|
Persona: personaConfig,
|
|
Memories: memories,
|
|
HistoryLimit: 20,
|
|
})
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("构建上下文失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// 4. 调用LLM
|
|
llmResp, err := llmAdapter.Chat(ctx, llmMessages)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("LLM调用失败: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// 5. 异步提取记忆
|
|
if memExtractor != nil {
|
|
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, llmResp.Content)
|
|
}
|
|
|
|
// 6. 构建响应
|
|
resp := map[string]interface{}{
|
|
"text": llmResp.Content,
|
|
"mode": req.Mode,
|
|
"message_id": fmt.Sprintf("msg-%d", time.Now().UnixNano()),
|
|
}
|
|
|
|
// 语音助手模式断句
|
|
if req.Mode == "voice_assistant" {
|
|
resp["segments"] = llm.SplitIntoSegments(llmResp.Content)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
// 确保未使用变量不报错
|
|
var _ = personaInjector
|