Files
gochat/backend/internal/llm/fake_provider.go
T
Rogee 1c6b8c661c fix: 文件上传Metadata jsonb BUG + search config + fake:ai provider + QA报告
修复:
- message_service.go: 附件Metadata零值字符串''→'{}'(PG jsonb列报错)
- config.dev.yaml/config.development.yaml: search.engine改为db
  (避免依赖未运行的MeiliSearch导致搜索500)

新增:
- fake_provider.go: Captain/AI fake LLM Provider
- test-captain-llm-provider.sh: 测试脚本
- QA测试报告: 93项CDP功能点覆盖验证

文档:
- CDP_TESTING_HANDOFF.md: 进度更新(89%->92%->95%)
- 报告: 11.846~11.897 完整测试日志
2026-07-27 22:01:28 +08:00

140 lines
3.4 KiB
Go

// Package llm provides FakeLLMProvider — a zero-dependency LLM provider that
// returns canned responses for ChatCompletion, embeddings, and streaming.
//
// Usage: set FAKE_AI_ENABLED=true in the environment to activate; it overrides
// the standard provider manager so Captain/Copilot endpoints respond without a
// real LLM API key.
package llm
import (
"context"
"fmt"
"os"
"sync/atomic"
)
// FakeLLMProvider implements Provider with canned responses.
// Useful for integration testing, staging, and demo environments.
type FakeLLMProvider struct {
// CallCount tracks how many ChatCompletion calls were made.
callCount atomic.Int64
}
// NewFakeLLMProvider creates a new FakeLLMProvider.
// It is only active when the FAKE_AI_ENABLED env var is set to "true".
func NewFakeLLMProvider() *FakeLLMProvider {
return &FakeLLMProvider{}
}
// IsFakeAIEnabled returns true when the environment variable FAKE_AI_ENABLED=true.
func IsFakeAIEnabled() bool {
return os.Getenv("FAKE_AI_ENABLED") == "true"
}
// ChatCompletion returns a canned response echoing the last user message.
func (f *FakeLLMProvider) ChatCompletion(_ context.Context, req ChatRequest) (*ChatResponse, error) {
n := f.callCount.Add(1)
// Build a reply from the last user message.
userMsg := ""
for i := len(req.Messages) - 1; i >= 0; i-- {
if req.Messages[i].Role == "user" {
userMsg = req.Messages[i].Content
break
}
}
if userMsg == "" {
userMsg = "Hello! I'm a fake AI assistant. How can I help you today?"
}
reply := fmt.Sprintf("[FakeAI #%d] Received your message. Here is a simulated response: %s", n, userMsg)
return &ChatResponse{
ID: "fake-" + fmt.Sprint(n),
Object: "chat.completion",
Created: 1700000000,
Model: "fake-ai-model",
Choices: []ChatChoice{
{
Index: 0,
Message: ChatMessage{
Role: "assistant",
Content: reply,
},
FinishReason: "stop",
},
},
Usage: TokenUsage{
PromptTokens: 10,
CompletionTokens: 5,
TotalTokens: 15,
},
}, nil
}
// CreateEmbedding returns a zero-vector embedding of dimension 384.
func (f *FakeLLMProvider) CreateEmbedding(_ context.Context, req EmbeddingRequest) (*EmbeddingResponse, error) {
if len(req.Input) == 0 {
return &EmbeddingResponse{
Object: "list",
Data: []EmbeddingData{},
Model: "fake-embedding-model",
}, nil
}
dim := req.Dimensions
if dim <= 0 {
dim = 384
}
embeddings := make([]EmbeddingData, len(req.Input))
for i := range req.Input {
embeddings[i] = EmbeddingData{
Object: "embedding",
Index: i,
Embedding: make([]float64, dim),
}
}
return &EmbeddingResponse{
Object: "list",
Data: embeddings,
Model: "fake-embedding-model",
Usage: TokenUsage{
PromptTokens: len(req.Input),
CompletionTokens: 0,
TotalTokens: len(req.Input),
},
}, nil
}
// ChatCompletionStream simulates streaming by delivering a single chunk.
func (f *FakeLLMProvider) ChatCompletionStream(ctx context.Context, req ChatRequest, onChunk func(StreamChunk) error) error {
resp, err := f.ChatCompletion(ctx, req)
if err != nil {
return err
}
if len(resp.Choices) == 0 {
return nil
}
chunk := StreamChunk{
ID: resp.ID,
Object: "chat.completion.chunk",
Created: resp.Created,
Model: resp.Model,
Choices: []StreamChoice{
{
Index: 0,
Delta: StreamDelta{
Role: "assistant",
Content: resp.Choices[0].Message.Content,
},
FinishReason: "stop",
},
},
}
return onChunk(chunk)
}