Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Insert context using @ commands #174

Open
wants to merge 35 commits into
base: main
Choose a base branch
from

Conversation

Odie
Copy link
Contributor

@Odie Odie commented Jul 27, 2024

Hi there!

What this PR does

I’ve started implementing a feature in continue.dev that I think is quite useful. It allows users to use @ commands to automatically include additional context from their project. For example, users can use @file to insert the entire contents of a file or @code to insert a specific function by name.

Here’s an example user message:

@file:lua/gp/completion.lua

Please explain this file briefly.

In this case, the contents of lua/gp/completion.lua would be prepended to the user message before being sent out.

Command completion

The @ commands are assembled with assistance from a custom completion source for ease of use. To try this PR out, please add “hrsh7th/nvim-cmp” as a plugin dependency:

{
    "robitx/gp.nvim",
    dependencies = { "hrsh7th/nvim-cmp" },
    -- additional config
}

The completion source is automatically attached to the chat buffer, so no additional configuration is required.

TODOs

  • Completion source for @file
  • Context insertion for @file commands
  • Generate an index for all function names in project
  • Completion source for @code
  • Context insertion for @code commands

This feature is still a work in progress, but the @file command is now functional. Your feedback is welcome!

@qaptoR
Copy link

qaptoR commented Jul 31, 2024

So I merged this into my fork on this branch:
https://github.com/qaptoR-nvim/gp.nvim/tree/insert_context

there were minor changes needed due to the recent restructure of the main repo. Also, I removed the (```) code braces around the file context so that it would be more generally usable for other types of contexts needed in the conversation, because I'm using this feature to selectively include specific rule's files for my personal ttrpg.

but I do think that feature would be useful as like an @codefile target, or something similar that does insert the code braces.

image

@Robitx
Copy link
Owner

Robitx commented Jul 31, 2024

Just to backup a thought (haven't looked over the implementation yet).

I'll start neovim from dir A, make a chat with relative references and everything works.
Then ( https://www.youtube.com/watch?v=CwBjoH_ZaNw ) I run new neovim instance from dir B, open the old chat and wish to continue the thread - will it work?

The same issue will arise for any future @contexts we'll add to chats, we'll have to store necessary data for reproducible runs from anywhere.

@Odie
Copy link
Contributor Author

Odie commented Jul 31, 2024

@qaptoR

Hey, thanks the the feedback. I’m still actively working on this feature. I’ll rebase on main when it’s ready to be looked at, probably in the next few days. I’m not sure if that makes it easier or harder to merge into your personal beach though. :(

Also, I removed the (```) code braces around the file context so that it would be more generally usable for other types of contexts needed in the conversation

The @file command actually inserts both a relative file path and the file content. I thought the triple backtick fence would make it clear to the LLM where the file content starts and ends. Does the triple backtick fence actually confuse the LLM for your use case? Do you mind sharing a concrete example of the problem you ran into?

If it really is a problem, I suppose, we can try something like @include to make it so it neither puts in the file path nor add the backticks.

@qaptoR
Copy link

qaptoR commented Jul 31, 2024

@Odie the situation I was thinking ahead to regarding the backtick fence was if the included file was itself a markdown file with included triple backtick fence posts. So if the template inserts them, it will invert all of them.

instead, I think including the file path sets a clear demarcation for the LLM to understand that what follows is it's content, especially if newlines are used strategically, such as one newline between the filepath and the content and two or probably better three newlines between file contexts. The LLMs are pretty good at picking up patterns like that. I think two/three newlines before the actual message is also all that is necessary as well because it follows the same pattern.

@Robitx
Copy link
Owner

Robitx commented Jul 31, 2024

Checking for longest sequence of ` in the file and using N+1 as fence?

# 4 backticks

```python
def main():
    print("3 backticks")
