Skip to content

Commit 73b91b1

Browse files
committed
2 parents 9e2196a + a487a24 commit 73b91b1

10 files changed

+162
-108
lines changed

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ authors = [
1212
{ name="Stephen Freund", email="[email protected]" },
1313
]
1414
dependencies = [
15-
"llm-utils>=0.2.6",
15+
"llm-utils>=0.2.8",
1616
"openai>=1.6.1",
1717
"rich>=13.7.0",
1818
"ansicolors>=1.1.8",
@@ -22,6 +22,7 @@ dependencies = [
2222
"litellm>=1.26.6",
2323
"PyYAML>=6.0.1",
2424
"ipyflow>=0.0.130",
25+
"numpy>=1.26.3"
2526
]
2627
description = "AI-assisted debugging. Uses AI to answer 'why'."
2728
readme = "README.md"

src/chatdbg/assistant/assistant.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def run(self, prompt, client_print=print):
213213
)
214214
client_print()
215215
client_print(f"[Cost: ~${cost:.2f} USD]")
216-
return run.usage.total_tokens, cost, elapsed_time
216+
return run.usage.total_tokens,run.usage.prompt_tokens, run.usage.completion_tokens, cost, elapsed_time
217217
except OpenAIError as e:
218218
client_print(f"*** OpenAI Error: {e}")
219219
sys.exit(-1)

src/chatdbg/chatdbg_lldb.py

+64-48
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import json
99

1010
import llm_utils
11-
import openai
1211

