Skip to content

Commit f1851d2

Browse files
committed
Enhance review with output-format=json for GitHub actions!
The code review functionality has been expanded to support structured output in JSON format, and the response token size is now configurable. The review prompt has been updated to provide more detailed instructions. The test coverage has been improved to include tests for the new functionality. Additionally, the 'json' module has been imported in 'cli.py' for handling JSON output.
1 parent 6ea7694 commit f1851d2

File tree

3 files changed

+92
-46
lines changed

3 files changed

+92
-46
lines changed

aicodebot/cli.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from rich.live import Live
1414
from rich.markdown import Markdown
1515
from rich.style import Style
16-
import click, datetime, openai, os, random, subprocess, sys, tempfile, webbrowser, yaml
16+
import click, datetime, json, openai, os, random, subprocess, sys, tempfile, webbrowser, yaml
1717

1818
# ----------------------------- Default settings ----------------------------- #
1919

@@ -327,7 +327,9 @@ def fun_fact(verbose, response_token_size):
327327
@cli.command
328328
@click.option("-c", "--commit", help="The commit hash to review (otherwise look at [un]staged changes).")
329329
@click.option("-v", "--verbose", count=True)
330-
def review(commit, verbose):
330+
@click.option("--output-format", default="text", type=click.Choice(["text", "json"], case_sensitive=False))
331+
@click.option("-t", "--response-token-size", type=int, default=DEFAULT_MAX_TOKENS * 2)
332+
def review(commit, verbose, output_format, response_token_size):
331333
"""Do a code review, with [un]staged changes, or a specified commit."""
332334
setup_config()
333335

@@ -337,7 +339,7 @@ def review(commit, verbose):
337339
sys.exit(0)
338340

339341
# Load the prompt
340-
prompt = get_prompt("review")
342+
prompt = get_prompt("review", structured_output=output_format == "json")
341343
logger.trace(f"Prompt: {prompt}")
342344

343345
# Check the size of the diff context and adjust accordingly
@@ -347,19 +349,27 @@ def review(commit, verbose):
347349
if model_name is None:
348350
raise click.ClickException(f"The diff is too large to review ({request_token_size} tokens). 😢")
349351

350-
with Live(Markdown(""), auto_refresh=True) as live:
351-
llm = Coder.get_llm(
352-
model_name,
353-
verbose,
354-
response_token_size=response_token_size,
355-
streaming=True,
356-
callbacks=[RichLiveCallbackHandler(live, bot_style)],
357-
)
352+
llm = Coder.get_llm(model_name, verbose, response_token_size, streaming=True)
353+
chain = LLMChain(llm=llm, prompt=prompt, verbose=verbose)
358354

359-
# Set up the chain
360-
chain = LLMChain(llm=llm, prompt=prompt, verbose=verbose)
355+
if output_format == "json":
356+
with console.status("Examining the diff and generating the review", spinner=DEFAULT_SPINNER):
357+
response = chain.run(diff_context)
358+
359+
parsed_response = prompt.output_parser.parse(response)
360+
data = {"review_status": parsed_response.review_status, "review_comments": parsed_response.review_comments}
361+
if commit:
362+
data["commit"] = commit
363+
json_response = json.dumps(data, indent=4)
364+
print(json_response) # noqa: T201
365+
366+
else:
367+
# Stream live
368+
with Live(Markdown(""), auto_refresh=True) as live:
369+
llm.streaming = True
370+
llm.callbacks = [RichLiveCallbackHandler(live, bot_style)]
361371

362-
chain.run(diff_context)
372+
chain.run(diff_context)
363373

364374

365375
@cli.command

aicodebot/prompts.py

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from aicodebot.config import read_config
33
from aicodebot.helpers import logger
44
from langchain import PromptTemplate
5+
from langchain.output_parsers import PydanticOutputParser
56
from pathlib import Path
7+
from pydantic import BaseModel, Field
68
from types import SimpleNamespace
79
import functools, os
810

@@ -266,47 +268,73 @@ def generate_files_context(files):
266268
+ get_personality_prompt()
267269
+ """
268270
269-
DO NOT give comments that discuss formatting, as those will be handled with pre-commit hooks.
270-
DO NOT respond with line numbers, use function names or file names instead.
271-
272271
Here's the diff context:
273272
274273
BEGIN DIFF
275274
{diff_context}
276275
END DIFF
277276
278277
Remember:
279-
- Lines starting with "-" are being removed.
280-
- Lines starting with "+" are being added.
281-
- Lines starting with " " are unchanged.
278+
* Lines starting with "-" are being removed.
279+
* Lines starting with "+" are being added.
280+
* Lines starting with " " are unchanged.
281+
* Consider the file names for context (e.g., "README.md" is a markdown file, "*.py" is a Python file).
282+
* Understand the difference between code and comments. Comment lines start with ##, #, or //.
283+
* Point out obvious spelling mistakes in plain text files if you see them, but don't check for spelling in code.
284+
* Do not talk about minor changes. It's better to be terse and focus on issues.
285+
* Do not talk about formatting, as that will be handled with pre-commit hooks.
286+
287+
The main focus is to tell the developer how to make the code better.
288+
289+
The review_status can be one of the following:
290+
* "PASSED" (looks good to me) - there were no serious issues found,
291+
* "COMMENTS" - there were some issues found, but they should not block the build and are informational only
292+
* "FAILED" - there were serious, blocking issues found that should be fixed before merging the code
293+
294+
The review_message should be a markdown-formatted string for display with rich.Markdown or GitHub markdown.
295+
"""
296+
)
282297

