From c2044e7e3df6f2cfc006b5e475131f1861bd899a Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Wed, 16 Jul 2025 11:36:51 +0700 Subject: [PATCH 01/12] added migration files for pgvector extension and conversation_embeddings --- ...10_create_conversation_embeddings.down.sql | 7 +++++ ...create_conversation_embeddings.up copy.sql | 26 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 database/migrations/000010_create_conversation_embeddings.down.sql create mode 100644 database/migrations/000010_create_conversation_embeddings.up copy.sql diff --git a/database/migrations/000010_create_conversation_embeddings.down.sql b/database/migrations/000010_create_conversation_embeddings.down.sql new file mode 100644 index 0000000..970ef72 --- /dev/null +++ b/database/migrations/000010_create_conversation_embeddings.down.sql @@ -0,0 +1,7 @@ +DROP INDEX IF EXISTS conversation_embeddings_lookup_idx; +DROP INDEX IF EXISTS convo_embeddings_by_question_idx; +DROP INDEX IF EXISTS conversation_embeddings_embedding_idx; + +DROP TABLE IF EXISTS conversation_embeddings; + +DROP EXTENSION IF EXISTS vector; diff --git a/database/migrations/000010_create_conversation_embeddings.up copy.sql b/database/migrations/000010_create_conversation_embeddings.up copy.sql new file mode 100644 index 0000000..0ed5e46 --- /dev/null +++ b/database/migrations/000010_create_conversation_embeddings.up copy.sql @@ -0,0 +1,26 @@ +CREATE EXTENSION IF NOT EXISTS vector; + +CREATE TABLE conversation_embeddings ( + id SERIAL PRIMARY KEY, + interview_id INT NOT NULL, + conversation_id INT NOT NULL, + topic_id INT NOT NULL, + question_number INT NOT NULL, + message_id INT NOT NULL, + summary TEXT NOT NULL, + embedding VECTOR(1536) NOT NULL, + created_at TIMESTAMP DEFAULT now(), + UNIQUE (interview_id, conversation_id, topic_id, question_number, message_id) +); + +CREATE INDEX conversation_embeddings_embedding_idx + ON conversation_embeddings USING ivfflat (embedding vector_cosine_ops) + WITH (lists = 100); + +CREATE INDEX convo_embeddings_by_question_idx + ON conversation_embeddings (interview_id, topic_id, question_number); + +CREATE INDEX conversation_embeddings_lookup_idx + ON conversation_embeddings (interview_id, message_id); + +ANALYZE conversation_embeddings; From e78f8f52c03ad0e647f87c625c6f782028cb3784 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Wed, 16 Jul 2025 11:53:12 +0700 Subject: [PATCH 02/12] started adding chatgpt service and model --- chatgpt/model.go | 23 +++++++++++++++++ chatgpt/service.go | 25 +++++++++++++++++++ ...010_create_conversation_embeddings.up.sql} | 0 3 files changed, 48 insertions(+) rename database/migrations/{000010_create_conversation_embeddings.up copy.sql => 000010_create_conversation_embeddings.up.sql} (100%) diff --git a/chatgpt/model.go b/chatgpt/model.go index c143ab2..5503294 100644 --- a/chatgpt/model.go +++ b/chatgpt/model.go @@ -157,6 +157,29 @@ Here is the input data: %s`, jdSummary) } +func BuildUserResponseSummaryPrompt(response string, question string) string { + return fmt.Sprintf(`Extract the **key technical points** from the following backend interview answer. + +Break the response into a list of concise, self-contained statements. Each item should: +- Represent a distinct technical idea, method, or decision +- Be understandable without the original question +- Focus only on what the user **actually said**, not what they should have said +- Exclude filler, vague claims, or generalities + +Output only valid JSON in this format: +[ + "First technical point...", + "Second technical point...", + ... +] + +Interview question: +"%s" + +User’s answer: +"%s"`, question, response) +} + type AIClient interface { GetChatGPTResponse(prompt string) (*ChatGPTResponse, error) GetChatGPTResponseConversation(conversationHistory []map[string]string) (*ChatGPTResponse, error) diff --git a/chatgpt/service.go b/chatgpt/service.go index e627646..a687dd9 100644 --- a/chatgpt/service.go +++ b/chatgpt/service.go @@ -269,3 +269,28 @@ func (c *OpenAIClient) ExtractJDSummary(jdInput *JDParsedOutput) (string, error) return jdSummary, nil } + +func (c *OpenAIClient) SummarizeUserAnswer(userAnswer, question string) ([]string, error) { + prompt := BuildUserResponseSummaryPrompt(userAnswer, question) + + req := openai.ChatCompletionRequest{ + Model: openai.GPT3Dot5Turbo, + Messages: []openai.ChatCompletionMessage{ + {Role: "system", Content: "You are a precise backend technical summarizer."}, + {Role: "user", Content: prompt}, + }, + } + + resp, err := c.client.CreateChatCompletion(context.Background(), req) + if err != nil { + return nil, &OpenAIError{Message: err.Error()} + } + + var points []string + err = json.Unmarshal([]byte(resp.Choices[0].Message.Content), &points) + if err != nil { + return nil, fmt.Errorf("failed to parse summary JSON: %w", err) + } + + return points, nil +} diff --git a/database/migrations/000010_create_conversation_embeddings.up copy.sql b/database/migrations/000010_create_conversation_embeddings.up.sql similarity index 100% rename from database/migrations/000010_create_conversation_embeddings.up copy.sql rename to database/migrations/000010_create_conversation_embeddings.up.sql From c8dd528571ab9dfac8f9c7440ef1c6acb759f820 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Wed, 16 Jul 2025 12:25:47 +0700 Subject: [PATCH 03/12] added chatgpt service and prompt model. Started updating conversation history injection of summary. Should probably inject in user response as system prompt is getting large --- chatgpt/model.go | 4 +++- chatgpt/service.go | 25 +++++-------------------- conversation/helpers.go | 5 ++++- conversation/service.go | 4 ++-- 4 files changed, 14 insertions(+), 24 deletions(-) diff --git a/chatgpt/model.go b/chatgpt/model.go index 5503294..5f8a99b 100644 --- a/chatgpt/model.go +++ b/chatgpt/model.go @@ -20,6 +20,7 @@ type ChatGPTResponse struct { Qualifications []string `json:"qualifications"` TechStack []string `json:"tech_stack"` Level string `json:"level"` + UserRespSummary []string `json:"user_response_summary"` } type OpenAIClient struct { @@ -157,7 +158,7 @@ Here is the input data: %s`, jdSummary) } -func BuildUserResponseSummaryPrompt(response string, question string) string { +func BuildResponseSummary(question, response string) string { return fmt.Sprintf(`Extract the **key technical points** from the following backend interview answer. Break the response into a list of concise, self-contained statements. Each item should: @@ -186,4 +187,5 @@ type AIClient interface { GetChatGPT35Response(prompt string) (*ChatGPTResponse, error) ExtractJDInput(jd string) (*JDParsedOutput, error) ExtractJDSummary(jdInput *JDParsedOutput) (string, error) + ExtractResponseSummary(userResponse, answer string) (*ChatGPTResponse, error) } diff --git a/chatgpt/service.go b/chatgpt/service.go index a687dd9..7b3bf01 100644 --- a/chatgpt/service.go +++ b/chatgpt/service.go @@ -270,27 +270,12 @@ func (c *OpenAIClient) ExtractJDSummary(jdInput *JDParsedOutput) (string, error) return jdSummary, nil } -func (c *OpenAIClient) SummarizeUserAnswer(userAnswer, question string) ([]string, error) { - prompt := BuildUserResponseSummaryPrompt(userAnswer, question) - - req := openai.ChatCompletionRequest{ - Model: openai.GPT3Dot5Turbo, - Messages: []openai.ChatCompletionMessage{ - {Role: "system", Content: "You are a precise backend technical summarizer."}, - {Role: "user", Content: prompt}, - }, - } - - resp, err := c.client.CreateChatCompletion(context.Background(), req) - if err != nil { - return nil, &OpenAIError{Message: err.Error()} - } - - var points []string - err = json.Unmarshal([]byte(resp.Choices[0].Message.Content), &points) +func (c *OpenAIClient) ExtractResponseSummary(question, response string) (*ChatGPTResponse, error) { + systemPrompt := BuildResponseSummary(question, response) + summarizedResponse, err := c.GetChatGPT35Response(systemPrompt) if err != nil { - return nil, fmt.Errorf("failed to parse summary JSON: %w", err) + return nil, err } - return points, nil + return summarizedResponse, nil } diff --git a/conversation/helpers.go b/conversation/helpers.go index a5d9159..80d58d4 100644 --- a/conversation/helpers.go +++ b/conversation/helpers.go @@ -11,12 +11,15 @@ import ( "github.com/michaelboegner/interviewer/interview" ) -func GetChatGPTResponses(conversation *Conversation, openAI chatgpt.AIClient, interviewRepo interview.InterviewRepo) (*chatgpt.ChatGPTResponse, string, error) { +func GetChatGPTResponses(conversation *Conversation, openAI chatgpt.AIClient, interviewRepo interview.InterviewRepo, question, message string) (*chatgpt.ChatGPTResponse, string, error) { + userResponseSummary, err := openAI.ExtractResponseSummary(question, message) + conversationHistory, err := GetConversationHistory(conversation, interviewRepo) if err != nil { log.Printf("GetConversationHistory failed: %v", err) return nil, "", err } + chatGPTResponse, err := openAI.GetChatGPTResponseConversation(conversationHistory) if err != nil { log.Printf("getNextQuestion failed: %v", err) diff --git a/conversation/service.go b/conversation/service.go index ad66d6f..0b3708b 100644 --- a/conversation/service.go +++ b/conversation/service.go @@ -67,7 +67,7 @@ func CreateConversation( return nil, err } - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo, firstQuestion, message) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err @@ -131,7 +131,7 @@ func AppendConversation( } conversation.Topics[topicID].Questions[questionNumber].Messages = append(conversation.Topics[topicID].Questions[questionNumber].Messages, messageUser) - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo, conversation.Topics[topicID].Questions[questionNumber].Prompt, message) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err From 4a0a94bb09d63d1d9ce44aaa99b764eac6856b9f Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Thu, 17 Jul 2025 13:13:01 +0700 Subject: [PATCH 04/12] started down the wrong path with this. Need to create an embedding packager and then call that embedding package from my createConversation service with the question answer and user response and then let the embedding call the chatGPT package to get summary items, then take those summary items and call the microservice to embed them, and then call the repo/database to store them in conversation_embeddings, and then search conversation embeddings for relevant items to the current response and then return those releveant items to the conversation service --- conversation/helpers.go | 4 +--- conversation/service.go | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/conversation/helpers.go b/conversation/helpers.go index 80d58d4..2af1b2e 100644 --- a/conversation/helpers.go +++ b/conversation/helpers.go @@ -11,9 +11,7 @@ import ( "github.com/michaelboegner/interviewer/interview" ) -func GetChatGPTResponses(conversation *Conversation, openAI chatgpt.AIClient, interviewRepo interview.InterviewRepo, question, message string) (*chatgpt.ChatGPTResponse, string, error) { - userResponseSummary, err := openAI.ExtractResponseSummary(question, message) - +func GetChatGPTResponses(conversation *Conversation, openAI chatgpt.AIClient, interviewRepo interview.InterviewRepo) (*chatgpt.ChatGPTResponse, string, error) { conversationHistory, err := GetConversationHistory(conversation, interviewRepo) if err != nil { log.Printf("GetConversationHistory failed: %v", err) diff --git a/conversation/service.go b/conversation/service.go index 0b3708b..ad66d6f 100644 --- a/conversation/service.go +++ b/conversation/service.go @@ -67,7 +67,7 @@ func CreateConversation( return nil, err } - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo, firstQuestion, message) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err @@ -131,7 +131,7 @@ func AppendConversation( } conversation.Topics[topicID].Questions[questionNumber].Messages = append(conversation.Topics[topicID].Questions[questionNumber].Messages, messageUser) - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo, conversation.Topics[topicID].Questions[questionNumber].Prompt, message) + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err From f321ebe24d1d0dadda06ca6f52320241f1414cab Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Thu, 17 Jul 2025 15:09:52 +0700 Subject: [PATCH 05/12] added embedding package. Reviewing python microservice to ensure functionality. --- embedding/embedder.go | 50 ++++++++++++++++++++++ embedding/model.go | 58 ++++++++++++++++++++++++++ embedding/repository.go | 91 +++++++++++++++++++++++++++++++++++++++++ embedding/service.go | 59 ++++++++++++++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 embedding/embedder.go create mode 100644 embedding/model.go create mode 100644 embedding/repository.go create mode 100644 embedding/service.go diff --git a/embedding/embedder.go b/embedding/embedder.go new file mode 100644 index 0000000..0c4c825 --- /dev/null +++ b/embedding/embedder.go @@ -0,0 +1,50 @@ +package embedding + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "time" +) + +type HTTPEmbedder struct { + Endpoint string + Timeout time.Duration +} + +func NewHTTPEmbedder(endpoint string) *HTTPEmbedder { + return &HTTPEmbedder{ + Endpoint: endpoint, + Timeout: 3 * time.Second, + } +} + +func (e *HTTPEmbedder) EmbedText(ctx context.Context, input string) ([]float32, error) { + body, err := json.Marshal(map[string]string{"text": input}) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", e.Endpoint, bytes.NewBuffer(body)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{Timeout: e.Timeout} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result struct { + Embedding []float32 `json:"embedding"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + return result.Embedding, nil +} diff --git a/embedding/model.go b/embedding/model.go new file mode 100644 index 0000000..03619e2 --- /dev/null +++ b/embedding/model.go @@ -0,0 +1,58 @@ +package embedding + +import ( + "context" + "time" + + "github.com/michaelboegner/interviewer/chatgpt" +) + +type EmbedInput struct { + InterviewID int + ConversationID int + MessageID int + TopicID int + QuestionNumber int + Question string + UserResponse string + CreatedAt time.Time +} + +type Embedding struct { + ID int + InterviewID int + ConversationID int + MessageID int + TopicID int + QuestionNumber int + Summary string + Vector []float32 + CreatedAt time.Time +} + +type Service struct { + Repo Repository + Embedder Embedder + Summarizer Summarizer +} + +type Repository interface { + StoreEmbedding(ctx context.Context, e Embedding) error + GetSimilarEmbeddings(ctx context.Context, interviewID, topicID, questionNumber, excludeMessageID int, queryVec []float32, limit int) ([]string, error) +} + +type Embedder interface { + EmbedText(ctx context.Context, input string) ([]float32, error) +} + +type Summarizer interface { + ExtractResponseSummary(question, response string) (*chatgpt.ChatGPTResponse, error) +} + +func NewService(repo Repository, embedder Embedder, summarizer Summarizer) *Service { + return &Service{ + Repo: repo, + Embedder: embedder, + Summarizer: summarizer, + } +} diff --git a/embedding/repository.go b/embedding/repository.go new file mode 100644 index 0000000..c48bea7 --- /dev/null +++ b/embedding/repository.go @@ -0,0 +1,91 @@ +package embedding + +import ( + "context" + "database/sql" + "fmt" + + "github.com/lib/pq" +) + +type PGRepository struct { + DB *sql.DB +} + +func NewPGRepository(db *sql.DB) *PGRepository { + return &PGRepository{DB: db} +} + +func (r *PGRepository) StoreEmbedding(ctx context.Context, e Embedding) error { + query := ` + INSERT INTO conversation_embeddings ( + interview_id, + conversation_id, + message_id, + topic_id, + question_number, + summary, + embedding, + created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (interview_id, conversation_id, topic_id, question_number, message_id) DO NOTHING; + ` + + _, err := r.DB.ExecContext(ctx, query, + e.InterviewID, + e.ConversationID, + e.MessageID, + e.TopicID, + e.QuestionNumber, + e.Summary, + pq.Array(e.Vector), + e.CreatedAt, + ) + return err +} + +func (r *PGRepository) GetSimilarEmbeddings( + ctx context.Context, + interviewID, topicID, questionNumber, excludeMessageID int, + queryVec []float32, + limit int, +) ([]string, error) { + query := ` + SELECT summary + FROM conversation_embeddings + WHERE interview_id = $1 + AND topic_id = $2 + AND question_number = $3 + AND message_id != $4 + ORDER BY embedding <-> $5 + LIMIT $6; + ` + + rows, err := r.DB.QueryContext(ctx, query, + interviewID, + topicID, + questionNumber, + excludeMessageID, + pq.Array(queryVec), + limit, + ) + if err != nil { + return nil, fmt.Errorf("query error: %w", err) + } + defer rows.Close() + + var summaries []string + for rows.Next() { + var s string + if err := rows.Scan(&s); err != nil { + return nil, fmt.Errorf("row scan error: %w", err) + } + summaries = append(summaries, s) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows error: %w", err) + } + + return summaries, nil +} diff --git a/embedding/service.go b/embedding/service.go new file mode 100644 index 0000000..b28ea98 --- /dev/null +++ b/embedding/service.go @@ -0,0 +1,59 @@ +package embedding + +import ( + "context" + "log" +) + +func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limit int) ([]string, error) { + summaryResp, err := s.Summarizer.ExtractResponseSummary(input.Question, input.UserResponse) + if err != nil { + log.Printf("s.Summarizer.ExtractResponseSummary failed: %v", err) + return nil, err + } + + for _, point := range summaryResp.UserRespSummary { + vector, err := s.Embedder.EmbedText(ctx, point) + if err != nil { + log.Printf("s.Embedder.EmbedText failed: %v", err) + return nil, err + } + + err = s.Repo.StoreEmbedding(ctx, Embedding{ + InterviewID: input.InterviewID, + ConversationID: input.ConversationID, + MessageID: input.MessageID, + TopicID: input.TopicID, + QuestionNumber: input.QuestionNumber, + Summary: point, + Vector: vector, + CreatedAt: input.CreatedAt, + }) + if err != nil { + log.Printf("s.Repo.StoreEmbedding failed: %v", err) + return nil, err + } + } + + responseVector, err := s.Embedder.EmbedText(ctx, input.UserResponse) + if err != nil { + log.Printf("s.Embedder.EmbedText failed: %v", err) + return nil, err + } + + relevant, err := s.Repo.GetSimilarEmbeddings( + ctx, + input.InterviewID, + input.TopicID, + input.QuestionNumber, + input.MessageID, + responseVector, + limit, + ) + if err != nil { + log.Printf("s.Repo.GetSimilarEmbeddings failed: %v", err) + return nil, err + } + + return relevant, nil +} From a1a51df83752abfd3231d73bffa0315395df5905 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Thu, 17 Jul 2025 16:02:25 +0700 Subject: [PATCH 06/12] added routing for embedding service --- embedding/embedder.go | 11 +++++++++-- embedding/repository.go | 2 +- handlers/model.go | 4 ++++ internal/server/server.go | 20 +++++++++++++++++++- 4 files changed, 33 insertions(+), 4 deletions(-) diff --git a/embedding/embedder.go b/embedding/embedder.go index 0c4c825..6ae6a86 100644 --- a/embedding/embedder.go +++ b/embedding/embedder.go @@ -4,7 +4,9 @@ import ( "bytes" "context" "encoding/json" + "errors" "net/http" + "os" "time" ) @@ -13,11 +15,16 @@ type HTTPEmbedder struct { Timeout time.Duration } -func NewHTTPEmbedder(endpoint string) *HTTPEmbedder { +func NewHTTPEmbedder() (*HTTPEmbedder, error) { + endpoint := os.Getenv("EMBEDDING_URL") + if endpoint == "" { + return nil, errors.New("env not set for EMBEDDING_URL") + } + return &HTTPEmbedder{ Endpoint: endpoint, Timeout: 3 * time.Second, - } + }, nil } func (e *HTTPEmbedder) EmbedText(ctx context.Context, input string) ([]float32, error) { diff --git a/embedding/repository.go b/embedding/repository.go index c48bea7..d25ce5d 100644 --- a/embedding/repository.go +++ b/embedding/repository.go @@ -12,7 +12,7 @@ type PGRepository struct { DB *sql.DB } -func NewPGRepository(db *sql.DB) *PGRepository { +func NewRepository(db *sql.DB) *PGRepository { return &PGRepository{DB: db} } diff --git a/handlers/model.go b/handlers/model.go index cd51199..e5460e9 100644 --- a/handlers/model.go +++ b/handlers/model.go @@ -6,6 +6,7 @@ import ( "github.com/michaelboegner/interviewer/billing" "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/conversation" + "github.com/michaelboegner/interviewer/embedding" "github.com/michaelboegner/interviewer/interview" "github.com/michaelboegner/interviewer/mailer" "github.com/michaelboegner/interviewer/token" @@ -59,6 +60,7 @@ type Handler struct { Billing *billing.Billing Mailer *mailer.Mailer OpenAI chatgpt.AIClient + Embedding *embedding.Service DB *sql.DB } @@ -71,6 +73,7 @@ func NewHandler( billing *billing.Billing, mailer *mailer.Mailer, openAI chatgpt.AIClient, + embeddingService *embedding.Service, db *sql.DB) *Handler { return &Handler{ InterviewRepo: interviewRepo, @@ -81,6 +84,7 @@ func NewHandler( Billing: billing, Mailer: mailer, OpenAI: openAI, + Embedding: embeddingService, DB: db, } } diff --git a/internal/server/server.go b/internal/server/server.go index 027f902..7204f3d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,6 +9,7 @@ import ( "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/conversation" "github.com/michaelboegner/interviewer/database" + "github.com/michaelboegner/interviewer/embedding" "github.com/michaelboegner/interviewer/handlers" "github.com/michaelboegner/interviewer/interview" "github.com/michaelboegner/interviewer/mailer" @@ -34,15 +35,32 @@ func NewServer() (*Server, error) { tokenRepo := token.NewRepository(db) conversationRepo := conversation.NewRepository(db) billingRepo := billing.NewRepository(db) + embeddingRepo := embedding.NewRepository(db) openAI := chatgpt.NewOpenAI() mailer := mailer.NewMailer() + embedder, err := embedding.NewHTTPEmbedder() + if err != nil { + log.Printf("embedding.NewHTTPEmbedder failed: %v", err) + return nil, err + } + embedding := embedding.NewService(embeddingRepo, embedder, openAI) billing, err := billing.NewBilling() if err != nil { log.Printf("billing.NewBilling failed: %v", err) return nil, err } - handler := handlers.NewHandler(interviewRepo, userRepo, tokenRepo, conversationRepo, billingRepo, billing, mailer, openAI, db) + handler := handlers.NewHandler( + interviewRepo, + userRepo, + tokenRepo, + conversationRepo, + billingRepo, + billing, + mailer, + openAI, + embedding, + db) mux.Handle("/api/users", http.HandlerFunc(handler.CreateUsersHandler)) mux.Handle("/api/auth/login", http.HandlerFunc(handler.LoginHandler)) From 9465ae26d13244c493a09cbb035e73dfc5ca0882 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Thu, 17 Jul 2025 16:24:24 +0700 Subject: [PATCH 07/12] started adding embeddingService call to handlers.CreateConversation --- conversation/service.go | 19 ++++++++++++++++++- embedding/model.go | 1 - handlers/handlers.go | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/conversation/service.go b/conversation/service.go index ad66d6f..c26d88f 100644 --- a/conversation/service.go +++ b/conversation/service.go @@ -1,10 +1,13 @@ package conversation import ( + "context" "errors" "log" + "time" "github.com/michaelboegner/interviewer/chatgpt" + "github.com/michaelboegner/interviewer/embedding" "github.com/michaelboegner/interviewer/interview" ) @@ -30,9 +33,11 @@ func CreateEmptyConversation(repo ConversationRepo, interviewID int, subTopic st } func CreateConversation( + ctx context.Context, repo ConversationRepo, interviewRepo interview.InterviewRepo, openAI chatgpt.AIClient, + embeddingService embedding.Service, conversation *Conversation, interviewID int, prompt, @@ -67,7 +72,19 @@ func CreateConversation( return nil, err } - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) + embedInput := embedding.EmbedInput{ + InterviewID: interviewID, + ConversationID: conversationID, + TopicID: topicID, + QuestionNumber: questionNumber, + Question: firstQuestion, + UserResponse: message, + CreatedAt: time.Now().UTC(), + } + + conversationContext, err := embeddingService.ProcessAndRetrieve(ctx, embedInput, 5) + + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo, conversationContext) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err diff --git a/embedding/model.go b/embedding/model.go index 03619e2..8fdd1ab 100644 --- a/embedding/model.go +++ b/embedding/model.go @@ -10,7 +10,6 @@ import ( type EmbedInput struct { InterviewID int ConversationID int - MessageID int TopicID int QuestionNumber int Question string diff --git a/handlers/handlers.go b/handlers/handlers.go index ba6474d..8f56b2e 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -697,9 +697,11 @@ func (h *Handler) CreateConversationsHandler(w http.ResponseWriter, r *http.Requ } conversationCreated, err := conversation.CreateConversation( + r.Context(), h.ConversationRepo, h.InterviewRepo, h.OpenAI, + *h.Embedding, conversationReturned, interviewID, interviewReturned.Prompt, From 349d4afb04acb0dd164a31857fb9847ee3bb95fc Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Fri, 18 Jul 2025 16:11:28 +0700 Subject: [PATCH 08/12] all elements in place. Debugging. --- chatgpt/model.go | 5 +++-- conversation/helpers.go | 31 +++++++++++++++++++++++++++---- conversation/model.go | 2 +- conversation/repository.go | 8 ++++---- conversation/service.go | 27 ++++++++++++++++++++++++--- embedding/model.go | 1 + embedding/service.go | 16 ++++++++++++++++ handlers/handlers.go | 2 ++ 8 files changed, 78 insertions(+), 14 deletions(-) diff --git a/chatgpt/model.go b/chatgpt/model.go index 5f8a99b..eaaf033 100644 --- a/chatgpt/model.go +++ b/chatgpt/model.go @@ -168,11 +168,12 @@ Break the response into a list of concise, self-contained statements. Each item - Exclude filler, vague claims, or generalities Output only valid JSON in this format: -[ +{ +"user_response_summary":[ "First technical point...", "Second technical point...", ... -] +]} Interview question: "%s" diff --git a/conversation/helpers.go b/conversation/helpers.go index 2af1b2e..c0d520c 100644 --- a/conversation/helpers.go +++ b/conversation/helpers.go @@ -3,16 +3,18 @@ package conversation import ( "encoding/json" "errors" + "fmt" "log" "sort" + "strings" "time" "github.com/michaelboegner/interviewer/chatgpt" "github.com/michaelboegner/interviewer/interview" ) -func GetChatGPTResponses(conversation *Conversation, openAI chatgpt.AIClient, interviewRepo interview.InterviewRepo) (*chatgpt.ChatGPTResponse, string, error) { - conversationHistory, err := GetConversationHistory(conversation, interviewRepo) +func GetChatGPTResponses(conversation *Conversation, openAI chatgpt.AIClient, interviewRepo interview.InterviewRepo, conversationContext []string) (*chatgpt.ChatGPTResponse, string, error) { + conversationHistory, err := GetConversationHistory(conversation, interviewRepo, conversationContext) if err != nil { log.Printf("GetConversationHistory failed: %v", err) return nil, "", err @@ -32,7 +34,7 @@ func GetChatGPTResponses(conversation *Conversation, openAI chatgpt.AIClient, in return chatGPTResponse, chatGPTResponseString, nil } -func GetConversationHistory(conversation *Conversation, interviewRepo interview.InterviewRepo) ([]map[string]string, error) { +func GetConversationHistory(conversation *Conversation, interviewRepo interview.InterviewRepo, conversationContext []string) ([]map[string]string, error) { var arrayOfTopics []string var currentTopic string chatGPTConversationArray := make([]map[string]string, 0) @@ -66,6 +68,7 @@ func GetConversationHistory(conversation *Conversation, interviewRepo interview. questionNumbersSorted = append(questionNumbersSorted, questionNumber) } sort.Ints(questionNumbersSorted) + lastQuestionNumber := questionNumbersSorted[len(questionNumbersSorted)-1] for _, questionNumber := range questionNumbersSorted { question := topic.Questions[questionNumber] for i, message := range question.Messages { @@ -79,13 +82,33 @@ func GetConversationHistory(conversation *Conversation, interviewRepo interview. if message.Author == "interviewer" { role = "assistant" } + + content := message.Content + isFinalInjectionTarget := questionNumber == lastQuestionNumber && + message.Author == "user" + // DEBUG + fmt.Printf("isFinalInjectionTarget: %v\n", isFinalInjectionTarget) + fmt.Printf("conversationContext: %v\n", conversationContext) + if isFinalInjectionTarget && len(conversationContext) > 0 { + formattedContext := strings.Join(conversationContext, "\n") + content = fmt.Sprintf("Relevant prior context:\n%s\n\n--- BEGIN USER'S ACTUAL RESPONSE ---\n%s", formattedContext, content) + } + chatGPTConversationArray = append(chatGPTConversationArray, map[string]string{ "role": role, - "content": message.Content, + "content": content, }) } } + fmt.Println("------ DEBUG: Formatted Conversation History ------") + for i, msg := range chatGPTConversationArray { + fmt.Printf("\n--- Message %d ---\n", i+1) + fmt.Printf("Role : %s\n", msg["role"]) + fmt.Printf("Content:\n%s\n", msg["content"]) + } + fmt.Println("------ END DEBUG ------") + return chatGPTConversationArray, nil } diff --git a/conversation/model.go b/conversation/model.go index 1ca74a5..34894eb 100644 --- a/conversation/model.go +++ b/conversation/model.go @@ -72,7 +72,7 @@ type ConversationRepo interface { CreateQuestion(conversation *Conversation, prompt string) (int, error) AddQuestion(question *Question) (int, error) GetQuestions(Conversation *Conversation) ([]*Question, error) - CreateMessages(conversation *Conversation, messages []Message) error + CreateMessages(conversation *Conversation, messages []Message) (int, error) AddMessage(conversationID, topic_id, questionNumber int, message Message) (int, error) GetMessages(conversationID, topic_id, questionNumber int) ([]Message, error) } diff --git a/conversation/repository.go b/conversation/repository.go index 4ac712d..b5d8f5e 100644 --- a/conversation/repository.go +++ b/conversation/repository.go @@ -210,7 +210,7 @@ func (repo *Repository) GetQuestions(conversation *Conversation) ([]*Question, e return questions, nil } -func (repo *Repository) CreateMessages(conversation *Conversation, messages []Message) error { +func (repo *Repository) CreateMessages(conversation *Conversation, messages []Message) (int, error) { var id int for _, message := range messages { query := ` @@ -229,14 +229,14 @@ func (repo *Repository) CreateMessages(conversation *Conversation, messages []Me ).Scan(&id) if err == sql.ErrNoRows { - return err + return 0, err } else if err != nil { log.Printf("Error querying conversation: %v\n", err) - return err + return 0, err } } - return nil + return id, nil } func (repo *Repository) AddMessage(conversationID, topic_id, questionNumber int, message Message) (int, error) { diff --git a/conversation/service.go b/conversation/service.go index c26d88f..48112ac 100644 --- a/conversation/service.go +++ b/conversation/service.go @@ -66,7 +66,7 @@ func CreateConversation( topic.Questions[questionNumber] = NewQuestion(conversationID, topicID, questionNumber, firstQuestion, messages) conversation.Topics[topicID] = topic - err = repo.CreateMessages(conversation, messages) + messageID, err := repo.CreateMessages(conversation, messages) if err != nil { log.Printf("repo.CreateMessages failed: %v", err) return nil, err @@ -77,12 +77,16 @@ func CreateConversation( ConversationID: conversationID, TopicID: topicID, QuestionNumber: questionNumber, + MessageID: messageID, Question: firstQuestion, UserResponse: message, CreatedAt: time.Now().UTC(), } conversationContext, err := embeddingService.ProcessAndRetrieve(ctx, embedInput, 5) + if err != nil { + log.Printf("embeddingService.ProcessAndRetrieve failed: %v", err) + } chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo, conversationContext) if err != nil { @@ -125,9 +129,11 @@ func CreateConversation( } func AppendConversation( + ctx context.Context, repo ConversationRepo, interviewRepo interview.InterviewRepo, openAI chatgpt.AIClient, + embeddingService embedding.Service, interviewID, userID int, conversation *Conversation, @@ -142,13 +148,28 @@ func AppendConversation( } messageUser := NewMessage(conversationID, topicID, questionNumber, User, message) - _, err := repo.AddMessage(conversationID, topicID, questionNumber, messageUser) + messageID, err := repo.AddMessage(conversationID, topicID, questionNumber, messageUser) if err != nil { return nil, err } conversation.Topics[topicID].Questions[questionNumber].Messages = append(conversation.Topics[topicID].Questions[questionNumber].Messages, messageUser) - chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo) + embedInput := embedding.EmbedInput{ + InterviewID: interviewID, + ConversationID: conversationID, + TopicID: topicID, + QuestionNumber: questionNumber, + MessageID: messageID, + Question: conversation.Topics[topicID].Questions[questionNumber].Prompt, + UserResponse: message, + CreatedAt: time.Now().UTC(), + } + + conversationContext, err := embeddingService.ProcessAndRetrieve(ctx, embedInput, 5) + if err != nil { + log.Printf("embeddingService.ProcessAndRetrieve failed: %v", err) + } + chatGPTResponse, chatGPTResponseString, err := GetChatGPTResponses(conversation, openAI, interviewRepo, conversationContext) if err != nil { log.Printf("getChatGPTResponses failed: %v", err) return nil, err diff --git a/embedding/model.go b/embedding/model.go index 8fdd1ab..03619e2 100644 --- a/embedding/model.go +++ b/embedding/model.go @@ -10,6 +10,7 @@ import ( type EmbedInput struct { InterviewID int ConversationID int + MessageID int TopicID int QuestionNumber int Question string diff --git a/embedding/service.go b/embedding/service.go index b28ea98..6414d93 100644 --- a/embedding/service.go +++ b/embedding/service.go @@ -2,16 +2,23 @@ package embedding import ( "context" + "fmt" "log" ) func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limit int) ([]string, error) { + // DEBUG + fmt.Printf("ProcessAndRetrive firing\n") + summaryResp, err := s.Summarizer.ExtractResponseSummary(input.Question, input.UserResponse) if err != nil { log.Printf("s.Summarizer.ExtractResponseSummary failed: %v", err) return nil, err } + // DEBUG + fmt.Printf("SummaryResp: %v\n", summaryResp) + for _, point := range summaryResp.UserRespSummary { vector, err := s.Embedder.EmbedText(ctx, point) if err != nil { @@ -19,6 +26,9 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi return nil, err } + // DEBUG + fmt.Printf("vector: %v\n", vector) + err = s.Repo.StoreEmbedding(ctx, Embedding{ InterviewID: input.InterviewID, ConversationID: input.ConversationID, @@ -41,6 +51,9 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi return nil, err } + // DEBUG + fmt.Printf("responseVector: %v\n", responseVector) + relevant, err := s.Repo.GetSimilarEmbeddings( ctx, input.InterviewID, @@ -55,5 +68,8 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi return nil, err } + // DEBUG + fmt.Printf("relevant: %v\n", relevant) + return relevant, nil } diff --git a/handlers/handlers.go b/handlers/handlers.go index 8f56b2e..b77b521 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -781,9 +781,11 @@ func (h *Handler) AppendConversationsHandler(w http.ResponseWriter, r *http.Requ } conversationReturned, err = conversation.AppendConversation( + r.Context(), h.ConversationRepo, h.InterviewRepo, h.OpenAI, + *h.Embedding, interviewID, userID, conversationReturned, From 7c3e090e46ce500af1fa236918684cd88de53187 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Fri, 18 Jul 2025 17:08:03 +0700 Subject: [PATCH 09/12] correct vector size for conversation_embeddings table and embedding being stored in table as expected. Now running into type error with s.Repo.GetSimilarEmbeddings --- .../000010_create_conversation_embeddings.up.sql | 2 +- embedding/embedder.go | 2 +- embedding/model.go | 2 +- embedding/repository.go | 4 ++-- embedding/service.go | 12 +++++++++++- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/database/migrations/000010_create_conversation_embeddings.up.sql b/database/migrations/000010_create_conversation_embeddings.up.sql index 0ed5e46..0f15fe3 100644 --- a/database/migrations/000010_create_conversation_embeddings.up.sql +++ b/database/migrations/000010_create_conversation_embeddings.up.sql @@ -8,7 +8,7 @@ CREATE TABLE conversation_embeddings ( question_number INT NOT NULL, message_id INT NOT NULL, summary TEXT NOT NULL, - embedding VECTOR(1536) NOT NULL, + embedding VECTOR(384) NOT NULL, created_at TIMESTAMP DEFAULT now(), UNIQUE (interview_id, conversation_id, topic_id, question_number, message_id) ); diff --git a/embedding/embedder.go b/embedding/embedder.go index 6ae6a86..ba7400f 100644 --- a/embedding/embedder.go +++ b/embedding/embedder.go @@ -23,7 +23,7 @@ func NewHTTPEmbedder() (*HTTPEmbedder, error) { return &HTTPEmbedder{ Endpoint: endpoint, - Timeout: 3 * time.Second, + Timeout: 10 * time.Second, }, nil } diff --git a/embedding/model.go b/embedding/model.go index 03619e2..0d9688c 100644 --- a/embedding/model.go +++ b/embedding/model.go @@ -26,7 +26,7 @@ type Embedding struct { TopicID int QuestionNumber int Summary string - Vector []float32 + Vector string CreatedAt time.Time } diff --git a/embedding/repository.go b/embedding/repository.go index d25ce5d..845e8e8 100644 --- a/embedding/repository.go +++ b/embedding/repository.go @@ -27,7 +27,7 @@ func (r *PGRepository) StoreEmbedding(ctx context.Context, e Embedding) error { summary, embedding, created_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ) VALUES ($1, $2, $3, $4, $5, $6, $7::vector, $8) ON CONFLICT (interview_id, conversation_id, topic_id, question_number, message_id) DO NOTHING; ` @@ -38,7 +38,7 @@ func (r *PGRepository) StoreEmbedding(ctx context.Context, e Embedding) error { e.TopicID, e.QuestionNumber, e.Summary, - pq.Array(e.Vector), + e.Vector, e.CreatedAt, ) return err diff --git a/embedding/service.go b/embedding/service.go index 6414d93..2a4b606 100644 --- a/embedding/service.go +++ b/embedding/service.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" ) func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limit int) ([]string, error) { @@ -25,6 +26,7 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi log.Printf("s.Embedder.EmbedText failed: %v", err) return nil, err } + vectorString := formatVector(vector) // DEBUG fmt.Printf("vector: %v\n", vector) @@ -36,7 +38,7 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi TopicID: input.TopicID, QuestionNumber: input.QuestionNumber, Summary: point, - Vector: vector, + Vector: vectorString, CreatedAt: input.CreatedAt, }) if err != nil { @@ -73,3 +75,11 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi return relevant, nil } + +func formatVector(vec []float32) string { + strs := make([]string, len(vec)) + for i, v := range vec { + strs[i] = fmt.Sprintf("%f", v) + } + return "[" + strings.Join(strs, ", ") + "]" +} From 50cd19e584912e41c2eedadccc66c10396d2b534 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Sat, 19 Jul 2025 11:42:37 +0700 Subject: [PATCH 10/12] rag is technically working at this point. Needs to be tuned --- conversation/service.go | 4 +- ...0010_create_conversation_embeddings.up.sql | 3 +- embedding/model.go | 5 +- embedding/repository.go | 22 +++---- embedding/service.go | 65 ++++++++----------- go.mod | 8 ++- go.sum | 5 ++ 7 files changed, 53 insertions(+), 59 deletions(-) diff --git a/conversation/service.go b/conversation/service.go index 48112ac..034a19b 100644 --- a/conversation/service.go +++ b/conversation/service.go @@ -83,7 +83,7 @@ func CreateConversation( CreatedAt: time.Now().UTC(), } - conversationContext, err := embeddingService.ProcessAndRetrieve(ctx, embedInput, 5) + conversationContext, err := embeddingService.ProcessAndRetrieve(ctx, embedInput) if err != nil { log.Printf("embeddingService.ProcessAndRetrieve failed: %v", err) } @@ -165,7 +165,7 @@ func AppendConversation( CreatedAt: time.Now().UTC(), } - conversationContext, err := embeddingService.ProcessAndRetrieve(ctx, embedInput, 5) + conversationContext, err := embeddingService.ProcessAndRetrieve(ctx, embedInput) if err != nil { log.Printf("embeddingService.ProcessAndRetrieve failed: %v", err) } diff --git a/database/migrations/000010_create_conversation_embeddings.up.sql b/database/migrations/000010_create_conversation_embeddings.up.sql index 0f15fe3..15054c3 100644 --- a/database/migrations/000010_create_conversation_embeddings.up.sql +++ b/database/migrations/000010_create_conversation_embeddings.up.sql @@ -9,8 +9,7 @@ CREATE TABLE conversation_embeddings ( message_id INT NOT NULL, summary TEXT NOT NULL, embedding VECTOR(384) NOT NULL, - created_at TIMESTAMP DEFAULT now(), - UNIQUE (interview_id, conversation_id, topic_id, question_number, message_id) + created_at TIMESTAMP DEFAULT now() ); CREATE INDEX conversation_embeddings_embedding_idx diff --git a/embedding/model.go b/embedding/model.go index 0d9688c..6f897a3 100644 --- a/embedding/model.go +++ b/embedding/model.go @@ -5,6 +5,7 @@ import ( "time" "github.com/michaelboegner/interviewer/chatgpt" + "github.com/pgvector/pgvector-go" ) type EmbedInput struct { @@ -26,7 +27,7 @@ type Embedding struct { TopicID int QuestionNumber int Summary string - Vector string + Vector pgvector.Vector CreatedAt time.Time } @@ -38,7 +39,7 @@ type Service struct { type Repository interface { StoreEmbedding(ctx context.Context, e Embedding) error - GetSimilarEmbeddings(ctx context.Context, interviewID, topicID, questionNumber, excludeMessageID int, queryVec []float32, limit int) ([]string, error) + GetSimilarEmbeddings(ctx context.Context, interviewID, topicID, questionNumber, excludeMessageID int, queryVec pgvector.Vector, limit int) ([]string, error) } type Embedder interface { diff --git a/embedding/repository.go b/embedding/repository.go index 845e8e8..7f9a015 100644 --- a/embedding/repository.go +++ b/embedding/repository.go @@ -5,7 +5,7 @@ import ( "database/sql" "fmt" - "github.com/lib/pq" + "github.com/pgvector/pgvector-go" ) type PGRepository struct { @@ -27,8 +27,7 @@ func (r *PGRepository) StoreEmbedding(ctx context.Context, e Embedding) error { summary, embedding, created_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7::vector, $8) - ON CONFLICT (interview_id, conversation_id, topic_id, question_number, message_id) DO NOTHING; + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ` _, err := r.DB.ExecContext(ctx, query, @@ -41,32 +40,31 @@ func (r *PGRepository) StoreEmbedding(ctx context.Context, e Embedding) error { e.Vector, e.CreatedAt, ) + + // DEBUG + fmt.Printf("EMBEDDING STORED\n\n") return err } func (r *PGRepository) GetSimilarEmbeddings( ctx context.Context, interviewID, topicID, questionNumber, excludeMessageID int, - queryVec []float32, + queryVec pgvector.Vector, limit int, ) ([]string, error) { query := ` SELECT summary FROM conversation_embeddings WHERE interview_id = $1 - AND topic_id = $2 - AND question_number = $3 - AND message_id != $4 - ORDER BY embedding <-> $5 - LIMIT $6; + AND message_id != $2 + ORDER BY embedding <-> $3 + LIMIT $4; ` rows, err := r.DB.QueryContext(ctx, query, interviewID, - topicID, - questionNumber, excludeMessageID, - pq.Array(queryVec), + queryVec, limit, ) if err != nil { diff --git a/embedding/service.go b/embedding/service.go index 2a4b606..fd9171f 100644 --- a/embedding/service.go +++ b/embedding/service.go @@ -4,10 +4,11 @@ import ( "context" "fmt" "log" - "strings" + + "github.com/pgvector/pgvector-go" ) -func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limit int) ([]string, error) { +func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput) ([]string, error) { // DEBUG fmt.Printf("ProcessAndRetrive firing\n") @@ -19,14 +20,15 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi // DEBUG fmt.Printf("SummaryResp: %v\n", summaryResp) - + allRelevant := []string{} + limit := 1 for _, point := range summaryResp.UserRespSummary { - vector, err := s.Embedder.EmbedText(ctx, point) + rawVec, err := s.Embedder.EmbedText(ctx, point) if err != nil { log.Printf("s.Embedder.EmbedText failed: %v", err) return nil, err } - vectorString := formatVector(vector) + vector := pgvector.NewVector(rawVec) // DEBUG fmt.Printf("vector: %v\n", vector) @@ -38,48 +40,35 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput, limi TopicID: input.TopicID, QuestionNumber: input.QuestionNumber, Summary: point, - Vector: vectorString, + Vector: vector, CreatedAt: input.CreatedAt, }) if err != nil { log.Printf("s.Repo.StoreEmbedding failed: %v", err) return nil, err } - } - responseVector, err := s.Embedder.EmbedText(ctx, input.UserResponse) - if err != nil { - log.Printf("s.Embedder.EmbedText failed: %v", err) - return nil, err - } + relevantEmbeddings, err := s.Repo.GetSimilarEmbeddings( + ctx, + input.InterviewID, + input.TopicID, + input.QuestionNumber, + input.MessageID, + vector, + limit, + ) + if err != nil { + log.Printf("s.Repo.GetSimilarEmbeddings failed: %v", err) + return nil, err + } + for _, point := range relevantEmbeddings { + allRelevant = append(allRelevant, point) + } - // DEBUG - fmt.Printf("responseVector: %v\n", responseVector) + // DEBUG + fmt.Printf("relevant: %v\n", allRelevant) - relevant, err := s.Repo.GetSimilarEmbeddings( - ctx, - input.InterviewID, - input.TopicID, - input.QuestionNumber, - input.MessageID, - responseVector, - limit, - ) - if err != nil { - log.Printf("s.Repo.GetSimilarEmbeddings failed: %v", err) - return nil, err } - // DEBUG - fmt.Printf("relevant: %v\n", relevant) - - return relevant, nil -} - -func formatVector(vec []float32) string { - strs := make([]string, len(vec)) - for i, v := range vec { - strs[i] = fmt.Sprintf("%f", v) - } - return "[" + strings.Join(strs, ", ") + "]" + return allRelevant, nil } diff --git a/go.mod b/go.mod index e48b15a..4f3533f 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,15 @@ module github.com/michaelboegner/interviewer -go 1.23 +go 1.23.0 toolchain go1.23.8 require ( github.com/golang-jwt/jwt/v5 v5.2.1 - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.7.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.36.0 ) + +require github.com/pgvector/pgvector-go v0.3.0 // indirect diff --git a/go.sum b/go.sum index 931a771..a1a397c 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,14 @@ github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17w github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc= +github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= From 29e2cead77f5952229cab51d9d4b7c20402e3713 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Sun, 20 Jul 2025 10:22:05 +0700 Subject: [PATCH 11/12] deduplicated embedding similarity results --- conversation/helpers.go | 2 +- embedding/service.go | 18 +++++++------ go.mod | 3 +-- go.sum | 57 ++++++++++++++++++++++++++++++++++++++--- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/conversation/helpers.go b/conversation/helpers.go index c0d520c..5631741 100644 --- a/conversation/helpers.go +++ b/conversation/helpers.go @@ -91,7 +91,7 @@ func GetConversationHistory(conversation *Conversation, interviewRepo interview. fmt.Printf("conversationContext: %v\n", conversationContext) if isFinalInjectionTarget && len(conversationContext) > 0 { formattedContext := strings.Join(conversationContext, "\n") - content = fmt.Sprintf("Relevant prior context:\n%s\n\n--- BEGIN USER'S ACTUAL RESPONSE ---\n%s", formattedContext, content) + content = fmt.Sprintf("Relevant prior user context:\n%s\n\n--- BEGIN USER'S ACTUAL RESPONSE ---\n%s", formattedContext, content) } chatGPTConversationArray = append(chatGPTConversationArray, map[string]string{ diff --git a/embedding/service.go b/embedding/service.go index fd9171f..6cf2358 100644 --- a/embedding/service.go +++ b/embedding/service.go @@ -9,8 +9,7 @@ import ( ) func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput) ([]string, error) { - // DEBUG - fmt.Printf("ProcessAndRetrive firing\n") + fmt.Printf("ProcessAndRetrieve firing\n") summaryResp, err := s.Summarizer.ExtractResponseSummary(input.Question, input.UserResponse) if err != nil { @@ -18,10 +17,12 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput) ([]s return nil, err } - // DEBUG fmt.Printf("SummaryResp: %v\n", summaryResp) + allRelevant := []string{} + seen := map[string]struct{}{} limit := 1 + for _, point := range summaryResp.UserRespSummary { rawVec, err := s.Embedder.EmbedText(ctx, point) if err != nil { @@ -30,7 +31,6 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput) ([]s } vector := pgvector.NewVector(rawVec) - // DEBUG fmt.Printf("vector: %v\n", vector) err = s.Repo.StoreEmbedding(ctx, Embedding{ @@ -61,13 +61,15 @@ func (s *Service) ProcessAndRetrieve(ctx context.Context, input EmbedInput) ([]s log.Printf("s.Repo.GetSimilarEmbeddings failed: %v", err) return nil, err } - for _, point := range relevantEmbeddings { - allRelevant = append(allRelevant, point) + + for _, r := range relevantEmbeddings { + if _, exists := seen[r]; !exists { + seen[r] = struct{}{} + allRelevant = append(allRelevant, r) + } } - // DEBUG fmt.Printf("relevant: %v\n", allRelevant) - } return allRelevant, nil diff --git a/go.mod b/go.mod index 4f3533f..98b0e5b 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,6 @@ require ( github.com/google/go-cmp v0.7.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 + github.com/pgvector/pgvector-go v0.3.0 golang.org/x/crypto v0.36.0 ) - -require github.com/pgvector/pgvector-go v0.3.0 // indirect diff --git a/go.sum b/go.sum index a1a397c..358b9d4 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,64 @@ +entgo.io/ent v0.14.3 h1:wokAV/kIlH9TeklJWGGS7AYJdVckr0DloWjIcO9iIIQ= +entgo.io/ent v0.14.3/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= +github.com/go-pg/pg/v10 v10.11.0 h1:CMKJqLgTrfpE/aOVeLdybezR2om071Vh38OLZjsyMI0= +github.com/go-pg/pg/v10 v10.11.0/go.mod h1:4BpHRoxE61y4Onpof3x1a2SQvi9c+q1dJnrNdMjsroA= +github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU= +github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/pgvector/pgvector-go v0.3.0 h1:Ij+Yt78R//uYqs3Zk35evZFvr+G0blW0OUN+Q2D1RWc= github.com/pgvector/pgvector-go v0.3.0/go.mod h1:duFy+PXWfW7QQd5ibqutBO4GxLsUZ9RVXhFZGIBsWSA= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.1.12 h1:sOjDVHxNTuM6dNGaba0wUuz7KvDE1BmNu9Gqs2gJSXQ= +github.com/uptrace/bun v1.1.12/go.mod h1:NPG6JGULBeQ9IU6yHp7YGELRa5Agmd7ATZdz4tGZ6z0= +github.com/uptrace/bun/dialect/pgdialect v1.1.12 h1:m/CM1UfOkoBTglGO5CUTKnIKKOApOYxkcP2qn0F9tJk= +github.com/uptrace/bun/dialect/pgdialect v1.1.12/go.mod h1:Ij6WIxQILxLlL2frUBxUBOZJtLElD2QQNDcu/PWDHTc= +github.com/uptrace/bun/driver/pgdriver v1.1.12 h1:3rRWB1GK0psTJrHwxzNfEij2MLibggiLdTqjTtfHc1w= +github.com/uptrace/bun/driver/pgdriver v1.1.12/go.mod h1:ssYUP+qwSEgeDDS1xm2XBip9el1y9Mi5mTAvLoiADLM= +github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= +github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= +github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo= +gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +mellium.im/sasl v0.3.1 h1:wE0LW6g7U83vhvxjC1IY8DnXM+EU095yeo8XClvCdfo= +mellium.im/sasl v0.3.1/go.mod h1:xm59PUYpZHhgQ9ZqoJ5QaCqzWMi8IeS49dhp6plPCzw= From fa689849874a9bf3649b2c6da9d5df3f595954d7 Mon Sep 17 00:00:00 2001 From: michaelboegner Date: Sun, 20 Jul 2025 10:26:41 +0700 Subject: [PATCH 12/12] tweaked summary prompt to write in past tense --- chatgpt/model.go | 1 + 1 file changed, 1 insertion(+) diff --git a/chatgpt/model.go b/chatgpt/model.go index eaaf033..a83b03e 100644 --- a/chatgpt/model.go +++ b/chatgpt/model.go @@ -166,6 +166,7 @@ Break the response into a list of concise, self-contained statements. Each item - Be understandable without the original question - Focus only on what the user **actually said**, not what they should have said - Exclude filler, vague claims, or generalities +- Be written in the past tense Output only valid JSON in this format: {