Files
resume/internal/service/knowledge/knowledge_base_service.go
T

362 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package knowledge
import (
"context"
"encoding/json"
"fmt"
"github.com/google/uuid"
"resume-platform/internal/model"
"resume-platform/internal/repository"
"resume-platform/pkg/logger"
"strings"
)
type KnowledgeBaseService struct {
embeddingService *EmbeddingService
vectorService *VectorService
repo repository.ResumeRepository
}
func NewKnowledgeBaseService(embeddingService *EmbeddingService, vectorService *VectorService, repo repository.ResumeRepository) *KnowledgeBaseService {
return &KnowledgeBaseService{
embeddingService: embeddingService,
vectorService: vectorService,
repo: repo,
}
}
func (s *KnowledgeBaseService) AddDocument(ctx context.Context, documentID, content, userID string) error {
if content == "" {
return fmt.Errorf("empty content")
}
chunks := s.chunkContent(content, 500, 50)
doc, _ := s.repo.GetDocumentByID(documentID)
sourceName := documentID[:8]
if doc != nil && doc.Name != "" {
sourceName = doc.Name
}
for i, chunk := range chunks {
documentChunk := &model.DocumentChunk{
ID: uuid.New().String(),
DocumentID: documentID,
ChunkIndex: i,
Content: chunk,
Metadata: "",
}
err := s.vectorService.StoreChunk(ctx, documentChunk, userID, sourceName)
if err != nil {
return err
}
}
logger.Infof("Added %d chunks to document %s", len(chunks), documentID)
return nil
}
func (s *KnowledgeBaseService) AddResume(ctx context.Context, resumeID string, resume *model.Resume) error {
if resume == nil {
return fmt.Errorf("nil resume")
}
resumeText := s.serializeResume(resume)
if resumeText == "" {
return fmt.Errorf("empty resume content")
}
chunks := s.chunkContent(resumeText, 500, 50)
sourceName := resume.BasicInfo.Name
if sourceName == "" {
sourceName = resumeID[:8]
}
for i, chunk := range chunks {
documentChunk := &model.DocumentChunk{
ID: uuid.New().String(),
DocumentID: resumeID,
ChunkIndex: i,
Content: chunk,
Metadata: "",
}
err := s.vectorService.StoreChunk(ctx, documentChunk, resume.UserID, sourceName)
if err != nil {
return err
}
}
err := s.vectorService.Store(ctx, resumeText, "resume", resumeID, sourceName, resume.UserID)
if err != nil {
return err
}
logger.Infof("Added resume %s to knowledge base with %d chunks", resumeID, len(chunks))
return nil
}
func (s *KnowledgeBaseService) AddText(ctx context.Context, text, sourceType, sourceID, sourceName, userID string) error {
if text == "" {
return fmt.Errorf("empty text")
}
return s.vectorService.Store(ctx, text, sourceType, sourceID, sourceName, userID)
}
func (s *KnowledgeBaseService) Retrieve(ctx context.Context, query string, topK int, userID string) ([]model.ChunkWithScore, error) {
return s.vectorService.SearchWithChunks(ctx, query, topK, userID)
}
func (s *KnowledgeBaseService) RetrieveAll(ctx context.Context, query string, topK int) ([]model.ChunkWithScore, error) {
return s.vectorService.Search(ctx, query, topK, "")
}
func (s *KnowledgeBaseService) DeleteDocument(ctx context.Context, documentID string) error {
err := s.repo.DeleteEmbeddingsBySource("document", documentID)
if err != nil {
return err
}
return s.vectorService.DeleteDocumentChunks(ctx, documentID)
}
func (s *KnowledgeBaseService) DeleteResume(ctx context.Context, resumeID string) error {
return s.repo.DeleteEmbeddingsBySource("resume", resumeID)
}
func (s *KnowledgeBaseService) GetStats(ctx context.Context, userID string) (map[string]int, error) {
var stats = map[string]int{}
if userID != "" {
embeddings, err := s.repo.GetEmbeddingsByUserID(userID)
if err != nil {
return nil, err
}
stats["embeddings"] = len(embeddings)
chunks, err := s.repo.GetDocumentChunksByUserID(userID)
if err != nil {
return nil, err
}
stats["chunks"] = len(chunks)
} else {
embeddings, err := s.repo.GetAllEmbeddings()
if err != nil {
return nil, err
}
stats["embeddings"] = len(embeddings)
chunks, err := s.repo.GetAllDocumentChunks()
if err != nil {
return nil, err
}
stats["chunks"] = len(chunks)
}
return stats, nil
}
func (s *KnowledgeBaseService) chunkContent(content string, chunkSize, overlap int) []string {
if content == "" {
return nil
}
content = strings.ReplaceAll(content, "\r\n", "\n")
content = strings.ReplaceAll(content, "\r", "\n")
var chunks []string
start := 0
contentLen := len(content)
for start < contentLen {
end := start + chunkSize
if end > contentLen {
end = contentLen
}
if end < contentLen {
for i := end; i > start && i > start+chunkSize-overlap; i-- {
c := rune(content[i])
if c == '\n' || c == ';' || c == '\r' {
end = i + 1
break
}
}
}
chunk := strings.TrimSpace(content[start:end])
if chunk != "" {
chunks = append(chunks, chunk)
}
start = end - overlap
if start < 0 {
start = 0
}
if start >= contentLen {
break
}
}
return chunks
}
func (s *KnowledgeBaseService) serializeResume(resume *model.Resume) string {
var builder strings.Builder
if resume.BasicInfo.Name != "" {
builder.WriteString("姓名:")
builder.WriteString(resume.BasicInfo.Name)
builder.WriteString("\n")
}
if resume.BasicInfo.Title != "" {
builder.WriteString("职位:")
builder.WriteString(resume.BasicInfo.Title)
builder.WriteString("\n")
}
if resume.BasicInfo.Email != "" {
builder.WriteString("邮箱:")
builder.WriteString(resume.BasicInfo.Email)
builder.WriteString("\n")
}
if resume.BasicInfo.Phone != "" {
builder.WriteString("电话:")
builder.WriteString(resume.BasicInfo.Phone)
builder.WriteString("\n")
}
if resume.BasicInfo.Location != "" {
builder.WriteString("所在地:")
builder.WriteString(resume.BasicInfo.Location)
builder.WriteString("\n")
}
if resume.BasicInfo.Summary != "" {
builder.WriteString("个人简介:")
builder.WriteString(resume.BasicInfo.Summary)
builder.WriteString("\n")
}
if resume.BasicInfo.JobTarget != "" {
builder.WriteString("求职目标:")
builder.WriteString(resume.BasicInfo.JobTarget)
builder.WriteString("\n")
}
if len(resume.Experience) > 0 {
builder.WriteString("\n【工作经历】\n")
for _, exp := range resume.Experience {
builder.WriteString("公司:")
builder.WriteString(exp.Company)
builder.WriteString("\n职位:")
builder.WriteString(exp.Position)
builder.WriteString("\n时间:")
builder.WriteString(exp.StartDate)
if exp.EndDate != "" {
builder.WriteString(" - ")
builder.WriteString(exp.EndDate)
}
builder.WriteString("\n职责:")
builder.WriteString(exp.Description)
builder.WriteString("\n")
if len(exp.Highlights) > 0 {
builder.WriteString("亮点:")
builder.WriteString(strings.Join(exp.Highlights, ""))
builder.WriteString("\n")
}
builder.WriteString("\n")
}
}
if len(resume.Education) > 0 {
builder.WriteString("\n【教育背景】\n")
for _, edu := range resume.Education {
builder.WriteString("学校:")
builder.WriteString(edu.School)
builder.WriteString("\n学位:")
builder.WriteString(edu.Degree)
builder.WriteString("\n专业:")
builder.WriteString(edu.Major)
builder.WriteString("\n时间:")
builder.WriteString(edu.StartDate)
if edu.EndDate != "" {
builder.WriteString(" - ")
builder.WriteString(edu.EndDate)
}
builder.WriteString("\n")
}
}
if len(resume.Skills) > 0 {
builder.WriteString("\n【专业技能】\n")
for _, skill := range resume.Skills {
builder.WriteString(skill.Name)
if skill.Level != "" {
builder.WriteString("")
builder.WriteString(skill.Level)
builder.WriteString("")
}
if skill.Category != "" {
builder.WriteString(" - ")
builder.WriteString(skill.Category)
}
builder.WriteString("\n")
}
}
if len(resume.Projects) > 0 {
builder.WriteString("\n【项目经验】\n")
for _, proj := range resume.Projects {
builder.WriteString("项目名称:")
builder.WriteString(proj.Name)
builder.WriteString("\n描述:")
builder.WriteString(proj.Description)
builder.WriteString("\n")
if len(proj.TechStack) > 0 {
builder.WriteString("技术栈:")
builder.WriteString(strings.Join(proj.TechStack, "、"))
builder.WriteString("\n")
}
if len(proj.Highlights) > 0 {
builder.WriteString("亮点:")
builder.WriteString(strings.Join(proj.Highlights, ""))
builder.WriteString("\n")
}
if len(proj.Achievements) > 0 {
builder.WriteString("成果:")
builder.WriteString(strings.Join(proj.Achievements, ""))
builder.WriteString("\n")
}
builder.WriteString("\n")
}
}
result := builder.String()
if len(result) > 15000 {
result = result[:15000]
}
return result
}
func (s *KnowledgeBaseService) BuildIndex(ctx context.Context) error {
documents, err := s.repo.GetAllDocumentChunks()
if err != nil {
return err
}
for _, chunk := range documents {
if chunk.Embedding == "" && chunk.Content != "" {
embedding, err := s.embeddingService.Generate(ctx, chunk.Content)
if err != nil {
return err
}
embeddingJSON, _ := json.Marshal(embedding)
chunk.Embedding = string(embeddingJSON)
err = s.repo.CreateDocumentChunk(chunk)
if err != nil {
return err
}
}
}
return nil
}