```

# 4 backticks

@Odie
Copy link
Contributor Author

Odie commented Jul 31, 2024

I'll start neovim from dir A, make a chat with relative references and everything works.
Then ( https://www.youtube.com/watch?v=CwBjoH_ZaNw ) I run new neovim instance from dir B, open the old chat and wish to continue the thread - will it work?

Hmm! Are both dir A and dir B inside the same git repo? I had meant for the relative oaths to work from the project git root. Though the current implementation doesn’t quite do it yet. (It’s maybe one or two lines modification to make it so.) So, this will work at some point soon in the future as I finish up the first pass of this feature and tries to clean up some loose ends.

If what you’re describing is to actually carry the chat across to different repos… then, for sure it doesn’t work that way as implemented. At the moment, the requested contexts are parsed out of the last chat message (presumably from the user). The msg is augmented with the contexts right before it goes out on the wire. The current state of those requested contexts are never recorded anywhere. :(

If we want the chat log to be a definitive record of what exactly was exchanged between the user and the LLM, I guess we’ll have to try to insert the text into the chat buffer instead? The chat log size might explode if the user is repeatedly iterating on a single file though. It’ll also eat up the available context window rather quickly.

@qaptoR
Copy link

qaptoR commented Jul 31, 2024

I think the files only need to be included where they are used. so if they are added early in the conversation then they should always be inserted early, it's all the same to the LLM I think, and then it keeps the entire conversation consistent as it progresses.

also, it solves the issue of having it included in the actual chat file. although I would appreciate the option to not insert the text directly into the chat file, as one reason that I like the system as it is, is because it keeps a clean chat history visually where it's more like each insertion from the chat file's perspective is like a reference to shared knowledge. Like a header file in c++, it helps keep things clean and organized

I plan on including tens of files in certain conversations to use with gpt-mini or claude haiku, basically selectively including different sets of what amounts to 100s of pages of rules for my ttrpg so that I can query for inconsistencies or new ideas.

@Robitx
Copy link
Owner

Robitx commented Jul 31, 2024

@Odie :GpChatFinder allows you to open old chats from anywhere, which means it should work when picked up again from anywhere. (I know it complicates things, sorry).

We have chat_dir where the chats are stored. One easy solution would be for a chat chat_dir/chatXY.md to have an artifact folder chat_dir/chatXY_artifacts/.. and have all the relevant data in there, without polluting the chat file itself.

If we wanted to complicate things further, we could remember from which dir was the artifact made and when generating new response first try to recreate a fresh instance of the artifact and if that failed fall for the the old instance backed up in the artifacts.

@qaptoR
Copy link

qaptoR commented Aug 1, 2024

@Robitx I think an artifacts directory as a solution adds more complexity than is necessary, since it would have to be maintained that when x file is deleted, x artifact file is also deleted.

For the time being I think it should take advantage of the makrdown yaml header section key\value pairs, and every conversation should include a 'cwd' key with the path inserted when it is created. then, all of the relative paths should use this value instead of the vim cwd to remain consistent.

I think this approach accomplishes a few things:

  • simplicity: no added complexity of managing an artefacts directory is necessary
  • visibility: each conversation will have it's variables in use immediately available without having to look elsewhere
  • alterability: as an aside to visibility, easy access means updating the path if necessary after file creation is quick

ultimately, a header with even 20 or 40 key value pairs at the start of the conversation is prefereable to trackign down an artifact file, and scales relatively well for the time being because conversations typically outweigh the header in length, so scrolling is already greatly necessary.

it may even be that for a given chat file x, there is some data which is preferable to maintain as artifact file x, and other data which is beneficial to be quickly visible. But at least I would argue that for 'cwd' it deserves to be visible.

@Robitx
Copy link
Owner

Robitx commented Aug 1, 2024

@qaptoR You're right, that's why I put a the One in there One easy solution would be.. 🙂

I don't know yet, which would be better. For online LLM chats this is non issue, since they have to store uploaded artifacts anyway, but we do have a choice.

Just a thought to the solution using cwd header - I can easily imagine situations where single cwd won't be sufficient (for example user working on something across multiple repositories like a project using micro services, or just simply project + referencing some library).

Instead of putting cwd in header use [some_syntax_for_optional_cwd_prefix]@context_macro and hiding it with vim.fn.matchadd("Conceal", .. so it doesn't visibly pollute the chat buffer.

@Odie
Copy link
Contributor Author

Odie commented Aug 2, 2024

Hi all,

The new @code command is now working, though only for lua files.

Function name Indexing

When the user opens the chat buffer via GpChatNew or GpChatToggle inside a project for the first time, it'll index all lua files and the top level functions. At the moment, this is done synchronously. Indexing gp.nvim itself, takes about 70ms on my machine. We're also updating the index on a per-file basis whenever a file is saved. However, this only happens when the gp has been loaded. We're still missing a periodic update on the index, to catch cases where files are altered without gp's knowledge.

@code completion

When the user enters @code: in the chat buffer, a full list of known function names and their origin files are listed in the completion menu. Once the user chooses something, the @code command takes the form of @code:rel_path/to/file:full_fn_name.

When the chat msg is submitted, we grab the relevant lines from the src file using info in the index and insert them.

So, a message like this should now work:

@code:lua/gp/db.lua:Db.open

Explain this, please.

New dependency

The index is being kept in a sqlite database, so we now have one additional dependency:

{
    "robitx/gp.nvim",
    dependencies = { "hrsh7th/nvim-cmp", "kkharji/sqlite.lua" },
    -- additional config
}

TODOs

  • Automatic periodic force rebuild of fn index
  • Attach completion source on chat buffer only (instead of on *.md)
  • Expose index rebuild as a command
  • Look into building the index in the background or asynchronously
  • Add support for python files
  • Look into skipping files to index based on .gitignore (plenary seems to have some support for this?)
  • merge with main branch

@Odie
Copy link
Contributor Author

Odie commented Aug 4, 2024

Hi all,

I think I've added all the features I set out to implement.

The latest commit is now depends on plenary to deal with gitignore.

The dependencies required now looks like:

{
    "robitx/gp.nvim",
    dependencies = { "hrsh7th/nvim-cmp", "kkharji/sqlite.lua", "nvim-lua/plenary.nvim" },
    -- additional config
}

I'm actually only using 1 utility function from plenary. So, if there are any objections to this, I can always just keep a local copy of that function instead.

@Odie
Copy link
Contributor Author

Odie commented Aug 4, 2024

Hi all!

I merged main into the branch. Hopefully, this makes it easier to try out. Please do use it for a bit and let me know if you've run into any issues. I'm sure there are lots of rough edges that needs to be fixed.

Python support

Symbol indexing now grabs plain functions, class methods, and classes using treesitter. They should show up when using the @code command and correctly marked as their corresponding types.

Synchronous indexing

Async support is left undone at the moment. I looked every so briefly into indexing asynchrously, but didn't pursue it further as indexing seem "fast enough" for the small projects I'm trying it with. I'd really like to start using the plugin for a bit and discover other perhaps more urgent problems before tackling async support.

@qaptoR I've added an @include command. It'll insert the file requested by the command as is, without any backticks fences.

the cursor

This simplifies sending the function under the cursor as the chat
context.
@qaptoR
Copy link

qaptoR commented Aug 8, 2024

@Odie there are two things I want to make clear 1) i appreciate all the work you've done on this, and 2) all the work involved with the sqlite db is incredible, and I plan on diving in to how it works because I want to write a plugin that mimics 'dataview' for obsidian, where it searches through a project and indexes data for searching that other plugins can then tap into.

however, I do not forsee myself using the @code feature because gp.nvim already had the ability to select code and insert it into the conversation with a file path annotation, which i think is faster and easier for me to target. I also don't want to incur the cost of indexing (however small) at this time, and I just think your first initial implementation was so elegant and simple I'm just adapting it for myself.

i'm also implementing an @import command, which targets a file that can have @file or @include (or even more @import commands). so it's a recursive feature that allows for writing single file with all the commonly used includes. though I'm still trying to solve the situation where there is infinite recursion, but I think it would be hard to get into that situation if the user writes their command files carefully to not create reference loops where a imports b and b imports a kind of thing.

@qaptoR
Copy link

qaptoR commented Aug 11, 2024

Just wanted to share this. it's the first time that I've used the import feature on a large set of large files.
as you can see, the recursive imports is working.

on the left of the image are the 'import files', top right is the 'import binder'.

Then in bottom right is the final import command which references the binder. Then I query the entire context.

Claude haiku says it is about 36K tokens of context, Chatgpt mini says 31K, and I can't figure out how to see that info on gemini.
Gotta figure out gemini flash who charged 4 cents, claude was ~1 cent, and chatgpt mini was <1 cent

image

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.

3 participants