283-
Consider the file names for context (e.g., "README.md" is a markdown file, "*.py" is a Python file).
284-
Understand the difference between code and comments. Comment lines start with ##, #, or //.
285298

286-
The main focus is to tell me how I could make the code better.
299+
def get_prompt(command, structured_output=False):
300+
"""Generates a prompt for the sidekick workflow."""
287301

288-
Point out spelling mistakes in plain text files if you see them, but don't try to spell
289-
function and variable names correctly.
302+
if command == "review":
303+
if structured_output:
304+
parser = PydanticOutputParser(pydantic_object=ReviewResult)
305+
return PromptTemplate(
306+
template=REVIEW_TEMPLATE + "\n{format_instructions}",
307+
input_variables=["diff_context"],
308+
partial_variables={"format_instructions": parser.get_format_instructions()},
309+
output_parser=parser,
310+
)
311+
else:
312+
return PromptTemplate(
313+
template=REVIEW_TEMPLATE + "\nRespond in markdown format", input_variables=["diff_context"]
314+
)
290315

291-
If the changes look good overall and don't require any feedback, then just respond with "LGTM" (looks good to me).
316+
else:
317+
prompt_map = {
318+
"alignment": PromptTemplate(template=ALIGNMENT_TEMPLATE, input_variables=[]),
319+
"commit": PromptTemplate(template=COMMIT_TEMPLATE, input_variables=["diff_context"]),
320+
"debug": PromptTemplate(template=DEBUG_TEMPLATE, input_variables=["command_output"]),
321+
"fun_fact": PromptTemplate(template=FUN_FACT_TEMPLATE, input_variables=["topic"]),
322+
"sidekick": PromptTemplate(template=SIDEKICK_TEMPLATE, input_variables=["chat_history", "task", "context"]),
323+
}
292324

293-
Respond in markdown format.
294-
"""
295-
)
325+
try:
326+
return prompt_map[command]
327+
except KeyError as e:
328+
raise ValueError(f"Unable to find prompt for command {command}") from e
296329

297330

298-
def get_prompt(command):
299-
"""Generates a prompt for the sidekick workflow."""
300-
prompt_map = {
301-
"alignment": PromptTemplate(template=ALIGNMENT_TEMPLATE, input_variables=[]),
302-
"commit": PromptTemplate(template=COMMIT_TEMPLATE, input_variables=["diff_context"]),
303-
"debug": PromptTemplate(template=DEBUG_TEMPLATE, input_variables=["command_output"]),
304-
"fun_fact": PromptTemplate(template=FUN_FACT_TEMPLATE, input_variables=["topic"]),
305-
"review": PromptTemplate(template=REVIEW_TEMPLATE, input_variables=["diff_context"]),
306-
"sidekick": PromptTemplate(template=SIDEKICK_TEMPLATE, input_variables=["chat_history", "task", "context"]),
307-
}
308-
309-
try:
310-
return prompt_map[command]
311-
except KeyError as e:
312-
raise ValueError(f"Unable to find prompt for command {command}") from e
331+
# ---------------------------------------------------------------------------- #
332+
# Output Parsers #
333+
# ---------------------------------------------------------------------------- #
334+
335+
336+
class ReviewResult(BaseModel):
337+
"""Review result from the sidekick."""
338+
339+
review_status: str = Field(description="The status of the review: PASSED, COMMENTS, or FAILED")
340+
review_comments: str = Field(description="The comments from the review")

tests/test_cli.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from aicodebot.prompts import DEFAULT_PERSONALITY
66
from git import Repo
77
from pathlib import Path
8-
import os, pytest
8+
import json, os, pytest
99

1010

1111
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="Skipping live tests without an API key.")
@@ -109,12 +109,20 @@ def test_review(cli_runner, temp_git_repo):
109109
repo.git.add("test.txt")
110110

111111
# Run the review command
112-
result = cli_runner.invoke(cli, ["review"])
112+
result = cli_runner.invoke(cli, ["review", "-t", "100"])
113113

114114
# Check that the review command ran successfully
115115
assert result.exit_code == 0
116116
assert len(result.output) > 20
117117

118+
# Again with json output
119+
result = cli_runner.invoke(cli, ["review", "-t", "100", "--output-format", "json"])
120+
121+
assert result.exit_code == 0
122+
# Check if it's valid json
123+
parsed = json.loads(result.output)
124+
assert parsed["review_status"] == "PASSED"
125+
118126

119127
@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="Skipping live tests without an API key.")
120128
def test_sidekick(cli_runner):

0 commit comments

Comments
 (0)