Files

175 lines
4.9 KiB
Go

package kb
import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"ai-operator/internal/dialogue/region"
)
var requiredFields = []string{"external_id", "title", "language", "region_code", "status", "content", "source_hash"}
func ParseJSONLFile(path string, allowDisabled bool) ([]Document, []string, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, err
}
defer f.Close()
return ParseJSONL(f, path, allowDisabled)
}
func ParseJSONL(r io.Reader, sourcePath string, allowDisabled bool) ([]Document, []string, error) {
resolver := region.NewDefaultResolver()
sc := bufio.NewScanner(r)
sc.Buffer(make([]byte, 1024), 10*1024*1024)
var docs []Document
var errs []string
line := 0
for sc.Scan() {
line++
raw := bytes.TrimSpace(sc.Bytes())
if len(raw) == 0 {
continue
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
errs = append(errs, fmt.Sprintf("%s:%d invalid json", sourcePath, line))
continue
}
missing := ""
for _, k := range requiredFields {
if strings.TrimSpace(asString(m[k])) == "" {
missing = k
break
}
}
if missing != "" {
errs = append(errs, fmt.Sprintf("%s:%d missing %s", sourcePath, line, missing))
continue
}
lang := asString(m["language"])
if lang != "ru" && lang != "kk" {
errs = append(errs, fmt.Sprintf("%s:%d invalid language", sourcePath, line))
continue
}
regCode := asString(m["region_code"])
if regCode != "global" {
reg, ok := resolver.GetByCode(regCode)
if !ok || (!reg.Enabled && !allowDisabled) {
errs = append(errs, fmt.Sprintf("%s:%d invalid region_code", sourcePath, line))
continue
}
}
status := asString(m["status"])
if status != "draft" && status != "published" && status != "archived" {
errs = append(errs, fmt.Sprintf("%s:%d invalid status", sourcePath, line))
continue
}
metadata := map[string]any{}
if mm, ok := m["metadata"].(map[string]any); ok {
for k, v := range mm {
metadata[k] = v
}
}
known := map[string]bool{"external_id": true, "title": true, "language": true, "region_code": true, "category": true, "status": true, "content": true, "question": true, "short_answer": true, "full_answer": true, "keywords": true, "source": true, "source_type": true, "source_uri": true, "source_hash": true, "metadata": true}
for k, v := range m {
if !known[k] {
metadata[k] = v
}
}
docs = append(docs, Document{ExternalID: asString(m["external_id"]), Title: asString(m["title"]), Language: lang, RegionCode: regCode, Category: defaultString(asString(m["category"]), "general"), SourceType: defaultString(asString(m["source_type"]), "jsonl"), SourceURI: defaultString(asString(m["source_uri"]), filepath.Base(sourcePath)), SourceHash: asString(m["source_hash"]), Status: status, Content: asString(m["content"]), Question: asString(m["question"]), ShortAnswer: asString(m["short_answer"]), FullAnswer: asString(m["full_answer"]), Source: asString(m["source"]), Keywords: nonNilStrings(asStringSlice(m["keywords"])), Metadata: metadata})
}
return docs, errs, sc.Err()
}
func LoadJSONLDocuments(ctx context.Context, root string, allowDisabled bool) ([]Document, []string, error) {
var docs []Document
var errs []string
err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() || !strings.HasSuffix(path, ".jsonl") {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
default:
}
dd, ee, err := ParseJSONLFile(path, allowDisabled)
if err != nil {
return err
}
docs = append(docs, dd...)
errs = append(errs, ee...)
return nil
})
return docs, errs, err
}
func BuildChunkText(doc Document) string {
parts := []string{doc.Title}
if doc.Question != "" {
parts = append(parts, "Вопрос: "+doc.Question)
}
if doc.ShortAnswer != "" {
parts = append(parts, "Краткий ответ: "+doc.ShortAnswer)
}
if doc.FullAnswer != "" {
parts = append(parts, "Расширенное пояснение: "+doc.FullAnswer)
}
if len(doc.Keywords) > 0 {
parts = append(parts, "Ключевые слова: "+strings.Join(doc.Keywords, ", "))
}
if doc.Source != "" {
parts = append(parts, "Источник: "+doc.Source)
}
if doc.Content != "" {
parts = append(parts, doc.Content)
}
return strings.Join(parts, "\n\n")
}
func ContentHash(s string) string { h := sha256.Sum256([]byte(s)); return hex.EncodeToString(h[:]) }
func asString(v any) string {
if s, ok := v.(string); ok {
return strings.TrimSpace(s)
}
return ""
}
func defaultString(v, d string) string {
if v == "" {
return d
}
return v
}
func asStringSlice(v any) []string {
arr, ok := v.([]any)
if !ok {
return nil
}
out := []string{}
for _, x := range arr {
if s := asString(x); s != "" {
out = append(out, s)
}
}
return out
}
func nonNilStrings(v []string) []string {
if v == nil {
return []string{}
}
return v
}