1312
from assistant.lite_assistant import LiteAssistant
1413
import chatdbg_utils
@@ -234,7 +233,7 @@ def why(
234233
sys.exit(1)
235234

236235
the_prompt = buildPrompt(debugger)
237-
args, _ = chatdbg_utils.parse_known_args(command)
236+
args, _ = chatdbg_utils.parse_known_args(command.split())
238237
chatdbg_utils.explain(the_prompt[0], the_prompt[1], the_prompt[2], args)
239238

240239

@@ -389,7 +388,12 @@ def _instructions():
389388
You are an assistant debugger.
390389
The user is having an issue with their code, and you are trying to help them find the root cause.
391390
They will provide a short summary of the issue and a question to be answered.
391+
392392
Call the `lldb` function to run lldb debugger commands on the stopped program.
393+
Call the `get_code_surrounding` function to retrieve user code and give more context back to the user on their problem.
394+
Call the `find_definition` function to retrieve the definition of a particular symbol.
395+
You should call `find_definition` on every symbol that could be linked to the issue.
396+
393397
Don't hesitate to use as many function calls as needed to give the best possible answer.
394398
Once you have identified the root cause of the problem, explain it and provide a way to fix the issue if you can.
395399
"""
@@ -440,52 +444,6 @@ def get_code_surrounding(filename: str, lineno: int) -> str:
440444
(lines, first) = llm_utils.read_lines(filename, lineno - 7, lineno + 3)
441445
return llm_utils.number_group_of_lines(lines, first)
442446

443-
clangd = clangd_lsp_integration.clangd()
444-
445-
def find_definition(filename: str, lineno: int, character: int) -> str:
446-
"""
447-
{
448-
"name": "find_definition",
449-
"description": "Returns the definition for the symbol at the given source location.",
450-
"parameters": {
451-
"type": "object",
452-
"properties": {
453-
"filename": {
454-
"type": "string",
455-
"description": "The filename the code location is from."
456-
},
457-
"lineno": {
458-
"type": "integer",
459-
"description": "The line number where the symbol is present."
460-
},
461-
"character": {
462-
"type": "integer",
463-
"description": "The column number where the symbol is present."
464-
}
465-
},
466-
"required": [ "filename", "lineno", "character" ]
467-
}
468-
}
469-
"""
470-
clangd.didOpen(filename, "c" if filename.endswith(".c") else "cpp")
471-
definition = clangd.definition(filename, lineno, character)
472-
clangd.didClose(filename)
473-
474-
if "result" not in definition or not definition["result"]:
475-
return "No definition found."
476-
477-
path = clangd_lsp_integration.uri_to_path(definition["result"][0]["uri"])
478-
start_lineno = definition["result"][0]["range"]["start"]["line"] + 1
479-
end_lineno = definition["result"][0]["range"]["end"]["line"] + 1
480-
(lines, first) = llm_utils.read_lines(path, start_lineno - 5, end_lineno + 5)
481-
content = llm_utils.number_group_of_lines(lines, first)
482-
line_string = (
483-
f"line {start_lineno}"
484-
if start_lineno == end_lineno
485-
else f"lines {start_lineno}-{end_lineno}"
486-
)
487-
return f"""File '{path}' at {line_string}:\n```\n{content}\n```"""
488-
489447
assistant = LiteAssistant(
490448
_instructions(),
491449
model=args.llm,
@@ -500,6 +458,62 @@ def find_definition(filename: str, lineno: int, character: int) -> str:
500458
print("[WARNING] clangd is not available.")
501459
print("[WARNING] The `find_definition` function will not be made available.")
502460
else:
461+
clangd = clangd_lsp_integration.clangd()
462+
463+
def find_definition(filename: str, lineno: int, symbol: str) -> str:
464+
"""
465+
{
466+
"name": "find_definition",
467+
"description": "Returns the definition for the given symbol at the given source line number.",
468+
"parameters": {
469+
"type": "object",
470+
"properties": {
471+
"filename": {
472+
"type": "string",
473+
"description": "The filename the symbol is from."
474+
},
475+
"lineno": {
476+
"type": "integer",
477+
"description": "The line number where the symbol is present."
478+
},
479+
"symbol": {
480+
"type": "string",
481+
"description": "The symbol to lookup."
482+
}
483+
},
484+
"required": [ "filename", "lineno", "symbol" ]
485+
}
486+
}
487+
"""
488+
# We just return the first match here. Maybe we should find all definitions.
489+
with open(filename, "r") as file:
490+
lines = file.readlines()
491+
if lineno - 1 >= len(lines):
492+
return "Symbol not found at that location!"
493+
character = lines[lineno - 1].find(symbol)
494+
if character == -1:
495+
return "Symbol not found at that location!"
496+
clangd.didOpen(filename, "c" if filename.endswith(".c") else "cpp")
497+
definition = clangd.definition(filename, lineno, character + 1)
498+
clangd.didClose(filename)
499+
500+
if "result" not in definition or not definition["result"]:
501+
return "No definition found."
502+
503+
path = clangd_lsp_integration.uri_to_path(definition["result"][0]["uri"])
504+
start_lineno = definition["result"][0]["range"]["start"]["line"] + 1
505+
end_lineno = definition["result"][0]["range"]["end"]["line"] + 1
506+
(lines, first) = llm_utils.read_lines(
507+
path, start_lineno - 5, end_lineno + 5
508+
)
509+
content = llm_utils.number_group_of_lines(lines, first)
510+
line_string = (
511+
f"line {start_lineno}"
512+
if start_lineno == end_lineno
513+
else f"lines {start_lineno}-{end_lineno}"
514+
)
515+
return f"""File '{path}' at {line_string}:\n```\n{content}\n```"""
516+
503517
assistant.add_function(find_definition)
504518

505519
return assistant
@@ -517,6 +531,8 @@ def get_frame_summary() -> str:
517531

518532
summaries = []
519533
for i, frame in enumerate(thread):
534+
if not frame.GetDisplayFunctionName():
535+
continue
520536
name = frame.GetDisplayFunctionName().split("(")[0]
521537
arguments = []
522538
for j in range(

src/chatdbg/chatdbg_pdb.py

+17-45
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from .ipdb_util.logging import ChatDBGLog, CopyingTextIOWrapper
2222
from .ipdb_util.prompts import pdb_instructions
2323
from .ipdb_util.text import *
24+
from .ipdb_util.locals import *
2425

2526
_valid_models = [
2627
"gpt-4-turbo-preview",
@@ -194,7 +195,7 @@ def onecmd(self, line: str) -> bool:
194195
output = strip_color(hist_file.getvalue())
195196
if line not in [ 'quit', 'EOF']:
196197
self._log.user_command(line, output)
197-
if line not in [ 'hist', 'test_prompt' ] and not self.was_chat:
198+
if line not in [ 'hist', 'test_prompt', 'c', 'continue' ] and not self.was_chat:
198199
self._history += [ (line, output) ]
199200

200201
def message(self, msg) -> None:
@@ -389,56 +390,27 @@ def print_stack_trace(self, context=None, locals=None):
389390
pass
390391

391392

392-
def _get_defined_locals_and_params(self, frame):
393-
394-
class SymbolFinder(ast.NodeVisitor):
395-
def __init__(self):
396-
self.defined_symbols = set()
397-
398-
def visit_Assign(self, node):
399-
for target in node.targets:
400-
if isinstance(target, ast.Name):
401-
self.defined_symbols.add(target.id)
402-
self.generic_visit(node)
403-
404-
def visit_For(self, node):
405-
if isinstance(node.target, ast.Name):
406-
self.defined_symbols.add(node.target.id)
407-
self.generic_visit(node)
408-
409-
def visit_comprehension(self, node):
410-
if isinstance(node.target, ast.Name):
411-
self.defined_symbols.add(node.target.id)
412-
self.generic_visit(node)
413-
414-
415-
try:
416-
source = textwrap.dedent(inspect.getsource(frame))
417-
tree = ast.parse(source)
418-
419-
finder = SymbolFinder()
420-
finder.visit(tree)
421-
422-
args, varargs, keywords, locals = inspect.getargvalues(frame)
423-
parameter_symbols = set(args + [ varargs, keywords ])
424-
parameter_symbols.discard(None)
425-
426-
return (finder.defined_symbols | parameter_symbols) & locals.keys()
427-
except OSError as e:
428-
# yipes -silent fail if getsource fails
429-
return set()
430-
431393
def _print_locals(self, frame):
432394
locals = frame.f_locals
433-
defined_locals = self._get_defined_locals_and_params(frame)
395+
in_global_scope = locals is frame.f_globals
396+
defined_locals = extract_locals(frame)
397+
# if in_global_scope and "In" in locals: # in notebook
398+
# defined_locals = defined_locals | extract_nb_globals(locals)
434399
if len(defined_locals) > 0:
435-
if locals is frame.f_globals:
400+
if in_global_scope:
436401
print(f' Global variables:', file=self.stdout)
437402
else:
438403
print(f' Variables in this frame:', file=self.stdout)
439404
for name in sorted(defined_locals):
440405
value = locals[name]
441-
print(f" {name}= {format_limited(value, limit=20)}", file=self.stdout)
406+
prefix = f' {name}= '
407+
rep = format_limited(value, limit=20).split('\n')
408+
if len(rep) > 1:
409+
rep = prefix + rep[0] + '\n' + textwrap.indent('\n'.join(rep[1:]),
410+
prefix = ' ' * len(prefix))
411+
else:
412+
rep = prefix + rep[0]
413+
print(rep, file=self.stdout)
442414
print(file=self.stdout)
443415

444416
def _stack_prompt(self):
@@ -499,8 +471,8 @@ def client_print(line=""):
499471
full_prompt = truncate_proportionally(full_prompt)
500472

501473
self._log.push_chat(arg, full_prompt)
502-
tokens, cost, time = self._assistant.run(full_prompt, client_print)
503-
self._log.pop_chat(tokens, cost, time)
474+
total_tokens, prompt_tokens, completion_tokens, cost, time = self._assistant.run(full_prompt, client_print)
475+
self._log.pop_chat(total_tokens, prompt_tokens, completion_tokens, cost, time)
504476

505477
def do_mark(self, arg):
506478
marks = [ 'Full', 'Partial', 'Wrong', 'None', '?' ]

src/chatdbg/chatdbg_utils.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import argparse
2-
import os
32
import textwrap
43
from typing import Any, List, Optional, Tuple
54

src/chatdbg/clangd_lsp_integration.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,12 @@ def uri_to_path(uri):
6161
return urllib.parse.unquote(path) # clangd seems to escape paths.
6262

6363

64-
def is_available():
64+
def is_available(executable="clangd"):
6565
try:
66-
clangd = subprocess.Popen(
67-
["clangd", "--version"],
66+
clangd = subprocess.run(
67+
[executable, "--version"],
6868
stdout=subprocess.DEVNULL,
6969
stderr=subprocess.DEVNULL,
70-
text=True,
7170
)
7271
return clangd.returncode == 0
7372
except FileNotFoundError:

src/chatdbg/ipdb_util/locals.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import ast
2+
import inspect
3+
import textwrap
4+
5+
class SymbolFinder(ast.NodeVisitor):
6+
def __init__(self):
7+
self.defined_symbols = set()
8+
9+
def visit_Assign(self, node):
10+
for target in node.targets:
11+
if isinstance(target, ast.Name):
12+
self.defined_symbols.add(target.id)
13+
self.generic_visit(node)
14+
15+
def visit_For(self, node):
16+
if isinstance(node.target, ast.Name):
17+
self.defined_symbols.add(node.target.id)
18+
self.generic_visit(node)
19+
20+
def visit_comprehension(self, node):
21+
if isinstance(node.target, ast.Name):
22+
self.defined_symbols.add(node.target.id)
23+
self.generic_visit(node)
24+
25+
def extract_locals(frame):
26+
try:
27+
source = textwrap.dedent(inspect.getsource(frame))
28+
tree = ast.parse(source)
29+
30+
finder = SymbolFinder()
31+
finder.visit(tree)
32+
33+
args, varargs, keywords, locals = inspect.getargvalues(frame)
34+
parameter_symbols = set(args + [ varargs, keywords ])
35+
parameter_symbols.discard(None)
36+
37+
return (finder.defined_symbols | parameter_symbols) & locals.keys()
38+
except:
39+
# ipes
40+
return set()
41+
42+
def extract_nb_globals(globals):
43+
result = set()
44+
for source in globals["In"]:
45+
try:
46+
tree = ast.parse(source)
47+
finder = SymbolFinder()
48+
finder.visit(tree)
49+
result = result | (finder.defined_symbols & globals.keys())
50+
except Exception as e:
51+
pass
52+
return result

src/chatdbg/ipdb_util/logging.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,11 @@ def push_chat(self, line, full_prompt):
106106
}
107107
}
108108

109-
def pop_chat(self, tokens, cost, time):
109+
def pop_chat(self, total_tokens, prompt_tokens, completion_tokens, cost, time):
110110
self.chat_step['stats'] = {
111-
'tokens' : tokens,
111+
'tokens' : total_tokens,
112+
'prompt' : prompt_tokens,
113+
'completion' : completion_tokens,
112114
'cost' : cost,
113115
'time' : time
114116
}

src/chatdbg/ipdb_util/prompts.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,11 @@
3434
"""
3535

3636
_general_instructions=f"""\
37-
The root cause of any error is likely due to a problem in the source code within
38-
the {os.getcwd()} directory.
37+
The root cause of any error is likely due to a problem in the source code from the user.
3938
4039
Explain why each variable contributing to the error has been set to the value that it has.
4140
42-
Keep your answers under 10 paragraphs.
41+
Continue with your explanations until you reach the root cause of the error. Your answer may be as long as necessary.
4342
4443
End your answer with a section titled "##### Recommendation\\n" that contains one of:
4544
* a fix if you have identified the root cause

0 commit comments

Comments
 (0)