Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c12c33b
feat: frontend enable beta features
wjiayis Feb 1, 2026
324e602
feat: trigger auto-completion if text before is "\cite{"
wjiayis Feb 1, 2026
9a4b2d4
feat: extract last sentence
wjiayis Feb 1, 2026
4992a04
chore: remove debug logging
wjiayis Feb 1, 2026
5592197
feat: end to end inline suggestion (lots of hardcoding)
wjiayis Feb 1, 2026
bc942c3
chore: minor reformatting
wjiayis Feb 1, 2026
72167c5
chore: minor comment improvement
wjiayis Feb 1, 2026
05abfd7
chore: rename method
wjiayis Feb 1, 2026
d688433
chore: rename method
wjiayis Feb 1, 2026
56260eb
refactor: use abstracted methods
wjiayis Feb 1, 2026
d8fd357
fix: use debug conversation mode
wjiayis Feb 1, 2026
f453065
refactor: move citation method to backend
wjiayis Feb 1, 2026
ac64b91
chore: revert edit package-lock.json
wjiayis Feb 1, 2026
b6cf906
feat: always use gpt-5-nano
wjiayis Feb 1, 2026
13e8553
feat: access docs on backend
wjiayis Feb 3, 2026
672e569
feat: get bibfiles from backend
wjiayis Feb 3, 2026
99243ca
Merge pull request #109 from wjiayis/feat/tab-completion
wjiayis Feb 4, 2026
1ff6a69
feat: improve citation prompt
wjiayis Feb 4, 2026
888b66b
feat: improve citation prompt
wjiayis Feb 4, 2026
60421cb
feat: override default overleaf autocomplete
wjiayis Feb 4, 2026
a54d354
refactor: make suggestion triggers generalised
wjiayis Feb 5, 2026
65cf022
feat: use gpt-5.2 instead of gpt-5-nano to reduce latency
wjiayis Feb 5, 2026
2a00ddc
feat: move bib to the start to make use of prompt caching
wjiayis Feb 6, 2026
71e7c3f
feat: skip unimportant bib fields
wjiayis Feb 6, 2026
f049902
chore: remove debug log
wjiayis Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ overleaf.kubeconfig
# coverage report
coverage.out
coverage.html

# claude code
CLAUDE.md
43 changes: 43 additions & 0 deletions internal/api/chat/get_citation_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package chat

import (
"context"

"paperdebugger/internal/libs/contextutil"
"paperdebugger/internal/models"
chatv2 "paperdebugger/pkg/gen/api/chat/v2"
)

func (s *ChatServerV2) GetCitationKeys(
ctx context.Context,
req *chatv2.GetCitationKeysRequest,
) (*chatv2.GetCitationKeysResponse, error) {
actor, err := contextutil.GetActor(ctx)
if err != nil {
return nil, err
}

settings, err := s.userService.GetUserSettings(ctx, actor.ID)
if err != nil {
return nil, err
}

llmProvider := &models.LLMProviderConfig{
APIKey: settings.OpenAIAPIKey,
}

citationKeys, err := s.aiClientV2.GetCitationKeys(
ctx,
req.GetSentence(),
actor.ID,
req.GetProjectId(),
llmProvider,
)
if err != nil {
return nil, err
}

return &chatv2.GetCitationKeysResponse{
CitationKeys: citationKeys,
}, nil
}
119 changes: 119 additions & 0 deletions internal/services/toolkit/client/get_citation_keys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package client

// TODO: This file should not place in the client package.
import (
"context"
"fmt"
"paperdebugger/internal/models"
"regexp"
"strings"

"github.com/openai/openai-go/v3"
"go.mongodb.org/mongo-driver/v2/bson"
)

