Skip to content

Commit f596f5e

Browse files
committed
Add /command interface to sidekick with tools for adding/removeing files
In `aicodebot/helpers.py`, a new class `SidekickCompleter` has been added to provide command completion functionality in the sidekick feature. In `aicodebot/learn.py`, the error messages have been made more informative and user-friendly. The `requirements.in` and `requirements.txt` files have been updated with the addition of the `humanize` library.
1 parent 28e056e commit f596f5e

File tree

6 files changed

+122
-42
lines changed

6 files changed

+122
-42
lines changed

aicodebot/cli.py

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
from aicodebot import version as aicodebot_version
22
from aicodebot.coder import CREATIVE_TEMPERATURE, DEFAULT_MAX_TOKENS, Coder
33
from aicodebot.config import get_config_file, get_local_data_dir, read_config
4-
from aicodebot.helpers import RichLiveCallbackHandler, create_and_write_file, exec_and_get_output, logger
4+
from aicodebot.helpers import (
5+
RichLiveCallbackHandler,
6+
SidekickCompleter,
7+
create_and_write_file,
8+
exec_and_get_output,
9+
logger,
10+
)
511
from aicodebot.learn import load_documents_from_repo, store_documents
612
from aicodebot.prompts import DEFAULT_PERSONALITY, PERSONALITIES, generate_files_context, get_prompt
13+
from datetime import datetime
714
from langchain.chains import LLMChain
815
from langchain.memory import ConversationTokenBufferMemory
916
from openai.api_resources import engine
@@ -14,7 +21,7 @@
1421
from rich.live import Live
1522
from rich.markdown import Markdown
1623
from rich.style import Style
17-
import click, datetime, json, langchain, openai, os, random, shutil, subprocess, sys, tempfile, webbrowser, yaml
24+
import click, humanize, json, langchain, openai, os, random, shutil, subprocess, sys, tempfile, webbrowser, yaml
1825

1926
# ----------------------------- Default settings ----------------------------- #
2027

@@ -159,7 +166,9 @@ def commit(verbose, response_token_size, yes, skip_pre_commit, files): # noqa:
159166
chain = LLMChain(llm=llm, prompt=prompt, verbose=verbose)
160167
response = chain.run(diff_context)
161168

162-
commit_message_approved = click.confirm("Do you want to use this commit message (type n to edit)?", default=True)
169+
commit_message_approved = click.confirm(
170+
"Do you want to use this commit message (type n to edit)?", default=True
171+
)
163172

164173
# Write the commit message to a temporary file
165174
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp:
@@ -349,7 +358,7 @@ def fun_fact(verbose, response_token_size):
349358
# Set up the chain
350359
chain = LLMChain(llm=llm, prompt=prompt, verbose=verbose)
351360

352-
year = random.randint(1942, datetime.datetime.utcnow().year)
361+
year = random.randint(1942, datetime.utcnow().year)
353362
chain.run(f"programming and artificial intelligence in the year {year}")
354363

355364

@@ -368,7 +377,7 @@ def learn(repo_url, verbose):
368377

369378
owner, repo_name = Coder.parse_github_url(repo_url)
370379

371-
start_time = datetime.datetime.utcnow()
380+
start_time = datetime.utcnow()
372381

373382
local_data_dir = get_local_data_dir()
374383

@@ -382,7 +391,7 @@ def learn(repo_url, verbose):
382391

383392
with console.status("Storing the repo in the vector store", spinner=DEFAULT_SPINNER):
384393
store_documents(documents, vector_store_dir)
385-
console.print(f"✅ Repo loaded and indexed in {datetime.datetime.utcnow() - start_time} seconds.")
394+
console.print(f"✅ Repo loaded and indexed in {datetime.utcnow() - start_time} seconds.")
386395

387396

388397
@cli.command
@@ -433,7 +442,9 @@ def review(commit, verbose, output_format, response_token_size, files):
433442

