Add Redis Store

This commit is contained in:
2026-03-02 15:28:03 +00:00
parent 09d687b45b
commit 259d02d140
7 changed files with 167 additions and 23 deletions

View File

@@ -18,12 +18,12 @@ type Config struct {
// ConversationConfig controls conversation storage.
type ConversationConfig struct {
// Store is the storage backend: "memory" (default) or "sql".
// Store is the storage backend: "memory" (default), "sql", or "redis".
Store string `yaml:"store"`
// TTL is the conversation expiration duration (e.g. "1h", "30m"). Defaults to "1h".
TTL string `yaml:"ttl"`
// DSN is the database connection string, required when store is "sql".
// Examples: "conversations.db" (SQLite), "postgres://user:pass@host/db".
// DSN is the database/Redis connection string, required when store is "sql" or "redis".
// Examples: "conversations.db" (SQLite), "postgres://user:pass@host/db", "redis://:password@localhost:6379/0".
DSN string `yaml:"dsn"`
// Driver is the SQL driver name, required when store is "sql".
// Examples: "sqlite3", "postgres", "mysql".

View File

@@ -0,0 +1,106 @@
package conversation
import (
"context"
"encoding/json"
"time"
"github.com/ajac-zero/latticelm/internal/api"
"github.com/redis/go-redis/v9"
)
// RedisStore manages conversation history in Redis with automatic expiration.
type RedisStore struct {
client *redis.Client
ttl time.Duration
ctx context.Context
}
// NewRedisStore creates a Redis-backed conversation store.
func NewRedisStore(client *redis.Client, ttl time.Duration) *RedisStore {
return &RedisStore{
client: client,
ttl: ttl,
ctx: context.Background(),
}
}
// key returns the Redis key for a conversation ID.
func (s *RedisStore) key(id string) string {
return "conv:" + id
}
// Get retrieves a conversation by ID from Redis.
func (s *RedisStore) Get(id string) (*Conversation, bool) {
data, err := s.client.Get(s.ctx, s.key(id)).Bytes()
if err != nil {
return nil, false
}
var conv Conversation
if err := json.Unmarshal(data, &conv); err != nil {
return nil, false
}
return &conv, true
}
// Create creates a new conversation with the given messages.
func (s *RedisStore) Create(id string, model string, messages []api.Message) *Conversation {
now := time.Now()
conv := &Conversation{
ID: id,
Messages: messages,
Model: model,
CreatedAt: now,
UpdatedAt: now,
}
data, _ := json.Marshal(conv)
_ = s.client.Set(s.ctx, s.key(id), data, s.ttl).Err()
return conv
}
// Append adds new messages to an existing conversation.
func (s *RedisStore) Append(id string, messages ...api.Message) (*Conversation, bool) {
conv, ok := s.Get(id)
if !ok {
return nil, false
}
conv.Messages = append(conv.Messages, messages...)
conv.UpdatedAt = time.Now()
data, _ := json.Marshal(conv)
_ = s.client.Set(s.ctx, s.key(id), data, s.ttl).Err()
return conv, true
}
// Delete removes a conversation from Redis.
func (s *RedisStore) Delete(id string) {
_ = s.client.Del(s.ctx, s.key(id)).Err()
}
// Size returns the number of active conversations in Redis.
func (s *RedisStore) Size() int {
var count int
var cursor uint64
for {
keys, nextCursor, err := s.client.Scan(s.ctx, cursor, "conv:*", 100).Result()
if err != nil {
return 0
}
count += len(keys)
cursor = nextCursor
if cursor == 0 {
break
}
}
return count
}

View File

@@ -5,12 +5,12 @@ import (
"fmt"
"github.com/ajac-zero/latticelm/internal/api"
"github.com/openai/openai-go"
"github.com/openai/openai-go/shared"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/shared"
)
// parseTools converts Open Responses tools to OpenAI format
func parseTools(req *api.ResponseRequest) ([]openai.ChatCompletionToolParam, error) {
func parseTools(req *api.ResponseRequest) ([]openai.ChatCompletionToolUnionParam, error) {
if req.Tools == nil || len(req.Tools) == 0 {
return nil, nil
}
@@ -20,29 +20,27 @@ func parseTools(req *api.ResponseRequest) ([]openai.ChatCompletionToolParam, err
return nil, fmt.Errorf("unmarshal tools: %w", err)
}
var tools []openai.ChatCompletionToolParam
var tools []openai.ChatCompletionToolUnionParam
for _, td := range toolDefs {
// Convert Open Responses tool to OpenAI ChatCompletionToolParam
// Convert Open Responses tool to OpenAI function tool
// Extract: name, description, parameters
name, _ := td["name"].(string)
desc, _ := td["description"].(string)
params, _ := td["parameters"].(map[string]interface{})
tool := openai.ChatCompletionToolParam{
Function: shared.FunctionDefinitionParam{
Name: name,
},
funcDef := shared.FunctionDefinitionParam{
Name: name,
}
if desc != "" {
tool.Function.Description = openai.String(desc)
funcDef.Description = openai.String(desc)
}
if params != nil {
tool.Function.Parameters = shared.FunctionParameters(params)
funcDef.Parameters = shared.FunctionParameters(params)
}
tools = append(tools, tool)
tools = append(tools, openai.ChatCompletionFunctionTool(funcDef))
}
return tools, nil
@@ -67,17 +65,16 @@ func parseToolChoice(req *api.ResponseRequest) (openai.ChatCompletionToolChoiceO
return result, nil
}
// Handle specific function selection: {"type": "function", "name": "..."}
// Handle specific function selection: {"type": "function", "function": {"name": "..."}}
if obj, ok := choice.(map[string]interface{}); ok {
funcObj, _ := obj["function"].(map[string]interface{})
name, _ := funcObj["name"].(string)
result.OfChatCompletionNamedToolChoice = &openai.ChatCompletionNamedToolChoiceParam{
Function: openai.ChatCompletionNamedToolChoiceFunctionParam{
return openai.ToolChoiceOptionFunctionToolChoice(
openai.ChatCompletionNamedToolChoiceFunctionParam{
Name: name,
},
}
return result, nil
), nil
}
return result, fmt.Errorf("invalid tool_choice format")