Compare commits
13 Commits
feat/resp-
...
6c03a1a277
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c03a1a277 | ||
|
|
27288e2aaa | ||
|
|
1c728ec7a7 | ||
|
|
78059083c2 | ||
|
|
34cd4ac141 | ||
|
|
343366b12d | ||
|
|
978369eeaa | ||
|
|
c39e1c267d | ||
|
|
9af21895c6 | ||
|
|
e3bd6f219f | ||
|
|
ae62c2c8d8 | ||
|
|
04db7c2f01 | ||
|
|
3d889e70b5 |
8
Makefile
8
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs noextra-run
|
.PHONY: setconfig run lint setup-whisper build-whisper download-whisper-model docker-up docker-down docker-logs noextra-run installdelve checkdelve
|
||||||
|
|
||||||
run: setconfig
|
run: setconfig
|
||||||
go build -tags extra -o gf-lt && ./gf-lt
|
go build -tags extra -o gf-lt && ./gf-lt
|
||||||
@@ -15,6 +15,12 @@ noextra-run: setconfig
|
|||||||
setconfig:
|
setconfig:
|
||||||
find config.toml &>/dev/null || cp config.example.toml config.toml
|
find config.toml &>/dev/null || cp config.example.toml config.toml
|
||||||
|
|
||||||
|
installdelve:
|
||||||
|
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||||
|
|
||||||
|
checkdelve:
|
||||||
|
which dlv &>/dev/null || installdelve
|
||||||
|
|
||||||
lint: ## Run linters. Use make install-linters first.
|
lint: ## Run linters. Use make install-linters first.
|
||||||
golangci-lint run -c .golangci.yml ./...
|
golangci-lint run -c .golangci.yml ./...
|
||||||
|
|
||||||
|
|||||||
61
bot.go
61
bot.go
@@ -411,14 +411,21 @@ func fetchLCPModelsWithLoadStatus() ([]string, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result := make([]string, 0, len(models.Data))
|
result := make([]string, 0, len(models.Data))
|
||||||
for _, m := range models.Data {
|
li := 0 // loaded index
|
||||||
|
for i, m := range models.Data {
|
||||||
modelName := m.ID
|
modelName := m.ID
|
||||||
if m.Status.Value == "loaded" {
|
if m.Status.Value == "loaded" {
|
||||||
modelName = "(loaded) " + modelName
|
modelName = "(loaded) " + modelName
|
||||||
|
li = i
|
||||||
}
|
}
|
||||||
result = append(result, modelName)
|
result = append(result, modelName)
|
||||||
}
|
}
|
||||||
return result, nil
|
if li == 0 {
|
||||||
|
return result, nil // no loaded models
|
||||||
|
}
|
||||||
|
loadedModel := result[li]
|
||||||
|
result = append(result[:li], result[li+1:]...)
|
||||||
|
return slices.Concat([]string{loadedModel}, result), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchLCPModelsWithStatus returns the full LCPModels struct including status information.
|
// fetchLCPModelsWithStatus returns the full LCPModels struct including status information.
|
||||||
@@ -569,7 +576,6 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
streamDone <- true
|
streamDone <- true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the initial response is an error before starting to stream
|
// Check if the initial response is an error before starting to stream
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode >= 400 {
|
||||||
// Read the response body to get detailed error information
|
// Read the response body to get detailed error information
|
||||||
@@ -584,7 +590,6 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
streamDone <- true
|
streamDone <- true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the error response for detailed information
|
// Parse the error response for detailed information
|
||||||
detailedError := extractDetailedErrorFromBytes(bodyBytes, resp.StatusCode)
|
detailedError := extractDetailedErrorFromBytes(bodyBytes, resp.StatusCode)
|
||||||
logger.Error("API returned error status", "status_code", resp.StatusCode, "detailed_error", detailedError)
|
logger.Error("API returned error status", "status_code", resp.StatusCode, "detailed_error", detailedError)
|
||||||
@@ -710,7 +715,6 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
tokenCount++
|
tokenCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// When we get content and have been streaming reasoning, close the thinking block
|
// When we get content and have been streaming reasoning, close the thinking block
|
||||||
if chunk.Chunk != "" && hasReasoning && !reasoningSent {
|
if chunk.Chunk != "" && hasReasoning && !reasoningSent {
|
||||||
// Close the thinking block before sending actual content
|
// Close the thinking block before sending actual content
|
||||||
@@ -718,7 +722,6 @@ func sendMsgToLLM(body io.Reader) {
|
|||||||
tokenCount++
|
tokenCount++
|
||||||
reasoningSent = true
|
reasoningSent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// bot sends way too many \n
|
// bot sends way too many \n
|
||||||
answerText = strings.ReplaceAll(chunk.Chunk, "\n\n", "\n")
|
answerText = strings.ReplaceAll(chunk.Chunk, "\n\n", "\n")
|
||||||
// Accumulate text to check for stop strings that might span across chunks
|
// Accumulate text to check for stop strings that might span across chunks
|
||||||
@@ -764,12 +767,10 @@ func chatRagUse(qText string) (string, error) {
|
|||||||
questions[i] = q.Text
|
questions[i] = q.Text
|
||||||
logger.Debug("RAG question extracted", "index", i, "question", q.Text)
|
logger.Debug("RAG question extracted", "index", i, "question", q.Text)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(questions) == 0 {
|
if len(questions) == 0 {
|
||||||
logger.Warn("No questions extracted from query text", "query", qText)
|
logger.Warn("No questions extracted from query text", "query", qText)
|
||||||
return "No related results from RAG vector storage.", nil
|
return "No related results from RAG vector storage.", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
respVecs := []models.VectorRow{}
|
respVecs := []models.VectorRow{}
|
||||||
for i, q := range questions {
|
for i, q := range questions {
|
||||||
logger.Debug("Processing RAG question", "index", i, "question", q)
|
logger.Debug("Processing RAG question", "index", i, "question", q)
|
||||||
@@ -779,7 +780,6 @@ func chatRagUse(qText string) (string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
logger.Debug("Got embeddings for question", "index", i, "question_len", len(q), "embedding_len", len(emb))
|
logger.Debug("Got embeddings for question", "index", i, "question_len", len(q), "embedding_len", len(emb))
|
||||||
|
|
||||||
// Create EmbeddingResp struct for the search
|
// Create EmbeddingResp struct for the search
|
||||||
embeddingResp := &models.EmbeddingResp{
|
embeddingResp := &models.EmbeddingResp{
|
||||||
Embedding: emb,
|
Embedding: emb,
|
||||||
@@ -793,7 +793,6 @@ func chatRagUse(qText string) (string, error) {
|
|||||||
logger.Debug("RAG search returned vectors", "index", i, "question", q, "vector_count", len(vecs))
|
logger.Debug("RAG search returned vectors", "index", i, "question", q, "vector_count", len(vecs))
|
||||||
respVecs = append(respVecs, vecs...)
|
respVecs = append(respVecs, vecs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get raw text
|
// get raw text
|
||||||
resps := []string{}
|
resps := []string{}
|
||||||
logger.Debug("RAG query final results", "total_vecs_found", len(respVecs))
|
logger.Debug("RAG query final results", "total_vecs_found", len(respVecs))
|
||||||
@@ -801,12 +800,10 @@ func chatRagUse(qText string) (string, error) {
|
|||||||
resps = append(resps, rv.RawText)
|
resps = append(resps, rv.RawText)
|
||||||
logger.Debug("RAG result", "slug", rv.Slug, "filename", rv.FileName, "raw_text_len", len(rv.RawText))
|
logger.Debug("RAG result", "slug", rv.Slug, "filename", rv.FileName, "raw_text_len", len(rv.RawText))
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(resps) == 0 {
|
if len(resps) == 0 {
|
||||||
logger.Info("No RAG results found for query", "original_query", qText, "question_count", len(questions))
|
logger.Info("No RAG results found for query", "original_query", qText, "question_count", len(questions))
|
||||||
return "No related results from RAG vector storage.", nil
|
return "No related results from RAG vector storage.", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := strings.Join(resps, "\n")
|
result := strings.Join(resps, "\n")
|
||||||
logger.Debug("RAG query completed", "result_len", len(result), "response_count", len(resps))
|
logger.Debug("RAG query completed", "result_len", len(result), "response_count", len(resps))
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -829,14 +826,35 @@ func chatWatcher(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inpired by https://github.com/rivo/tview/issues/225
|
||||||
|
func showSpinner() {
|
||||||
|
spinners := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||||
|
var i int
|
||||||
|
for botRespMode {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
spin := i % len(spinners)
|
||||||
|
app.QueueUpdateDraw(func() {
|
||||||
|
textArea.SetTitle(spinners[spin] + " input")
|
||||||
|
})
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
app.QueueUpdateDraw(func() {
|
||||||
|
textArea.SetTitle("input")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func chatRound(r *models.ChatRoundReq) error {
|
func chatRound(r *models.ChatRoundReq) error {
|
||||||
botRespMode = true
|
botRespMode = true
|
||||||
|
go showSpinner()
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
botPersona := cfg.AssistantRole
|
botPersona := cfg.AssistantRole
|
||||||
if cfg.WriteNextMsgAsCompletionAgent != "" {
|
if cfg.WriteNextMsgAsCompletionAgent != "" {
|
||||||
botPersona = cfg.WriteNextMsgAsCompletionAgent
|
botPersona = cfg.WriteNextMsgAsCompletionAgent
|
||||||
}
|
}
|
||||||
defer func() { botRespMode = false }()
|
defer func() {
|
||||||
|
botRespMode = false
|
||||||
|
ClearImageAttachment()
|
||||||
|
}()
|
||||||
// check that there is a model set to use if is not local
|
// check that there is a model set to use if is not local
|
||||||
choseChunkParser()
|
choseChunkParser()
|
||||||
reader, err := chunkParser.FormMsg(r.UserMsg, r.Role, r.Resume)
|
reader, err := chunkParser.FormMsg(r.UserMsg, r.Role, r.Resume)
|
||||||
@@ -855,13 +873,14 @@ func chatRound(r *models.ChatRoundReq) error {
|
|||||||
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{
|
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{
|
||||||
Role: botPersona, Content: "",
|
Role: botPersona, Content: "",
|
||||||
})
|
})
|
||||||
fmt.Fprintf(textView, "\n[-:-:b](%d) ", msgIdx)
|
nl := "\n\n"
|
||||||
fmt.Fprint(textView, roleToIcon(botPersona))
|
prevText := textView.GetText(true)
|
||||||
fmt.Fprint(textView, "[-:-:-]\n")
|
if strings.HasSuffix(prevText, nl) {
|
||||||
if cfg.ThinkUse && !strings.Contains(cfg.CurrentAPI, "v1") {
|
nl = ""
|
||||||
// fmt.Fprint(textView, "<think>")
|
} else if strings.HasSuffix(prevText, "\n") {
|
||||||
chunkChan <- "<think>"
|
nl = "\n"
|
||||||
}
|
}
|
||||||
|
fmt.Fprintf(textView, "%s[-:-:b](%d) %s[-:-:-]\n", nl, msgIdx, roleToIcon(botPersona))
|
||||||
} else {
|
} else {
|
||||||
msgIdx = len(chatBody.Messages) - 1
|
msgIdx = len(chatBody.Messages) - 1
|
||||||
}
|
}
|
||||||
@@ -1198,6 +1217,8 @@ func findCall(msg, toolCall string) bool {
|
|||||||
chatRoundChan <- crr
|
chatRoundChan <- crr
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
// Show tool call progress indicator before execution
|
||||||
|
fmt.Fprintf(textView, "\n[yellow::i][tool: %s...][-:-:-]", fc.Name)
|
||||||
resp := callToolWithAgent(fc.Name, fc.Args)
|
resp := callToolWithAgent(fc.Name, fc.Args)
|
||||||
toolMsg := string(resp) // Remove the "tool response: " prefix and %+v formatting
|
toolMsg := string(resp) // Remove the "tool response: " prefix and %+v formatting
|
||||||
logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg)
|
logger.Info("llm used a tool call", "tool_name", fc.Name, "too_args", fc.Args, "id", fc.ID, "tool_resp", toolMsg)
|
||||||
@@ -1237,7 +1258,6 @@ func chatToTextSlice(messages []models.RoleMsg, showSys bool) []string {
|
|||||||
func chatToText(messages []models.RoleMsg, showSys bool) string {
|
func chatToText(messages []models.RoleMsg, showSys bool) string {
|
||||||
s := chatToTextSlice(messages, showSys)
|
s := chatToTextSlice(messages, showSys)
|
||||||
text := strings.Join(s, "\n")
|
text := strings.Join(s, "\n")
|
||||||
|
|
||||||
// Collapse thinking blocks if enabled
|
// Collapse thinking blocks if enabled
|
||||||
if thinkingCollapsed {
|
if thinkingCollapsed {
|
||||||
text = thinkRE.ReplaceAllStringFunc(text, func(match string) string {
|
text = thinkRE.ReplaceAllStringFunc(text, func(match string) string {
|
||||||
@@ -1261,7 +1281,6 @@ func chatToText(messages []models.RoleMsg, showSys bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ OpenRouterChatAPI = "https://openrouter.ai/api/v1/chat/completions"
|
|||||||
# OpenRouterToken = ""
|
# OpenRouterToken = ""
|
||||||
# embeddings
|
# embeddings
|
||||||
EmbedURL = "http://localhost:8082/v1/embeddings"
|
EmbedURL = "http://localhost:8082/v1/embeddings"
|
||||||
HFToken = false
|
HFToken = ""
|
||||||
#
|
#
|
||||||
ShowSys = true
|
ShowSys = true
|
||||||
LogFile = "log.txt"
|
LogFile = "log.txt"
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ type Config struct {
|
|||||||
UserRole string `toml:"UserRole"`
|
UserRole string `toml:"UserRole"`
|
||||||
ToolRole string `toml:"ToolRole"`
|
ToolRole string `toml:"ToolRole"`
|
||||||
ToolUse bool `toml:"ToolUse"`
|
ToolUse bool `toml:"ToolUse"`
|
||||||
ThinkUse bool `toml:"ThinkUse"`
|
|
||||||
StripThinkingFromAPI bool `toml:"StripThinkingFromAPI"`
|
StripThinkingFromAPI bool `toml:"StripThinkingFromAPI"`
|
||||||
ReasoningEffort string `toml:"ReasoningEffort"`
|
ReasoningEffort string `toml:"ReasoningEffort"`
|
||||||
AssistantRole string `toml:"AssistantRole"`
|
AssistantRole string `toml:"AssistantRole"`
|
||||||
@@ -125,6 +124,9 @@ func LoadConfig(fn string) (*Config, error) {
|
|||||||
if config.CompletionAPI != "" {
|
if config.CompletionAPI != "" {
|
||||||
config.ApiLinks = append(config.ApiLinks, config.CompletionAPI)
|
config.ApiLinks = append(config.ApiLinks, config.CompletionAPI)
|
||||||
}
|
}
|
||||||
|
if config.RAGDir == "" {
|
||||||
|
config.RAGDir = "ragimport"
|
||||||
|
}
|
||||||
// if any value is empty fill with default
|
// if any value is empty fill with default
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,9 +165,6 @@ Those could be switched in program, but also bould be setup in config.
|
|||||||
#### ToolUse
|
#### ToolUse
|
||||||
- Enable or disable explanation of tools to llm, so it could use them.
|
- Enable or disable explanation of tools to llm, so it could use them.
|
||||||
|
|
||||||
#### ThinkUse
|
|
||||||
- Enable or disable insertion of JsonSerializerToken at the beggining of llm resp.
|
|
||||||
|
|
||||||
### StripThinkingFromAPI (`true`)
|
### StripThinkingFromAPI (`true`)
|
||||||
- Strip thinking blocks from messages before sending to LLM. Keeps them in chat history for local viewing but reduces token usage in API calls.
|
- Strip thinking blocks from messages before sending to LLM. Keeps them in chat history for local viewing but reduces token usage in API calls.
|
||||||
|
|
||||||
|
|||||||
107
llm.go
107
llm.go
@@ -11,7 +11,6 @@ import (
|
|||||||
|
|
||||||
var imageAttachmentPath string // Global variable to track image attachment for next message
|
var imageAttachmentPath string // Global variable to track image attachment for next message
|
||||||
var lastImg string // for ctrl+j
|
var lastImg string // for ctrl+j
|
||||||
var RAGMsg = "Retrieved context for user's query:\n"
|
|
||||||
|
|
||||||
// containsToolSysMsg checks if the toolSysMsg already exists in the chat body
|
// containsToolSysMsg checks if the toolSysMsg already exists in the chat body
|
||||||
func containsToolSysMsg() bool {
|
func containsToolSysMsg() bool {
|
||||||
@@ -142,22 +141,6 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
|
|||||||
newMsg = *processMessageTag(&newMsg)
|
newMsg = *processMessageTag(&newMsg)
|
||||||
chatBody.Messages = append(chatBody.Messages, newMsg)
|
chatBody.Messages = append(chatBody.Messages, newMsg)
|
||||||
}
|
}
|
||||||
// if rag - add as system message to avoid conflicts with tool usage
|
|
||||||
if !resume && cfg.RAGEnabled {
|
|
||||||
um := chatBody.Messages[len(chatBody.Messages)-1].Content
|
|
||||||
logger.Debug("RAG is enabled, preparing RAG context", "user_message", um)
|
|
||||||
ragResp, err := chatRagUse(um)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to form a rag msg", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debug("RAG response received", "response_len", len(ragResp),
|
|
||||||
"response_preview", ragResp[:min(len(ragResp), 100)])
|
|
||||||
// Use system role for RAG context to avoid conflicts with tool usage
|
|
||||||
ragMsg := models.RoleMsg{Role: "system", Content: RAGMsg + ragResp}
|
|
||||||
chatBody.Messages = append(chatBody.Messages, ragMsg)
|
|
||||||
logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages))
|
|
||||||
}
|
|
||||||
// sending description of the tools and how to use them
|
// sending description of the tools and how to use them
|
||||||
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() {
|
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() {
|
||||||
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
|
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
|
||||||
@@ -184,9 +167,6 @@ func (lcp LCPCompletion) FormMsg(msg, role string, resume bool) (io.Reader, erro
|
|||||||
botMsgStart := "\n" + botPersona + ":\n"
|
botMsgStart := "\n" + botPersona + ":\n"
|
||||||
prompt += botMsgStart
|
prompt += botMsgStart
|
||||||
}
|
}
|
||||||
if cfg.ThinkUse && !cfg.ToolUse {
|
|
||||||
prompt += "<think>"
|
|
||||||
}
|
|
||||||
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
||||||
"msg", msg, "resume", resume, "prompt", prompt, "multimodal_data_count", len(multimodalData))
|
"msg", msg, "resume", resume, "prompt", prompt, "multimodal_data_count", len(multimodalData))
|
||||||
payload := models.NewLCPReq(prompt, chatBody.Model, multimodalData,
|
payload := models.NewLCPReq(prompt, chatBody.Model, multimodalData,
|
||||||
@@ -304,23 +284,6 @@ func (op LCPChat) FormMsg(msg, role string, resume bool) (io.Reader, error) {
|
|||||||
logger.Debug("LCPChat FormMsg: added message to chatBody", "role", newMsg.Role,
|
logger.Debug("LCPChat FormMsg: added message to chatBody", "role", newMsg.Role,
|
||||||
"content_len", len(newMsg.Content), "message_count_after_add", len(chatBody.Messages))
|
"content_len", len(newMsg.Content), "message_count_after_add", len(chatBody.Messages))
|
||||||
}
|
}
|
||||||
// if rag - add as system message to avoid conflicts with tool usage
|
|
||||||
if !resume && cfg.RAGEnabled {
|
|
||||||
um := chatBody.Messages[len(chatBody.Messages)-1].Content
|
|
||||||
logger.Debug("LCPChat: RAG is enabled, preparing RAG context", "user_message", um)
|
|
||||||
ragResp, err := chatRagUse(um)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("LCPChat: failed to form a rag msg", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debug("LCPChat: RAG response received",
|
|
||||||
"response_len", len(ragResp), "response_preview", ragResp[:min(len(ragResp), 100)])
|
|
||||||
// Use system role for RAG context to avoid conflicts with tool usage
|
|
||||||
ragMsg := models.RoleMsg{Role: "system", Content: RAGMsg + ragResp}
|
|
||||||
chatBody.Messages = append(chatBody.Messages, ragMsg)
|
|
||||||
logger.Debug("LCPChat: RAG message added to chat body", "role", ragMsg.Role,
|
|
||||||
"rag_content_len", len(ragMsg.Content), "message_count_after_rag", len(chatBody.Messages))
|
|
||||||
}
|
|
||||||
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
|
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
|
||||||
// openai /v1/chat does not support custom roles; needs to be user, assistant, system
|
// openai /v1/chat does not support custom roles; needs to be user, assistant, system
|
||||||
// Add persona suffix to the last user message to indicate who the assistant should reply as
|
// Add persona suffix to the last user message to indicate who the assistant should reply as
|
||||||
@@ -392,22 +355,6 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
|
|||||||
newMsg = *processMessageTag(&newMsg)
|
newMsg = *processMessageTag(&newMsg)
|
||||||
chatBody.Messages = append(chatBody.Messages, newMsg)
|
chatBody.Messages = append(chatBody.Messages, newMsg)
|
||||||
}
|
}
|
||||||
// if rag - add as system message to avoid conflicts with tool usage
|
|
||||||
if !resume && cfg.RAGEnabled {
|
|
||||||
um := chatBody.Messages[len(chatBody.Messages)-1].Content
|
|
||||||
logger.Debug("DeepSeekerCompletion: RAG is enabled, preparing RAG context", "user_message", um)
|
|
||||||
ragResp, err := chatRagUse(um)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("DeepSeekerCompletion: failed to form a rag msg", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debug("DeepSeekerCompletion: RAG response received",
|
|
||||||
"response_len", len(ragResp), "response_preview", ragResp[:min(len(ragResp), 100)])
|
|
||||||
// Use system role for RAG context to avoid conflicts with tool usage
|
|
||||||
ragMsg := models.RoleMsg{Role: "system", Content: RAGMsg + ragResp}
|
|
||||||
chatBody.Messages = append(chatBody.Messages, ragMsg)
|
|
||||||
logger.Debug("DeepSeekerCompletion: RAG message added to chat body", "message_count", len(chatBody.Messages))
|
|
||||||
}
|
|
||||||
// sending description of the tools and how to use them
|
// sending description of the tools and how to use them
|
||||||
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() {
|
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() {
|
||||||
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
|
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
|
||||||
@@ -423,9 +370,6 @@ func (ds DeepSeekerCompletion) FormMsg(msg, role string, resume bool) (io.Reader
|
|||||||
botMsgStart := "\n" + botPersona + ":\n"
|
botMsgStart := "\n" + botPersona + ":\n"
|
||||||
prompt += botMsgStart
|
prompt += botMsgStart
|
||||||
}
|
}
|
||||||
if cfg.ThinkUse && !cfg.ToolUse {
|
|
||||||
prompt += "<think>"
|
|
||||||
}
|
|
||||||
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
||||||
"msg", msg, "resume", resume, "prompt", prompt)
|
"msg", msg, "resume", resume, "prompt", prompt)
|
||||||
payload := models.NewDSCompletionReq(prompt, chatBody.Model,
|
payload := models.NewDSCompletionReq(prompt, chatBody.Model,
|
||||||
@@ -480,22 +424,6 @@ func (ds DeepSeekerChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
|
|||||||
newMsg = *processMessageTag(&newMsg)
|
newMsg = *processMessageTag(&newMsg)
|
||||||
chatBody.Messages = append(chatBody.Messages, newMsg)
|
chatBody.Messages = append(chatBody.Messages, newMsg)
|
||||||
}
|
}
|
||||||
// if rag - add as system message to avoid conflicts with tool usage
|
|
||||||
if !resume && cfg.RAGEnabled {
|
|
||||||
um := chatBody.Messages[len(chatBody.Messages)-1].Content
|
|
||||||
logger.Debug("RAG is enabled, preparing RAG context", "user_message", um)
|
|
||||||
ragResp, err := chatRagUse(um)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to form a rag msg", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debug("RAG response received", "response_len", len(ragResp),
|
|
||||||
"response_preview", ragResp[:min(len(ragResp), 100)])
|
|
||||||
// Use system role for RAG context to avoid conflicts with tool usage
|
|
||||||
ragMsg := models.RoleMsg{Role: "system", Content: RAGMsg + ragResp}
|
|
||||||
chatBody.Messages = append(chatBody.Messages, ragMsg)
|
|
||||||
logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages))
|
|
||||||
}
|
|
||||||
// Create copy of chat body with standardized user role
|
// Create copy of chat body with standardized user role
|
||||||
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
|
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
|
||||||
// Add persona suffix to the last user message to indicate who the assistant should reply as
|
// Add persona suffix to the last user message to indicate who the assistant should reply as
|
||||||
@@ -558,22 +486,6 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
|
|||||||
newMsg = *processMessageTag(&newMsg)
|
newMsg = *processMessageTag(&newMsg)
|
||||||
chatBody.Messages = append(chatBody.Messages, newMsg)
|
chatBody.Messages = append(chatBody.Messages, newMsg)
|
||||||
}
|
}
|
||||||
// if rag - add as system message to avoid conflicts with tool usage
|
|
||||||
if !resume && cfg.RAGEnabled {
|
|
||||||
um := chatBody.Messages[len(chatBody.Messages)-1].Content
|
|
||||||
logger.Debug("RAG is enabled, preparing RAG context", "user_message", um)
|
|
||||||
ragResp, err := chatRagUse(um)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to form a rag msg", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debug("RAG response received", "response_len",
|
|
||||||
len(ragResp), "response_preview", ragResp[:min(len(ragResp), 100)])
|
|
||||||
// Use system role for RAG context to avoid conflicts with tool usage
|
|
||||||
ragMsg := models.RoleMsg{Role: "system", Content: RAGMsg + ragResp}
|
|
||||||
chatBody.Messages = append(chatBody.Messages, ragMsg)
|
|
||||||
logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages))
|
|
||||||
}
|
|
||||||
// sending description of the tools and how to use them
|
// sending description of the tools and how to use them
|
||||||
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() {
|
if cfg.ToolUse && !resume && role == cfg.UserRole && !containsToolSysMsg() {
|
||||||
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
|
chatBody.Messages = append(chatBody.Messages, models.RoleMsg{Role: cfg.ToolRole, Content: toolSysMsg})
|
||||||
@@ -589,9 +501,6 @@ func (or OpenRouterCompletion) FormMsg(msg, role string, resume bool) (io.Reader
|
|||||||
botMsgStart := "\n" + botPersona + ":\n"
|
botMsgStart := "\n" + botPersona + ":\n"
|
||||||
prompt += botMsgStart
|
prompt += botMsgStart
|
||||||
}
|
}
|
||||||
if cfg.ThinkUse && !cfg.ToolUse {
|
|
||||||
prompt += "<think>"
|
|
||||||
}
|
|
||||||
stopSlice := chatBody.MakeStopSliceExcluding("", listChatRoles())
|
stopSlice := chatBody.MakeStopSliceExcluding("", listChatRoles())
|
||||||
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
logger.Debug("checking prompt for /completion", "tool_use", cfg.ToolUse,
|
||||||
"msg", msg, "resume", resume, "prompt", prompt, "stop_strings", stopSlice)
|
"msg", msg, "resume", resume, "prompt", prompt, "stop_strings", stopSlice)
|
||||||
@@ -679,22 +588,6 @@ func (or OpenRouterChat) FormMsg(msg, role string, resume bool) (io.Reader, erro
|
|||||||
newMsg = *processMessageTag(&newMsg)
|
newMsg = *processMessageTag(&newMsg)
|
||||||
chatBody.Messages = append(chatBody.Messages, newMsg)
|
chatBody.Messages = append(chatBody.Messages, newMsg)
|
||||||
}
|
}
|
||||||
// if rag - add as system message to avoid conflicts with tool usage
|
|
||||||
if !resume && cfg.RAGEnabled {
|
|
||||||
um := chatBody.Messages[len(chatBody.Messages)-1].Content
|
|
||||||
logger.Debug("RAG is enabled, preparing RAG context", "user_message", um)
|
|
||||||
ragResp, err := chatRagUse(um)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("failed to form a rag msg", "error", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
logger.Debug("RAG response received", "response_len", len(ragResp),
|
|
||||||
"response_preview", ragResp[:min(len(ragResp), 100)])
|
|
||||||
// Use system role for RAG context to avoid conflicts with tool usage
|
|
||||||
ragMsg := models.RoleMsg{Role: "system", Content: RAGMsg + ragResp}
|
|
||||||
chatBody.Messages = append(chatBody.Messages, ragMsg)
|
|
||||||
logger.Debug("RAG message added to chat body", "message_count", len(chatBody.Messages))
|
|
||||||
}
|
|
||||||
// Create copy of chat body with standardized user role
|
// Create copy of chat body with standardized user role
|
||||||
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
|
filteredMessages, _ := filterMessagesForCurrentCharacter(chatBody.Messages)
|
||||||
// Add persona suffix to the last user message to indicate who the assistant should reply as
|
// Add persona suffix to the last user message to indicate who the assistant should reply as
|
||||||
|
|||||||
@@ -241,8 +241,7 @@ func (m *RoleMsg) ToText(i int) string {
|
|||||||
}
|
}
|
||||||
finalContent.WriteString(contentStr)
|
finalContent.WriteString(contentStr)
|
||||||
if m.Stats != nil {
|
if m.Stats != nil {
|
||||||
finalContent.WriteString(fmt.Sprintf("\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]",
|
fmt.Fprintf(&finalContent, "\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]", m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec)
|
||||||
m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec))
|
|
||||||
}
|
}
|
||||||
textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String())
|
textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String())
|
||||||
return strings.ReplaceAll(textMsg, "\n\n", "\n")
|
return strings.ReplaceAll(textMsg, "\n\n", "\n")
|
||||||
|
|||||||
20
popups.go
20
popups.go
@@ -51,7 +51,7 @@ func showModelSelectionPopup() {
|
|||||||
// Find the current model index to set as selected
|
// Find the current model index to set as selected
|
||||||
currentModelIndex := -1
|
currentModelIndex := -1
|
||||||
for i, model := range modelList {
|
for i, model := range modelList {
|
||||||
if model == chatBody.Model {
|
if strings.TrimPrefix(model, "(loaded) ") == chatBody.Model {
|
||||||
currentModelIndex = i
|
currentModelIndex = i
|
||||||
}
|
}
|
||||||
modelListWidget.AddItem(model, "", 0, nil)
|
modelListWidget.AddItem(model, "", 0, nil)
|
||||||
@@ -65,16 +65,19 @@ func showModelSelectionPopup() {
|
|||||||
chatBody.Model = modelName
|
chatBody.Model = modelName
|
||||||
cfg.CurrentModel = chatBody.Model
|
cfg.CurrentModel = chatBody.Model
|
||||||
pages.RemovePage("modelSelectionPopup")
|
pages.RemovePage("modelSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
updateCachedModelColor()
|
updateCachedModelColor()
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
})
|
})
|
||||||
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
modelListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
pages.RemovePage("modelSelectionPopup")
|
pages.RemovePage("modelSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||||
pages.RemovePage("modelSelectionPopup")
|
pages.RemovePage("modelSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
@@ -160,6 +163,7 @@ func showAPILinkSelectionPopup() {
|
|||||||
cfg.CurrentModel = chatBody.Model
|
cfg.CurrentModel = chatBody.Model
|
||||||
}
|
}
|
||||||
pages.RemovePage("apiLinkSelectionPopup")
|
pages.RemovePage("apiLinkSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
choseChunkParser()
|
choseChunkParser()
|
||||||
updateCachedModelColor()
|
updateCachedModelColor()
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
@@ -167,10 +171,12 @@ func showAPILinkSelectionPopup() {
|
|||||||
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
apiListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
pages.RemovePage("apiLinkSelectionPopup")
|
pages.RemovePage("apiLinkSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||||
pages.RemovePage("apiLinkSelectionPopup")
|
pages.RemovePage("apiLinkSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
@@ -230,6 +236,7 @@ func showUserRoleSelectionPopup() {
|
|||||||
textView.SetText(chatToText(filtered, cfg.ShowSys))
|
textView.SetText(chatToText(filtered, cfg.ShowSys))
|
||||||
// Remove the popup page
|
// Remove the popup page
|
||||||
pages.RemovePage("userRoleSelectionPopup")
|
pages.RemovePage("userRoleSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
// Update the status line to reflect the change
|
// Update the status line to reflect the change
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
colorText()
|
colorText()
|
||||||
@@ -237,10 +244,12 @@ func showUserRoleSelectionPopup() {
|
|||||||
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
pages.RemovePage("userRoleSelectionPopup")
|
pages.RemovePage("userRoleSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||||
pages.RemovePage("userRoleSelectionPopup")
|
pages.RemovePage("userRoleSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
@@ -303,16 +312,19 @@ func showBotRoleSelectionPopup() {
|
|||||||
cfg.WriteNextMsgAsCompletionAgent = mainText
|
cfg.WriteNextMsgAsCompletionAgent = mainText
|
||||||
// Remove the popup page
|
// Remove the popup page
|
||||||
pages.RemovePage("botRoleSelectionPopup")
|
pages.RemovePage("botRoleSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
// Update the status line to reflect the change
|
// Update the status line to reflect the change
|
||||||
updateStatusLine()
|
updateStatusLine()
|
||||||
})
|
})
|
||||||
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
roleListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
pages.RemovePage("botRoleSelectionPopup")
|
pages.RemovePage("botRoleSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||||
pages.RemovePage("botRoleSelectionPopup")
|
pages.RemovePage("botRoleSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
@@ -364,14 +376,17 @@ func showFileCompletionPopup(filter string) {
|
|||||||
textArea.SetText(before+mainText, true)
|
textArea.SetText(before+mainText, true)
|
||||||
}
|
}
|
||||||
pages.RemovePage("fileCompletionPopup")
|
pages.RemovePage("fileCompletionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
})
|
})
|
||||||
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
widget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
pages.RemovePage("fileCompletionPopup")
|
pages.RemovePage("fileCompletionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||||
pages.RemovePage("fileCompletionPopup")
|
pages.RemovePage("fileCompletionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
@@ -484,14 +499,17 @@ func showColorschemeSelectionPopup() {
|
|||||||
}
|
}
|
||||||
// Remove the popup page
|
// Remove the popup page
|
||||||
pages.RemovePage("colorschemeSelectionPopup")
|
pages.RemovePage("colorschemeSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
})
|
})
|
||||||
schemeListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
schemeListWidget.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||||
if event.Key() == tcell.KeyEscape {
|
if event.Key() == tcell.KeyEscape {
|
||||||
pages.RemovePage("colorschemeSelectionPopup")
|
pages.RemovePage("colorschemeSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'x' {
|
||||||
pages.RemovePage("colorschemeSelectionPopup")
|
pages.RemovePage("colorschemeSelectionPopup")
|
||||||
|
app.SetFocus(textArea)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return event
|
return event
|
||||||
|
|||||||
@@ -115,9 +115,6 @@ func makePropsTable(props map[string]float32) *tview.Table {
|
|||||||
row++
|
row++
|
||||||
}
|
}
|
||||||
// Add checkboxes
|
// Add checkboxes
|
||||||
addCheckboxRow("Insert <think> tag (/completion only)", cfg.ThinkUse, func(checked bool) {
|
|
||||||
cfg.ThinkUse = checked
|
|
||||||
})
|
|
||||||
addCheckboxRow("RAG use", cfg.RAGEnabled, func(checked bool) {
|
addCheckboxRow("RAG use", cfg.RAGEnabled, func(checked bool) {
|
||||||
cfg.RAGEnabled = checked
|
cfg.RAGEnabled = checked
|
||||||
})
|
})
|
||||||
|
|||||||
559
rag/rag.go
559
rag/rag.go
@@ -9,6 +9,8 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -23,13 +25,13 @@ var (
|
|||||||
ErrRAGStatus = "some error occurred; failed to transfer data to vector db"
|
ErrRAGStatus = "some error occurred; failed to transfer data to vector db"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type RAG struct {
|
type RAG struct {
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
store storage.FullRepo
|
store storage.FullRepo
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
embedder Embedder
|
embedder Embedder
|
||||||
storage *VectorStorage
|
storage *VectorStorage
|
||||||
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(l *slog.Logger, s storage.FullRepo, cfg *config.Config) *RAG {
|
func New(l *slog.Logger, s storage.FullRepo, cfg *config.Config) *RAG {
|
||||||
@@ -54,6 +56,8 @@ func wordCounter(sentence string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RAG) LoadRAG(fpath string) error {
|
func (r *RAG) LoadRAG(fpath string) error {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
data, err := os.ReadFile(fpath)
|
data, err := os.ReadFile(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -63,9 +67,7 @@ func (r *RAG) LoadRAG(fpath string) error {
|
|||||||
case LongJobStatusCh <- LoadedFileRAGStatus:
|
case LongJobStatusCh <- LoadedFileRAGStatus:
|
||||||
default:
|
default:
|
||||||
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", LoadedFileRAGStatus)
|
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", LoadedFileRAGStatus)
|
||||||
// Channel is full or closed, ignore the message to prevent panic
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fileText := string(data)
|
fileText := string(data)
|
||||||
tokenizer, err := english.NewSentenceTokenizer(nil)
|
tokenizer, err := english.NewSentenceTokenizer(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -76,19 +78,16 @@ func (r *RAG) LoadRAG(fpath string) error {
|
|||||||
for i, s := range sentences {
|
for i, s := range sentences {
|
||||||
sents[i] = s.Text
|
sents[i] = s.Text
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group sentences into paragraphs based on word limit
|
// Group sentences into paragraphs based on word limit
|
||||||
paragraphs := []string{}
|
paragraphs := []string{}
|
||||||
par := strings.Builder{}
|
par := strings.Builder{}
|
||||||
for i := 0; i < len(sents); i++ {
|
for i := 0; i < len(sents); i++ {
|
||||||
// Only add sentences that aren't empty
|
|
||||||
if strings.TrimSpace(sents[i]) != "" {
|
if strings.TrimSpace(sents[i]) != "" {
|
||||||
if par.Len() > 0 {
|
if par.Len() > 0 {
|
||||||
par.WriteString(" ") // Add space between sentences
|
par.WriteString(" ")
|
||||||
}
|
}
|
||||||
par.WriteString(sents[i])
|
par.WriteString(sents[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
if wordCounter(par.String()) > int(r.cfg.RAGWordLimit) {
|
if wordCounter(par.String()) > int(r.cfg.RAGWordLimit) {
|
||||||
paragraph := strings.TrimSpace(par.String())
|
paragraph := strings.TrimSpace(par.String())
|
||||||
if paragraph != "" {
|
if paragraph != "" {
|
||||||
@@ -97,7 +96,6 @@ func (r *RAG) LoadRAG(fpath string) error {
|
|||||||
par.Reset()
|
par.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle any remaining content in the paragraph buffer
|
// Handle any remaining content in the paragraph buffer
|
||||||
if par.Len() > 0 {
|
if par.Len() > 0 {
|
||||||
paragraph := strings.TrimSpace(par.String())
|
paragraph := strings.TrimSpace(par.String())
|
||||||
@@ -105,215 +103,82 @@ func (r *RAG) LoadRAG(fpath string) error {
|
|||||||
paragraphs = append(paragraphs, paragraph)
|
paragraphs = append(paragraphs, paragraph)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust batch size if needed
|
// Adjust batch size if needed
|
||||||
if len(paragraphs) < r.cfg.RAGBatchSize && len(paragraphs) > 0 {
|
if len(paragraphs) < r.cfg.RAGBatchSize && len(paragraphs) > 0 {
|
||||||
r.cfg.RAGBatchSize = len(paragraphs)
|
r.cfg.RAGBatchSize = len(paragraphs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(paragraphs) == 0 {
|
if len(paragraphs) == 0 {
|
||||||
return errors.New("no valid paragraphs found in file")
|
return errors.New("no valid paragraphs found in file")
|
||||||
}
|
}
|
||||||
|
// Process paragraphs in batches synchronously
|
||||||
var (
|
batchCount := 0
|
||||||
maxChSize = 100
|
for i := 0; i < len(paragraphs); i += r.cfg.RAGBatchSize {
|
||||||
left = 0
|
end := i + r.cfg.RAGBatchSize
|
||||||
right = r.cfg.RAGBatchSize
|
if end > len(paragraphs) {
|
||||||
batchCh = make(chan map[int][]string, maxChSize)
|
end = len(paragraphs)
|
||||||
vectorCh = make(chan []models.VectorRow, maxChSize)
|
|
||||||
errCh = make(chan error, 1)
|
|
||||||
wg = new(sync.WaitGroup)
|
|
||||||
lock = new(sync.Mutex)
|
|
||||||
)
|
|
||||||
|
|
||||||
defer close(errCh)
|
|
||||||
defer close(batchCh)
|
|
||||||
|
|
||||||
// Fill input channel with batches
|
|
||||||
ctn := 0
|
|
||||||
totalParagraphs := len(paragraphs)
|
|
||||||
for {
|
|
||||||
if right > totalParagraphs {
|
|
||||||
batchCh <- map[int][]string{left: paragraphs[left:]}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
batchCh <- map[int][]string{left: paragraphs[left:right]}
|
batch := paragraphs[i:end]
|
||||||
left, right = right, right+r.cfg.RAGBatchSize
|
batchCount++
|
||||||
ctn++
|
// Filter empty paragraphs
|
||||||
}
|
nonEmptyBatch := make([]string, 0, len(batch))
|
||||||
|
for _, p := range batch {
|
||||||
finishedBatchesMsg := fmt.Sprintf("finished batching batches#: %d; paragraphs: %d; sentences: %d\n", ctn+1, len(paragraphs), len(sents))
|
if strings.TrimSpace(p) != "" {
|
||||||
r.logger.Debug(finishedBatchesMsg)
|
nonEmptyBatch = append(nonEmptyBatch, strings.TrimSpace(p))
|
||||||
select {
|
}
|
||||||
case LongJobStatusCh <- finishedBatchesMsg:
|
}
|
||||||
default:
|
if len(nonEmptyBatch) == 0 {
|
||||||
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", finishedBatchesMsg)
|
continue
|
||||||
// Channel is full or closed, ignore the message to prevent panic
|
}
|
||||||
}
|
// Embed the batch
|
||||||
|
embeddings, err := r.embedder.EmbedSlice(nonEmptyBatch)
|
||||||
// Start worker goroutines with WaitGroup
|
|
||||||
wg.Add(int(r.cfg.RAGWorkers))
|
|
||||||
for w := 0; w < int(r.cfg.RAGWorkers); w++ {
|
|
||||||
go func(workerID int) {
|
|
||||||
defer wg.Done()
|
|
||||||
r.batchToVectorAsync(lock, workerID, batchCh, vectorCh, errCh, path.Base(fpath))
|
|
||||||
}(w)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a goroutine to close the batchCh when all batches are sent
|
|
||||||
go func() {
|
|
||||||
wg.Wait()
|
|
||||||
close(vectorCh) // Close vectorCh when all workers are done
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Check for errors from workers
|
|
||||||
// Use a non-blocking check for errors
|
|
||||||
select {
|
|
||||||
case err := <-errCh:
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Error("error during RAG processing", "error", err)
|
r.logger.Error("failed to embed batch", "error", err, "batch", batchCount)
|
||||||
|
select {
|
||||||
|
case LongJobStatusCh <- ErrRAGStatus:
|
||||||
|
default:
|
||||||
|
r.logger.Warn("LongJobStatusCh channel full, dropping message")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to embed batch %d: %w", batchCount, err)
|
||||||
|
}
|
||||||
|
if len(embeddings) != len(nonEmptyBatch) {
|
||||||
|
err := errors.New("embedding count mismatch")
|
||||||
|
r.logger.Error("embedding mismatch", "expected", len(nonEmptyBatch), "got", len(embeddings))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
// Write vectors to storage
|
||||||
// No immediate error, continue
|
filename := path.Base(fpath)
|
||||||
}
|
for j, text := range nonEmptyBatch {
|
||||||
|
vector := models.VectorRow{
|
||||||
// Write vectors to storage - this will block until vectorCh is closed
|
Embeddings: embeddings[j],
|
||||||
return r.writeVectors(vectorCh)
|
RawText: text,
|
||||||
}
|
Slug: fmt.Sprintf("%s_%d_%d", filename, batchCount, j),
|
||||||
|
FileName: filename,
|
||||||
func (r *RAG) writeVectors(vectorCh chan []models.VectorRow) error {
|
|
||||||
for {
|
|
||||||
for batch := range vectorCh {
|
|
||||||
for _, vector := range batch {
|
|
||||||
if err := r.storage.WriteVector(&vector); err != nil {
|
|
||||||
r.logger.Error("failed to write vector to DB", "error", err, "slug", vector.Slug)
|
|
||||||
select {
|
|
||||||
case LongJobStatusCh <- ErrRAGStatus:
|
|
||||||
default:
|
|
||||||
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", ErrRAGStatus)
|
|
||||||
// Channel is full or closed, ignore the message to prevent panic
|
|
||||||
}
|
|
||||||
return err // Stop the entire RAG operation on DB error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
r.logger.Debug("wrote batch to db", "size", len(batch), "vector_chan_len", len(vectorCh))
|
if err := r.storage.WriteVector(&vector); err != nil {
|
||||||
if len(vectorCh) == 0 {
|
r.logger.Error("failed to write vector to DB", "error", err, "slug", vector.Slug)
|
||||||
r.logger.Debug("finished writing vectors")
|
|
||||||
select {
|
select {
|
||||||
case LongJobStatusCh <- FinishedRAGStatus:
|
case LongJobStatusCh <- ErrRAGStatus:
|
||||||
default:
|
default:
|
||||||
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", FinishedRAGStatus)
|
r.logger.Warn("LongJobStatusCh channel full, dropping message")
|
||||||
// Channel is full or closed, ignore the message to prevent panic
|
|
||||||
}
|
}
|
||||||
return nil
|
return fmt.Errorf("failed to write vector: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
r.logger.Debug("wrote batch to db", "batch", batchCount, "size", len(nonEmptyBatch))
|
||||||
}
|
// Send progress status
|
||||||
|
statusMsg := fmt.Sprintf("processed batch %d/%d", batchCount, (len(paragraphs)+r.cfg.RAGBatchSize-1)/r.cfg.RAGBatchSize)
|
||||||
func (r *RAG) batchToVectorAsync(lock *sync.Mutex, id int, inputCh <-chan map[int][]string,
|
|
||||||
vectorCh chan<- []models.VectorRow, errCh chan error, filename string) {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
// For errCh, make sure we only send if there's actually an error and the channel can accept it
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case errCh <- err:
|
|
||||||
default:
|
|
||||||
// errCh might be full or closed, log but don't panic
|
|
||||||
r.logger.Warn("errCh channel full or closed, skipping error propagation", "worker", id, "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
lock.Lock()
|
|
||||||
if len(inputCh) == 0 {
|
|
||||||
lock.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case linesMap := <-inputCh:
|
|
||||||
for leftI, lines := range linesMap {
|
|
||||||
if err := r.fetchEmb(lines, errCh, vectorCh, fmt.Sprintf("%s_%d", filename, leftI), filename); err != nil {
|
|
||||||
r.logger.Error("error fetching embeddings", "error", err, "worker", id)
|
|
||||||
lock.Unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lock.Unlock()
|
|
||||||
case err = <-errCh:
|
|
||||||
r.logger.Error("got an error from error channel", "error", err)
|
|
||||||
lock.Unlock()
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
lock.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
r.logger.Debug("processed batch", "batches#", len(inputCh), "worker#", id)
|
|
||||||
statusMsg := fmt.Sprintf("converted to vector; batches: %d, worker#: %d", len(inputCh), id)
|
|
||||||
select {
|
select {
|
||||||
case LongJobStatusCh <- statusMsg:
|
case LongJobStatusCh <- statusMsg:
|
||||||
default:
|
default:
|
||||||
r.logger.Warn("LongJobStatusCh channel full or closed, dropping status message", "message", statusMsg)
|
r.logger.Warn("LongJobStatusCh channel full, dropping message")
|
||||||
// Channel is full or closed, ignore the message to prevent panic
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
r.logger.Debug("finished writing vectors", "batches", batchCount)
|
||||||
|
select {
|
||||||
func (r *RAG) fetchEmb(lines []string, errCh chan error, vectorCh chan<- []models.VectorRow, slug, filename string) error {
|
case LongJobStatusCh <- FinishedRAGStatus:
|
||||||
// Filter out empty lines before sending to embedder
|
default:
|
||||||
nonEmptyLines := make([]string, 0, len(lines))
|
r.logger.Warn("LongJobStatusCh channel is full or closed, dropping status message", "message", FinishedRAGStatus)
|
||||||
for _, line := range lines {
|
|
||||||
trimmed := strings.TrimSpace(line)
|
|
||||||
if trimmed != "" {
|
|
||||||
nonEmptyLines = append(nonEmptyLines, trimmed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if no non-empty lines
|
|
||||||
if len(nonEmptyLines) == 0 {
|
|
||||||
// Send empty result but don't error
|
|
||||||
vectorCh <- []models.VectorRow{}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
embeddings, err := r.embedder.EmbedSlice(nonEmptyLines)
|
|
||||||
if err != nil {
|
|
||||||
r.logger.Error("failed to embed lines", "err", err.Error())
|
|
||||||
errCh <- err
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(embeddings) == 0 {
|
|
||||||
err := errors.New("no embeddings returned")
|
|
||||||
r.logger.Error("empty embeddings")
|
|
||||||
errCh <- err
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(embeddings) != len(nonEmptyLines) {
|
|
||||||
err := errors.New("mismatch between number of lines and embeddings returned")
|
|
||||||
r.logger.Error("embedding mismatch", "err", err.Error())
|
|
||||||
errCh <- err
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a VectorRow for each line in the batch
|
|
||||||
vectors := make([]models.VectorRow, len(nonEmptyLines))
|
|
||||||
for i, line := range nonEmptyLines {
|
|
||||||
vectors[i] = models.VectorRow{
|
|
||||||
Embeddings: embeddings[i],
|
|
||||||
RawText: line,
|
|
||||||
Slug: fmt.Sprintf("%s_%d", slug, i),
|
|
||||||
FileName: filename,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vectorCh <- vectors
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,3 +197,309 @@ func (r *RAG) ListLoaded() ([]string, error) {
|
|||||||
func (r *RAG) RemoveFile(filename string) error {
|
func (r *RAG) RemoveFile(filename string) error {
|
||||||
return r.storage.RemoveEmbByFileName(filename)
|
return r.storage.RemoveEmbByFileName(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
queryRefinementPattern = regexp.MustCompile(`(?i)(based on my (vector db|vector db|vector database|rags?|past (conversations?|chat|messages?))|from my (files?|documents?|data|information|memory)|search (in|my) (vector db|database|rags?)|rag search for)`)
|
||||||
|
importantKeywords = []string{"project", "architecture", "code", "file", "chat", "conversation", "topic", "summary", "details", "history", "previous", "my", "user", "me"}
|
||||||
|
stopWords = []string{"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by", "from", "up", "down", "left", "right"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (r *RAG) RefineQuery(query string) string {
|
||||||
|
original := query
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
|
||||||
|
if len(query) == 0 {
|
||||||
|
return original
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(query) <= 3 {
|
||||||
|
return original
|
||||||
|
}
|
||||||
|
|
||||||
|
query = strings.ToLower(query)
|
||||||
|
|
||||||
|
for _, stopWord := range stopWords {
|
||||||
|
wordPattern := `\b` + stopWord + `\b`
|
||||||
|
re := regexp.MustCompile(wordPattern)
|
||||||
|
query = re.ReplaceAllString(query, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
query = strings.TrimSpace(query)
|
||||||
|
|
||||||
|
if len(query) < 5 {
|
||||||
|
return original
|
||||||
|
}
|
||||||
|
|
||||||
|
if queryRefinementPattern.MatchString(original) {
|
||||||
|
cleaned := queryRefinementPattern.ReplaceAllString(original, "")
|
||||||
|
cleaned = strings.TrimSpace(cleaned)
|
||||||
|
if len(cleaned) >= 5 {
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query = r.extractImportantPhrases(query)
|
||||||
|
|
||||||
|
if len(query) < 5 {
|
||||||
|
return original
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RAG) extractImportantPhrases(query string) string {
|
||||||
|
words := strings.Fields(query)
|
||||||
|
|
||||||
|
var important []string
|
||||||
|
for _, word := range words {
|
||||||
|
word = strings.Trim(word, ".,!?;:'\"()[]{}")
|
||||||
|
|
||||||
|
isImportant := false
|
||||||
|
for _, kw := range importantKeywords {
|
||||||
|
if strings.Contains(strings.ToLower(word), kw) {
|
||||||
|
isImportant = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isImportant || len(word) > 3 {
|
||||||
|
important = append(important, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(important) == 0 {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(important, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RAG) GenerateQueryVariations(query string) []string {
|
||||||
|
variations := []string{query}
|
||||||
|
|
||||||
|
if len(query) < 5 {
|
||||||
|
return variations
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Fields(query)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return variations
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
trimmed := strings.Join(parts[:len(parts)-1], " ")
|
||||||
|
if len(trimmed) >= 5 {
|
||||||
|
variations = append(variations, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
trimmed := strings.Join(parts[1:], " ")
|
||||||
|
if len(trimmed) >= 5 {
|
||||||
|
variations = append(variations, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(query, " explanation") {
|
||||||
|
variations = append(variations, query+" explanation")
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(query, "what is ") {
|
||||||
|
variations = append(variations, "what is "+query)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(query, " details") {
|
||||||
|
variations = append(variations, query+" details")
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(query, " summary") {
|
||||||
|
variations = append(variations, query+" summary")
|
||||||
|
}
|
||||||
|
|
||||||
|
return variations
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RAG) RerankResults(results []models.VectorRow, query string) []models.VectorRow {
|
||||||
|
type scoredResult struct {
|
||||||
|
row models.VectorRow
|
||||||
|
distance float32
|
||||||
|
}
|
||||||
|
|
||||||
|
scored := make([]scoredResult, 0, len(results))
|
||||||
|
|
||||||
|
for i := range results {
|
||||||
|
row := results[i]
|
||||||
|
|
||||||
|
score := float32(0)
|
||||||
|
|
||||||
|
rawTextLower := strings.ToLower(row.RawText)
|
||||||
|
queryLower := strings.ToLower(query)
|
||||||
|
|
||||||
|
if strings.Contains(rawTextLower, queryLower) {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
|
||||||
|
queryWords := strings.Fields(queryLower)
|
||||||
|
matchCount := 0
|
||||||
|
for _, word := range queryWords {
|
||||||
|
if len(word) > 2 && strings.Contains(rawTextLower, word) {
|
||||||
|
matchCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(queryWords) > 0 {
|
||||||
|
score += float32(matchCount) / float32(len(queryWords)) * 5
|
||||||
|
}
|
||||||
|
|
||||||
|
if row.FileName == "chat" || strings.Contains(strings.ToLower(row.FileName), "conversation") {
|
||||||
|
score += 3
|
||||||
|
}
|
||||||
|
|
||||||
|
distance := row.Distance - score/100
|
||||||
|
|
||||||
|
scored = append(scored, scoredResult{row: row, distance: distance})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(scored, func(i, j int) bool {
|
||||||
|
return scored[i].distance < scored[j].distance
|
||||||
|
})
|
||||||
|
|
||||||
|
unique := make([]models.VectorRow, 0)
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for i := range scored {
|
||||||
|
if !seen[scored[i].row.Slug] {
|
||||||
|
seen[scored[i].row.Slug] = true
|
||||||
|
unique = append(unique, scored[i].row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unique) > 10 {
|
||||||
|
unique = unique[:10]
|
||||||
|
}
|
||||||
|
|
||||||
|
return unique
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RAG) SynthesizeAnswer(results []models.VectorRow, query string) (string, error) {
|
||||||
|
if len(results) == 0 {
|
||||||
|
return "No relevant information found in the vector database.", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextBuilder strings.Builder
|
||||||
|
contextBuilder.WriteString("User Query: ")
|
||||||
|
contextBuilder.WriteString(query)
|
||||||
|
contextBuilder.WriteString("\n\nRetrieved Context:\n")
|
||||||
|
|
||||||
|
for i, row := range results {
|
||||||
|
contextBuilder.WriteString(fmt.Sprintf("[Source %d: %s]\n", i+1, row.FileName))
|
||||||
|
contextBuilder.WriteString(row.RawText)
|
||||||
|
contextBuilder.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
contextBuilder.WriteString("Instructions: ")
|
||||||
|
contextBuilder.WriteString("Based on the retrieved context above, provide a concise, coherent answer to the user's query. ")
|
||||||
|
contextBuilder.WriteString("Extract only the most relevant information. ")
|
||||||
|
contextBuilder.WriteString("If no relevant information is found, state that clearly. ")
|
||||||
|
contextBuilder.WriteString("Cite sources by filename when relevant. ")
|
||||||
|
contextBuilder.WriteString("Do not include unnecessary preamble or explanations.")
|
||||||
|
|
||||||
|
synthesisPrompt := contextBuilder.String()
|
||||||
|
|
||||||
|
emb, err := r.LineToVector(synthesisPrompt)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("failed to embed synthesis prompt", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
embResp := &models.EmbeddingResp{
|
||||||
|
Embedding: emb,
|
||||||
|
Index: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
topResults, err := r.SearchEmb(embResp)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("failed to search for synthesis context", "error", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(topResults) > 0 && topResults[0].RawText != synthesisPrompt {
|
||||||
|
return topResults[0].RawText, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var finalAnswer strings.Builder
|
||||||
|
finalAnswer.WriteString("Based on the retrieved context:\n\n")
|
||||||
|
|
||||||
|
for i, row := range results {
|
||||||
|
if i >= 5 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
finalAnswer.WriteString(fmt.Sprintf("- From %s: %s\n", row.FileName, truncateString(row.RawText, 200)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalAnswer.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncateString(s string, maxLen int) string {
|
||||||
|
if len(s) <= maxLen {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:maxLen] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RAG) Search(query string, limit int) ([]models.VectorRow, error) {
|
||||||
|
refined := r.RefineQuery(query)
|
||||||
|
variations := r.GenerateQueryVariations(refined)
|
||||||
|
|
||||||
|
allResults := make([]models.VectorRow, 0)
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, q := range variations {
|
||||||
|
emb, err := r.LineToVector(q)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("failed to embed query variation", "error", err, "query", q)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
embResp := &models.EmbeddingResp{
|
||||||
|
Embedding: emb,
|
||||||
|
Index: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := r.SearchEmb(embResp)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Error("failed to search embeddings", "error", err, "query", q)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range results {
|
||||||
|
if !seen[row.Slug] {
|
||||||
|
seen[row.Slug] = true
|
||||||
|
allResults = append(allResults, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reranked := r.RerankResults(allResults, query)
|
||||||
|
|
||||||
|
if len(reranked) > limit {
|
||||||
|
reranked = reranked[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
return reranked, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ragInstance *RAG
|
||||||
|
ragOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(c *config.Config, l *slog.Logger, s storage.FullRepo) error {
|
||||||
|
ragOnce.Do(func() {
|
||||||
|
if c == nil || l == nil || s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ragInstance = New(l, s, c)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetInstance() *RAG {
|
||||||
|
return ragInstance
|
||||||
|
}
|
||||||
|
|||||||
153
tables.go
153
tables.go
@@ -236,9 +236,20 @@ func makeChatTable(chatMap map[string]models.Chat) *tview.Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// nolint:unused
|
// nolint:unused
|
||||||
|
func formatSize(size int64) string {
|
||||||
|
units := []string{"B", "KB", "MB", "GB", "TB"}
|
||||||
|
i := 0
|
||||||
|
s := float64(size)
|
||||||
|
for s >= 1024 && i < len(units)-1 {
|
||||||
|
s /= 1024
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f%s", s, units[i])
|
||||||
|
}
|
||||||
|
|
||||||
func makeRAGTable(fileList []string) *tview.Flex {
|
func makeRAGTable(fileList []string) *tview.Flex {
|
||||||
actions := []string{"load", "delete"}
|
actions := []string{"load", "delete"}
|
||||||
rows, cols := len(fileList), len(actions)+1
|
rows, cols := len(fileList), len(actions)+2
|
||||||
fileTable := tview.NewTable().
|
fileTable := tview.NewTable().
|
||||||
SetBorders(true)
|
SetBorders(true)
|
||||||
longStatusView := tview.NewTextView()
|
longStatusView := tview.NewTextView()
|
||||||
@@ -252,39 +263,62 @@ func makeRAGTable(fileList []string) *tview.Flex {
|
|||||||
AddItem(fileTable, 0, 60, true)
|
AddItem(fileTable, 0, 60, true)
|
||||||
// Add the exit option as the first row (row 0)
|
// Add the exit option as the first row (row 0)
|
||||||
fileTable.SetCell(0, 0,
|
fileTable.SetCell(0, 0,
|
||||||
tview.NewTableCell("Exit RAG manager").
|
tview.NewTableCell("File Name").
|
||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
SetSelectable(false))
|
SetSelectable(false))
|
||||||
fileTable.SetCell(0, 1,
|
fileTable.SetCell(0, 1,
|
||||||
tview.NewTableCell("(Close without action)").
|
tview.NewTableCell("Preview").
|
||||||
SetTextColor(tcell.ColorGray).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
SetSelectable(false))
|
SetSelectable(false))
|
||||||
fileTable.SetCell(0, 2,
|
fileTable.SetCell(0, 2,
|
||||||
tview.NewTableCell("exit").
|
tview.NewTableCell("Load").
|
||||||
SetTextColor(tcell.ColorGray).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetAlign(tview.AlignCenter))
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
|
fileTable.SetCell(0, 3,
|
||||||
|
tview.NewTableCell("Delete").
|
||||||
|
SetTextColor(tcell.ColorWhite).
|
||||||
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
// Add the file rows starting from row 1
|
// Add the file rows starting from row 1
|
||||||
for r := 0; r < rows; r++ {
|
for r := 0; r < rows; r++ {
|
||||||
for c := 0; c < cols; c++ {
|
for c := 0; c < cols; c++ {
|
||||||
color := tcell.ColorWhite
|
color := tcell.ColorWhite
|
||||||
switch {
|
switch {
|
||||||
case c < 1:
|
case c == 0:
|
||||||
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
|
fileTable.SetCell(r+1, c,
|
||||||
tview.NewTableCell(fileList[r]).
|
tview.NewTableCell(fileList[r]).
|
||||||
SetTextColor(color).
|
SetTextColor(color).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
SetSelectable(false))
|
SetSelectable(false))
|
||||||
case c == 1: // Action description column - not selectable
|
case c == 1:
|
||||||
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
|
fpath := path.Join(cfg.RAGDir, fileList[r])
|
||||||
tview.NewTableCell("(Action)").
|
if fi, err := os.Stat(fpath); err == nil {
|
||||||
|
size := fi.Size()
|
||||||
|
modTime := fi.ModTime()
|
||||||
|
preview := fmt.Sprintf("%s | %s", formatSize(size), modTime.Format("2006-01-02 15:04"))
|
||||||
|
fileTable.SetCell(r+1, c,
|
||||||
|
tview.NewTableCell(preview).
|
||||||
|
SetTextColor(color).
|
||||||
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
|
} else {
|
||||||
|
fileTable.SetCell(r+1, c,
|
||||||
|
tview.NewTableCell("error").
|
||||||
|
SetTextColor(color).
|
||||||
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
|
}
|
||||||
|
case c == 2:
|
||||||
|
fileTable.SetCell(r+1, c,
|
||||||
|
tview.NewTableCell("load").
|
||||||
SetTextColor(color).
|
SetTextColor(color).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter))
|
||||||
SetSelectable(false))
|
default:
|
||||||
default: // Action button column - selectable
|
fileTable.SetCell(r+1, c,
|
||||||
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
|
tview.NewTableCell("delete").
|
||||||
tview.NewTableCell(actions[c-1]).
|
|
||||||
SetTextColor(color).
|
SetTextColor(color).
|
||||||
SetAlign(tview.AlignCenter))
|
SetAlign(tview.AlignCenter))
|
||||||
}
|
}
|
||||||
@@ -318,7 +352,7 @@ func makeRAGTable(fileList []string) *tview.Flex {
|
|||||||
}()
|
}()
|
||||||
fileTable.Select(0, 0).
|
fileTable.Select(0, 0).
|
||||||
SetFixed(1, 1).
|
SetFixed(1, 1).
|
||||||
SetSelectable(true, false).
|
SetSelectable(true, true).
|
||||||
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
||||||
SetDoneFunc(func(key tcell.Key) {
|
SetDoneFunc(func(key tcell.Key) {
|
||||||
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
|
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
|
||||||
@@ -335,6 +369,8 @@ func makeRAGTable(fileList []string) *tview.Flex {
|
|||||||
}
|
}
|
||||||
// defer pages.RemovePage(RAGPage)
|
// defer pages.RemovePage(RAGPage)
|
||||||
tc := fileTable.GetCell(row, column)
|
tc := fileTable.GetCell(row, column)
|
||||||
|
tc.SetTextColor(tcell.ColorRed)
|
||||||
|
fileTable.SetSelectable(false, false)
|
||||||
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues
|
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues
|
||||||
if row == 0 {
|
if row == 0 {
|
||||||
pages.RemovePage(RAGPage)
|
pages.RemovePage(RAGPage)
|
||||||
@@ -351,10 +387,15 @@ func makeRAGTable(fileList []string) *tview.Flex {
|
|||||||
if err := ragger.LoadRAG(fpath); err != nil {
|
if err := ragger.LoadRAG(fpath); err != nil {
|
||||||
logger.Error("failed to embed file", "chat", fpath, "error", err)
|
logger.Error("failed to embed file", "chat", fpath, "error", err)
|
||||||
_ = notifyUser("RAG", "failed to embed file; error: "+err.Error())
|
_ = notifyUser("RAG", "failed to embed file; error: "+err.Error())
|
||||||
errCh <- err
|
app.QueueUpdate(func() {
|
||||||
// pages.RemovePage(RAGPage)
|
pages.RemovePage(RAGPage)
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
_ = notifyUser("RAG", "file loaded successfully")
|
||||||
|
app.QueueUpdate(func() {
|
||||||
|
pages.RemovePage(RAGPage)
|
||||||
|
})
|
||||||
}()
|
}()
|
||||||
return
|
return
|
||||||
case "delete":
|
case "delete":
|
||||||
@@ -385,7 +426,7 @@ func makeRAGTable(fileList []string) *tview.Flex {
|
|||||||
|
|
||||||
func makeLoadedRAGTable(fileList []string) *tview.Flex {
|
func makeLoadedRAGTable(fileList []string) *tview.Flex {
|
||||||
actions := []string{"delete"}
|
actions := []string{"delete"}
|
||||||
rows, cols := len(fileList), len(actions)+1
|
rows, cols := len(fileList), len(actions)+2
|
||||||
// Add 1 extra row for the "exit" option at the top
|
// Add 1 extra row for the "exit" option at the top
|
||||||
fileTable := tview.NewTable().
|
fileTable := tview.NewTable().
|
||||||
SetBorders(true)
|
SetBorders(true)
|
||||||
@@ -400,39 +441,61 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
|
|||||||
AddItem(fileTable, 0, 60, true)
|
AddItem(fileTable, 0, 60, true)
|
||||||
// Add the exit option as the first row (row 0)
|
// Add the exit option as the first row (row 0)
|
||||||
fileTable.SetCell(0, 0,
|
fileTable.SetCell(0, 0,
|
||||||
tview.NewTableCell("Exit Loaded Files manager").
|
tview.NewTableCell("File Name").
|
||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
SetSelectable(false))
|
SetSelectable(false))
|
||||||
fileTable.SetCell(0, 1,
|
fileTable.SetCell(0, 1,
|
||||||
tview.NewTableCell("(Close without action)").
|
tview.NewTableCell("Preview").
|
||||||
SetTextColor(tcell.ColorGray).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
SetSelectable(false))
|
SetSelectable(false))
|
||||||
fileTable.SetCell(0, 2,
|
fileTable.SetCell(0, 2,
|
||||||
tview.NewTableCell("exit").
|
tview.NewTableCell("Load").
|
||||||
SetTextColor(tcell.ColorGray).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetAlign(tview.AlignCenter))
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
|
fileTable.SetCell(0, 3,
|
||||||
|
tview.NewTableCell("Delete").
|
||||||
|
SetTextColor(tcell.ColorWhite).
|
||||||
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
// Add the file rows starting from row 1
|
// Add the file rows starting from row 1
|
||||||
for r := 0; r < rows; r++ {
|
for r := 0; r < rows; r++ {
|
||||||
for c := 0; c < cols; c++ {
|
for c := 0; c < cols; c++ {
|
||||||
color := tcell.ColorWhite
|
color := tcell.ColorWhite
|
||||||
switch {
|
switch {
|
||||||
case c < 1:
|
case c == 0:
|
||||||
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
|
fileTable.SetCell(r+1, c,
|
||||||
tview.NewTableCell(fileList[r]).
|
tview.NewTableCell(fileList[r]).
|
||||||
SetTextColor(color).
|
SetTextColor(color).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter).
|
||||||
SetSelectable(false))
|
SetSelectable(false))
|
||||||
case c == 1: // Action description column - not selectable
|
case c == 1:
|
||||||
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
|
if fi, err := os.Stat(fileList[r]); err == nil {
|
||||||
tview.NewTableCell("(Action)").
|
size := fi.Size()
|
||||||
|
modTime := fi.ModTime()
|
||||||
|
preview := fmt.Sprintf("%s | %s", formatSize(size), modTime.Format("2006-01-02 15:04"))
|
||||||
|
fileTable.SetCell(r+1, c,
|
||||||
|
tview.NewTableCell(preview).
|
||||||
|
SetTextColor(color).
|
||||||
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
|
} else {
|
||||||
|
fileTable.SetCell(r+1, c,
|
||||||
|
tview.NewTableCell("error").
|
||||||
|
SetTextColor(color).
|
||||||
|
SetAlign(tview.AlignCenter).
|
||||||
|
SetSelectable(false))
|
||||||
|
}
|
||||||
|
case c == 2:
|
||||||
|
fileTable.SetCell(r+1, c,
|
||||||
|
tview.NewTableCell("load").
|
||||||
SetTextColor(color).
|
SetTextColor(color).
|
||||||
SetAlign(tview.AlignCenter).
|
SetAlign(tview.AlignCenter))
|
||||||
SetSelectable(false))
|
default:
|
||||||
default: // Action button column - selectable
|
fileTable.SetCell(r+1, c,
|
||||||
fileTable.SetCell(r+1, c, // +1 to account for the exit row at index 0
|
tview.NewTableCell("delete").
|
||||||
tview.NewTableCell(actions[c-1]).
|
|
||||||
SetTextColor(color).
|
SetTextColor(color).
|
||||||
SetAlign(tview.AlignCenter))
|
SetAlign(tview.AlignCenter))
|
||||||
}
|
}
|
||||||
@@ -440,7 +503,7 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
|
|||||||
}
|
}
|
||||||
fileTable.Select(0, 0).
|
fileTable.Select(0, 0).
|
||||||
SetFixed(1, 1).
|
SetFixed(1, 1).
|
||||||
SetSelectable(true, false).
|
SetSelectable(true, true).
|
||||||
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
||||||
SetDoneFunc(func(key tcell.Key) {
|
SetDoneFunc(func(key tcell.Key) {
|
||||||
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
|
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') || key == tcell.KeyCtrlX {
|
||||||
@@ -456,6 +519,8 @@ func makeLoadedRAGTable(fileList []string) *tview.Flex {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tc := fileTable.GetCell(row, column)
|
tc := fileTable.GetCell(row, column)
|
||||||
|
tc.SetTextColor(tcell.ColorRed)
|
||||||
|
fileTable.SetSelectable(false, false)
|
||||||
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues
|
// Check if the selected row is the exit row (row 0) - do this first to avoid index issues
|
||||||
if row == 0 {
|
if row == 0 {
|
||||||
pages.RemovePage(RAGLoadedPage)
|
pages.RemovePage(RAGLoadedPage)
|
||||||
@@ -533,7 +598,7 @@ func makeAgentTable(agentList []string) *tview.Table {
|
|||||||
}
|
}
|
||||||
chatActTable.Select(0, 0).
|
chatActTable.Select(0, 0).
|
||||||
SetFixed(1, 1).
|
SetFixed(1, 1).
|
||||||
SetSelectable(true, false).
|
SetSelectable(true, true).
|
||||||
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
||||||
SetDoneFunc(func(key tcell.Key) {
|
SetDoneFunc(func(key tcell.Key) {
|
||||||
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
|
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
|
||||||
@@ -549,6 +614,8 @@ func makeAgentTable(agentList []string) *tview.Table {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tc := chatActTable.GetCell(row, column)
|
tc := chatActTable.GetCell(row, column)
|
||||||
|
tc.SetTextColor(tcell.ColorRed)
|
||||||
|
chatActTable.SetSelectable(false, false)
|
||||||
selected := agentList[row]
|
selected := agentList[row]
|
||||||
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
||||||
switch tc.Text {
|
switch tc.Text {
|
||||||
@@ -630,7 +697,7 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
|
|||||||
}
|
}
|
||||||
table.Select(0, 0).
|
table.Select(0, 0).
|
||||||
SetFixed(1, 1).
|
SetFixed(1, 1).
|
||||||
SetSelectable(true, false).
|
SetSelectable(true, true).
|
||||||
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
||||||
SetDoneFunc(func(key tcell.Key) {
|
SetDoneFunc(func(key tcell.Key) {
|
||||||
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
|
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
|
||||||
@@ -646,6 +713,8 @@ func makeCodeBlockTable(codeBlocks []string) *tview.Table {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tc := table.GetCell(row, column)
|
tc := table.GetCell(row, column)
|
||||||
|
tc.SetTextColor(tcell.ColorRed)
|
||||||
|
table.SetSelectable(false, false)
|
||||||
selected := codeBlocks[row]
|
selected := codeBlocks[row]
|
||||||
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
||||||
switch tc.Text {
|
switch tc.Text {
|
||||||
@@ -702,7 +771,7 @@ func makeImportChatTable(filenames []string) *tview.Table {
|
|||||||
}
|
}
|
||||||
chatActTable.Select(0, 0).
|
chatActTable.Select(0, 0).
|
||||||
SetFixed(1, 1).
|
SetFixed(1, 1).
|
||||||
SetSelectable(true, false).
|
SetSelectable(true, true).
|
||||||
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
SetSelectedStyle(tcell.StyleDefault.Background(tcell.ColorGray).Foreground(tcell.ColorWhite)).
|
||||||
SetDoneFunc(func(key tcell.Key) {
|
SetDoneFunc(func(key tcell.Key) {
|
||||||
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
|
if key == tcell.KeyEsc || key == tcell.KeyF1 || key == tcell.Key('x') {
|
||||||
@@ -718,6 +787,8 @@ func makeImportChatTable(filenames []string) *tview.Table {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tc := chatActTable.GetCell(row, column)
|
tc := chatActTable.GetCell(row, column)
|
||||||
|
tc.SetTextColor(tcell.ColorRed)
|
||||||
|
chatActTable.SetSelectable(false, false)
|
||||||
selected := filenames[row]
|
selected := filenames[row]
|
||||||
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
// notification := fmt.Sprintf("chat: %s; action: %s", selectedChat, tc.Text)
|
||||||
switch tc.Text {
|
switch tc.Text {
|
||||||
|
|||||||
77
tools.go
77
tools.go
@@ -16,6 +16,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gf-lt/rag"
|
||||||
"github.com/GrailFinder/searchagent/searcher"
|
"github.com/GrailFinder/searchagent/searcher"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,9 +59,9 @@ Your current tools:
|
|||||||
"when_to_use": "when asked to search the web for information; returns clean summary without html,css and other web elements; limit is optional (default 3)"
|
"when_to_use": "when asked to search the web for information; returns clean summary without html,css and other web elements; limit is optional (default 3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"websearch_raw",
|
"name":"rag_search",
|
||||||
"args": ["query", "limit"],
|
"args": ["query", "limit"],
|
||||||
"when_to_use": "when asked to search the web for information; returns raw data as is without processing; limit is optional (default 3)"
|
"when_to_use": "when asked to search the local document database for information; performs query refinement, semantic search, reranking, and synthesis; returns clean summary with sources; limit is optional (default 3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name":"read_url",
|
"name":"read_url",
|
||||||
@@ -146,6 +147,7 @@ under the topic: Adam's number is stored:
|
|||||||
After that you are free to respond to the user.
|
After that you are free to respond to the user.
|
||||||
`
|
`
|
||||||
webSearchSysPrompt = `Summarize the web search results, extracting key information and presenting a concise answer. Provide sources and URLs where relevant.`
|
webSearchSysPrompt = `Summarize the web search results, extracting key information and presenting a concise answer. Provide sources and URLs where relevant.`
|
||||||
|
ragSearchSysPrompt = `Synthesize the document search results, extracting key information and presenting a concise answer. Provide sources and document IDs where relevant.`
|
||||||
readURLSysPrompt = `Extract and summarize the content from the webpage. Provide key information, main points, and any relevant details.`
|
readURLSysPrompt = `Extract and summarize the content from the webpage. Provide key information, main points, and any relevant details.`
|
||||||
summarySysPrompt = `Please provide a concise summary of the following conversation. Focus on key points, decisions, and actions. Provide only the summary, no additional commentary.`
|
summarySysPrompt = `Please provide a concise summary of the following conversation. Focus on key points, decisions, and actions. Provide only the summary, no additional commentary.`
|
||||||
basicCard = &models.CharCard{
|
basicCard = &models.CharCard{
|
||||||
@@ -170,6 +172,10 @@ func init() {
|
|||||||
panic("failed to init seachagent; error: " + err.Error())
|
panic("failed to init seachagent; error: " + err.Error())
|
||||||
}
|
}
|
||||||
WebSearcher = sa
|
WebSearcher = sa
|
||||||
|
|
||||||
|
if err := rag.Init(cfg, logger, store); err != nil {
|
||||||
|
logger.Warn("failed to init rag; rag_search tool will not be available", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWebAgentClient returns a singleton AgentClient for web agents.
|
// getWebAgentClient returns a singleton AgentClient for web agents.
|
||||||
@@ -196,6 +202,8 @@ func getWebAgentClient() *agent.AgentClient {
|
|||||||
func registerWebAgents() {
|
func registerWebAgents() {
|
||||||
webAgentsOnce.Do(func() {
|
webAgentsOnce.Do(func() {
|
||||||
client := getWebAgentClient()
|
client := getWebAgentClient()
|
||||||
|
// Register rag_search agent
|
||||||
|
agent.Register("rag_search", agent.NewWebAgentB(client, ragSearchSysPrompt))
|
||||||
// Register websearch agent
|
// Register websearch agent
|
||||||
agent.Register("websearch", agent.NewWebAgentB(client, webSearchSysPrompt))
|
agent.Register("websearch", agent.NewWebAgentB(client, webSearchSysPrompt))
|
||||||
// Register read_url agent
|
// Register read_url agent
|
||||||
@@ -239,6 +247,48 @@ func websearch(args map[string]string) []byte {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rag search (searches local document database)
|
||||||
|
func ragsearch(args map[string]string) []byte {
|
||||||
|
query, ok := args["query"]
|
||||||
|
if !ok || query == "" {
|
||||||
|
msg := "query not provided to rag_search tool"
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
limitS, ok := args["limit"]
|
||||||
|
if !ok || limitS == "" {
|
||||||
|
limitS = "3"
|
||||||
|
}
|
||||||
|
limit, err := strconv.Atoi(limitS)
|
||||||
|
if err != nil || limit == 0 {
|
||||||
|
logger.Warn("ragsearch limit; passed bad value; setting to default (3)",
|
||||||
|
"limit_arg", limitS, "error", err)
|
||||||
|
limit = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
ragInstance := rag.GetInstance()
|
||||||
|
if ragInstance == nil {
|
||||||
|
msg := "rag not initialized; rag_search tool is not available"
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := ragInstance.Search(query, limit)
|
||||||
|
if err != nil {
|
||||||
|
msg := "rag search failed; error: " + err.Error()
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(results)
|
||||||
|
if err != nil {
|
||||||
|
msg := "failed to marshal rag search result; error: " + err.Error()
|
||||||
|
logger.Error(msg)
|
||||||
|
return []byte(msg)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
// web search raw (returns raw data without processing)
|
// web search raw (returns raw data without processing)
|
||||||
func websearchRaw(args map[string]string) []byte {
|
func websearchRaw(args map[string]string) []byte {
|
||||||
// make http request return bytes
|
// make http request return bytes
|
||||||
@@ -997,6 +1047,7 @@ var fnMap = map[string]fnSig{
|
|||||||
"recall": recall,
|
"recall": recall,
|
||||||
"recall_topics": recallTopics,
|
"recall_topics": recallTopics,
|
||||||
"memorise": memorise,
|
"memorise": memorise,
|
||||||
|
"rag_search": ragsearch,
|
||||||
"websearch": websearch,
|
"websearch": websearch,
|
||||||
"websearch_raw": websearchRaw,
|
"websearch_raw": websearchRaw,
|
||||||
"read_url": readURL,
|
"read_url": readURL,
|
||||||
@@ -1033,6 +1084,28 @@ func callToolWithAgent(name string, args map[string]string) []byte {
|
|||||||
|
|
||||||
// openai style def
|
// openai style def
|
||||||
var baseTools = []models.Tool{
|
var baseTools = []models.Tool{
|
||||||
|
// rag_search
|
||||||
|
models.Tool{
|
||||||
|
Type: "function",
|
||||||
|
Function: models.ToolFunc{
|
||||||
|
Name: "rag_search",
|
||||||
|
Description: "Search local document database given query, limit of sources (default 3). Performs query refinement, semantic search, reranking, and synthesis.",
|
||||||
|
Parameters: models.ToolFuncParams{
|
||||||
|
Type: "object",
|
||||||
|
Required: []string{"query", "limit"},
|
||||||
|
Properties: map[string]models.ToolArgProps{
|
||||||
|
"query": models.ToolArgProps{
|
||||||
|
Type: "string",
|
||||||
|
Description: "search query",
|
||||||
|
},
|
||||||
|
"limit": models.ToolArgProps{
|
||||||
|
Type: "string",
|
||||||
|
Description: "limit of the document results",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
// websearch
|
// websearch
|
||||||
models.Tool{
|
models.Tool{
|
||||||
Type: "function",
|
Type: "function",
|
||||||
|
|||||||
27
tui.go
27
tui.go
@@ -15,6 +15,11 @@ import (
|
|||||||
"github.com/rivo/tview"
|
"github.com/rivo/tview"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func isFullScreenPageActive() bool {
|
||||||
|
name, _ := pages.GetFrontPage()
|
||||||
|
return name != "main"
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
app *tview.Application
|
app *tview.Application
|
||||||
pages *tview.Pages
|
pages *tview.Pages
|
||||||
@@ -525,6 +530,9 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyRune && event.Rune() == 'i' && event.Modifiers()&tcell.ModAlt != 0 {
|
if event.Key() == tcell.KeyRune && event.Rune() == 'i' && event.Modifiers()&tcell.ModAlt != 0 {
|
||||||
|
if isFullScreenPageActive() {
|
||||||
|
return event
|
||||||
|
}
|
||||||
showColorschemeSelectionPopup()
|
showColorschemeSelectionPopup()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -731,6 +739,9 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyCtrlL {
|
if event.Key() == tcell.KeyCtrlL {
|
||||||
|
if isFullScreenPageActive() {
|
||||||
|
return event
|
||||||
|
}
|
||||||
// Show model selection popup instead of rotating models
|
// Show model selection popup instead of rotating models
|
||||||
showModelSelectionPopup()
|
showModelSelectionPopup()
|
||||||
return nil
|
return nil
|
||||||
@@ -744,6 +755,9 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyCtrlV {
|
if event.Key() == tcell.KeyCtrlV {
|
||||||
|
if isFullScreenPageActive() {
|
||||||
|
return event
|
||||||
|
}
|
||||||
// Show API link selection popup instead of rotating APIs
|
// Show API link selection popup instead of rotating APIs
|
||||||
showAPILinkSelectionPopup()
|
showAPILinkSelectionPopup()
|
||||||
return nil
|
return nil
|
||||||
@@ -850,11 +864,17 @@ func init() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyCtrlQ {
|
if event.Key() == tcell.KeyCtrlQ {
|
||||||
|
if isFullScreenPageActive() {
|
||||||
|
return event
|
||||||
|
}
|
||||||
// Show user role selection popup instead of cycling through roles
|
// Show user role selection popup instead of cycling through roles
|
||||||
showUserRoleSelectionPopup()
|
showUserRoleSelectionPopup()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if event.Key() == tcell.KeyCtrlX {
|
if event.Key() == tcell.KeyCtrlX {
|
||||||
|
if isFullScreenPageActive() {
|
||||||
|
return event
|
||||||
|
}
|
||||||
// Show bot role selection popup instead of cycling through roles
|
// Show bot role selection popup instead of cycling through roles
|
||||||
showBotRoleSelectionPopup()
|
showBotRoleSelectionPopup()
|
||||||
return nil
|
return nil
|
||||||
@@ -975,13 +995,6 @@ func init() {
|
|||||||
}
|
}
|
||||||
// go chatRound(msgText, persona, textView, false, false)
|
// go chatRound(msgText, persona, textView, false, false)
|
||||||
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText}
|
chatRoundChan <- &models.ChatRoundReq{Role: persona, UserMsg: msgText}
|
||||||
// Also clear any image attachment after sending the message
|
|
||||||
go func() {
|
|
||||||
// Wait a short moment for the message to be processed, then clear the image attachment
|
|
||||||
// This allows the image to be sent with the current message if it was attached
|
|
||||||
// But clears it for the next message
|
|
||||||
ClearImageAttachment()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user