Added call to embedding openai

This commit is contained in:
Radu Macocian (admac)
2026-06-23 14:36:34 +02:00
parent 8599de09de
commit 05b1f53f93
8 changed files with 458 additions and 181 deletions
+49 -12
View File
@@ -5,26 +5,47 @@ import (
"errors" "errors"
"io" "io"
"log/slog" "log/slog"
"net"
"net/url"
"os" "os"
) )
var ENV_PREFIX = "SOUV__" var ENV_PREFIX = "SOUV__"
type Config struct { type Config struct {
Api ApiConfig Api ApiConfig
Llm LLMConfig Llm LLMConfig
Logging []LogConfig Logging []LogConfig
Db DbConfig Db DbConfig
Embedding EmbeddingConfig
} }
type DbConfig struct { type DbConfig struct {
Url string Url string
Port string Port string
DbName string DbName string
User string User string
Password string Password string
History HistoryConfig History HistoryConfig
Memory MemoryConfig Memory MemoryConfig
ChunkSize int
ChunkOverlap int
}
func (config DbConfig) ConnectionString() string {
u := &url.URL{
Scheme: "postgres",
User: url.UserPassword(config.User, config.Password),
Host: net.JoinHostPort(config.Url, config.Port),
Path: config.DbName,
}
return u.String()
}
type EmbeddingConfig struct {
Url string
Key string
Model string
} }
type MemoryConfig struct { type MemoryConfig struct {
@@ -114,6 +135,13 @@ func defaultConfig() *Config {
Password: "", Password: "",
DbName: "souvenir", DbName: "souvenir",
History: HistoryConfig{Enabled: true}, History: HistoryConfig{Enabled: true},
ChunkSize: 400,
ChunkOverlap: 40,
},
EmbeddingConfig{
Url: "",
Key: "",
Model: "",
}, },
} }
} }
@@ -126,13 +154,16 @@ func overrideFromEnv(cfg *Config) {
{conf: &cfg.Api.Url, key: ENV_PREFIX + "api__url"}, {conf: &cfg.Api.Url, key: ENV_PREFIX + "api__url"},
{conf: &cfg.Api.Key, key: ENV_PREFIX + "api__key"}, {conf: &cfg.Api.Key, key: ENV_PREFIX + "api__key"},
{conf: &cfg.Llm.Model, key: ENV_PREFIX + "llm__model"}, {conf: &cfg.Llm.Model, key: ENV_PREFIX + "llm__model"},
{conf: &cfg.Embedding.Url, key: ENV_PREFIX + "embedding__url"},
{conf: &cfg.Embedding.Key, key: ENV_PREFIX + "embedding__key"},
{conf: &cfg.Embedding.Model, key: ENV_PREFIX + "embedding__model"},
{conf: &cfg.Db.DbName, key: ENV_PREFIX + "db__dbName"}, {conf: &cfg.Db.DbName, key: ENV_PREFIX + "db__dbName"},
{conf: &cfg.Db.Url, key: ENV_PREFIX + "db__url"}, {conf: &cfg.Db.Url, key: ENV_PREFIX + "db__url"},
{conf: &cfg.Db.User, key: ENV_PREFIX + "db__user"}, {conf: &cfg.Db.User, key: ENV_PREFIX + "db__user"},
{conf: &cfg.Db.Password, key: ENV_PREFIX + "db__password"}, {conf: &cfg.Db.Password, key: ENV_PREFIX + "db__password"},
{conf: &cfg.Db.Port, key: ENV_PREFIX + "db__port"}, {conf: &cfg.Db.Port, key: ENV_PREFIX + "db__port"},
} }
for _, override := range(overrides) { for _, override := range overrides {
val := os.Getenv(override.key) val := os.Getenv(override.key)
if val != "" { if val != "" {
*override.conf = val *override.conf = val
@@ -144,5 +175,11 @@ func (cfg Config) validate() error {
if cfg.Api.Url == "" { if cfg.Api.Url == "" {
return errors.New("No API url configured") return errors.New("No API url configured")
} }
if cfg.Embedding.Model == "" {
return errors.New("No embedding model configured")
}
if cfg.Embedding.Url == "" {
return errors.New("No embedding url configured")
}
return nil return nil
} }
+10
View File
@@ -0,0 +1,10 @@
package embed
type Chuner struct {
MaxChunks int
Overlap int
}
func Chunk(text string) []uint32 {
splitSentences
}
+108
View File
@@ -0,0 +1,108 @@
package embed
import (
"context"
"fmt"
"log/slog"
"git.estatecloud.org/radumaco/souvenir/config"
"git.estatecloud.org/radumaco/souvenir/llm"
"github.com/gopsql/pgx"
"github.com/jackc/pgx/v5/pgxpool"
)
type EmbedStore struct {
cfg config.DbConfig
logger slog.Logger
embedder llm.Embedder
tableName string
pool *pgxpool.Pool
}
func (e EmbedStore) TableName() string {
if e.tableName == "" {
e.tableName = fmt.Sprintf(`message_chunks_vec_%s`, e.embedder.ID())
}
return e.tableName
}
func NewEmbedStore(ctx context.Context, cfg config.DbConfig, embedder llm.Embedder) (*EmbedStore, error) {
logger := *slog.Default().With("Component", "EmbedStore")
pool, err := pgxpool.New(ctx, cfg.ConnectionString())
if err != nil {
logger.ErrorContext(ctx, "Could not create pgxpool")
return nil, err
}
store := EmbedStore{
cfg: cfg,
logger: logger,
embedder: embedder,
pool: pool,
}
const createVectorTableTemplate = `
CREATE TABLE IF NOT EXISTS %s (
chunk_id UUID PRIMARY KEY REFERENCES message_chunks(id) ON DELETE CASCADE,
embedding vector(384),
content_hash text not null
)`
conn := pgx.MustOpen(cfg.ConnectionString())
var exists bool
if err := conn.QueryRow(`SELECT EXISTS
(SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name =$1)`, store.TableName()).Scan(&exists); err != nil {
logger.Error("Error while checking table", "table", store.TableName(), "error", err)
return nil, err
}
if exists {
logger.Debug("Table found", "table", store.TableName())
} else {
logger.Debug("Table not found. Creating", "table", store.TableName())
script := fmt.Sprintf(createVectorTableTemplate, store.TableName())
if _, err := conn.Exec(script); err != nil {
logger.Error("Could not create table", "table", store.TableName(), "error", err)
return nil, err
}
}
return &store, nil
}
func (e EmbedStore) EmbedConversation(ctx context.Context, conversationId string) error {
rows, err := e.pool.Query(ctx, `SELECT c.id, c.content, c.content_hash
FROM message_chunks c
LEFT JOIN $1 v
ON v.chunk_id = c.id AND v.content_hash = c.content_hash
WHERE c.conversation_id = $2 and v.chunk_id IS NULL`,
e.TableName(), conversationId)
if err != nil {
e.logger.Error("Could not get message chunks", "conversation_id", conversationId, "error", err)
return err
}
var pending []struct{
id, content, contentHash string
}
for rows.Next() {
var p struct{id, content, contentHash string}
if err := rows.Scan(&p.id, &p.content, &p.contentHash); err != nil {
rows.Close()
e.logger.Error("Error reading chunk", "error", err)
return err
}
pending = append(pending, p)
}
rows.Close()
if len(pending) == 0 {
e.logger.Debug("Nothing to embed", "conversation_id", conversationId)
return nil
}
for start := 0; start < len(pending); start += e.cfg.ChunkSize {
e.logger.Debug("", "a", start)
}
return nil
}
+107 -133
View File
@@ -1,152 +1,142 @@
package history package history
import ( import (
"context"
"errors" "errors"
"log/slog" "log/slog"
"net" "net"
"net/url" "net/url"
"strconv"
"strings"
"git.estatecloud.org/radumaco/souvenir/config" "git.estatecloud.org/radumaco/souvenir/config"
"git.estatecloud.org/radumaco/souvenir/model" "git.estatecloud.org/radumaco/souvenir/model"
"github.com/gopsql/db"
"github.com/gopsql/pgx" "github.com/gopsql/pgx"
"github.com/jackc/pgx/v5/pgxpool"
) )
type DbClient struct { type DbClient struct {
Cfg config.DbConfig cfg config.DbConfig
Logger slog.Logger logger slog.Logger
pool *pgxpool.Pool
} }
func (client DbClient) Init() error { func Init(ctx context.Context, cfg config.DbConfig) (*DbClient, error) {
logger := *slog.Default().With("Component", "History")
const createConversationTable = ` const createConversationTable = `
CREATE TABLE IF NOT EXISTS conversations ( CREATE TABLE IF NOT EXISTS conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT, title TEXT,
summary TEXT, title_source TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now() summary TEXT,
summary_through_seq int default 0
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)` )`
const createMessageTable = ` const createMessageTable = `
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role TEXT NOT NULL, role TEXT NOT NULL,
seq INTEGER NOT NULL, seq INTEGER NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
tsv tsvector generated always as (to_tsvector('english', content)) stored,
UNIQUE (conversation_id, seq) UNIQUE (conversation_id, seq)
)` );
const createToolCallTable = ` CREATE INDEX on messages using gin (tsv);
CREATE TABLE IF NOT EXISTS tool_calls ( CREATE INDEX on messages (conversation_id, seq);`
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), const createMessageChunkTable = `
type TEXT NOT NULL, CREATE TABLE IF NOT EXISTS message_chunks (
functionName TEXT NOT NULL, id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
functionArgs TEXT NOT NULL, conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now() content TEXT NOT NULL,
content_hash TEXT NOT NULL
)` )`
if err := client.ensureDbExists(); err != nil { if err := ensureDbExists(cfg, logger); err != nil {
return err return nil, err
}
pool, err := pgxpool.New(ctx, cfg.ConnectionString())
if err != nil {
logger.ErrorContext(ctx, "Could not create pgxpool")
return nil, err
} }
conn := pgx.MustOpen(client.connectionString())
defer conn.Close()
tables := []struct { tables := []struct {
tableName string tableName string
script string script string
}{ }{
{"conversations", createConversationTable}, {"conversations", createConversationTable},
{"messages", createMessageTable}, {"messages", createMessageTable},
{"tool_calls", createToolCallTable}, {"message_chunks", createMessageChunkTable},
} }
for _, table := range tables { for _, table := range tables {
var exists bool var exists bool
if err := conn.QueryRow(`SELECT EXISTS if err := pool.QueryRow(ctx, `SELECT EXISTS
(SELECT 1 FROM information_schema.tables (SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' WHERE table_schema = 'public'
AND table_name =$1)`, table.tableName).Scan(&exists); err != nil { AND table_name =$1)`, table.tableName).Scan(&exists); err != nil {
client.Logger.Error("Error while checking table", "table", table.tableName, "error", err) logger.Error("Error while checking table", "table", table.tableName, "error", err)
return err return nil, err
} }
if exists { if exists {
client.Logger.Debug("Table found", "table", table.tableName) logger.Debug("Table found", "table", table.tableName)
} else { } else {
client.Logger.Debug("Table not found. Creating", "table", table.tableName) logger.Debug("Table not found. Creating", "table", table.tableName)
if _, err := conn.Exec(table.script); err != nil { if _, err := pool.Exec(ctx, table.script); err != nil {
client.Logger.Error("Could not create table", "table", table.tableName, "error", err) logger.Error("Could not create table", "table", table.tableName, "error", err)
return err return nil, err
} }
} }
} }
return nil return &DbClient{
logger: logger,
cfg: cfg,
pool: pool,
}, nil
} }
func (client DbClient) GetConversation(id string) (model.Conversation, error) { func (cl DbClient) GetConversation(ctx context.Context, id string) (model.Conversation, error) {
conn := pgx.MustOpen(client.connectionString()) cl.logger.Debug("Searching for conversation", "id", id)
defer conn.Close()
client.Logger.Debug("Searching for conversation", "id", id)
var conv model.Conversation var conv model.Conversation
if err := conn.QueryRow("SELECT id, name, summary FROM conversations WHERE id = $1", id).Scan(&conv.Id, &conv.Name, &conv.Summary); err != nil { if err := cl.pool.QueryRow(ctx, "SELECT id, name, summary FROM conversations WHERE id = $1", id).Scan(&conv.Id, &conv.Name, &conv.Summary); err != nil {
client.Logger.Error("Could not fetch conversation", "id", id, "error", err) cl.logger.Error("Could not fetch conversation", "id", id, "error", err)
return conv, err return conv, err
} }
rows, err := conn.Query(`SELECT t.id, t.type, t.functionName, t.functionArgs, m.id, m.role, m.content, m.seq rows, err := cl.pool.Query(ctx, `SELECT id, role, content, seq
FROM messages m FROM messages
LEFT JOIN tool_calls t WHERE conversation_id = $1 ORDER BY seq`, id)
ON m.id = t.message_id
WHERE m.conversation_id = $1 ORDER BY m.seq`, id)
if err != nil { if err != nil {
client.Logger.Error("Could not fetch messages", "conversation_id", id, "error", err) cl.logger.Error("Could not fetch messages", "conversation_id", id, "error", err)
return conv, err return conv, err
} }
defer rows.Close() defer rows.Close()
var messages []model.Message var messages []model.Message
for rows.Next() { for rows.Next() {
var ( var (
tcId, tcType, tcName, tcArgs *string msgId, role, content string
msgId, role, content string seq int
seq int
) )
err = rows.Scan(&tcId, &tcType, &tcName, &tcArgs, &msgId, &role, &content, &seq) err = rows.Scan(&msgId, &role, &content, &seq)
if err != nil { if err != nil {
client.Logger.Error("Could not get message", "error", err) cl.logger.Error("Could not get message", "error", err)
} }
if len(messages) == 0 || messages[len(messages) - 1].Id != msgId { if len(messages) == 0 || messages[len(messages)-1].Id != msgId {
messages = append(messages, model.Message{ messages = append(messages, model.Message{
Id: msgId, Role: role, Content: content, Seq: seq, Id: msgId, Role: role, Content: content, Seq: seq,
}) })
} }
if tcId != nil {
deref := func(s *string) string {
if s == nil {
return ""
}
return *s
}
messages[len(messages) - 1].ToolCalls = append(messages[len(messages)-1].ToolCalls, model.ToolCall{
Id: *tcId,
Type: deref(tcType),
FunctionName: deref(tcName),
FunctionArguments: deref(tcArgs),
})
}
} }
conv.Messages = messages conv.Messages = messages
client.Logger.Debug("Found conversation", "conversation", conv) cl.logger.Debug("Found conversation", "conversation", conv)
return conv, nil return conv, nil
} }
func (client DbClient) GetConversations() ([]model.Conversation, error) { func (cl DbClient) GetConversations(ctx context.Context) ([]model.Conversation, error) {
client.Logger.Debug("Fetching conversations") cl.logger.Debug("Fetching conversations")
conn := pgx.MustOpen(client.connectionString()) rows, err := cl.pool.Query(ctx, "SELECT id, name, summary FROM conversations")
defer conn.Close()
rows, err := conn.Query("SELECT id, name, summary FROM conversations")
if err != nil { if err != nil {
client.Logger.Error("Could not fetch conversations", "error", err) cl.logger.Error("Could not fetch conversations", "error", err)
return []model.Conversation{}, err return []model.Conversation{}, err
} }
defer rows.Close() defer rows.Close()
@@ -156,37 +146,35 @@ func (client DbClient) GetConversations() ([]model.Conversation, error) {
rows.Scan(&conv.Id, &conv.Name, &conv.Summary) rows.Scan(&conv.Id, &conv.Name, &conv.Summary)
conversations = append(conversations, conv) conversations = append(conversations, conv)
} }
client.Logger.Debug("Found conversations", "count", len(conversations)) cl.logger.Debug("Found conversations", "count", len(conversations))
return conversations, nil return conversations, nil
} }
func (client DbClient) SaveConversation(conv model.Conversation) (model.Conversation, error) { func (cl DbClient) SaveConversation(ctx context.Context, conv model.Conversation) (model.Conversation, error) {
client.Logger.Debug("Saving conversation", "conversation", conv) cl.logger.Debug("Saving conversation", "conversation", conv)
conn := pgx.MustOpen(client.connectionString())
defer conn.Close()
delta := 0 delta := 0
if conv.Id == "" { if conv.Id == "" {
if err := conn.QueryRow(`INSERT INTO conversations(name, summary) VALUES($1, $2) RETURNING id`, if err := cl.pool.QueryRow(ctx, `INSERT INTO conversations(name, summary) VALUES($1, $2) RETURNING id`,
conv.Name, conv.Name,
conv.Summary).Scan(&conv.Id); err != nil { conv.Summary).Scan(&conv.Id); err != nil {
client.Logger.Error("Could not create conversation", "name", conv.Name, "error", err) cl.logger.Error("Could not create conversation", "name", conv.Name, "error", err)
return conv, err return conv, err
} }
} }
if err := conn.QueryRow(`SELECT COALESCE(MAX(seq) + 1, 1) FROM messages WHERE conversation_id = $1`, conv.Id).Scan(&delta); err != nil { if err := cl.pool.QueryRow(ctx, `SELECT COALESCE(MAX(seq) + 1, 1) FROM messages WHERE conversation_id = $1`, conv.Id).Scan(&delta); err != nil {
client.Logger.Warn("Could not get delta", "conversation_id", conv.Id, "error", err) cl.logger.Warn("Could not get delta", "conversation_id", conv.Id, "error", err)
return conv, errors.New("Could not get delta") return conv, errors.New("Could not get delta")
} }
client.Logger.Debug("Found delta.", "delta", delta, "Unsaved messages", len(conv.Messages[delta - 1:])) cl.logger.Debug("Found delta.", "delta", delta, "Unsaved messages", len(conv.Messages[delta-1:]))
if delta >= len(conv.Messages) { if delta >= len(conv.Messages) {
return conv, nil return conv, nil
} }
for _, message := range conv.Messages[delta - 1:] { for _, message := range conv.Messages[delta-1:] {
id, err := client.saveMessage(conv.Id, message, conn) id, err := cl.saveMessage(ctx, conv.Id, message)
if err != nil { if err != nil {
client.Logger.Error("Could not save message", "message_id", message.Id, "seq", message.Seq, "error", err) cl.logger.Error("Could not save message", "message_id", message.Id, "seq", message.Seq, "error", err)
return conv, errors.New("Could not save message") return conv, errors.New("Could not save message")
} }
message.Id = id message.Id = id
@@ -194,81 +182,67 @@ func (client DbClient) SaveConversation(conv model.Conversation) (model.Conversa
return conv, nil return conv, nil
} }
func (client DbClient) saveMessage(conversation_id string, message model.Message, conn db.DB) (string, error) { func (cl DbClient) saveMessage(ctx context.Context, conversation_id string, message model.Message) (string, error) {
var exists bool var exists bool
if message.Id != "" { if message.Id != "" {
if err := conn.QueryRow(`SELECT EXISTS if err := cl.pool.QueryRow(ctx, `SELECT EXISTS
(SELECT 1 FROM messages (SELECT 1 FROM messages
WHERE id = $1)`, message.Id).Scan(&exists); err != nil { WHERE id = $1)`, message.Id).Scan(&exists); err != nil {
client.Logger.Warn("Error while scanning for message", "id", message.Id, "error", err) cl.logger.Warn("Error while scanning for message", "id", message.Id, "error", err)
return message.Id, err return message.Id, err
} }
} }
if exists { if exists {
client.Logger.Warn("Message found. Skipping", "id", message.Id) cl.logger.Warn("Insertting message ", "seq", message.Seq)
if err := cl.pool.QueryRow(ctx, `INSERT INTO messages (seq, role, content, conversation_id) VALUES ($1, $2, $3, $4) RETURNING id`,
message.Seq, message.Role, message.Content, conversation_id).Scan(&message.Id); err != nil {
cl.logger.Error("Could not save message", "seq", message.Seq, "error", err)
return message.Id, errors.New("Could not save message")
}
}
if err := cl.pool.QueryRow(ctx, `SELECT EXISTS (SELECT 1 FROM message_chunks WHERE id = $1)`,
message.Id).Scan(&exists); err != nil {
cl.logger.Warn("Error while scanning for message_chunk", "message id", message.Id, "error", err)
return message.Id, errors.New("Could not save message chunk")
}
if exists {
cl.logger.Debug("Message chunk already exists. Skipping")
return message.Id, nil return message.Id, nil
} }
if err := conn.QueryRow(`INSERT INTO messages (seq, role, content, conversation_id) VALUES ($1, $2, $3, $4) RETURNING id`, if _, err := cl.pool.Exec(ctx, `INSERT INTO message_chunks (conversation_id, message_id, content, content_hash)
message.Seq, message.Role, message.Content, conversation_id).Scan(&message.Id); err != nil { VALUES ($1, $2, $3, $4)`); err != nil {
client.Logger.Error("Could not save message", "seq", message.Seq, "error", err) cl.logger.Warn("Error while creating message chunk", "message id", message.Id, "error", err)
return message.Id, errors.New("Could not save message")
} }
placeholder := 0
var placeholders strings.Builder
args := []any{}
const cols = 4
for _, call := range message.ToolCalls {
placeholders.WriteString("($")
placeholders.WriteString(strconv.Itoa(placeholder + 1))
placeholders.WriteString(",$")
placeholders.WriteString(strconv.Itoa(placeholder + 2))
placeholders.WriteString(",$")
placeholders.WriteString(strconv.Itoa(placeholder + 3))
placeholders.WriteString(",$")
placeholders.WriteString(strconv.Itoa(placeholder + 4))
placeholders.WriteString(")")
args = append(args, call.Type, call.FunctionName, call.FunctionArguments, message.Id)
placeholder += cols
}
if _, err := conn.Exec(`INSERT INTO tool_calls(type, functionName, functionArgs, message_id) VALUES `+placeholders.String(),
args...); err != nil && placeholder > 0 {
client.Logger.Warn("Could not insert tool_calls", "message_id", message.Id, "error", err)
}
return message.Id, nil return message.Id, nil
} }
func (client DbClient) ensureDbExists() error { func (cl DbClient) Close() {
cl.pool.Close()
}
func ensureDbExists(cfg config.DbConfig, logger slog.Logger) error {
maintenanceUrl := &url.URL{ maintenanceUrl := &url.URL{
Scheme: "postgres", Scheme: "postgres",
User: url.UserPassword(client.Cfg.User, client.Cfg.Password), User: url.UserPassword(cfg.User, cfg.Password),
Host: net.JoinHostPort(client.Cfg.Url, client.Cfg.Port), Host: net.JoinHostPort(cfg.Url, cfg.Port),
Path: "postgres", Path: "postgres",
} }
conn := pgx.MustOpen(maintenanceUrl.String()) conn := pgx.MustOpen(maintenanceUrl.String())
defer conn.Close() defer conn.Close()
var exists bool var exists bool
if err := conn.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)`, client.Cfg.DbName).Scan(&exists); err != nil { if err := conn.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)`, cfg.DbName).Scan(&exists); err != nil {
return err return err
} }
if exists { if exists {
client.Logger.Debug("Database already exists", "name", client.Cfg.DbName) logger.Debug("Database already exists", "name", cfg.DbName)
return nil return nil
} }
client.Logger.Info("Creating database", "name", client.Cfg.DbName) logger.Info("Creating database", "name", cfg.DbName)
_, err := conn.Exec(`CREATE DATABASE "` + client.Cfg.DbName + `"`) _, err := conn.Exec(`CREATE DATABASE "` + cfg.DbName + `"`)
return err return err
} }
func (client DbClient) connectionString() string {
u := &url.URL{
Scheme: "postgres",
User: url.UserPassword(client.Cfg.User, client.Cfg.Password),
Host: net.JoinHostPort(client.Cfg.Url, client.Cfg.Port),
Path: client.Cfg.DbName,
}
return u.String()
}
+5 -3
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"context"
"log/slog" "log/slog"
"os" "os"
@@ -12,6 +13,7 @@ import (
func main() { func main() {
ctx := context.Background()
config, err := config.Load(".config.json") config, err := config.Load(".config.json")
if err != nil { if err != nil {
slog.Error("Config error:", "message", err.Error()) slog.Error("Config error:", "message", err.Error())
@@ -20,14 +22,14 @@ func main() {
var logger = slog.Default().With("Compoent", "Main") var logger = slog.Default().With("Compoent", "Main")
logger.Debug("Config initialized") logger.Debug("Config initialized")
db := history.DbClient{Cfg: config.Db, Logger: *slog.Default().With("Component", "History")}; db, err := history.Init(ctx, config.Db)
err = db.Init()
if err != nil { if err != nil {
logger.Error("Error while initializing database", "message", err) logger.Error("Error while initializing database", "message", err)
os.Exit(1) os.Exit(1)
} }
defer db.Close()
p := tea.NewProgram(ui.InitialModel(*config, db)) p := tea.NewProgram(ui.InitialModel(ctx, *config, *db))
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
logger.Error("Alas, there's been an error:", "message", err) logger.Error("Alas, there's been an error:", "message", err)
os.Exit(1) os.Exit(1)
+37 -27
View File
@@ -16,9 +16,9 @@ import (
const COMPLETIONS_API = "/v1/chat/completions" const COMPLETIONS_API = "/v1/chat/completions"
const MODELS_API = "/v1/models" const MODELS_API = "/v1/models"
type LLMClient struct { type ChatClient struct {
Cfg config.Config Cfg config.Config
Logger slog.Logger logger slog.Logger
} }
// Request Types // Request Types
@@ -52,22 +52,29 @@ type Usage struct {
TotalTokens int `json:"total_tokens"` TotalTokens int `json:"total_tokens"`
} }
func (client LLMClient) Models() ([]string, error) { func NewLLMClient(cfg config.Config) *ChatClient {
resp, err := http.Get(client.Cfg.Api.Url + MODELS_API) return &ChatClient{
Cfg: cfg,
logger: *slog.Default().With("Component", "ChatClient"),
}
}
func (cl ChatClient) Models() ([]string, error) {
resp, err := http.Get(cl.Cfg.Api.Url + MODELS_API)
if err != nil { if err != nil {
client.Logger.Error("There was an error during the network request", "error", err.Error()) cl.logger.Error("There was an error during the network request", "error", err.Error())
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
client.Logger.Error("There was an error reading the response body", "error", err.Error()) cl.logger.Error("There was an error reading the response body", "error", err.Error())
return nil, err return nil, err
} }
if resp.StatusCode != 200 && resp.StatusCode != 202 { if resp.StatusCode != 200 && resp.StatusCode != 202 {
client.Logger.Error("Call returned non 200 status", cl.logger.Error("Call returned non 200 status",
"statusCode", resp.StatusCode, "statusCode", resp.StatusCode,
"status", resp.Status) "status", resp.Status)
return nil, return nil,
@@ -75,18 +82,18 @@ func (client LLMClient) Models() ([]string, error) {
strconv.Itoa(resp.StatusCode) + strconv.Itoa(resp.StatusCode) +
resp.Status) resp.Status)
} }
client.Logger.Debug("Received data", "data", data) cl.logger.Debug("Received data", "data", data)
var response struct{ var response struct {
Data []struct{ Data []struct {
Id string Id string
} }
} }
if err := json.Unmarshal(data, &response); err != nil { if err := json.Unmarshal(data, &response); err != nil {
client.Logger.Error("There was an error parsing the response", "error", err.Error()) cl.logger.Error("There was an error parsing the response", "error", err.Error())
return nil, err return nil, err
} }
client.Logger.Debug("Unmarshal response", "data", response) cl.logger.Debug("Unmarshal response", "data", response)
var ret []string var ret []string
for _, m := range response.Data { for _, m := range response.Data {
ret = append(ret, m.Id) ret = append(ret, m.Id)
@@ -94,43 +101,46 @@ func (client LLMClient) Models() ([]string, error) {
return ret, nil return ret, nil
} }
func (client LLMClient) Call(query string, messages []model.Message) ([]model.Message, error) { func (cl ChatClient) Call(query string, messages []model.Message) ([]model.Message, error) {
requestBody := ChatRequest{ requestBody := ChatRequest{
Model: client.Cfg.Llm.Model, Model: cl.Cfg.Llm.Model,
Messages: messages, Messages: messages,
Tools: config.AvailableTools(), Tools: config.AvailableTools(),
} }
serialized, err := json.Marshal(requestBody) serialized, err := json.Marshal(requestBody)
if err != nil { if err != nil {
cl.logger.Error("There was an error marshelling the request body",
"request", requestBody,
"error", err)
return []model.Message{}, err return []model.Message{}, err
} }
body := bytes.NewBuffer(serialized) body := bytes.NewBuffer(serialized)
req, err := http.NewRequest("POST", client.Cfg.Api.Url + COMPLETIONS_API, body) req, err := http.NewRequest("POST", cl.Cfg.Api.Url+COMPLETIONS_API, body)
if err != nil { if err != nil {
client.Logger.Error("There was an error creating the request", "error", err.Error()) cl.logger.Error("There was an error creating the request", "error", err.Error())
return []model.Message{}, err return []model.Message{}, err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
if client.Cfg.Api.Key != "" { if cl.Cfg.Api.Key != "" {
req.Header.Set("Authorization", "Bearer "+client.Cfg.Api.Key) req.Header.Set("Authorization", "Bearer "+cl.Cfg.Api.Key)
} }
client.Logger.Debug("Executing llm call", "req", req) cl.logger.Debug("Executing llm call", "req", req)
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
client.Logger.Error("There was an error during http call", "error", err.Error()) cl.logger.Error("There was an error during http call", "error", err.Error())
return []model.Message{}, err return []model.Message{}, err
} }
client.Logger.Debug("LLM Call returned", "response", resp)
defer resp.Body.Close() defer resp.Body.Close()
cl.logger.Debug("LLM Call returned", "response", resp)
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
client.Logger.Error("There was an error reading the response body", "error", err.Error()) cl.logger.Error("There was an error reading the response body", "error", err.Error())
return []model.Message{}, err return []model.Message{}, err
} }
if resp.StatusCode != 200 && resp.StatusCode != 202 { if resp.StatusCode != 200 && resp.StatusCode != 202 {
client.Logger.Error("Call returned non 200 status", cl.logger.Error("Call returned non 200 status",
"statusCode", resp.StatusCode, "statusCode", resp.StatusCode,
"status", resp.Status) "status", resp.Status)
return []model.Message{}, return []model.Message{},
@@ -139,20 +149,20 @@ func (client LLMClient) Call(query string, messages []model.Message) ([]model.Me
resp.Status) resp.Status)
} }
client.Logger.Debug("Received data", "data", data) cl.logger.Debug("Received data", "data", data)
var response ChatResponse var response ChatResponse
if err := json.Unmarshal(data, &response); err != nil { if err := json.Unmarshal(data, &response); err != nil {
client.Logger.Error("There was an error parsing the response", "error", err.Error()) cl.logger.Error("There was an error parsing the response", "error", err.Error())
return []model.Message{}, err return []model.Message{}, err
} }
var result []model.Message var result []model.Message
if len(response.Choices) == 0 { if len(response.Choices) == 0 {
client.Logger.Error("The call returned an empty response") cl.logger.Error("The call returned an empty response")
return []model.Message{}, errors.New("The call returned an empty response") return []model.Message{}, errors.New("The call returned an empty response")
} }
for _, choice := range response.Choices { for _, choice := range response.Choices {
client.Logger.Debug("Appending response message", "message", choice.Message) cl.logger.Debug("Appending response message", "message", choice.Message)
result = append(result, choice.Message) result = append(result, choice.Message)
} }
+133
View File
@@ -0,0 +1,133 @@
package llm
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"strings"
config "git.estatecloud.org/radumaco/souvenir/config"
)
const EMBEDDINGS_API = "/v1/embeddings"
type Embedder struct {
Cfg config.EmbeddingConfig
logger slog.Logger
id string
}
type EmbedRequest struct {
Input []string `json:"input"`
Model string `json:"model"`
EncodingFormat string `json:"encoding_format"`
}
type EmbedResponse struct {
Data []Embedding `json:"data"`
Model string `json:"model"`
Object string `json:"object"`
Usage Usage `json:"usage"`
}
type Embedding struct {
Embedding []float32 `json:"embeddgin"`
Index int `json:"index"`
Object string `json:"object"`
}
func NewEmbedder(cfg config.EmbeddingConfig) *Embedder {
return &Embedder{
Cfg: cfg,
logger: *slog.Default().With("Component", "Embedder"),
}
}
func (e Embedder) ID() string {
if e.id == "" {
e.id = strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r <= '0' && r <= '9':
return r
default:
return '_'
}
}, e.Cfg.Model)
}
return e.id
}
func (e Embedder) EmbedBatch(text []string) ([][]float32, error) {
requestBody := EmbedRequest{
Model: e.Cfg.Model,
Input: text,
EncodingFormat: "float",
}
serialized, err := json.Marshal(requestBody)
if err != nil {
e.logger.Error("There was an error marshelling the request body",
"request", requestBody,
"error", err)
return [][]float32{}, err
}
body := bytes.NewBuffer(serialized)
req, err := http.NewRequest("POST", e.Cfg.Url+EMBEDDINGS_API, body)
if err != nil {
e.logger.Error("There was an error creating the request", "error", err.Error())
return [][]float32{}, err
}
req.Header.Set("Content-Type", "application/json")
if e.Cfg.Key != "" {
req.Header.Set("Authorization", "Bearer "+e.Cfg.Key)
}
e.logger.Debug("Executing embedding call", "req", req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
e.logger.Error("There was an error during http call", "error", err.Error())
return [][]float32{}, err
}
defer resp.Body.Close()
e.logger.Debug("LLM Call returned", "response", resp)
data, err := io.ReadAll(resp.Body)
if err != nil {
e.logger.Error("There was an error reading the response body", "error", err.Error())
return [][]float32{}, err
}
if resp.StatusCode != 200 && resp.StatusCode != 202 {
e.logger.Error("Call returned non 200 status",
"statusCode", resp.StatusCode,
"status", resp.Status)
return [][]float32{},
errors.New("Call returned invalid status " +
resp.Status)
}
e.logger.Debug("Received data", "data", data)
var response EmbedResponse
if err := json.Unmarshal(data, &response); err != nil {
e.logger.Error("There was an error parsing the response", "error", err.Error())
return [][]float32{}, err
}
if len(response.Data) == 0 {
e.logger.Error("The call returned empty data")
return [][]float32{}, errors.New("The call returned empty data")
}
result := make([][]float32, len(text))
for _, embed := range response.Data {
if embed.Index < 0 || embed.Index >= len(result) {
return nil, fmt.Errorf("Embedding index out of range. Index:%d Length:%d",
embed.Index, len(result))
}
e.logger.Debug("Appening embbeding", "embed", embed.Embedding)
result[embed.Index] = embed.Embedding
}
return result, nil
}
+9 -6
View File
@@ -1,6 +1,7 @@
package ui package ui
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"strings" "strings"
@@ -34,9 +35,10 @@ type uiModel struct {
senderStyle lipgloss.Style senderStyle lipgloss.Style
agentStyle lipgloss.Style agentStyle lipgloss.Style
errorStyle lipgloss.Style errorStyle lipgloss.Style
client llm.LLMClient client llm.ChatClient
errorMessage string errorMessage string
history history.DbClient history history.DbClient
ctx context.Context
width int width int
height int height int
waiting bool waiting bool
@@ -53,7 +55,7 @@ type conversationSavedMessage struct {
err error err error
} }
func InitialModel(config config.Config, client history.DbClient) uiModel { func InitialModel(ctx context.Context, config config.Config, client history.DbClient) uiModel {
ta := textarea.New() ta := textarea.New()
ta.Placeholder = "Send a message..." ta.Placeholder = "Send a message..."
ta.SetVirtualCursor(false) ta.SetVirtualCursor(false)
@@ -85,9 +87,10 @@ func InitialModel(config config.Config, client history.DbClient) uiModel {
senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")), senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")),
errorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("9")), errorStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("9")),
agentStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("86")), agentStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("86")),
client: llm.LLMClient{Cfg: config, Logger: *slog.Default().With("Component", "LLM")}, client: *llm.NewLLMClient(config),
focus: focusChat, focus: focusChat,
history: client, history: client,
ctx: ctx,
logger: *slog.Default().With("Component", "TUI"), logger: *slog.Default().With("Component", "TUI"),
err: nil, err: nil,
} }
@@ -212,7 +215,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.focus = focusChat m.focus = focusChat
case focusHistory: case focusHistory:
m.logger.Debug("Received pickerChosenMsg", "history", msg.id) m.logger.Debug("Received pickerChosenMsg", "history", msg.id)
conv, err := m.history.GetConversation(msg.id) conv, err := m.history.GetConversation(m.ctx, msg.id)
if err != nil { if err != nil {
m.logger.Error("Could not retrieve conversation", "id", msg.id) m.logger.Error("Could not retrieve conversation", "id", msg.id)
} }
@@ -291,7 +294,7 @@ func (m uiModel) callAgent(input string, messages []model.Message) tea.Cmd {
func (m uiModel) saveConversation() tea.Cmd { func (m uiModel) saveConversation() tea.Cmd {
m.logger.Debug("Saving conversation", "conversation", m.conversation) m.logger.Debug("Saving conversation", "conversation", m.conversation)
return func() tea.Msg { return func() tea.Msg {
conv, err := m.history.SaveConversation(m.conversation) conv, err := m.history.SaveConversation(m.ctx, m.conversation)
return conversationSavedMessage{conversation: conv, err: err} return conversationSavedMessage{conversation: conv, err: err}
} }
} }
@@ -315,7 +318,7 @@ func (m uiModel) getModels() tea.Cmd {
func (m uiModel) getHistory() tea.Cmd { func (m uiModel) getHistory() tea.Cmd {
m.logger.Debug("Querying history") m.logger.Debug("Querying history")
return func() tea.Msg { return func() tea.Msg {
resp, err := m.history.GetConversations() resp, err := m.history.GetConversations(m.ctx)
if err != nil { if err != nil {
m.logger.Error("Could not fetch history", "error", err) m.logger.Error("Could not fetch history", "error", err)
} }