// GetBibliography extracts bibliography content from a project's .bib files.
// It excludes non-essential fields to save tokens when extracting relevant citation keys.
func (a *AIClientV2) GetBibliographyForCitation(ctx context.Context, userId bson.ObjectID, projectId string) (string, error) {
project, err := a.projectService.GetProject(ctx, userId, projectId)
if err != nil {
return "", err
}

// Exclude fields that aren't useful for citation matching
var excludeRe, excludeBraceRe, excludeQuoteRe *regexp.Regexp

excludeFields := []string{
"address", "institution", "pages", "eprint", "primaryclass", "volume", "number", "edition", "numpages", "articleno",
"publisher", "editor", "doi", "url", "acmid", "issn", "archivePrefix", "year", "month", "day",
"eid", "lastaccessed", "organization", "school", "isbn", "mrclass", "mrnumber", "mrreviewer", "type", "order_no",
"location", "howpublished", "distincturl", "issue_date", "archived", "series", "source",
}

fieldsPattern := strings.Join(excludeFields, "|")
excludeRe = regexp.MustCompile(`(?i)^\s*(` + fieldsPattern + `)\s*=`)
excludeBraceRe = regexp.MustCompile(`(?i)^\s*(` + fieldsPattern + `)\s*=\s*\{`)
excludeQuoteRe = regexp.MustCompile(`(?i)^\s*(` + fieldsPattern + `)\s*=\s*"`)

var bibLines []string
for _, doc := range project.Docs {
if doc.Filepath == "" || !strings.HasSuffix(doc.Filepath, ".bib") {
continue
}
braceDepth := 0
inQuote := false
for _, line := range doc.Lines {
// Handle ongoing multi-line exclusion
if braceDepth > 0 {
braceDepth += strings.Count(line, "{") - strings.Count(line, "}")
continue
}
if inQuote {
if strings.Count(line, `"`)%2 == 1 {
inQuote = false
}
continue
}
// Skip comments
if strings.HasPrefix(strings.TrimSpace(line), "%") {
continue
}
// Skip empty lines
if strings.TrimSpace(line) == "" {
continue
}
// Skip excluded fields
if excludeRe != nil && excludeRe.MatchString(line) {
if excludeBraceRe.MatchString(line) {
braceDepth = strings.Count(line, "{") - strings.Count(line, "}")
} else if excludeQuoteRe.MatchString(line) && strings.Count(line, `"`)%2 == 1 {
inQuote = true
}
continue
}

bibLines = append(bibLines, line)
}
}

bibliography := strings.Join(bibLines, "\n")

// Normalize multiple spaces
multiSpaceRe := regexp.MustCompile(` {2,}`)
bibliography = multiSpaceRe.ReplaceAllString(bibliography, " ")

return bibliography, nil
}

func (a *AIClientV2) GetCitationKeys(ctx context.Context, sentence string, userId bson.ObjectID, projectId string, llmProvider *models.LLMProviderConfig) (string, error) {
bibliography, err := a.GetBibliographyForCitation(ctx, userId, projectId)

if err != nil {
return "", err
}

// Get citation keys from LLM
emptyCitation := "none"
message := fmt.Sprintf("Bibliography: %s\nSentence: %s\nBased on the sentence and bibliography, suggest only the most relevant citation keys separated by commas with no spaces (e.g. key1,key2). Be selective and only include citations that are directly relevant. Avoid suggesting more than 3 citations. If no relevant citations are found, return '%s'.", sentence, bibliography, emptyCitation)

_, resp, err := a.ChatCompletionV2(ctx, "gpt-5.2", OpenAIChatHistory{
openai.SystemMessage("You are a helpful assistant that suggests relevant citation keys."),
openai.UserMessage(message),
}, llmProvider)

if err != nil {
return "", err
}

if len(resp) == 0 {
return "", nil
}

citationKeys := strings.TrimSpace(resp[0].Payload.GetAssistant().GetContent())

if citationKeys == emptyCitation {
return "", nil
}

return citationKeys, nil
}
148 changes: 135 additions & 13 deletions pkg/gen/api/chat/v2/chat.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading