Skip to content

feat: improve answer quality to match llama-server behavior - #13

Merged
bagustris merged 5 commits into
masterfrom
copilot/mimicking-llama-server-behavior
May 28, 2026
Merged

feat: improve answer quality to match llama-server behavior#13
bagustris merged 5 commits into
masterfrom
copilot/mimicking-llama-server-behavior

Conversation

Copilot AI commented May 28, 2026

Copy link
Copy Markdown
Contributor

PaperRAG gives significantly worse answers than llama-server's web UI for the same model because it sends too little context (2 chunks × 750 chars), caps output at 256 tokens, and fails on follow-up questions when retrieval misses.

Default tuning

  • top_k: 2 → 5
  • max_tokens: 256 → 1024
  • ctx_size: 2048 → 4096
  • _MAX_CHUNK_CHARS: 750 → 2000
  • Preset tokens scaled (reviewer/summarizer: 2048)

Full-document fallback for focused papers

When /focus is active and retrieval returns nothing, all chunks for the focused paper are loaded into context — matching llama-server's full-document approach:

paperrag (paper.pdf)> What is the remaining problem?
(No retrieval match — using full paper context)
Answer: The remaining problem discussed in the paper is...

New Retriever.get_all_chunks_for_file() method supports this.

Adaptive prompt style

Switches from "Answer concisely" to "Answer thoroughly... Provide detailed reasoning and cite specific statements" when context exceeds 3000 chars (i.e., full-document mode or rich retrieval).

Review fixes from PR #13

  • Follow-up system prompt now derives from the user's configured system_prompt (respects /preset and /prompt)
  • Stronger follow-up gating: requires ≥1 complete Q&A turn in history before attempting history-only answers
  • Removed duplicate comment
  • Added tests: mocked stream_followup with history, full-document fallback path, follow-up gating, context preservation with history

Copilot AI added 2 commits May 28, 2026 06:12
When retrieval returns no results but conversation history exists,
the REPL now sends the question to the LLM with previous Q&A context
instead of showing "No results found". This mimics llama server's
ability to answer follow-up questions.

Changes:
- Add conversation_history parameter to _build_messages, _prepare,
  generate_answer, and stream_answer in llm.py
- Add stream_followup function for history-only queries
- Maintain conversation history in the REPL loop (bounded to 10 turns)
- When retrieval returns no results and history exists, use
  _handle_followup to answer from conversation context
- Add tests for conversation history functionality
Copilot AI changed the title [WIP] Mimic llama server behavior in CLI toolkit feat: add conversation history for follow-up questions in REPL May 28, 2026
Copilot AI requested a review from bagustris May 28, 2026 06:14
@bagustris
bagustris marked this pull request as ready for review May 28, 2026 06:19
Copilot AI review requested due to automatic review settings May 28, 2026 06:19
@sourcery-ai

sourcery-ai Bot commented May 28, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds conversation-history-aware answering to the PaperRAG REPL so that follow-up questions can be answered from prior dialogue when retrieval yields no new sources, while keeping existing LLM APIs backward compatible.

Sequence diagram for follow-up handling in REPL query flow

sequenceDiagram
    actor User
    participant REPL
    participant LLMBackend

    User->>REPL: start_repl
    loop Each_command
        User->>REPL: question
        REPL->>REPL: _handle_query
        alt [retrieval results found]
            REPL->>LLMBackend: stream_answer
            LLMBackend-->>REPL: answer_stream
        else [no results and conversation_history exists]
            REPL->>REPL: _handle_followup
            REPL->>LLMBackend: stream_followup
            LLMBackend-->>REPL: followup_answer_stream
        end
    end
Loading

File-Level Changes

Change Details Files
Thread conversation history into LLM message construction and streaming APIs, and add a dedicated follow-up streaming path that relies only on prior dialogue.
  • Introduce a follow-up-specific system prompt used when answering from conversation history only.
  • Extend _build_messages to accept an optional conversation_history list and insert it between the system and current user messages.
  • Update _prepare, generate_answer, and stream_answer to accept and propagate conversation_history through both llama-server and Ollama paths.
  • Implement stream_followup to stream responses for follow-up questions using only conversation history, supporting both llama-server and Ollama backends, with a graceful message when no history is available.
  • Add tests covering message construction with/without conversation history and the no-history behavior of stream_followup.
paperrag/llm.py
tests/test_llm.py
Maintain a bounded conversation history in the REPL and use it to answer queries when retrieval returns no results via a dedicated follow-up handler.
  • Add a per-session conversation_history buffer in start_repl, updated after each successful LLM answer, and bounded to a maximum number of user/assistant turn pairs.
  • Extend _handle_query to accept conversation_history and, when retrieval returns no results but history exists, route the query to a new _handle_followup handler instead of immediately reporting "No results found".
  • Ensure streaming answers for normal queries include conversation_history in the LLM call so that follow-up questions can leverage prior turns even when retrieval succeeds.
  • Implement _handle_followup to call stream_followup, stream the answer to the console with timing info, and return a session_log entry without sources.
paperrag/repl.py

Assessment against linked issues

Issue Objective Addressed Explanation
#12 Enable the Paperrag CLI REPL to support follow-up questions (e.g., "what is the remaining problem in that paper?") by using conversation history, so it behaves like llama server instead of returning "No results found" when retrieval fails.

Possibly linked issues

  • #(no explicit number provided): PR adds REPL conversation history and follow-up answering when retrieval fails, matching the requested llama server behavior.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The new stream_followup implementation reimplements message construction and client setup logic that already exists in _build_messages and _prepare; consider refactoring to reuse those helpers so behavior stays consistent and future changes only need to be made in one place.
  • In start_repl, the comment about keeping conversation history bounded is duplicated on consecutive lines; clean up the duplicate to keep the codebase tidy.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `stream_followup` implementation reimplements message construction and client setup logic that already exists in `_build_messages` and `_prepare`; consider refactoring to reuse those helpers so behavior stays consistent and future changes only need to be made in one place.
- In `start_repl`, the comment about keeping conversation history bounded is duplicated on consecutive lines; clean up the duplicate to keep the codebase tidy.

## Individual Comments

### Comment 1
<location path="paperrag/llm.py" line_range="710" />
<code_context>
+        if not config.think:
+            user_prompt += " /no_think"
+
+    messages: list[dict] = [{"role": "system", "content": _FOLLOWUP_SYSTEM_PROMPT}]
+    messages.extend(conversation_history)
+    messages.append({"role": "user", "content": user_prompt})
</code_context>
<issue_to_address>
**question:** Consider whether follow-up responses should respect the configured system_prompt instead of a hard-coded one.

Here, follow-ups always use the fixed `_FOLLOWUP_SYSTEM_PROMPT` and ignore `config.llm.system_prompt`. That means customized prompts (tone, language, constraints) won’t apply to follow-up answers. Consider deriving or augmenting `_FOLLOWUP_SYSTEM_PROMPT` from `config.llm.system_prompt` so follow-up behavior remains consistent with normal responses while still emphasizing conversation history.
</issue_to_address>

### Comment 2
<location path="tests/test_llm.py" line_range="347-359" />
<code_context>
+# ---------------------------------------------------------------------------
+
+
+def test_build_messages_with_conversation_history():
+    """Conversation history should be inserted between system prompt and user message."""
+    history = [
+        {"role": "user", "content": "What is speech chain?"},
+        {"role": "assistant", "content": "Speech chain is a method of voice conversion."},
+    ]
+    msgs = _build_messages("What is the remaining problem?", ["ctx"], "llama3", "System", conversation_history=history)
+    assert len(msgs) == 4
+    assert msgs[0]["role"] == "system"
+    assert msgs[1] == history[0]
+    assert msgs[2] == history[1]
+    assert msgs[3]["role"] == "user"
+    assert "remaining problem" in msgs[3]["content"]
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Also assert that the constructed user message still embeds the retrieval context when conversation history is present.

This test currently only checks the placement of history and the question in the user message. Please also assert that `context_chunks` are still present in `msgs[3]['content']` (e.g., by checking for a token from `'ctx'`) so we catch regressions where adding `conversation_history` unintentionally drops or replaces the retrieval context.

```suggestion
def test_build_messages_with_conversation_history():
    """Conversation history should be inserted between system prompt and user message."""
    history = [
        {"role": "user", "content": "What is speech chain?"},
        {"role": "assistant", "content": "Speech chain is a method of voice conversion."},
    ]
    msgs = _build_messages(
        "What is the remaining problem?",
        ["ctx"],
        "llama3",
        "System",
        conversation_history=history,
    )
    assert len(msgs) == 4
    assert msgs[0]["role"] == "system"
    assert msgs[1] == history[0]
    assert msgs[2] == history[1]
    assert msgs[3]["role"] == "user"
    # Ensure the question is present in the final user message
    assert "remaining problem" in msgs[3]["content"]
    # Ensure retrieval context is still embedded when conversation history is present
    assert "ctx" in msgs[3]["content"]
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread paperrag/llm.py Outdated
Comment thread tests/test_llm.py
Comment on lines +347 to +359
def test_build_messages_with_conversation_history():
"""Conversation history should be inserted between system prompt and user message."""
history = [
{"role": "user", "content": "What is speech chain?"},
{"role": "assistant", "content": "Speech chain is a method of voice conversion."},
]
msgs = _build_messages("What is the remaining problem?", ["ctx"], "llama3", "System", conversation_history=history)
assert len(msgs) == 4
assert msgs[0]["role"] == "system"
assert msgs[1] == history[0]
assert msgs[2] == history[1]
assert msgs[3]["role"] == "user"
assert "remaining problem" in msgs[3]["content"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Also assert that the constructed user message still embeds the retrieval context when conversation history is present.

This test currently only checks the placement of history and the question in the user message. Please also assert that context_chunks are still present in msgs[3]['content'] (e.g., by checking for a token from 'ctx') so we catch regressions where adding conversation_history unintentionally drops or replaces the retrieval context.

Suggested change
def test_build_messages_with_conversation_history():
"""Conversation history should be inserted between system prompt and user message."""
history = [
{"role": "user", "content": "What is speech chain?"},
{"role": "assistant", "content": "Speech chain is a method of voice conversion."},
]
msgs = _build_messages("What is the remaining problem?", ["ctx"], "llama3", "System", conversation_history=history)
assert len(msgs) == 4
assert msgs[0]["role"] == "system"
assert msgs[1] == history[0]
assert msgs[2] == history[1]
assert msgs[3]["role"] == "user"
assert "remaining problem" in msgs[3]["content"]
def test_build_messages_with_conversation_history():
"""Conversation history should be inserted between system prompt and user message."""
history = [
{"role": "user", "content": "What is speech chain?"},
{"role": "assistant", "content": "Speech chain is a method of voice conversion."},
]
msgs = _build_messages(
"What is the remaining problem?",
["ctx"],
"llama3",
"System",
conversation_history=history,
)
assert len(msgs) == 4
assert msgs[0]["role"] == "system"
assert msgs[1] == history[0]
assert msgs[2] == history[1]
assert msgs[3]["role"] == "user"
# Ensure the question is present in the final user message
assert "remaining problem" in msgs[3]["content"]
# Ensure retrieval context is still embedded when conversation history is present
assert "ctx" in msgs[3]["content"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This assertion is already in the current code at line 369 of tests/test_llm.py:

# Ensure retrieval context is still embedded when conversation history is present
assert "ctx" in msgs[3]["content"]

All 48 tests pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b3e02c737d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread paperrag/repl.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds REPL conversation history so follow-up questions can be answered from prior LLM context when retrieval finds no new matches.

Changes:

  • Adds optional conversation_history support to LLM message construction and answer generation/streaming.
  • Introduces stream_followup for history-only responses.
  • Tracks bounded REPL history and falls back to follow-up answering when retrieval returns no results.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
paperrag/llm.py Adds conversation history plumbing and a follow-up streaming path.
paperrag/repl.py Maintains REPL conversation history and invokes follow-up handling on empty retrieval results.
tests/test_llm.py Adds basic tests for history insertion and empty-history follow-up behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread paperrag/repl.py Outdated
Comment thread paperrag/repl.py Outdated
Comment thread paperrag/llm.py
Comment on lines +687 to +690
def stream_followup(
question: str,
conversation_history: list[dict],
config: LLMConfig | None = None,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Two mocked backend tests for stream_followup with non-empty history were added to tests/test_llm.py:

  • test_stream_followup_with_history_ollama (line 404): verifies the Ollama path — checks message structure is system + history (2 msgs) + user question = 4 messages, that the system prompt derives from config.system_prompt, and that the question is in the user message.
  • test_stream_followup_with_history_qwen_no_think (line 441): verifies that Qwen3 models get /no_think appended to the user message when think=False.

Both cover the llama-server bypass path by patching _client_cache. All 48 tests pass.

Copilot AI added 2 commits May 28, 2026 06:50
…allback, fix review issues

- Increase defaults: top_k=5, max_tokens=1024, ctx_size=4096, _MAX_CHUNK_CHARS=2000
- Add full-document context fallback for focused single-paper sessions
- Use config system_prompt for follow-ups (not hardcoded)
- Remove duplicate comment in repl.py
- Add stronger gating for history-only answers (require >= 2 history entries)
- Adapt prompt style based on context size (thorough for large contexts)
- Add tests for stream_followup with non-empty history (mocked, both Ollama and Qwen)
- Add test for full-document fallback and follow-up gating in REPL
- Update REPL help text defaults
Copilot AI changed the title feat: add conversation history for follow-up questions in REPL feat: improve answer quality to match llama-server behavior May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mimicking llama server behavior/output to this CLI toolkit (at least for llama.cpp back end)

3 participants