Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 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
cc43f36
chore: update comments
wjiayis Feb 7, 2026
2faa944
chore: removal of redundant code
wjiayis Feb 7, 2026
21d89f6
chore: update comments
wjiayis Feb 7, 2026
8b7ae4c
chore: llm response edge case handling
wjiayis Feb 7, 2026
dc79e9c
chore: update comments
wjiayis Feb 7, 2026
0dacb49
refactor: update comments and improve variable naming
wjiayis Feb 7, 2026
5f6f7b8
refactor: improve variable naming
wjiayis Feb 7, 2026
f1c82af
refactor: remove unnecessary abstraction
wjiayis Feb 7, 2026
4f6b8c9
chore: simplify comments
wjiayis Feb 7, 2026
6cdc60f
feat: update frontend settings button text
wjiayis Feb 7, 2026
ebdb3d1
chore: remove a comment
wjiayis Feb 7, 2026
bae5318
feat: skip @String{} to save tokens and reduce latency
wjiayis Feb 7, 2026
90140df
fix: debug bibliography and sentence ordering
wjiayis Feb 9, 2026
aeaff3c
chore: fix typo in comment
wjiayis Feb 9, 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(),
Comment on lines +29 to +33
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetCitationKeysRequest includes an optional model_slug, but this handler doesn’t read req.GetModelSlug() (and the downstream call currently hardcodes a model). Either plumb the requested model through to the client call, or remove the field from the proto to avoid a misleading API surface.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +33
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please validate required inputs (at least sentence and project_id) and return a clear bad-request error when missing, consistent with other chat handlers (e.g. shared.ErrBadRequest("title is required")). Otherwise empty values will fail deeper in services with less actionable errors.

Copilot uses AI. Check for mistakes.
llmProvider,
)
if err != nil {
return nil, err
}

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

// TODO: This file should not place in the client package.
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new file includes a TODO noting it should not live in the client package. Leaving known-misplaced code in-tree makes future refactors harder (imports, package boundaries). Please move this logic into an appropriate package (e.g. a citation/bibliography helper or toolkit subpackage) before merging, and keep client focused on LLM client orchestration.

Suggested change
// TODO: This file should not place in the client package.

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Junyi-99 I'm planning to eventually move this file to wherever internal/services/toolkit/client/get_conversation_title_v2.go will be moved to, since both files contain high-order business logic that doesn't belong to the client folder. Shall I leave this file here for now?

Copy link
Member

@4ndrelim 4ndrelim Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i got copilot to review the PR. I didnt really look through its suggestions. Could you resolve copilot's comments and integrate any valid / viable suggestions? I think jun yi is busy these few days, i'll review later on this week if its still pending review! Thanks for your hardwork jiayi!

@Junyi-99 actually do we have a recommended PR to main workflow? Should we review only review PRs to main and allow freely merging to staging / dev branch for testing in a simulated prod env? Or should there be a preliminary review before merging to staging too?

import (
"context"
"fmt"
"paperdebugger/internal/models"
"regexp"
"strings"

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

// GetBibliographyForCitation 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*"`)

// Match @String{ entries
stringEntryRe := regexp.MustCompile(`^\s*@String\s*\{`)

var bibLines []string
for _, doc := range project.Docs {
if doc.Filepath == "" || !strings.HasSuffix(doc.Filepath, ".bib") {
continue
}
braceDepth := 0
inQuote := false
inStringEntry := false
stringEntryBraceDepth := 0
for _, line := range doc.Lines {
// Handle ongoing @String{...} entry (which can span multiple lines)
if inStringEntry {
stringEntryBraceDepth += strings.Count(line, "{") - strings.Count(line, "}")
if stringEntryBraceDepth <= 0 {
inStringEntry = false
stringEntryBraceDepth = 0
}
continue
}
// Handle ongoing exclusion of some fields (which can span multiple lines)
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 @String{...} entries
if stringEntryRe.MatchString(line) {
stringEntryBraceDepth = strings.Count(line, "{") - strings.Count(line, "}")
if stringEntryBraceDepth > 0 {
inStringEntry = true
}
continue
}
// Skip excluded fields
if 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)

Comment on lines +110 to +112
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New citation-key functionality is added here but there are existing tests for other toolkit client behaviors (e.g. conversation title). Please add unit tests for GetBibliographyForCitation/GetCitationKeys (at minimum: field-exclusion behavior and prompt formatting / empty-citation handling) to prevent regressions.

Copilot uses AI. Check for mistakes.
if err != nil {
return "", err
}

emptyCitation := "none"

// Bibliography is placed at the start of the prompt to leverage prompt caching
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'.", bibliography, sentence, 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 == "" || 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