434443
else:
435444
# Stream live
436-
console.print("Examining the diff and generating the review for the following files:\n\t" + "\n\t".join(files))
445+
console.print(
446+
"Examining the diff and generating the review for the following files:\n\t" + "\n\t".join(files)
447+
)
437448
with Live(Markdown(""), auto_refresh=True) as live:
438449
llm.streaming = True
439450
llm.callbacks = [RichLiveCallbackHandler(live, bot_style)]
@@ -446,7 +457,7 @@ def review(commit, verbose, output_format, response_token_size, files):
446457
@click.option("-v", "--verbose", count=True)
447458
@click.option("-t", "--response-token-size", type=int, default=DEFAULT_MAX_TOKENS * 3)
448459
@click.argument("files", nargs=-1)
449-
def sidekick(request, verbose, response_token_size, files):
460+
def sidekick(request, verbose, response_token_size, files): # noqa: PLR0915
450461
"""
451462
EXPERIMENTAL: Coding help from your AI sidekick\n
452463
FILES: List of files to be used as context for the session
@@ -462,6 +473,16 @@ def sidekick(request, verbose, response_token_size, files):
462473
# git history
463474
context = generate_files_context(files)
464475

476+
def show_file_context(files):
477+
console.print("Files loaded in this session:")
478+
for file in files:
479+
token_length = Coder.get_token_length(Path(file).read_text())
480+
console.print(f"\t{file} ({humanize.intcomma(token_length)} tokens)")
481+
482+
if files:
483+
files = set(files) # Dedupe
484+
show_file_context(files)
485+
465486
# Generate the prompt and set up the model
466487
prompt = get_prompt("sidekick")
467488
memory_token_size = response_token_size * 2 # Allow decent history
@@ -474,33 +495,64 @@ def sidekick(request, verbose, response_token_size, files):
474495

475496
llm = Coder.get_llm(model_name, verbose, response_token_size, streaming=True)
476497

477-
# Open the temporary file in the user's editor
478-
editor = Path(os.getenv("EDITOR", "/usr/bin/vim")).name
479-
480498
# Set up the chain
481499
memory = ConversationTokenBufferMemory(
482500
memory_key="chat_history", input_key="task", llm=llm, max_token_limit=memory_token_size
483501
)
484502
chain = LLMChain(llm=llm, prompt=prompt, memory=memory, verbose=verbose)
485503
history_file = Path.home() / ".aicodebot_request_history"
486504

487-
console.print(f"Enter a request OR (q) quit, OR (e) to edit using {editor}")
505+
console.print(
506+
"Enter a request for your AICodeBot sidekick. Type / to see available commands.\n", style=bot_style
507+
)
488508
while True: # continuous loop for multiple questions
489509
edited_input = None
490510
if request:
491511
human_input = request
492512
else:
493-
human_input = input_prompt("🤖 ➤ ", history=FileHistory(history_file)).strip()
513+
human_input = input_prompt("🤖 ➤ ", history=FileHistory(history_file), completer=SidekickCompleter())
514+
human_input = human_input.strip()
494515
if not human_input:
495516
# Must have been spaces or blank line
496517
continue
497-
elif len(human_input) == 1:
498-
if human_input.lower() == "q":
499-
break
500-
elif human_input.lower() == "e":
518+
519+
if human_input.startswith("/"):
520+
cmd = human_input.lower().split()[0]
521+
# Handle commands
522+
if cmd in ["/add", "/drop"]:
523+
# Get the filename
524+
# If they didn't specify a file, then ignore
525+
try:
526+
filename = human_input.split()[1]
527+
except IndexError:
528+
continue
529+
530+
# If the file doesn't exist, or we can't open it, let them know
531+
if not Path(filename).exists():
532+
console.print(f"File '{filename}' doesn't exist.", style=error_style)
533+
continue
534+
535+
if cmd == "/add":
536+
files.add(filename)
537+
console.print(f"✅ Added '{filename}' to the list of files.")
538+
elif cmd == "/drop":
539+
# Drop the file from the list
540+
files.discard(filename)
541+
console.print(f"✅ Dropped '{filename}' from the list of files.")
542+
543+
context = generate_files_context(files)
544+
show_file_context(files)
545+
continue
546+
elif cmd == "/edit":
501547
human_input = edited_input = click.edit()
548+
elif cmd == "/files":
549+
show_file_context(files)
550+
continue
551+
elif cmd == "/quit":
552+
break
553+
502554
elif human_input.lower()[-2:] == r"\e":
503-
# If the text ends with \e then we want to edit it
555+
# If the text ends wit then we want to edit it
504556
human_input = edited_input = click.edit(human_input[:-2])
505557

506558
if edited_input:
@@ -535,5 +587,5 @@ def setup_config():
535587
return existing_config
536588

537589

538-
if __name__ == "__main__":
590+
if __name__ == "__main__": # pragma: no cover
539591
cli()

aicodebot/helpers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from langchain.callbacks.base import BaseCallbackHandler
22
from loguru import logger
33
from pathlib import Path
4+
from prompt_toolkit.completion import Completer, Completion
45
from rich.markdown import Markdown
56
import os, subprocess, sys
67

@@ -31,6 +32,29 @@ def create_and_write_file(filename, text, overwrite=False):
3132
f.write(text)
3233

3334

35+
class SidekickCompleter(Completer):
36+
"""A custom prompt_toolkit completer for sidekick."""
37+
38+
def get_completions(self, document, complete_event):
39+
# Get the text before the cursor
40+
text = document.text_before_cursor
41+
42+
supported_commands = ["/add", "/drop", "/edit", "/files", "/quit"]
43+
44+
# If the text starts with a slash, it's a command
45+
if text.startswith("/"):
46+
for command in supported_commands:
47+
if command.startswith(text):
48+
yield Completion(command, start_position=-len(text))
49+
50+
if text.startswith(("/add ", "/drop ")):
51+
# If the text starts with /add or /drop, it's a file
52+
files = Path().rglob("*")
53+
for file in files:
54+
if file.name.startswith(text.split()[-1]):
55+
yield Completion(file.name, start_position=-len(text.split()[-1]))
56+
57+
3458
def exec_and_get_output(command):
3559
"""Execute a command and return its output as a string."""
3660
logger.debug(f"Executing command: {' '.join(command)}")

aicodebot/learn.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,9 @@ def store_documents(documents, vector_store_dir):
9999
f"Processing {document.metadata['file_path']} as {language_extension_map[file_type].value} code"
100100
)
101101
splitter = RecursiveCharacterTextSplitter.from_language(
102-
language=language_extension_map[document.metadata["file_type"].lower()], chunk_size=50, chunk_overlap=0
102+
language=language_extension_map[document.metadata["file_type"].lower()],
103+
chunk_size=50,
104+
chunk_overlap=0,
103105
)
104106
else:
105107
# TODO: Check if it's a text file
@@ -119,7 +121,9 @@ def load_learned_repo(repo_name):
119121
"""Load a vector store from a learned repo."""
120122
vector_store_file = Path(get_local_data_dir() / "vector_stores" / repo_name / "faiss_index")
121123
if not vector_store_file.exists():
122-
raise ValueError(f"Vector store for {repo_name} does not exist. Please run `aicodebot learn $githuburl` first.")
124+
raise ValueError(
125+
f"Vector store for {repo_name} does not exist. Please run `aicodebot learn $githuburl` first."
126+
)
123127

124128
embeddings = OpenAIEmbeddings()
125129
return FAISS.load_local(vector_store_file, embeddings)

aicodebot/prompts.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@
8787
),
8888
"Her": SimpleNamespace(name="Her", prompt=HER, description="The AI character from the movie Her"),
8989
"Jules": SimpleNamespace(
90-
name="Jules", prompt=JULES, description="Samuel L. Jackson's character from Pulp Fiction (warning: profanity))"
90+
name="Jules",
91+
prompt=JULES,
92+
description="Samuel L. Jackson's character from Pulp Fiction (warning: profanity))",
9193
),
9294
"Michael": SimpleNamespace(
9395
name="Michael", prompt=MICHAEL, description="Michael Scott from The Office (warning: TWSS))"
@@ -183,11 +185,6 @@ def generate_files_context(files):
183185
files_context += "Here are the relevant files we are working with in this session:\n"
184186
for file_name in files:
185187
contents = Path(file_name).read_text()
186-
token_length = Coder.get_token_length(contents)
187-
if token_length > 2_000:
188-
logger.warning(f"File {file_name} is large, using {token_length} tokens")
189-
else:
190-
logger.debug(f"File {file_name} is {token_length} tokens")
191188
files_context += f"--- START OF FILE: {file_name} ---\n"
192189
files_context += contents
193190
files_context += f"\n--- END OF FILE: {file_name} ---\n\n"
@@ -341,7 +338,9 @@ def get_prompt(command, structured_output=False):
341338
"commit": PromptTemplate(template=COMMIT_TEMPLATE, input_variables=["diff_context"]),
342339
"debug": PromptTemplate(template=DEBUG_TEMPLATE, input_variables=["command_output"]),
343340
"fun_fact": PromptTemplate(template=FUN_FACT_TEMPLATE, input_variables=["topic"]),
344-
"sidekick": PromptTemplate(template=SIDEKICK_TEMPLATE, input_variables=["chat_history", "task", "context"]),
341+
"sidekick": PromptTemplate(
342+
template=SIDEKICK_TEMPLATE, input_variables=["chat_history", "task", "context"]
343+
),
345344
}
346345

347346
try:

requirements/requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ beautifulsoup4 # needed by langchain
99
click # command line interface helpers
1010
faiss-cpu
1111
GitPython
12+
humanize
1213
langchain
1314
loguru
1415
openai

requirements/requirements.txt

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This file is autogenerated by pip-compile with Python 3.11
33
# by the following command:
44
#
5-
# pip-compile requirements.in
5+
# pip-compile
66
#
77
aiohttp==3.8.4
88
# via
@@ -15,39 +15,39 @@ async-timeout==4.0.2
1515
attrs==23.1.0
1616
# via aiohttp
1717
beautifulsoup4==4.12.2
18-
# via -r requirements/requirements.in
18+
# via -r requirements.in
1919
certifi==2023.5.7
2020
# via requests
2121
charset-normalizer==3.1.0
2222
# via
2323
# aiohttp
2424
# requests
2525
click==8.1.6
26-
# via -r requirements/requirements.in
26+
# via -r requirements.in
2727
dataclasses-json==0.5.8
2828
# via langchain
2929
faiss-cpu==1.7.4
30-
# via -r requirements/requirements.in
30+
# via -r requirements.in
3131
frozenlist==1.3.3
3232
# via
3333
# aiohttp
3434
# aiosignal
3535
gitdb==4.0.10
3636
# via gitpython
3737
gitpython==3.1.32
38-
# via -r requirements/requirements.in
39-
greenlet==2.0.2
40-
# via sqlalchemy
38+
# via -r requirements.in
39+
humanize==4.7.0
40+
# via -r requirements.in
4141
idna==3.4
4242
# via
4343
# requests
4444
# yarl
4545
langchain==0.0.238
46-
# via -r requirements/requirements.in
46+
# via -r requirements.in
4747
langsmith==0.0.11
4848
# via langchain
4949
loguru==0.7.0
50-
# via -r requirements/requirements.in
50+
# via -r requirements.in
5151
markdown-it-py==3.0.0
5252
# via rich
5353
marshmallow==3.19.0
@@ -71,13 +71,13 @@ numpy==1.25.0
7171
# langchain
7272
# numexpr
7373
openai==0.27.8
74-
# via -r requirements/requirements.in
74+
# via -r requirements.in
7575
openapi-schema-pydantic==1.2.4
7676
# via langchain
7777
packaging==23.1
7878
# via marshmallow
7979
prompt-toolkit==3.0.39
80-
# via -r requirements/requirements.in
80+
# via -r requirements.in
8181
pydantic==1.10.9
8282
# via
8383
# langchain
@@ -87,7 +87,7 @@ pygments==2.15.1
8787
# via rich
8888
pyyaml==6.0.1
8989
# via
90-
# -r requirements/requirements.in
90+
# -r requirements.in
9191
# langchain
9292
regex==2023.6.3
9393
# via tiktoken
@@ -98,7 +98,7 @@ requests==2.31.0
9898
# openai
9999
# tiktoken
100100
rich==13.4.2
101-
# via -r requirements/requirements.in
101+
# via -r requirements.in
102102
smmap==5.0.0
103103
# via gitdb
104104
soupsieve==2.4.1
@@ -108,7 +108,7 @@ sqlalchemy==2.0.16
108108
tenacity==8.2.2
109109
# via langchain
110110
tiktoken==0.4.0
111-
# via -r requirements/requirements.in
111+
# via -r requirements.in
112112
tqdm==4.65.0
113113
# via openai
114114
typing-extensions==4.6.3

0 commit comments

Comments
 (0)