From 6fe67d1132792d6f581ac8e8d9bc8b7704bd8063 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Thu, 16 May 2024 18:06:04 -0700 Subject: [PATCH 001/102] added blog --- _posts/2024-05-13-models-over-time.md | 297 ++++++++++++++++++++++++++ assets/models-over-time.png | Bin 0 -> 87658 bytes 2 files changed, 297 insertions(+) create mode 100644 _posts/2024-05-13-models-over-time.md create mode 100644 assets/models-over-time.png diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md new file mode 100644 index 000000000..dbac39361 --- /dev/null +++ b/_posts/2024-05-13-models-over-time.md @@ -0,0 +1,297 @@ +--- +title: Drawing graphs with aider, GPT-4o and matplotlib +excerpt: Use GPT-4o to draw graphs with matplotlib, including adjusting styles and making visual changes. You get the graph, but you also get the code in your repo. +highlight_image: assets/models-over-time.png +--- +# Drawing graphs with aider, GPT-4o and matplotlib + +![LLM coding skill over time](assets/models-over-time.svg) + +I recently wanted to draw a graph showing how LLM code editing skill has been +changing over time as new models have been released by OpenAI, Anthropic and others. +I have all the +[data in the yaml file](https://github.com/paul-gauthier/aider/blob/main/_data/edit_leaderboard.yml) that is used to render +[aider's LLM leaderboards](https://aider.chat/docs/leaderboards/). + +Below is the aider chat transcript, which shows: + +- I launch aider with the yaml file, a file with other plots I've done recently (so GPT can crib the style) and an empty file called `over_time.py`. +- Then I ask GPT to draw the scatterplot I want. +- I ask it to color the points for GPT-4 and GPT-3.5 family models differently, to better see trends within those model families. +- I add a PNG of the graph to the chat, so GPT can see the current graph and understand why I ask it to move the point labels so they don't overlap the points. +- I work through a series of other small style changes, like changing fonts and the graph border. + +In the end I have the graph, but I also have the python code in my repo. +So I can update this graph easily whenever I add new entries to the yaml data file. + +
+ +> Aider v0.35.1-dev +> Models: gpt-4o with diff edit format, weak model gpt-3.5-turbo +> Added _data/edit_leaderboard.yml to the chat. +> Added benchmark/over_time.py to the chat. +> Added benchmark/plots.py to the chat. + +#### in over_time.py, draw a graph for each of the yaml entries with a `released` date. plot date on x, `pass_rate_2` on y and label with the `model` name. do a scatter plot. use colors/styles similar to the other graphs. + +To draw a scatter plot for each of the YAML entries with a `released` date, we need to: + +1. Read the YAML file. +2. Extract the `released` date and `pass_rate_2` for each entry. +3. Plot the data using matplotlib. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH +======= +import matplotlib.pyplot as plt +import yaml +from datetime import datetime + +def plot_over_time(yaml_file): + with open(yaml_file, 'r') as file: + data = yaml.safe_load(file) + + dates = [] + pass_rates = [] + models = [] + + for entry in data: + if 'released' in entry and 'pass_rate_2' in entry: + dates.append(datetime.strptime(entry['released'], '%Y-%m-%d')) + pass_rates.append(entry['pass_rate_2']) + models.append(entry['model']) + + plt.figure(figsize=(10, 6)) + plt.scatter(dates, pass_rates, c='blue', alpha=0.5) + + for i, model in enumerate(models): + plt.annotate(model, (dates[i], pass_rates[i]), fontsize=8, alpha=0.75) + + plt.xlabel('Release Date') + plt.ylabel('Pass Rate 2') + plt.title('Model Performance Over Time') + plt.grid(True) + plt.tight_layout() + plt.show() + +# Example usage +plot_over_time('_data/edit_leaderboard.yml') +>>>>>>> REPLACE +``` + +> Applied edit to benchmark/over_time.py +> Commit 49ac9ca aider: Added scatter plot functionality for YAML entries with a `released` date and `pass_rate_2`. + +#### /run python benchmark/over_time.py +> Traceback (most recent call last): + File "/Users/gauthier/Projects/aider/benchmark/over_time.py", line 33, in + plot_over_time('_data/edit_leaderboard.yml') + File "/Users/gauthier/Projects/aider/benchmark/over_time.py", line 15, in plot_over_time + dates.append(datetime.strptime(entry['released'], '%Y-%m-%d')) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +TypeError: strptime() argument 1 must be str, not datetime.date +> Add the output to the chat? y +> Traceback (most recent call last): +> File "/Users/gauthier/Projects/aider/benchmark/over_time.py", line 33, in +> plot_over_time('_data/edit_leaderboard.yml') +> File "/Users/gauthier/Projects/aider/benchmark/over_time.py", line 15, in plot_over_time +> dates.append(datetime.strptime(entry['released'], '%Y-%m-%d')) +> ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> TypeError: strptime() argument 1 must be str, not datetime.date + +The error indicates that the `released` field in the YAML file is already a `datetime.date` object, so we don't need to parse it with `datetime.strptime`. We can directly use it in the plot. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH + dates.append(datetime.strptime(entry['released'], '%Y-%m-%d')) +======= + dates.append(entry['released']) +>>>>>>> REPLACE +``` + +> Applied edit to benchmark/over_time.py +> Commit af2f4b7 aider: Use the `released` field directly in the plot instead of parsing it with `datetime.strptime`. + +#### color the "gpt-4" models differently + +To color the "gpt-4" models differently, we need to modify the plotting code in `benchmark/over_time.py` to check for "gpt-4" in the model name and assign a different color. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH + plt.scatter(dates, pass_rates, c='blue', alpha=0.5) +======= + colors = ['red' if 'gpt-4' in model else 'blue' for model in models] + plt.scatter(dates, pass_rates, c=colors, alpha=0.5) +>>>>>>> REPLACE +``` + +This change will color the points red if the model name contains "gpt-4", otherwise, it will color them blue. + +> Applied edit to benchmark/over_time.py +> Commit 0d11357 aider: Color the "gpt-4" models differently. + +#### color the gpt-3.5 models a new color + +To color the "gpt-3.5" models a new color, we need to modify the plotting code in `benchmark/over_time.py` to check for "gpt-3.5" in the model name and assign a different color. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH + colors = ['red' if 'gpt-4' in model else 'blue' for model in models] +======= + colors = ['red' if 'gpt-4' in model else 'green' if 'gpt-3.5' in model else 'blue' for model in models] +>>>>>>> REPLACE +``` + +> Applied edit to benchmark/over_time.py +> Commit 85d9d84 aider: Color the "gpt-3.5" models green in the plotting code. + +#### use the colorscheme and fonts from the other graphs in plots.py + +To use the colorscheme and fonts from the graphs in `plots.py`, we need to: + +1. Import the necessary settings from `plots.py`. +2. Apply these settings to the graph in `over_time.py`. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH +import yaml +from datetime import datetime +======= +import yaml +from datetime import datetime +from matplotlib import rc +>>>>>>> REPLACE +``` + +benchmark/over_time.py +```python +<<<<<<< SEARCH + plt.figure(figsize=(10, 6)) +======= + plt.rcParams["hatch.linewidth"] = 0.5 + plt.rcParams["hatch.color"] = "#444444" + + rc("font", **{"family": "sans-serif", "sans-serif": ["Helvetica"], "size": 10}) + + fig, ax = plt.subplots(figsize=(6, 4)) + ax.grid(axis="y", zorder=0, lw=0.2) +>>>>>>> REPLACE +``` + +benchmark/over_time.py +```python +<<<<<<< SEARCH + plt.scatter(dates, pass_rates, c=colors, alpha=0.5) +======= + ax.scatter(dates, pass_rates, c=colors, alpha=0.5) +>>>>>>> REPLACE +``` + +benchmark/over_time.py +```python +<<<<<<< SEARCH + plt.annotate(model, (dates[i], pass_rates[i]), fontsize=8, alpha=0.75) + + plt.xlabel('Release Date') + plt.ylabel('Pass Rate 2') + plt.title('Model Performance Over Time') + plt.grid(True) + plt.tight_layout() + plt.show() +======= + ax.annotate(model, (dates[i], pass_rates[i]), fontsize=8, alpha=0.75) + + ax.set_xlabel('Release Date') + ax.set_ylabel('Pass Rate 2') + ax.set_title('Model Performance Over Time') + plt.tight_layout() + plt.savefig("tmp_over_time.svg") + imgcat(fig) +>>>>>>> REPLACE +``` + +> Applied edit to benchmark/over_time.py +> Commit 8d670f1 aider: Used the colorscheme and fonts from the graphs in `plots.py` in `over_time.py`. + + +#### /add tmp_over_time.png +> Added /Users/gauthier/Projects/aider/tmp_over_time.png to the chat + +#### make the labels not overlap the dots + +To make the labels not overlap the dots, we can adjust the position of the labels slightly using the `xytext` parameter in the `annotate` function. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH + ax.annotate(model, (dates[i], pass_rates[i]), fontsize=8, alpha=0.75) +======= + ax.annotate(model, (dates[i], pass_rates[i]), fontsize=8, alpha=0.75, + xytext=(5, 5), textcoords='offset points') +>>>>>>> REPLACE +``` + +> Committing benchmark/over_time.py before applying edits. +> Commit 49eb72c Updated image file format from SVG to PNG and added imgcat library for displaying the plot. +> Applied edit to benchmark/over_time.py +> Commit 75fa061 aider: Adjust the position of labels to prevent overlap with dots. + + +#### make the dots bigger + +To make the dots bigger, we can adjust the `s` parameter in the `scatter` function, which controls the size of the dots. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH + ax.scatter(dates, pass_rates, c=colors, alpha=0.5) +======= + ax.scatter(dates, pass_rates, c=colors, alpha=0.5, s=100) +>>>>>>> REPLACE +``` + +> Applied edit to benchmark/over_time.py +> Commit a018fdf aider: Make the dots bigger. + +#### make the border of the graph thinner and lighter + +To make the border of the graph thinner and lighter, we can adjust the `spines` properties of the axes. + +Here are the changes: + +benchmark/over_time.py +```python +<<<<<<< SEARCH + colors = ['red' if 'gpt-4' in model else 'green' if 'gpt-3.5' in model else 'blue' for model in models] +======= + for spine in ax.spines.values(): + spine.set_edgecolor('#DDDDDD') + spine.set_linewidth(0.5) + + colors = ['red' if 'gpt-4' in model else 'green' if 'gpt-3.5' in model else 'blue' for model in models] +>>>>>>> REPLACE +``` + +> Applied edit to benchmark/over_time.py +> Commit 77ba518 aider: Made the border of the graph thinner and lighter by adjusting the `spines` properties of the axes. + +
diff --git a/assets/models-over-time.png b/assets/models-over-time.png new file mode 100644 index 0000000000000000000000000000000000000000..eaed94a53d03e3b2056d23bd149269f28dad82fd GIT binary patch literal 87658 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%0DSx7aUQQ~uFK2uj1B<4RuzqHK7tZhTZO zXYvZX3{2`O&Gzvq@%3%QykPaH&9@-&^{88pJC1M>q48VoNLaK2ub!+?hi4%T>7T$H zeH{3gG&!h}PSNf6>^XLoo?X*#Qt2U>uG>;a@G8l;@FdYF5caD#9Aj;^1Kui_olwr! ze7)5lIG)MM5CFuHe!AVkmw=B?b3a8aL4q!BXk5NKY@q&0F65CN4q?B_EQf}a91;Qo zo;}}od_&z%&dj9M1S3kKwz9^i2b}vQBqo!mF%1pFZoSM*nY4FuL|MYZxyPm~ZPd7d zUxQo*T;BDtF}ON+#@)VWQtv+rNKxBKv}*U^XHe-PMxqXM7sBJAMMsBHY<%lkfR)zU zu=|_3+*H3mH-@#Um8+HS%wZ_Hrs#f_Gz6qLzitzE00`a1dq|X)GEJp`fH-cIcDqt7 z?419Zs$67ZNOiK0YCktNqD74|)7e4eb^m3> z?&j)Sf2IVSs`)czF>=Mq7O3QXRO>Tm&RE8FD+Pd3%GKM0+5?x~A)u_)gr;!K>14g$durK|BJO9Z20ve`>jd>bey;BIvrJRA7+UlWU#Fl!3|N zDK=8a=>aRVaeHj5x8`u%?sJomckbQW{QNApUAuN6Z$J?$Q&$Y6VPfAqDw7>Ct3{?q z(ks5r-wS1M2CSrh?SY0E(hT6g0xD!Yk=jkWn>W1rdw2Li z{U9AjpUW-RIotnmDrQ#@vv4;dS%0P^(I~7i*zKJNipe`1$s9(BgBG&GY+45st1wL^ zZ?Mjt$xj~QJG$@0xW&saww$LuZ}c{;{Bu5~+axR_;wr3ugxuL@QZ8`y>d*4Qiep=_ zuLsAF2UFvhdC~i7rW65K65g^-5<$A;j3up;Vr5s8*Fj-o1EkY-!v+4)HSj*7Z$^j_ zlqaT-kX_@57d|@Y!5kg=6Vx9e7Exdw&LMsC5oU3?3BC>6pk*;x*73P3^mv)@u$L+A zmM3`4j&?&&Jjo-dp}uNHhlOsm_}cuy1Fy~ENvvx>6^D4%e{-{8@JVh1UFZxX6@0Kg z?9Sj*L=b{kJKz@M*Q2VZYP*O@J7!G%bYa))UQU>^Y#V=lfWz{%q^C^vN7jx!Hg-~= z_2&9Box72&yL+y^5)#AhL@8(>Z{`oT_#KG(8ojyH%R*jMp^JC4MN7_Z(kX*z+ zpOI-N8fI}dg35`j5#4Oo6z78zK=Ne!7MVk^G-=_v&_;l?VkCn&-l~Aylnjg*GkH5)KamPS z9cEA^bns3WQl`z#O?2G{!0&;~3=iqBxDQh-vd$cz0{w+?kBCa7XZ-Nt7GOT8y&A+9 zPDH{NYDSY2Ws2NHx1tiT?0%2F7_KRt`eEvlv=_scS?R>Sj;hdV*0|}gCHr19j@DE! z>P6W$`c=PM+g(;TFRY&1VYlN6tDt7sMD+`uZ*^n5f!^Iq`lnAX{yk{w#aeoL@hQqT zZw5~{IBc%;B=`C8U$Yr&Bh&QSycq{=(bX~yr)%k=qvOZaYR25TrnK0Bbg5*n*MMSp zmC39)!AZY={4unoJ9gavscVG{7OPb3%0MAS&?!TAglf{Q0E>L(;);@#+|DLs1+AbE z!$ix;XoOo~%&(BiC>x+xdp+{;>HQ)In?dzwjbL}pV&}Y&R3X}8&tXK5#Qbr)?LcI( z!i!uhI)Cx|B*Q16)k{j`ccO2l3M}3*-IkFkpt*8tM#Rw0CkKt#q5Pb70h8f zEDc~(tNTG+=AS1f_T&Eo4)5R?C4Ap#I4eA><@B9rfhEl;+vqO=J%;;OUVQoTrS-*? zePn*ak9mb{TjArOZI;a&$j!&;X7rJ`m6lOfCbP`f1bObOy{IT54v3QB-9~;+>d%sY7;C}MC&)xh4lb0d)?mTf>zSDH9 z|6$8xJKnGF$x5Wav<;e0+OIl~ShK^t>lC$Pw|pwk)b57r@AkI8cHY!a6#|{eEE&J8 zBx`P&vMpiN*BXpbMA`=?mUYjTBcoimZa%{fCWf7CmWN#@9|&Rt*qL^=oG_$4|6y-w z#zbIXKZ|R?cu&Tx)YiD`<4pf*Y=EkUbWq_F1HOE^XSCEhzr)V)Z4>l|7`O3QcW&sS z8BfhGAFA^E(p$57=@qXN_fQ!<8h+LI)Z4e3V+Rjhzrm||y?*s3?I~ZBI{jk5gS2wW zwdyBPvMx$UX2n@XgtuVeJ-#hVFM=5s>srKYwE&bBl*J_Rv8d zwh8XHT;=|%qRrdL$!5JKG)n)Rk}|Nobb#JT*UfjY7V15SQ8gPpu=`T;JEuEfC=@w8 zHthT1rWH2h#`Rv&{o>lXMWgLqPtMLrdfaktg^lqY-wuDsU_;>ZrnPmasan105nP(C)v?jsiV^+%>_$)%&^2^8*8&Hj1SE>b&n-9~X#F;0>sAk!@6#Xl2L68e zW4E>No?s+XTnrV83gRy`aE6P{l*J`5UZf}j$Q{lLK$kK;{;`;t05gVD=+f1^F^VrY zNo_T}lDN9L!unF%6(6fjd~i1CU4Hd3d-b0s)+m|~(WUYksV~8CZaVk{)2v;z*V;I& z5&t}}(1)TYoe5CTY1Ru;_u=Y2%J0tL9;?59yd~f@J}FY|5>7Ftu63sI!ZY^}rsOWD zq$}4_)0(hyK$vs&qk(}R*QjY}W4lIQDMCI<4>7@~xV%mY+|d8-+!!I+0~z}g*O$db z-kZx#4qEc1pI;28?ceiaVBw>hHkIF|*5}WA0u9!z`Eh&uPP3e|E7kNy7;iknTE6W_ zHqJ0UhQ1;r1InT_y?ln|BFPtX1Js}O=Yk4aOSPo$*fBh)v^eqZUuVyD!%s|&|Mtq! z#`EaCx3PBVJql|N>pXi`-vSd63l1329PFLy1PjytlXA%xd6dkuT7x0%ICR$@_bX1{ zn5-%k$XaUUZysqkAKG^QeDpopLCqS5{0!`^q+eVrmNFYnXk<(1S-*mdvaYtRe-tY@Ih}K9hHFUAW=+C#em;5FeDybXi*UWU{WsiDtzqr)5z-N& znN`1k^kwAaz?@yAW0cr-Fl|_cAci}yZg|r+HK5AjD`)`b#y_qN{gnb<#x+dTEwE;< zpWta`c;EfcFZVv%s{i!#rYlQTv*+e^J@4i9@!6q6@ktk_8x%ZjkQVSeJK^`m%8O*b zFNP;2vTuyd(*WiA^f8%woK}L?RxzEgCaw*f>4>95`PZ_pOxh&+o+x~Gu?nLcv$Oa6 z73PL$HuC6@KX3YKO8)B_olKKHlIG~_ zT^6%@oFoYjmZxe|=skDi&{hL?$Gmp@!*^{^3bpR6c< zsdUk}sAZNg#SEteyUmT8?}Bg$9lx(49b#j1Re+6ZcRGR)8)JnGw|xZb7|c= zBc1IO3c_WXkyJGDL3y{b0Xo>hk;&M4f%yW7N`3@Sy& z0D<-FoICCSHA~@gQ1y_1XTfx9mu)+BYHOJf(6M#`YIN?^(N0CUa@Nq4(0Z2zmuR`G z&-?TA4F6Jms~0E)8K)}gtfQlo`eysKZOyUJ6~yJ{&G#OU@%FDuFd6Y@C?^FQT3hI< zP^_F7Z}LB?=rd;=fE5x>TkWzV4*e5qVFg#-@s`0JJ(Zu}8KLF=wU}05=Dc}e zbv0oP+Tr6M76ch7DJk^QZGc$}U=y&rL`*7awbxy-{nyz^P4}~Yq9o`xqu5%?$_C*U zN>KRRIquBPJLu`PWp~6@6l_c&lW*Vt*r;ScpVWxI2D3Nj$cClBrPbO56KW_)H!ZsK zyD5k>2aT2kmPwWE#u)!~LYOWdeS8|%6_?NdJydW`>$EV~>E$KxF9#gV--x5MdF$5X z?Ck8!F-q{Qh3iYt-MK@DD*RYaX0+L?ehqYn)72i0qF>=r#KjE0STB`#*#>7=Bjj6nnbXpy+}PiLLd_{bbT zl3~w#+}SYn(~m3HwZlyBp7>yiJ1g@Zy>kihsaKc0H=&ee0axf#C#+MbQH8t4^zlYm z`uo2L$#9JUjHjW%B0QV`r1-o*Xo@vy@9%UIqk6{BRgZFw<(7m$|?<0MpkGMBURA4p{H(xBqe;w=6AM7|1(rKmr`~!iE33AUEdJNPPj5 z)PRIFH)e9LKKp2WfD%lvoQvB@}9pHu!rnfR9AP_=5+)P4PXq1`5p z4YK**(D=WOUX2z88Bin?U>q6&veH%obJoP!MY-ea&Yd;79Fp}^3T9;g`AI65A5~8R zL$U0BfncJ5jIirt2~WLxK}3AyEBMI@+T5YJAO3aATHIpu1eR`|e-^j>VzCu4TxU&j zI*eZy<#TQUdAV@mNhauAS|kcLL`^^cJkd~?o}g26m?FmK{BfYv_SL=6Kb^c6ozCVRi_Y00tg{=2?at$z*8R+7qQ)< z4P;y=b0Ywt#Z>5c{?u6#RiooPgFgxFuh?L^(x8Iq^Yu9wixw5B2W6n>z-Fg59rw$c zaZMUEO2&LiDpc5Iy#}@^a9=4QCmM-2Bz?VeV~j*jP?=OagjQAvI{Idd@lYa$c+iYO z;=*&>97va0hyAB<^RZoKSkddR=O!-$bGYqLal{y)Wxx4zGE3^1JndTf0}VHC>r8F! zt)>(|vdw=-V4ti1dK)j2!!CN4>H>h-@7f5xi|R{fBqa7f*J9YL-LDn0wYr95X>QoU zfBx~+-Q8~~#WT+ENr?lXFLp!z6&lKo z*vt44SWN0yYymJCG7#2G_cs_u!`%G@VHpw$9Drh@eYdmKtv&wZF{zt8ftfrOz88_! zwBLZ5Anf)Ob{Nn0fsAaV^UvyC!(UyR+gBTclwXQB0>-v&PuqR{Bfk-!SKhN&<9Gfe zL*_gG)sS?oG-oD|Mo#L7(YFA|%i<TL^~3ZQOqoJ~(pmzI`VZQsJ4mqu>XO%d_`iZU-|YTsxn!*VGy8Co$tX09z% zWT>!S+qdUaRTHkZ0N#>Yn5E={m?6@RJW>uB&7c2k!nK)jbi8}T*=4TKC%9PyY#!_$ zXWwJHE7Z#{OGR(`j1(0LmMuJ87M3*Y`%l1Jv{Fe`DG=ZfX!R9 zxWgc}aR$?uZXAs;0CKW|{S~*%bmxE0+~4Y@X>b=f*AApgEI8`X@q>^tCJm+>!Dg!- z5N!R0$v1A)5*gminL{ro2krVVAyA&ukCa^j4T>`Y|NCD!B_?0F09uW*iK`x^fnZPAYUbGrdoh_4WD8}CSr zm0cEP4S?tl2(EW{%s)w{Lum8)TYiG2N@<7^lrBT;68~M=L8yR+P4*CD7>Yw#{jn^j z35K!9C&!$_plTQE4>kQ&wj2ANDAC$7y6=DLrnXgj%X6x6Be}xNoPcK@()|N8W^wG` z9?SYH5N4j_$oR*tD08t6s}am}0I<7>uI`2U33dMYtS@}~;FOQ#;o$}6UJyHGFx_Uw z3ED_$p~Pm3eJi-iIxeJNUL#5qrfj|Ge2Z87^!3%X{-0sU&VI8gQL8>(zbp3S&ro7@;=gwi2YnvvYpfTj<+m~7eE$0NM%+-0jZ>dF{n-Awgt)+W4+#c{L^b?Rj$J9 zu3z&L4itQd7j#JK0M&2osS9dFcBSU`=)k9U}L_9DQa!MpLHGL^%BAsuN6Ww1*{ua?&I2?+PY7 zc&q&}&KK?wckgS#osKBf8~IDE!?n^`^mISr@{#JTXk1lLsR_>*NaqcPwGLS6DiB?? zU`hr1X7;Ywe{T2YZT7nqt`G^B5y9%eO>Vgjo8#NTUJOuZZNqtPlp=VMq%y+NJA6w9 zCcG>s_!7JnPEOXS(X;LCuTt!m;Tz3iepoZ+-=A6M-;1MA#{k-I@bEAS(GolZ%u6I_ z&>!hZ?&cnZs6jP}4UyO}^E8I(>$}+oic1WF9UyxlDhZp)`Hy!w&v%%y_DHw2bfgNd z)V>*yJPSX-QXT2+!s={;&^wIMZnU`nk-R8qA#4s%5T{3-eoqEdl64sI zIWCNp6)^2I0>;dFldJLRpM!Aq%~kFp^&54#IKDGQR-e+7*UVNn4m0}*t>be>)r#kS z5+_vmu-2?^?uSldqWN^pdd`c*iKzpHHE$KwkfCTjtG6O%ybX{iXAd_I=o(eKW*+oIC z^&Iw+W!p`Qs=^ij_tc{32EMH z%k;~(Gp{~uG}Ei4%NU)2<}gsGt>+E#~fd;*zQ0(c8Y7$jtDbLxM7;&qo&=#{kOi6lLEv=^jAWQ)*scU0n zg9~jls3V?-4yoB|waCiKn(pAR0sMC+{++wh^O4g`Snunkn1lqg_}e$pV$GsFS5 z(3$mfIJAjR> zvm(cgsU_2 zy9^v?T{x@KfHU(1HrRp=MAdqqhD5_p4bdM5_{L4clDV2s(0$7u>hF6 z4afh?{CT7XraVWoVyU4nr=^)LS8Bx18Rr#Z-c`${T9vGJm$Dx`=(Ujp)LW;YK{G>H zd@YP{PgT=-r-rWjQjMEsL`UBTwlw;~1btVrVPiJ#&WBXA@6n^~((|or)Tl9X%$Qrt zx~=1F$4+Z36f4O6;~qC{*REF#d@7(j1#{G^05PY5B&Mbx)N^eKbe4Wri~{NB91s44 zbub78PjAPpuqxM{au_gc3M4DQxgg+e%wl33L+DL2=~@ses4SYpESMI^il z>oz;We%Z&_Jg!pQoWq`RU$&ydlXYR#r9#1J|G_ssUlzij24?G?ll9<1Msbf>=r$8) z%ApD|n~22(Oh)fFUl?_W{p4D9^@M+5HOQ)kFByV}MHW=esln$c*!A zRgA!@5$G=S@mPpmAMd?h(|%LWC&4W!bmk8+>$ZcqgHNL#lixdZf{cx8fd^oIyWMn$ zx5N>2B33?EW%l*c|?oP~u}6DM0@&=kkphyDB-)99%{X&6EM^ci4EaJH-goSK?4 z8&H5YHYE1nvYbEcNSODdS3sr;eMg%w$L2UX+L1FwI6<*Cv0yEL^j5gnrkDEjoT$*d zFS{VO)T&sfJG_HXA_z;{Wg#it=eq5GSJ^qbb22zpShvI$sjFG7i)NR9yk#j_PQD`q zA-O^<-AlsUE%^BH?YPTM=bymI3ilgOwJ>94e-^J&CZ_PI#LNz&)X1H70StqP`c-$W zO+R<((z=5?0M%w^-o2YVahmG}iaPw7MB;@5>$(%ob_n?e=0O#=tk8W$={-&m`0$Gr zDVO7g#6&&d5^<+>%waT&ZlKDtqNi=J>}1lAOggKfR*hF5v|vAvU!*DH=yhpJm4S_A z82*VOk>_u$8HZmA^7C3$SjlJ*M34o%XUD`qA!939oG2F)%VH1Ky2%s>4C)#q*iUSk zQX?XQjqTB%T~9Esmt`F_RWwa9wabIUBO;E3oS*7Y%rff(^B!#*+7yc9@Rkye8$_KX z?Fz$9RsqV+8ilwNB(vd?B9M#JUlG>Dm*8TA$|ii+kiLC|q~zoCmIQ5d$Xs^7 zW@$f@<`lM|2LOT{y_iJ>|6tdI8gnuFz{PGuaB#3;8%tAD75quPNbtf~21_WVnw7VL zH5^>dIP(v0gpR2QJGr=64Uf|B3)NmN9Z2Mmho;!o*(ZnOxg;5h$8b9k_Ze7`#kT@`MK<#CqizWpSk4A2H81!3X2 zc7auQtEF3AsFR5Z&@qm0dA)Re;+j>8B)DNNVeN#XFladHjJnhXq)Ign z4P~vsI^fcQAES#)N+$fsefl&pQ+r-*wcgNwA!k;RSip?05A+Cbej^I~YrGUS&s{Up zS4LTkjk}j_uN7|MgMWS4tHCqZ(E@+^Rpx|Lr|Hu3Q#Ed9XIq6uM0Ak3Y^(kgO!w^5 zadhVfcRL>Jx1_&*4OoidPJ_Xdm%Y1Rml|20s|Cd6j;1^;JiN)<)~ZoHrYC|0JA_LE z1ljEqO86rosjZh6W>uH13$$%ZD5${GW?T2{)Tug&cm3$j>BT8~{>1cc7c_=mta_Kv z@#9f?dTmPSs}zW^xI6(FujJ;o(a_Kk$l%1ZOFIAwM+OZ66%enU@cgyf_;hyty%0Oy z;tQiYdl(+-n%$@H-Mdh@Xo0c)xg+Q?3?UKHa(rSLbJq_;aXmcr!*$yAvh=s<8$p)} zx9IFT*n3^qmxDR~Gl+{>+Ifo1h%v3uGN?K_I(8j@#Lr?#j~>TIe{d~r{d?1!QW{sG z4$>r3L%Lu{OP$s9;@yDkEv5}+?9#D!Z&gZ%WbSzHg--^A&t7P?fKKHhiXl1Q0Ba(( zfdb!`8(I}{(O{R7It1nGbbMaOd9P$EJiW2UPOcw*$o!nXr=9aN3Ka3Pei9{A&D^SBY4JUwO49~@Bg$!Bz5 z9*L!5VdV}$y-$a&hv4oJ&x{Z&l0^ z+i-ardryMNE8IZ8Wyefb2||pCKdkw|6s)qF94f##Gyw`}Zaz_-KPR0KN;g?0l6*OD z`uLuPRkN`2LZFFl;Gf+*o;j34l?B3(#vgOO4 z7Z!R!WX`)#l<3^w( zU&1JneJi6LuR!o6_m&iwZn+93XdU<4&)j8{uUVt0+WzVV9EjUzdq7D{UzDnK zv7*N`1ED2Qc@RljouZ=pMJY9M^k~15iN_{2AOlIpDEl=~if84C@L1sKPUkxEM~2~l zy%?Qtc(pdN-a^`Fj6+PmwmpC0!b}h+-oCHTiuSG>g6xtvKF30!1R**d2k#6?ym?*xboCUW!IlIUYH zAq#fz5}CdQS|7dODU4p0eSFe_B$ajZ8>CJ#@1~#$ODz}0tGGL_s^+?k+=iSra?7kQ zVbfcMLp#o9^q>>Th$QQr1mPO%PuY*6jq`g>P7sj>I%WV-=e46xLkNv z&A^?a0ArZ6pBX#IMTiUB2yJ#;tKQn$MzDUhdw9Kn^~#gLVu#!VhUiA>dVanCyYoj- znn=mZ+)#;w>xr&SNRd47Hv_LrcS?OJRx{YxDk1#jY}N+pASVvS8KEwjNN3@b=1{e+ zGrXKWckUKmLDN@$s!jHcuBEm`ck{2MOO~vUjqQhXsri#bjZJa(if!~niiL6afBjy+ zu3YKk)9OzXs@AEi)Z=)u`jEa&x{g;lJgpr$uXq29<2}}i!j~S`9q*_SUO@#jXFfeN z&}wP*-!o}rsC)a+JXihtr2H)g-3{(^F{qXI%4GW2S?x8HyIR&LU7QwrRwwUT?de6f z?Yq}$KB#e?P1>+94XfySKYp@pLzc>manRzWOIeas)vfr=0EEj^3cj^f64O;x!|4>L z8y0Sr45R~Flk?>k+s{1Q^!2OOBcHv=+m$%9x9X&XmLo@&mY#0dpowSN!X>ggTPX2l zzKhSofEg?-EIy^QTQ?o5jkLQc*(V)-r0(nME1Mo*v?~h>0yOVXWMt%)ZNiO&rMZNoLF_zsY0*!{N|sYs!Si&sovLmw+{JCd7|C$HB-XmhYy=`uZQzq zCAUXy!%7!Dj4Xqf`vmobVz#UKx)x`wBErL^loBby^S5vJzKdQ+hUsB0-R>}}i|!tG z7g;+mNp)%C>Pmx(J?)&VYMFO#Y|`~QtUfL^D2fqA9%T?-Ee#p*>>xf@akFs$NWf#F zjd4h02HEt$8aN8y9L{7%}3gUe694##@@WuVr1FLd=?-dc0!;nxa)LqL312_Iuq`L#PZl(aJbqy9ybDtg zR>Ze(YWC*){goX;UIhD8OL)v59*B7IgahM%Itq3ZxNF9XmqWzpKz9;bFfsBlu9J#9 zR+K!XO-)SHk>0Pvw^y%gT)qDK5-ge@3qFn=t6AS`^T>QFpWyEwau^7(n5-dG^hW`* z20-TfG|5PBn;prZ0t_LSfpT9V#4P5iO})6X0llF@jv6|?stmWFimyEMqcTXnxEHJx z9J#xq@2#LARrP+>F~1X|)*+-rI3FHVP(0J5a^|Xs5Rs+MSLoMT4^%Ldc{Iz=WTy+R z%iE|_nw=X;%RxZq*DGOs=}MVm>cEV%3RNErolr{jkz>d9ELD;_IPUZj3$`&Tb2lp% zP01L;_x$>9drPIMnVUB2tRKDkV!2nk^^6&nR*oFTjtS4Zm#e6#<*{lUHf8>R9G8WH zWk{O}lONwBeW)`E1unRmckURZRm@VtSl*B~%Q6y7(VI4YJ8FFhEO|ldUexp8t&QyX zm2RcjSlOMVYNN&%4(P>vc%@QS@mgbA2KCVhQVIPu1_-G0?PJ;SCsF1lZ*;O?f>fejq%`P&#&>;cyLT=HRx z)Yl*@Sta#P23IXSbFEdYR>{;GOgwt+SbXN%qG-O)<}bav*BP*?;kl*N+2l}hGOV`u z!EG1Lg>`u|W^dTgxUp7QedOy{*Ze&*eNUiGulnnjXs7XmV@Li$basYL9T>cBU0!;x z_3s|6e{gbrwpCJQ?;!(EPWGR8%WI@|zOF$~zXf|u2L+_s?-{&m>H4?v=g&Xqg-fjk zKH=286^)2uL*ns+@@cZS9`Av`e8~udVPS*H#%8jH`OJPr&!L{G8%#0jE7}2ftv*x2 zs}QmB{BB(dh>D65Fsi+8Y9?rr;jqyXZ-_W&zZ?i=pL3jbXPG~j0$VE-w{PEOb(r^E(z=+={1mJLgd>NTz|&J-(y`vJT1AZGXh}{ZdWDHu zjgA@Njp6#A9p&BdcIIjZs`nf;s3nc0K1BpTf(Lw_S(>2$H&h8i&5+xL*gkm5=1m@u zA=k?^iyZjK_D(ZHsbc{DhJhs3ell^~bNjk~zDD*Q=Q;KKyPshRmd@bS#kG(lXJ`$q znKgS-yFN3jk%K)L?#zH%wc+gKqFp6QyM$HAj@=ghLGsmU&_(%t> za06n^pIpM%R_uD`Uvg1sRiIO1#R?_Uz|5>Z+(0M@!^p}~n(>znzgPAkywhwV8BD)7 zq)zf37~9R*so;rVY1IQq2fX`{f1Yoo!n(jM=!ax&k<=nIwwxQ)7A?H6l$i z`%+0vXv&f$k|Gj9i*0CARAv~mFCi)wGG%#WPo`vIq$x`Jz0P>H-*No@_#Mah+aJ%& zGj-qhbzj%#bAHbA{eGYC(~=XHg@91uH}^b!$78eFgINVKCVTg5QvMG(u}ESq7!V2a z1Z1=-l}8tT+5Dex%^g?7MBxe3A5okOSkk}unu8JWD79lHLuVRwzW0$K9!C zL4NA@K+kK)>$7Zi80pxQe!rypr861u{^jTTg~D3=mx! zSq+8=-BO6jDt}LP{9$_if3Np$&((Y=tYZ!xst++1@9`Pf>Hwhpb8kh){(+!MpB#5a zxrzx60S8xtkychJWT@f`r>uv&#^#~_>26_}I5|C1NsCth@5d&9@TBzC)v252>^=G^ z>8hW>e@22k1(Dhu%Lyc9{w3r0^&gq`JEV#Hv_8JWN416&O8l|Ucs>uolI-T|vK{wN z&nOr){on5qqI)HvOUstJdauF7RQ{?ByYcPyEhb*jwYiJX=mZ_aQeepGuQSZ}7%(M; zmXx|9e;@GeZ+DGym>%#IgMH4}`ULUo)MbD@p-YjF2u$m=?z5i2vK--)y~logEaf%^ zjsd{Pi$pFeuK~l1nP3nXH*56hM(`t4s;~`I+f!_=etU&=lGvW8D&pwYMSf^&)TCkzYi5_&W{``Xp>iu%7Bm%b+(pT5>E#~JFh)>aK>P6ce$`w`yfpf z(&`L&n!if!_afxQ5On3!NlK*}<6>Xo6~V^dfEbhe4q4Dhxk{qc55S)0MNPs?8o@}a zT?meAckVQ&zOw{50ZkEFk16ICCj70);7jVu3~*|w1;#X*t)>5)4cc&L%7agWeWK(Q zWJ2y{XlUpJJUq8)51(T=H6JRc0%nH@CPq#?_uF}^tKs4i;L5bD#rdbyfC@(wn@$=H z0hN;gLvTq29SxyzPL47f?5rWll7Gd+YX>=u?c@F-i&!kk4Ld6@0EcCfazmV*o!4{9 z3M$SFK$!#lb)w zBeR$f-I=B%?7cRFkF##_G6DPY!yr=Nn_8EUBHhCSRiWSh46+8`uRgw9ck<2ha;*4d9AKgz)&wA|;) z4)7X7y&`b(#Kj92Cgm=X37}Q8S&NipY3!{D6S#GfkZ!4;aPwasdo2oMYISmybjb34PW`fE`r{)=m_m}cKdnNb%x^=bL zDmpbywiFf?8oB0VZ7w=S8}QwS4>M3bNI_5@-Kuc z0f>s~ON)wpQFw@MUAxxf{-jgux)5-%0*@`Ot2Tn}g##rkI=E?EnB7UCCC*Dq``V}* zSB$X@l355nZfb~|GD)A9zk+P2D{P~yTzc5?75!7%VGCkO))`ld)Xa8j%INJK;=_AI z7*5iEJn+h=f+pfraAm;(C!LQ6>swSUd-JIG_8OP|8Vb3~*6QaqW5}dA>lQH0v zx`QY-mxtu#?JebE^7e?OQ{yUiiaDeRJQ6cdREOYA5lcYWnK|#$(}S|76B;A|$g?9+ z)aZC5cxL>zu&{gS)-79hrIahVS{1_7KD|?Gj;4^O&~x?xzgk;! zrKz@gCxTBy1se6*?c3vgB5SYPd@-ms&yQVAaUtEM!tBy3CAO>DM;FztuAXx}pDD&p zDv62fQ_P(`HP(8a9i&9B{ahvxf{a*(9U(V+iKIkg1lgK+3y7r;#sVX$(;=(^(6o01 z<)Ig7+fctHf+7;Y#8m%oZmt#OKW1`5q4#_` zF1jZPCvThv%}@_s<Asz9IR?$h-uD*2T^^X4ckho<_r#Q>n5v>?I#>FIoUw$ zY$dcZT73S`pC&zPot;#{MbHCfw`6&p#KXF0>;y$a8v?xko<8-MHS3x8py6T(yE+w4 z?HjmX`NQbb%+~^__vXL{l#((OTc-5TF{v_j>Qs)&iH0k=ogES2FhP0qcm9P_>zMoYl&8kLej4pt)M#9}Ubntt*P1PD@PoIyfm4^?PG+SXu-hGZ_2-}K4_T?= z@2_ni_sG3OWB7r&qPEaWx2Dke^umL49yt z^80$ag+-Oe+t0pnkFM2gtgaF{)Nuc~Aam7k@L!NYj!ON!1?~^;-*y_-WNZ2KXP!@c zhW-|0uJ*6r&z-B(-|*#OSZ3~mCH7+`_}W=Vsq6pqDgsxpo7LFdZ+y7czy8yM2Nn(v zU*D?j`r3csl>h$g(~FMu$Tu|ne6+I4yJ}hS{iN15)2yQ!{OhIf-o07$^XO539G^3< zJ2q85vvspNXf3b1Gg&6xxc!*N>ez``22O~*-Gc~W~ORlHW z*^??pz4P`sgk|SxReo9(?Pv(*U4FF4I^5)+SLf;DW04bSaeWii_0`WGfwU}U%Oz=3I91yuxrqv?MVKc9%1FUX5w^k1;{V)->v;zlWAlg=rURSa$ zW~dpw+gX5 zE{#~`)uyr&V&6j?><*OX1S8Us&gAHuDR^xk<=@+2>ezI!^M8w2AiaNTV4?Ilr)`G&~Rq?mN0jU;T!O%NyfO`A5^ zx@otZC z{~?ak(9#Oi-5OPwyauf-UD$k%WzIEpId?JXjQ0Sd5$G0eeYpO{@8Yuv{g5?LNL^&a z+ngCQhSq%h=HVgj=QgLRUqk{R6@i=$B*=YH842;k%;#&Q?4V9f{QUeRJJzpLevMnQ z`CI6=?hy1gx~PB*{Tc%47PoO5@&IDirn4(|NAD)C9T4qxWJzFC}yONSN zF6Ey=11VtZBQ`B7N$K!nRf2ZNma2k(B(x3LT=jBqf<{PSm7$tzD9{TkgH|isg*Sq_ zE(6LH5j*|0FmAoxdD#yA_UFbS+kIYsdyf~>cq_HUF#C_a>b4{Q)9iYK@3((Uv%Z$! zvN>Ic`n+N6@#Dg4S~xjfIX9xK*U)<{GOT@RY-MvN`R{UekL9fsszQ5Oi ztKY+~+8FVCLZT7p3w+y-iJSP3IWtU_rOrI4p&8RK2?zo}kggOqLm;B3nEX8J{r$V> zsbyFXbI&@5TtxhrfeSXBKW`=v6+@bYzw8n;ygP(7l_9JY2N9)T!9!fB9#nsraeIQJ zYgr1IPbzOpx?`3e`tXVqXLru&-s9p7T_-x2d)C`;qHi6jKjskA&TM(8qea8YVKj;; zF?lV>qpqIbNNoQsWxOj0@fJXOnI?cmI{hsLurXG2y-}r4kYCaq)I=%Z7)G%Tvu_A8 zc!RvLTOX$sBS>OvYUY#MUvUMgxw#HP9cYCd_8zK2+u#F_=qR-3w3J4;~EzqoPGB!0Ls)_eRN7qoxm+uy{42j5kTFg?ADK7%b%PhB|@ zAFoEE!3ioDZpju?_n}OZ=mmddL@@1knczS3rb+ZVQ#U)<(D3vER#5?RztksBX2HE2 z`>Ti5?H{ww(IFBM5mBD3Y1N=%Lq3Fnr8;cw>A3N(x*IiFk1FexoracvpZ%RfjP9-j)yJP8g^~o!xl%mwtwZQRfqi|7x?c z85Wartx)D8^6~C#q=+N5Yy`v{?oK>$uc~r)#e#M_+TRlHMRJ^x()X#Eg_DVU6ojtw zg34@q`ckL~QEgSt21PW^}Ji~%Sq9OuNbesovtuiJSOG8|KpE_IyEcO?Y9ixuBLSQ!Ap@TBGmh<=?g;f#>(e0u2PT0_ydP*(6~O}yet?8DMyyRUtwV!FuA zHfup2a_6+p%IkyZiw_qC3!f!*AXz0KsBN|Fo<7^5Z4`wZGWOJauoDp+0x2j89fi9p z($dlxig#X417^h}Z__g}YD#H;`<~O7F=PB?@0)hPoLQ(90)(&@c!0?CPfDfRl}nDA zjO!3qSxU|?*a|R1WXPmE9y&}Xo;h>1d;y=0ssrdn%97$es-x3H`ml$8HFdwkLZx`VeSUSJs*lYUZ zU4@*^nkIsB?TjO%w9>m@=Q^d|g8rO@Peuva3ChCS(Z^eogsI2`t&k^%CVX~U; z;9hBCJEw09PxP@90z$|htkpIiJNDDzuj(Hw-%dJW<2tdWg!6v-dZi|96T5gWdDvGv z!+~T;p_)-2qSsawB68f!ekP;`(X^N=xwT$&J#}IUO=Hs0CJpU6a7v6nDKWQ#kEDXO zayha}g%bi05^>DA)gKSv`-x7zWq%rNF%T<-M_#FN{4s0q zylFHrlLqa1-DrdTmc!e|@}ea|LNYmz{t%WS-k#RT#)AKR(yA>nyUK55KYh9vNRD$% zo`zNvIHO5V1(x%#UFVx+>SAP@%aSxnZ(uN;7!=g1(#Sh}CHM_H&q}0T0ydRCPF}DB zS4T}w+gyS0f`Po%y5{a_-X91EoPS?_vu4d&D>uMJ<2}>^t>lOUR2`T-4Xbej?1Q@1 zMDE|;6TmEu)KC3hq<(Jr2XeWV(4eU~IrjP5ZQJ%hk(UOs-ekpJ3vDN*cSp<@r06h$ zMSaE94pw35Y0O}x1=~9R3J9Nzr)QMmPvee^=SW5-Gw=MZ84c+}wO`SuGPXk)`I+6( zQHmxyq&)zWIsJEE`cjj%-rlRt%5m+thL)`!+xT|-u+Z{-TZ5ZxEpK)byA=T0UbqFO zQOJkS0g8#m9e*x(B%?YlP&ABUCihkXA!iNRrPdU>w;bPtFESnHwLU8C-s36bQ1Y|9KM&5^PUX?)uiDH?$ zN2YuTQ5Rfzpw%Gnj$uvp%q)$w`hA`-C$!|r*LAOt^d5KU+s;&rneOV~AgRMHwXW6= z23@Hus?b;LOgN!6~1olFNg7!M*&au4gH;eXtqX*MWEz{4dqwje)fdWK*yClDEi;$PE?ulk5 zCAQhd;%?_?rr|A5V2M}I9FX1CKuX;3UxAtneOgzN$3h}!jt&k=D4um?U1u0@w}A21 z)@hsS-t0|sj(Po^)sxGqJyaB)%8bH(0yOw$?TyxIF>yCG7EN_38w_*x1K$cBD$sPS zeS5hzS-I#W)u5(id^oR|zWY2`H|bCll2Dl>mQg_i^atQJdNNwme}sb8ib4qqdq}6Z z9%%9@S=WO#v2%Z?)EzpkV=elA+~3*}dN%{5A2@K}E?qJ(8bvb(8Dph@VyGd&nBg6c zPs^!O2jy*aw~J>=;v~|MW~?N}b~hB31{8MLFDtQT z5alF^SKq#U*OP#9N}j-KgcEQ6^diuBK@26}lQ^+?50U(#US4#jCN3_{nR6d8MF5wM zT;c9-E+b@)583T!^EnDlBbpE?YMkvbKQapN6Y9@pzwUYHdS~xmsp zD!FYDUI7Rc{1EM`D)lT#uPh|*QZ#`%Z=d`^9;vE)z$+t|;%=-rD*uSAgTwviP>+qR z9=;ZR`N&J$^hwpveJf|5@g`?5tob6Go?89-8;st%!!%OuFG#{E*hmQ?`!}&K>iKFF zF!~LSEThxLVVTzKWXGRKwI6k1;!r$IiF+L`YKL5_=0^2M+jsvc3r5$>%xuyxBpR7# znky3z+qEZ*9CIup>qxzEqx<8*pqbhC>>VA$Nn>)(gq%HlRy+0E zFV-rTP{;;Xf}mo9(cswg7=ma$IYBQirEhm7h=ZZ$>C1F*CtijKoWrz(Y2;SSCN!2z zH@wqjV*`URAlYh40p9Q}dE3@-8rV#1p$510<~hYX9*sUBfmGv0#A7G?Qw6I01sw;j)<#7=+JuL+jP5a0@$2F}$U2RVI!MIAD zVby2PR``cth?bt08$BDoS=DTQ*Ns#L2NfO8RFgD^yRs4|uB$84GdE|xgl5`kr>~XI zj5fBSty|R4)w)V!D$QN2X^()&C3?xYkm(}ao)kqQ)k@h2pcB3`iyIDDuLO|<_M3DN zJop2054U~MwCh+Ynw^KNxK$={+CEB9j(N46`x;BF<%+Zp?WPnJ37ncsxGk9;WC3!&pnfIv9ekb^m>KiYEVZyZ>$tN?&_LlaIfB%D8v$aTl(? zy58WjPKJ^79W@`z=Tk|`K3`jas?uO4Ai5-7u>QaK)vk;8ZJ&Hrgg|{wO>dn{LE!^F zrYLNJ4P?fL`}2-SWlVUQ<(>YP)QuUSD0E&CE;Ai7&_xjdZ=Vt`7O|yPu0DJ?D0^@J zlBe+4+|lW~GmrDD1^H({$x(+kWJLlpS|6?fSQoHDc|n$NzTyOHUAYPwOmZLT?njfJ zuxlWL8Z@qpIPgtf{PldI|w3=aduWAW>x5Zto^bZ4 z{pj`|k-MUcOxILz?>I=aZQDjV!&>BwPj9fSPRGg8uct^uT9YPQjozA~=FK}p3xlGR zmzRSL-!^&+8_*m;P-SPzyVE_%Yr8$3##RCx2y?qk(x{?HvmuYp?eSOEXWW(|0ET2E zge)&!zgD&Qe2xe%9k7YdSnI;0;n32Q${-y=+2r(O><-?HT~BT93PwQ#^CnFd9Rx}MoMe>u%uSQiA;HwwD^#xOv0b=Be zTgZqEO0!Gei9HylgDOPiRmff8Tt!y^b12OOxkY{9R>z@>=V?|+e&08IB zvSC)AMoOX`zg>U8)ACM2*wcX?9;JV3l=zkTY+YtK?}lzz=F*`RP3asse@3;i^c+gE z_$d1PH!?V^1BA5zcr?dgt*OU3E}>3zlS~G@XXhbKF|riNkA7SEevWj05L{b-{aWoQ zgdP#G6XNqA@&dy&0Go(KEvwh9eUzEKM!8w#JK?VDow3?!H7Sz2bgau=CX>xbe-(vr zG<}r0rSHt1K5PD^HsUpDI=ZFBo_~jI+txr~Or^^D<$9k%3?7|1ct8Ko_J5PEB+q_~ z$Ra47gLYr!bK@UpD$;0ECj>~^1A0mw(yd>=7T6!9vie*WFaE{x5BfsPDu%eEHn7{< zw;mOe2w!MfUQbli0c`;O^+#fjbQZk~%^76;OMRURN6vXxpLLbntyOz<9#2|qb2%}q zPJL)E8QA3XWyLZlk0l2Y)(AZ{kGpW%ae}j=1X}s?&D>`8dA0B3wxTuCP^X;pe!DyT zl3y`E(!Mip-H$<=XP$|)in&VDn)p&+*jv=j&t={XT-}~7&L#vp(^fSNaZZTGWIVh% z?arS$%L93+b- zv9l%v=wg&8!Yrbn6zwAPriIJwLG_=>IdMhVCi6dky-U#g09mW-j9lQEceokz0(S9a z;40jbwA!}a40Jj_|J}=%{`h4n3brqmM%C3|!hMNMc9BEPvJ1ye7F{Dfh}e%gxlj4K zavTqih#5ZrcuojiAM#s;#-Js;-K43FbV<bBbKVNu|4V5_F%j-AON6)Jnxwk{Q zQ^>-nSG<1tvTMzOCB3R>o>f)oymi}s_U-7ABW*so4RwkP6Vy2^O;zd3ld-FQz0?fk zb{_}j4znI5<#p?KRzKs9qX5FYUmS2QP;)MN^;~mBcpcm7&<*l zQ9-FHzcVr`-j_|k`@!fa{5QxyHA#NRtG5DEt1;)!eRiq9SL9xt`E$|r7LdVMfy07Y zfp0XG`c?@-t}`xij>`i}zuHyL_na7bhEnwtbxazeXXK0-`(<^(E*)uARQ)z!!Jvkm zW(-zwR5S@snpkkOpqa1$8euNGJQaLG(FAJUQ_{VAcQJpeXF<2kG}K%(ImCiV6M|70 zsFCure~gsIc;)h}jS82Z=hTc-bm)H)z`&&QLrT85)1@XL@M9J|JPUL#UyRS)wR5Kq zLBW8|GgU=0ip4n(H%r8LGfNan=$XezlErbMP6Kxg%|UR}5f3bli~BX{dV}W~f4UH9 zOhT)%|I7xY4D4>EC!J|iZ13WweEih#iyH?R8rqz@cya6&dk*0nuwS~8eGu?f6-ioo zqa*hsXZ2~zU6@c}@+zKcgLG35*8mf}VyiEHOQW7HLV?EZHn?vX+@J;oR=Ob`rK1+n z5vy;<9!L5yN9SDl+WCK%RaCU0ikOmwIdSOXY`~dmI4wB3&m@f}SB+(%<(_|s_oB$i zZtNKTwGKatAi22c-~{5D+g5e@W%?T=zddl|(F?fLC@ zSjA`(a9V>l2s|M02lvH6P>RM~Pma4ASjSexk_ZV1L0Y~Aax$3Hc`KmS{OKtMEz7sL zqJlky$v_O}LJP=;SloT*-rU)>08(XNR*`8#tGFJ!09@MO?%Be}#|Lz5JY}aft&_GF zT7HQkur@}H7jyi$F;R6S;P(vzg*EFGS8(!eb=I8AqD8$ZQZb<-)HTNW&nV*-D~H}h z-Ru<5&@}W+B83S=mnSd=Y*1^ku(q65=OZ8f0#IsCDl-8$G&NX9>cS>Vm~qr~pD2z;5Dbo-8+j!#_0 zc%d<4e|dxchKT;-vrA^=vcW@4 Date: Thu, 16 May 2024 18:09:41 -0700 Subject: [PATCH 002/102] copy --- _posts/2024-05-13-models-over-time.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md index dbac39361..1690d4073 100644 --- a/_posts/2024-05-13-models-over-time.md +++ b/_posts/2024-05-13-models-over-time.md @@ -1,11 +1,11 @@ --- title: Drawing graphs with aider, GPT-4o and matplotlib excerpt: Use GPT-4o to draw graphs with matplotlib, including adjusting styles and making visual changes. You get the graph, but you also get the code in your repo. -highlight_image: assets/models-over-time.png +highlight_image: /assets/models-over-time.png --- # Drawing graphs with aider, GPT-4o and matplotlib -![LLM coding skill over time](assets/models-over-time.svg) +![LLM coding skill over time](/assets/models-over-time.svg) I recently wanted to draw a graph showing how LLM code editing skill has been changing over time as new models have been released by OpenAI, Anthropic and others. From 47eebdb7164695b68f792fc019691bcf8319f03f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Thu, 16 May 2024 18:12:24 -0700 Subject: [PATCH 003/102] copy --- _posts/2024-05-13-models-over-time.md | 29 +++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md index 1690d4073..987b41c2a 100644 --- a/_posts/2024-05-13-models-over-time.md +++ b/_posts/2024-05-13-models-over-time.md @@ -10,20 +10,24 @@ highlight_image: /assets/models-over-time.png I recently wanted to draw a graph showing how LLM code editing skill has been changing over time as new models have been released by OpenAI, Anthropic and others. I have all the -[data in the yaml file](https://github.com/paul-gauthier/aider/blob/main/_data/edit_leaderboard.yml) that is used to render +[data in a yaml file](https://github.com/paul-gauthier/aider/blob/main/_data/edit_leaderboard.yml) that is used to render [aider's LLM leaderboards](https://aider.chat/docs/leaderboards/). Below is the aider chat transcript, which shows: - I launch aider with the yaml file, a file with other plots I've done recently (so GPT can crib the style) and an empty file called `over_time.py`. - Then I ask GPT to draw the scatterplot I want. +- I run the resulting script and share the output with GPT so it can fix a small bug. - I ask it to color the points for GPT-4 and GPT-3.5 family models differently, to better see trends within those model families. -- I add a PNG of the graph to the chat, so GPT can see the current graph and understand why I ask it to move the point labels so they don't overlap the points. +- I add a PNG of the graph to the chat, so GPT can see the current graph and understand why I want it to move the point labels so they don't overlap the points. - I work through a series of other small style changes, like changing fonts and the graph border. In the end I have the graph, but I also have the python code in my repo. So I can update this graph easily whenever I add new entries to the yaml data file. + +## Aider chat transcript +
> Aider v0.35.1-dev @@ -85,6 +89,9 @@ plot_over_time('_data/edit_leaderboard.yml') > Applied edit to benchmark/over_time.py > Commit 49ac9ca aider: Added scatter plot functionality for YAML entries with a `released` date and `pass_rate_2`. +
+
+ #### /run python benchmark/over_time.py > Traceback (most recent call last): File "/Users/gauthier/Projects/aider/benchmark/over_time.py", line 33, in @@ -118,6 +125,9 @@ benchmark/over_time.py > Applied edit to benchmark/over_time.py > Commit af2f4b7 aider: Use the `released` field directly in the plot instead of parsing it with `datetime.strptime`. +
+
+ #### color the "gpt-4" models differently To color the "gpt-4" models differently, we need to modify the plotting code in `benchmark/over_time.py` to check for "gpt-4" in the model name and assign a different color. @@ -139,6 +149,9 @@ This change will color the points red if the model name contains "gpt-4", otherw > Applied edit to benchmark/over_time.py > Commit 0d11357 aider: Color the "gpt-4" models differently. +
+
+ #### color the gpt-3.5 models a new color To color the "gpt-3.5" models a new color, we need to modify the plotting code in `benchmark/over_time.py` to check for "gpt-3.5" in the model name and assign a different color. @@ -157,6 +170,9 @@ benchmark/over_time.py > Applied edit to benchmark/over_time.py > Commit 85d9d84 aider: Color the "gpt-3.5" models green in the plotting code. +
+
+ #### use the colorscheme and fonts from the other graphs in plots.py To use the colorscheme and fonts from the graphs in `plots.py`, we need to: @@ -229,6 +245,9 @@ benchmark/over_time.py > Commit 8d670f1 aider: Used the colorscheme and fonts from the graphs in `plots.py` in `over_time.py`. +
+
+ #### /add tmp_over_time.png > Added /Users/gauthier/Projects/aider/tmp_over_time.png to the chat @@ -254,6 +273,9 @@ benchmark/over_time.py > Commit 75fa061 aider: Adjust the position of labels to prevent overlap with dots. +
+
+ #### make the dots bigger To make the dots bigger, we can adjust the `s` parameter in the `scatter` function, which controls the size of the dots. @@ -272,6 +294,9 @@ benchmark/over_time.py > Applied edit to benchmark/over_time.py > Commit a018fdf aider: Make the dots bigger. +
+
+ #### make the border of the graph thinner and lighter To make the border of the graph thinner and lighter, we can adjust the `spines` properties of the axes. From 6331edd9549c4e2d88dbd52c31d3678e7c9ec36e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Thu, 16 May 2024 18:15:11 -0700 Subject: [PATCH 004/102] copy --- _posts/2024-05-13-models-over-time.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md index 987b41c2a..9734b37cf 100644 --- a/_posts/2024-05-13-models-over-time.md +++ b/_posts/2024-05-13-models-over-time.md @@ -90,6 +90,7 @@ plot_over_time('_data/edit_leaderboard.yml') > Commit 49ac9ca aider: Added scatter plot functionality for YAML entries with a `released` date and `pass_rate_2`.
+
#### /run python benchmark/over_time.py @@ -295,6 +296,7 @@ benchmark/over_time.py > Commit a018fdf aider: Make the dots bigger.
+
#### make the border of the graph thinner and lighter From 2cd2f5439abb314fea7a936f6b6edea081933544 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Thu, 16 May 2024 18:15:28 -0700 Subject: [PATCH 005/102] copy --- _posts/2024-05-13-models-over-time.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md index 9734b37cf..4cee4048b 100644 --- a/_posts/2024-05-13-models-over-time.md +++ b/_posts/2024-05-13-models-over-time.md @@ -3,10 +3,11 @@ title: Drawing graphs with aider, GPT-4o and matplotlib excerpt: Use GPT-4o to draw graphs with matplotlib, including adjusting styles and making visual changes. You get the graph, but you also get the code in your repo. highlight_image: /assets/models-over-time.png --- -# Drawing graphs with aider, GPT-4o and matplotlib ![LLM coding skill over time](/assets/models-over-time.svg) +# Drawing graphs with aider, GPT-4o and matplotlib + I recently wanted to draw a graph showing how LLM code editing skill has been changing over time as new models have been released by OpenAI, Anthropic and others. I have all the From 07c5cae98296edd7cb2c73fb14a76a67e18a0860 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Thu, 16 May 2024 18:17:17 -0700 Subject: [PATCH 006/102] copy --- _posts/2024-05-13-models-over-time.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md index 4cee4048b..3b7fa1980 100644 --- a/_posts/2024-05-13-models-over-time.md +++ b/_posts/2024-05-13-models-over-time.md @@ -91,7 +91,7 @@ plot_over_time('_data/edit_leaderboard.yml') > Commit 49ac9ca aider: Added scatter plot functionality for YAML entries with a `released` date and `pass_rate_2`.
-
+
#### /run python benchmark/over_time.py @@ -128,6 +128,7 @@ benchmark/over_time.py > Commit af2f4b7 aider: Use the `released` field directly in the plot instead of parsing it with `datetime.strptime`.
+
#### color the "gpt-4" models differently @@ -152,6 +153,7 @@ This change will color the points red if the model name contains "gpt-4", otherw > Commit 0d11357 aider: Color the "gpt-4" models differently.
+
#### color the gpt-3.5 models a new color @@ -173,6 +175,7 @@ benchmark/over_time.py > Commit 85d9d84 aider: Color the "gpt-3.5" models green in the plotting code.
+
#### use the colorscheme and fonts from the other graphs in plots.py @@ -248,6 +251,7 @@ benchmark/over_time.py
+
#### /add tmp_over_time.png @@ -276,6 +280,7 @@ benchmark/over_time.py
+
#### make the dots bigger From 014ec6365eb299752751cc7fab490b46f784e678 Mon Sep 17 00:00:00 2001 From: paul-gauthier <69695708+paul-gauthier@users.noreply.github.com> Date: Thu, 16 May 2024 19:51:21 -0700 Subject: [PATCH 007/102] Update 2024-05-13-models-over-time.md --- _posts/2024-05-13-models-over-time.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md index 3b7fa1980..34774b2df 100644 --- a/_posts/2024-05-13-models-over-time.md +++ b/_posts/2024-05-13-models-over-time.md @@ -18,7 +18,7 @@ Below is the aider chat transcript, which shows: - I launch aider with the yaml file, a file with other plots I've done recently (so GPT can crib the style) and an empty file called `over_time.py`. - Then I ask GPT to draw the scatterplot I want. -- I run the resulting script and share the output with GPT so it can fix a small bug. +- I run the resulting script and share the error output with GPT so it can fix a small bug. - I ask it to color the points for GPT-4 and GPT-3.5 family models differently, to better see trends within those model families. - I add a PNG of the graph to the chat, so GPT can see the current graph and understand why I want it to move the point labels so they don't overlap the points. - I work through a series of other small style changes, like changing fonts and the graph border. From 8a960083c75136a1f353e9945ac0525442f8fd53 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 12:51:13 -0700 Subject: [PATCH 008/102] updated chat transcript divs --- _posts/2024-05-13-models-over-time.md | 7 ----- assets/css/style.scss | 1 + examples/2048-game.md | 4 +++ examples/add-test.md | 4 +++ examples/census.md | 18 ++++++++++++ examples/chat-transcript-css.md | 41 ++++++++++++--------------- examples/complex-change.md | 14 +++++++++ examples/css-exercises.md | 6 ++++ examples/hello-world-flask.md | 6 ++++ examples/no-color.md | 4 +++ examples/pong.md | 8 ++++++ 11 files changed, 83 insertions(+), 30 deletions(-) diff --git a/_posts/2024-05-13-models-over-time.md b/_posts/2024-05-13-models-over-time.md index 34774b2df..e3fa8ef01 100644 --- a/_posts/2024-05-13-models-over-time.md +++ b/_posts/2024-05-13-models-over-time.md @@ -91,7 +91,6 @@ plot_over_time('_data/edit_leaderboard.yml') > Commit 49ac9ca aider: Added scatter plot functionality for YAML entries with a `released` date and `pass_rate_2`.
-
#### /run python benchmark/over_time.py @@ -128,7 +127,6 @@ benchmark/over_time.py > Commit af2f4b7 aider: Use the `released` field directly in the plot instead of parsing it with `datetime.strptime`.
-
#### color the "gpt-4" models differently @@ -153,7 +151,6 @@ This change will color the points red if the model name contains "gpt-4", otherw > Commit 0d11357 aider: Color the "gpt-4" models differently.
-
#### color the gpt-3.5 models a new color @@ -175,7 +172,6 @@ benchmark/over_time.py > Commit 85d9d84 aider: Color the "gpt-3.5" models green in the plotting code.
-
#### use the colorscheme and fonts from the other graphs in plots.py @@ -251,7 +247,6 @@ benchmark/over_time.py
-
#### /add tmp_over_time.png @@ -280,7 +275,6 @@ benchmark/over_time.py
-
#### make the dots bigger @@ -302,7 +296,6 @@ benchmark/over_time.py > Commit a018fdf aider: Make the dots bigger.
-
#### make the border of the graph thinner and lighter diff --git a/assets/css/style.scss b/assets/css/style.scss index 3f558490a..e15a84ea5 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -49,6 +49,7 @@ color: #0f0; padding: 1em 1em 1em 1em; border-radius: 5px; + margin-top: 50px; } .chat-transcript blockquote { diff --git a/examples/2048-game.md b/examples/2048-game.md index a1a8de1fd..4259caa63 100644 --- a/examples/2048-game.md +++ b/examples/2048-game.md @@ -18,6 +18,8 @@ Based on the file names you provided, it seems like this repository is for a web If you need help with any specific part of the code or want to add new features, please let me know, and I'll be happy to assist you. +
+
#### how does the scoring work? To help you with the scoring mechanism, I need to see the relevant code. Please provide the contents of the `js/game_manager.js` file, as it is likely to contain the scoring logic. @@ -58,6 +60,8 @@ if (next && next.value === tile.value && !next.mergedFrom) { In this code snippet, when two tiles with the same value (`tile` and `next`) are found, they are merged into a new tile (`merged`). The value of the new tile is added to the current score (`self.score += merged.value`). If the merged tile's value is 2048, the game is considered won (`self.won = true`). +
+
#### can we add some randomness? maybe 10% of the time give the user a 10x bonus? Sure, we can add a 10% chance of giving the user a 10x bonus when two tiles merge. To implement this, we'll modify the scoring logic in the `js/game_manager.js` file. Here's the step-by-step plan: diff --git a/examples/add-test.md b/examples/add-test.md index cada5104b..3151cd4b5 100644 --- a/examples/add-test.md +++ b/examples/add-test.md @@ -83,6 +83,8 @@ This test will ensure that the `cmd_add` method creates the specified files in t > Applied edit to tests/test_commands.py > Commit e2d5212 aider: Added a test method `test_cmd_add` that creates a temporary directory, calls the `cmd_add` method with 'foo.txt' and 'bar.txt', and checks if both files have been created in the temporary directory. +
+
#### /run pytest tests/test_commands.py > ``` @@ -157,6 +159,8 @@ pytest tests/test_commands.py > Applied edit to tests/test_commands.py > Commit d1551b4 aider: Updated test to pass a string instead of a list when calling `cmd_add` method. +
+
#### /run pytest tests/test_commands.py > ``` diff --git a/examples/census.md b/examples/census.md index 8988010b9..ddefc1782 100644 --- a/examples/census.md +++ b/examples/census.md @@ -30,6 +30,8 @@ Some interesting patterns to analyze in the ACS dataset could include: Remember to consider the dataset's documentation and guidelines to understand the data and its limitations. +
+
#### do you know how to download American Community Survey data? Yes, I can guide you on how to download American Community Survey (ACS) data. You can use the Census Bureau's API to access the data. To do this, you'll need an API key, which you can obtain from the Census Bureau's website. @@ -75,6 +77,8 @@ For more information on available variables and geographic areas, refer to the A - Geographic Areas: https://api.census.gov/data/2019/acs/acs5/geography.html +
+
#### how do i get an api key? To obtain an API key from the Census Bureau, follow these steps: @@ -90,6 +94,8 @@ To obtain an API key from the Census Bureau, follow these steps: Once you have your API key, you can use it in your Python code to access the American Community Survey data, as shown in the previous example. Replace `'your_api_key'` with the API key you received in the email. +
+
#### please save your download script as `getdata.py` Here's the `getdata.py` file with the download script: @@ -128,6 +134,8 @@ Save this code in a new file named `getdata.py`. Remember to replace `'your_api_ > Applied edit to new_file/getdata.py > Commit afcb9d1 aider: Added `getdata.py` file with download script. +
+
#### add my api key: 4ae31ed2a0f0dd62609e51cd95deadbeefe7774f I will update the API key in the file. @@ -145,6 +153,8 @@ api_key = '4ae31ed2a0f0dd62609e51cd95deadbeefe7774f' > Commit 1d8b4a6 aider: Updated the API key in new_file/getdata.py. +
+
#### what are some interesting hypothesis that you might be able to test using ACS data? Here are some interesting hypotheses that you can test using ACS (American Community Survey) data: @@ -158,6 +168,8 @@ Here are some interesting hypotheses that you can test using ACS (American Commu These are just a few examples. ACS data provides a wealth of information on various demographic, social, economic, and housing characteristics, allowing you to explore many different hypotheses. +
+
#### can you download the data to test #2 Impact of population density on housing prices: Higher population density areas have higher housing prices. As an AI language model, I cannot directly download or access data. However, I can help you with the code to download the required data for testing hypothesis #2 using the ACS data. @@ -183,6 +195,8 @@ After running the updated code, you'll have the population and housing price dat > Applied edit to new_file/getdata.py > Commit 81549ab aider: Updated code to fetch population and housing price data for testing hypothesis #2. +
+
#### instead of printing all the data, print a summary table that shows whether the data supports the hypothesis To print a summary table, you can use the pandas library to analyze the data and calculate the correlation between population density and housing prices. Here's how to modify the code: @@ -239,6 +253,8 @@ After applying these changes, the code will print a summary table showing the co > Commit 2f4fcd4 aider: Added pandas library to analyze data and calculate correlation between population density and housing prices, and printed the correlation as a summary table. +
+
#### /run python3 new_file/getdata.py >``` >Summary Table: @@ -250,6 +266,8 @@ After applying these changes, the code will print a summary table showing the co Great! The script seems to be working as expected. The correlation between population density and housing prices is 0.1602. If you need any further assistance or modifications to the code, please let me know. +
+
#### can you also plot a graph which would show any correlation? To plot a graph showing the correlation between population density and housing prices, we'll need to modify the `new_file/getdata.py` file. We'll use the `matplotlib` library to create the plot. If you don't have it installed, you can install it using `pip install matplotlib`. diff --git a/examples/chat-transcript-css.md b/examples/chat-transcript-css.md index 96e4f4f98..6a4c772a9 100644 --- a/examples/chat-transcript-css.md +++ b/examples/chat-transcript-css.md @@ -24,6 +24,8 @@ assets/css/style.css > ^C again to quit +
+
#### make all the edits to the embedded style tag in the html ```html @@ -63,12 +65,9 @@ index.html > Commit e7a5cb5 aider: Added rounded border to all sides of the boxquote. -#### (blank) -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? Changed border color and width of blockquote in index.html. -> Commit 6898a1d Changed border color and width of blockquote in index.html. - +
+
#### make the blockquote font color the same as the p color > Git repo has uncommitted changes. > Commit before the chat proceeds [y/n/commit message]? Added note about missing method and incorrect method calls in index.html. @@ -123,12 +122,8 @@ index.html > Commit 8645217 aider: Changed blockquote font color to match

color in index.html. -#### (blank) -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? Removed color and button styles from blockquote in index.html. -> Commit f8d4782 Removed color and button styles from blockquote in index.html. - - +

+
#### make the blockquote background a lighter shade of the same green as the border ```html @@ -153,6 +148,8 @@ index.html > Commit 57b7b10 aider: Changed blockquote background color to a lighter shade of green. +
+
#### add some space inside the border of the blockquote ```html @@ -179,6 +176,8 @@ index.html > Commit dd7dcb6 aider: Added padding inside the border of the blockquote. +
+
#### can we add a label "aider" along the top of the blockquote box only using style ```html @@ -220,6 +219,8 @@ index.html > Commit 0b0e332 aider: Added label "aider" to the top of the blockquote box using style. +
+
#### add more padding on the top of the box and bring the label into the box so it doesn't intersect the border ```html @@ -273,12 +274,8 @@ index.html > Commit eee902e aider: Added more padding on the top of the box and brought the label into the box so it doesn't intersect the border. -#### (blank) -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? Changed padding and added margin to main-content blockquote. -> Commit c9128d1 Changed padding and added margin to main-content blockquote. - - +
+
#### change all the blockquote colors to be shades of blue instead of green. choose the blue based on the blue in the header gradient ```html @@ -331,6 +328,8 @@ index.html > Applied edit to index.html > Commit a26c135 aider: Changed blockquote colors to shades of blue based on the blue in the header gradient. +
+
#### can you style the "aider" label on the blockquote as a "tab" that docks onto the top of the box? ```html @@ -385,12 +384,8 @@ index.html > Commit 292eda3 aider: Styled the "aider" label on the blockquote as a "tab" that docks onto the top of the box. -#### (blank) -> Git repo has uncommitted changes. -> Commit before the chat proceeds [y/n/commit message]? Changed padding and positioning of a CSS element and updated its border. -> Commit 01fda51 Changed padding and positioning of a CSS element and updated its border. - - +
+
#### apply the same box around the h4 element ```html diff --git a/examples/complex-change.md b/examples/complex-change.md index df2d21321..94cf41ebb 100644 --- a/examples/complex-change.md +++ b/examples/complex-change.md @@ -114,6 +114,8 @@ These changes will replace the usage of `/dev/null` with `prompt_toolkit`'s inpu > Commit 6724fcd Added import statement for sys module in test_main.py. +
+
#### /add aider/main.py > Added aider/main.py to the chat @@ -145,6 +147,8 @@ Now, the `main()` function will accept the `input` and `output` keyword argument > Applied edit to aider/main.py > Commit 79dfba9 aider: Fixed main() function to accept input and output keyword arguments. +
+
#### we need to pass those into InputOutput and to prompt_toolkit right? Yes, you're right. We need to pass the `input` and `output` arguments to the `InputOutput` class and then to the `prompt_toolkit.prompt()` function. Here's how to modify the `aider/getinput.py` and `aider/main.py` files: @@ -209,6 +213,8 @@ These changes will ensure that the `input` and `output` arguments are passed to > Applied edit to aider/main.py > Commit cea9d2a aider: Updated InputOutput class to accept input and output arguments and passed them to prompt_toolkit.prompt() function. +
+
#### FAILED tests/test_main.py::TestMain::test_main_with_empty_git_dir_new_file - TypeError: prompt() got an unexpected keyword argument 'input' I apologize for the confusion. It seems I made a mistake in the `prompt_toolkit.prompt()` function call. The `prompt()` function accepts `stdin` and `stdout` arguments instead of `input` and `output`. Here's the fix: @@ -231,6 +237,8 @@ These changes will ensure that the `input` and `output` arguments are passed cor > Applied edit to aider/getinput.py > Commit 55d3540 aider: Fixed `prompt()` function call in `get_input()` method to pass `stdin` and `stdout` arguments instead of `input` and `output`. +
+
#### prompt_toolkit.shortcuts.prompt(message: AnyFormattedText | None = None, *, history: History | None = None, editing_mode: EditingMode | None = None, refresh_interval: float | None = None, vi_mode: bool | None = None, lexer: Lexer | None = None, completer: Completer | None = None, complete_in_thread: bool | None = None, is_password: bool | None = None, key_bindings: KeyBindingsBase | None = None, bottom_toolbar: AnyFormattedText | None = None, style: BaseStyle | None = None, color_depth: ColorDepth | None = None, cursor: AnyCursorShapeConfig = None, include_default_pygments_style: FilterOrBool | None = None, style_transformation: StyleTransformation | None = None, swap_light_and_dark_colors: FilterOrBool | None = None, rprompt: AnyFormattedText | None = None, multiline: FilterOrBool | None = None, prompt_continuation: PromptContinuationText | None = None, wrap_lines: FilterOrBool | None = None, enable_history_search: FilterOrBool | None = None, search_ignore_case: FilterOrBool | None = None, complete_while_typing: FilterOrBool | None = None, validate_while_typing: FilterOrBool | None = None, complete_style: CompleteStyle | None = None, auto_suggest: AutoSuggest | None = None, validator: Validator | None = None, clipboard: Clipboard | None = None, mouse_support: FilterOrBool | None = None, input_processors: list[Processor] | None = None, placeholder: AnyFormattedText | None = None, reserve_space_for_menu: int | None = None, enable_system_prompt: FilterOrBool | None = None, enable_suspend: FilterOrBool | None = None, enable_open_in_editor: FilterOrBool | None = None, tempfile_suffix: str | Callable[[], str] | None = None, tempfile: str | Callable[[], str] | None = None, default: str = '', accept_default: bool = False, pre_run: Callable[[], None] | None = None) → str I apologize for the confusion earlier. It seems that the `prompt_toolkit.prompt()` function does not accept `stdin` and `stdout` arguments directly. Instead, we need to use the `Application` class from `prompt_toolkit` to create a custom application with the desired input and output. Here's how to modify the `aider/getinput.py` file: @@ -239,6 +247,8 @@ First, import the necessary classes and functions from `prompt_tool > ^C KeyboardInterrupt +
+
#### class prompt_toolkit.shortcuts.PromptSession(message: AnyFormattedText = '', *, multiline: FilterOrBool = False, wrap_lines: FilterOrBool = True, is_password: FilterOrBool = False, vi_mode: bool = False, editing_mode: EditingMode = , complete_while_typing: FilterOrBool = True, validate_while_typing: FilterOrBool = True, enable_history_search: FilterOrBool = False, search_ignore_case: FilterOrBool = False, lexer: Lexer | None = None, enable_system_prompt: FilterOrBool = False, enable_suspend: FilterOrBool = False, enable_open_in_editor: FilterOrBool = False, validator: Validator | None = None, completer: Completer | None = None, complete_in_thread: bool = False, reserve_space_for_menu: int = 8, complete_style: CompleteStyle = , auto_suggest: AutoSuggest | None = None, style: BaseStyle | None = None, style_transformation: StyleTransformation | None = None, swap_light_and_dark_colors: FilterOrBool = False, color_depth: ColorDepth | None = None, cursor: AnyCursorShapeConfig = None, include_default_pygments_style: FilterOrBool = True, history: History | None = None, clipboard: Clipboard | None = None, prompt_continuation: PromptContinuationText | None = None, rprompt: AnyFormattedText = None, bottom_toolbar: AnyFormattedText = None, mouse_support: FilterOrBool = False, input_processors: list[Processor] | None = None, placeholder: AnyFormattedText | None = None, key_bindings: KeyBindingsBase | None = None, erase_when_done: bool = False, tempfile_suffix: str | Callable[[], str] | None = '.txt', tempfile: str | Callable[[], str] | None = None, refresh_interval: float = 0, input: Input | None = None, output: Output | None = None) Thank you for providing the `PromptSession` class information. We can use the `PromptSession` class to create a custom session with the desired input and output. Here's how to modify the `aider/getinput.py` file: @@ -290,6 +300,8 @@ These changes will ensure that the `input` and `output` arguments are passed cor > Applied edit to aider/getinput.py > Commit ac92cca aider: Replaced `prompt()` with `PromptSession()` to correctly pass input and output arguments. +
+
#### try using create_input(StringIO('')) in the tests @@ -345,6 +357,8 @@ These changes will replace the usage of `create_pipe_input()` with `create_input > Applied edit to tests/test_main.py > Commit 4bb043f aider: Replaced the usage of `create_pipe_input()` with `create_input(StringIO(''))` in the test cases. +
+
#### nice, that worked! I'm glad the changes worked for you! If you have any more requests or need further assistance, feel free to ask. diff --git a/examples/css-exercises.md b/examples/css-exercises.md index 7a361965d..dfc0bd6fe 100644 --- a/examples/css-exercises.md +++ b/examples/css-exercises.md @@ -12,6 +12,8 @@ In this chat transcript, the user asks about the open source [CSS Exercises repo Based on the file list provided, it seems like this repository contains a collection of CSS and HTML exercises, solutions, and examples. The exercises cover various topics such as Flexbox, Grid, Animation, Foundations, and Margin & Padding. The repository also includes images and other assets used in the exercises. +
+
#### /add animation/03-dropdown-menu/README.md > Added animation/03-dropdown-menu/README.md to the chat @@ -21,6 +23,8 @@ Yes, I see the content of the `animation/03-dropdown-menu/README.md` file. It de Is there any specific change or improvement you would like me to make to this file or any other file in the repository? +
+
#### can you make the changes suggested in the readme? To make the changes suggested in the `animation/03-dropdown-menu/README.md`, I need to see the contents of the following files: @@ -70,6 +74,8 @@ These changes add the bounce animation to the dropdown menu by creating a new ke > Applied edit to animation/03-dropdown-menu/style.css > Commit bb3a813 aider: Added bounce animation to dropdown menu. +
+
#### that worked! I'm glad the changes worked for you! If you have any more requests or need assistance with other parts of the code, feel free to ask. I'm here to help! diff --git a/examples/hello-world-flask.md b/examples/hello-world-flask.md index af52a7a2a..29112952f 100644 --- a/examples/hello-world-flask.md +++ b/examples/hello-world-flask.md @@ -36,6 +36,8 @@ Make sure to install Flask in your project environment using `pip install flask` > Applied edit to app.py > Commit 414c394 aider: Added a `/hello` endpoint that returns "Hello, World!" to the Flask app. +
+
#### add an endpoint like /add/3/5 which returns the sum of the 2 numbers To add an endpoint like `/add/3/5` that returns the sum of the two numbers, you can create a new route with two URL parameters. Update the `app.py` file as follows: @@ -62,6 +64,8 @@ Now, when you visit `/add/3/5`, it will return the sum of the two numbers, which > Applied edit to app.py > Commit 0872c51 aider: Added an endpoint `/add//` that returns the sum of the two numbers. +
+
#### add /fibonacci/X To add a `/fibonacci/X` endpoint that returns the Xth number in the Fibonacci sequence, you can create a new route with a URL parameter for X and a function to calculate the Fibonacci number. Update the `app.py` file as follows: @@ -101,6 +105,8 @@ Now, when you visit `/fibonacci/X`, it will return the Xth number in the Fibonac > Applied edit to app.py > Commit 163fba8 aider: Added a `/fibonacci/X` endpoint that returns the Xth number in the Fibonacci sequence. +
+
#### remove the hello endpoint To remove the `/hello` endpoint, you can simply delete the corresponding route and function in the `app.py` file. Here's the update: diff --git a/examples/no-color.md b/examples/no-color.md index 8cc3042ce..8be3b8fc6 100644 --- a/examples/no-color.md +++ b/examples/no-color.md @@ -107,6 +107,8 @@ These changes will make the software check for the `NO_COLOR` environment variab > Applied edit to aider/io.py > Commit 3218242 aider: Added check for NO_COLOR environment variable to disable ANSI color. +
+
#### add a test To add a test for the `NO_COLOR` environment variable, we will create a new test function in the `tests/test_io.py` file. Since this file does not exist, we will create it first. @@ -138,6 +140,8 @@ This test function checks if the `pretty` attribute of the `InputOutput` class i > Applied edit to tests/test_io.py > Commit 52a9df5 aider: Added a test function to check if the `pretty` attribute of the `InputOutput` class is set to `False` when the `NO_COLOR` environment variable is set. +
+
#### /run pytest > ============================= test session starts ============================== diff --git a/examples/pong.md b/examples/pong.md index d5268563f..1da09dc57 100644 --- a/examples/pong.md +++ b/examples/pong.md @@ -68,6 +68,8 @@ This is the basic structure of the game. Now, let's add the Paddle and Ball clas > Applied edit to pong_game.py > Commit 14799cb aider: Added basic Pygame initialization and game loop structure to `pong_game.py`. +
+
#### go ahead Now, let's add the Paddle and Ball classes, handle user input, update the ball position, draw the objects, and handle scoring. I'll provide the code changes in an *EDIT BLOCK*. @@ -218,6 +220,8 @@ Now, you should have a simple Pong game using Pygame. You can further improve th > Applied edit to pong_game.py > Commit b20bcee aider: Added Paddle and Ball classes, handled user input, updated ball position, drew objects, and handled scoring. +
+
#### nice, it works. but the ball moves way too fast. I'm glad it works! Let's slow down the ball by reducing its speed. We can do this by changing the values of `self.dx` and `self.dy` in the `Ball` class. @@ -238,6 +242,8 @@ This change will reduce the ball's speed by half. You can adjust the values furt > Applied edit to pong_game.py > Commit f4ceed4 aider: Reduced ball speed by half. +
+
#### can you make the paddles a bit bigger? and make one paddle red and one blue? Sure, let's make the paddles bigger and change their colors. We can do this by modifying the `Paddle` class in the `pong_game.py` file. @@ -277,6 +283,8 @@ With these changes, the paddles will be larger, and the left paddle will be red > Applied edit to pong_game.py > Commit 2c645c9 aider: Changed paddle size and color, and set left paddle to red. +
+
#### each time the ball bounces off the paddle, can you make it go 10% faster? Sure, let's increase the ball's speed by 10% each time it bounces off a paddle. We can do this by modifying the `collide_with` method in the `Ball` class. From 05375fe279e73e5cd36b3d471be579919ccb355a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 12:55:09 -0700 Subject: [PATCH 009/102] link directly to examples --- README.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/README.md b/README.md index b9acf675d..c57b02ff5 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ It also supports [connecting to almost any LLM](https://aider.chat/docs/llms.htm

- [Getting started](#getting-started) -- [Example chat transcripts](#example-chat-transcripts) - [Features](#features) - [Usage](#usage) - [Tutorial videos](https://aider.chat/docs/install.html#tutorial-videos) @@ -33,6 +32,7 @@ It also supports [connecting to almost any LLM](https://aider.chat/docs/llms.htm - [Connecting to LLMs](https://aider.chat/docs/llms.html) - [LLM Leaderboards](https://aider.chat/docs/leaderboards/) - [Voice-to-code](https://aider.chat/docs/voice.html) +- [Example chat transcripts](https://aider.chat/examples/) - [FAQ](https://aider.chat/docs/faq.html) - [Discord](https://discord.gg/Tv2uQnR88V) - [Blog](https://aider.chat/blog/) @@ -57,20 +57,6 @@ $ export ANTHROPIC_API_KEY=your-key-goes-here $ aider --opus ``` -## Example chat transcripts - -Here are some example transcripts that show how you can chat with `aider` to write and edit code with GPT-4. - -* [**Hello World Flask App**](https://aider.chat/examples/hello-world-flask.html): Start from scratch and have aider create a simple Flask app with various endpoints, such as adding two numbers and calculating the Fibonacci sequence. - -* [**Javascript Game Modification**](https://aider.chat/examples/2048-game.html): Dive into an existing open-source repo, and get aider's help to understand it and make modifications. - -* [**Complex Multi-file Change with Debugging**](https://aider.chat/examples/complex-change.html): Aider makes a complex code change that is coordinated across multiple source files, and resolves bugs by reviewing error output and doc snippets. - -* [**Create a Black Box Test Case**](https://aider.chat/examples/add-test.html): Aider creates a "black box" test case without access to the source of the method being tested, using only a -[high level map of the repository based on tree-sitter](https://aider.chat/docs/repomap.html). - -You can find more chat transcripts on the [examples page](https://aider.chat/examples/). ## Features From 2eced6ce12fb8cae433731020f882d7197c03aae Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 13:05:29 -0700 Subject: [PATCH 010/102] copy --- README.md | 50 +++++++++++++++++++------------------------------ docs/install.md | 13 +++++-------- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index c57b02ff5..9e30d9921 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,8 @@ and [git commit the changes](https://aider.chat/docs/faq.html#how-does-aider-use with sensible commit messages. You can start a new project or work with an existing git repo. Aider is unique in that it lets you ask for changes to [pre-existing, larger codebases](https://aider.chat/docs/repomap.html). -Aider works well with GPT 3.5, GPT-4o, GPT-4 Turbo with Vision, -and Claude 3 Opus. -It also supports [connecting to almost any LLM](https://aider.chat/docs/llms.html). +Aider works well with GPT-4o, Claude 3 Opus, GPT-3.5 +and supports [connecting to almost any LLM](https://aider.chat/docs/llms.html).

aider screencast @@ -62,44 +61,26 @@ $ aider --opus * Chat with aider about your code by launching `aider` from the command line with set of source files to discuss and edit together. Aider lets the LLM see and edit the content of those files. * Aider can write and edit code in most popular languages: python, javascript, typescript, php, html, css, etc. -* Aider works well with GPT 3.5, GPT-4o, GPT-4 Turbo with Vision, -and Claude 3 Opus. It also supports [connecting to almost any LLM](https://aider.chat/docs/llms.html). +* Aider works well with GPT-4o, Claude 3 Opus, GPT-3.5 and supports [connecting to almost any LLM](https://aider.chat/docs/llms.html). * Request new features, changes, improvements, or bug fixes to your code. Ask for new test cases, updated documentation or code refactors. * Aider will apply the edits suggested by the LLM directly to your source files. * Aider will [automatically commit each changeset to your local git repo](https://aider.chat/docs/faq.html#how-does-aider-use-git) with a descriptive commit message. These frequent, automatic commits provide a safety net. It's easy to undo changes or use standard git workflows to manage longer sequences of changes. * You can use aider with multiple source files at once, so aider can make coordinated code changes across all of them in a single changeset/commit. * Aider can [give the LLM a map of your entire git repo](https://aider.chat/docs/repomap.html), which helps it understand and modify large codebases. * You can also edit files by hand using your editor while chatting with aider. Aider will notice these out-of-band edits and keep up to date with the latest versions of your files. This lets you bounce back and forth between the aider chat and your editor, to collaboratively code with an LLM. -* If you are using gpt-4 through openai directly, you can add image files to your context which will automatically switch you to the gpt-4-vision-preview model +* You can add images files to your chat if you are working with a vision capable OpenAI model (GPT-4o, GPT-4 Turbo, etc). ## Usage -Run the `aider` tool by executing the following command: +Run the `aider` tool by executing the following command with the source code files you want to edit. +These files will be "added to the chat session", so that the LLM can see their +contents and edit them according to your instructions. ``` aider ... ``` -If your pip install did not place the `aider` executable on your path, you can invoke aider like this: - -``` -python -m aider.main -``` - -Replace ``, ``, etc., with the paths to the source code files you want to work on. -These files will be "added to the chat session", so that the LLM can see their contents and edit them according to your instructions. - -You can also just launch `aider` anywhere in a git repo without naming -files on the command line. It will discover all the files in the -repo. You can then add and remove individual files in the chat -session with the `/add` and `/drop` chat commands described below. -If you or the LLM mention one of the repo's filenames in the conversation, -aider will ask if you'd like to add it to the chat. - -Think about the change you want to make and which files will need -to be edited -- add those files to the chat. -Don't add *all* the files in your repo to the chat. Be selective, and just add the files that the LLM will need to edit. If you add a bunch of unrelated files, the LLM can get overwhelmed and confused (and it costs more tokens). @@ -107,16 +88,23 @@ Aider will automatically share snippets from other, related files with the LLM so it can [understand the rest of your code base](https://aider.chat/docs/repomap.html). -Aider also has many -additional command-line options, environment variables or configuration file -to set many options. See `aider --help` for details. +You can also just launch `aider` anywhere in a git repo without naming +files on the command line. It will discover all the files in the +repo. You can then add and remove individual files in the chat +session with the `/add` and `/drop` chat commands described below. +If you or the LLM mention any of the repo's filenames in the conversation, +aider will ask if you'd like to add them to the chat. + +Aider also has many other options which can be set with +command line switches, environment variables or via a configuration file. +See `aider --help` for details. ## In-chat commands Aider supports commands from within the chat, which all start with `/`. Here are some of the most useful in-chat commands: -* `/add `: Add matching files to the chat session. +* `/add `: Add matching files to the chat session, including image files. * `/drop `: Remove matching files from the chat session. * `/undo`: Undo the last git commit if it was done by aider. * `/diff`: Display the diff of the last aider commit. @@ -130,7 +118,7 @@ See the [full command docs](https://aider.chat/docs/commands.html) for more info ## Tips * Think about which files need to be edited to make your change and add them to the chat. -Aider has some ability to help the LLM figure out which files to edit all by itself, but the most effective approach is to explicitly add the needed files to the chat yourself. +Aider can help the LLM figure out which files to edit all by itself, but the most efficient approach is to add the needed files to the chat yourself. * Large changes are best performed as a sequence of thoughtful bite sized steps, where you plan out the approach and overall design. Walk the LLM through changes like you might with a junior dev. Ask for a refactor to prepare, then ask for the actual change. Spend the time to ask for code quality/structure improvements. * Use Control-C to safely interrupt the LLM if it isn't providing a useful response. The partial response remains in the conversation, so you can refer to it when you reply to the LLM with more information or direction. * Use the `/run` command to run tests, linters, etc and show the output to the LLM so it can fix any issues. diff --git a/docs/install.md b/docs/install.md index 3c38edbc9..73ece46c9 100644 --- a/docs/install.md +++ b/docs/install.md @@ -29,8 +29,8 @@ To work with Anthropic's models like Claude 3 Opus you need a paid # Install aider py -m pip install aider-chat -# To work with GPT-4 Turbo: -$ aider --openai-api-key sk-xxx... --4turbo +# To work with GPT-4o: +$ aider --openai-api-key sk-xxx... # To work with Claude 3 Opus: $ aider --anthropic-api-key sk-xxx... --opus @@ -42,8 +42,8 @@ $ aider --anthropic-api-key sk-xxx... --opus # Install aider python -m pip install aider-chat -# To work with GPT-4 Turbo: -$ aider --openai-api-key sk-xxx... --4turbo +# To work with GPT-4o: +$ aider --openai-api-key sk-xxx... # To work with Claude 3 Opus: $ aider --anthropic-api-key sk-xxx... --opus @@ -51,10 +51,7 @@ $ aider --anthropic-api-key sk-xxx... --opus ## Working with other LLMs -Aider works well with GPT 3.5, GPT-4, GPT-4 Turbo with Vision, -and Claude 3 Opus. -It also has support for [connecting to almost any LLM](https://aider.chat/docs/llms.html). - +Aider works well with GPT-4o, Claude 3 Opus, GPT-3.5 and supports [connecting to almost any LLM](https://aider.chat/docs/llms.html). ## Tutorial videos From 771bea1fa47de03bba063c93d91eecbe398a237f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 13:12:26 -0700 Subject: [PATCH 011/102] copy --- README.md | 5 ++++- examples/2048-game.md | 4 ++-- examples/README.md | 22 +++++++++++----------- examples/add-test.md | 12 ++++++------ examples/asciinema.md | 2 +- examples/census.md | 2 +- examples/complex-change.md | 2 +- examples/css-exercises.md | 2 +- examples/hello-world-flask.md | 2 +- examples/no-color.md | 6 +++--- examples/pong.md | 2 +- examples/semantic-search-replace.md | 2 +- examples/update-docs.md | 2 +- 13 files changed, 34 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 9e30d9921..2cb94de0e 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,11 @@ Aider can help the LLM figure out which files to edit all by itself, but the mos * If your code is throwing an error, share the error output with the LLM using `/run` or by pasting it into the chat. Let the LLM figure out and fix the bug. * LLMs know about a lot of standard tools and libraries, but may get some of the fine details wrong about APIs and function arguments. You can paste doc snippets into the chat to resolve these issues. * The LLM can only see the content of the files you specifically "add to the chat". Aider also sends a [map of your entire git repo](https://aider.chat/docs/repomap.html). So the LLM may ask to see additional files if it feels that's needed for your requests. -* I also shared some general [LLM coding tips on Hacker News](https://news.ycombinator.com/item?id=36211879). +## Example chat transcripts + +[The example transcripts page](/examples/README.md) shows how you can chat with `aider` to write +and edit code. ## Installation diff --git a/examples/2048-game.md b/examples/2048-game.md index 4259caa63..1e1313530 100644 --- a/examples/2048-game.md +++ b/examples/2048-game.md @@ -1,9 +1,9 @@ -# Modify an open source 2048 game with aider and gpt-4 +# Modify an open source 2048 game with aider In this chat transcript, the user explores and modifies an open source [javascript 2048 game repo](https://github.com/gabrielecirulli/2048). -Notice that GPT-4 asks to see a particular source file, and aider automatically includes it in the chat session after getting permission from the user. +Notice that the LLM asks to see a particular source file, and aider automatically includes it in the chat session after getting permission from the user.

diff --git a/examples/README.md b/examples/README.md index e66ed963c..8224c0771 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,19 +1,19 @@ -# What's it like to code with GPT-4 and aider? +# What's it like to code with aider? -Below are some chat transcripts showing what it's like to code with the help of GPT-4 using the `aider` command-line chat tool. +Below are some chat transcripts showing what it's like to code with aider. In the chats, you'll see a varity of coding tasks like generating new code, editing existing code, debugging, exploring unfamiliar code, etc. -* [**Hello World Flask App**](https://aider.chat/examples/hello-world-flask.html): Start from scratch and have GPT create a simple Flask app with various endpoints, such as adding two numbers and calculating the Fibonacci sequence. +* [**Hello World Flask App**](https://aider.chat/examples/hello-world-flask.html): Start from scratch and have aider create a simple Flask app with various endpoints, such as adding two numbers and calculating the Fibonacci sequence. -* [**Javascript Game Modification**](https://aider.chat/examples/2048-game.html): Dive into an existing open-source repo, and get GPT's help to understand it and make modifications. +* [**Javascript Game Modification**](https://aider.chat/examples/2048-game.html): Dive into an existing open-source repo, and get aider's help to understand it and make modifications. -* [**Complex Multi-file Change with Debugging**](https://aider.chat/examples/complex-change.html): GPT makes a complex code change that is coordinated across multiple source files, and resolves bugs by reviewing error output and doc snippets. +* [**Complex Multi-file Change with Debugging**](https://aider.chat/examples/complex-change.html): Aider makes a complex code change that is coordinated across multiple source files, and resolves bugs by reviewing error output and doc snippets. -* [**Create a Black Box Test Case**](https://aider.chat/examples/add-test.html): GPT creates a "black box" test case without access to the source of the method being tested, using only a [high level map of the repository based on ctags](https://aider.chat/docs/ctags.html). +* [**Create a Black Box Test Case**](https://aider.chat/examples/add-test.html): Aider creates a "black box" test case without access to the source of the method being tested, using only a [high level map of the repository based on ctags](https://aider.chat/docs/ctags.html). -* [**Honor the NO_COLOR env var**](https://aider.chat/examples/no-color.html): The user pastes the NO_COLOR spec from no-color.org into the chat, and GPT-4 modifies the application to conform. +* [**Honor the NO_COLOR env var**](https://aider.chat/examples/no-color.html): The user pastes the NO_COLOR spec from no-color.org into the chat, and aider modifies the application to conform. -* [**Download, analyze and plot US Census data**](https://aider.chat/examples/census.html): GPT-4 downloads census data, suggests some hypotheses to test, tests one and then summarizes and plots a graph of the results. +* [**Download, analyze and plot US Census data**](https://aider.chat/examples/census.html): Aider downloads census data, suggests some hypotheses to test, tests one and then summarizes and plots a graph of the results. * [**Semantic Search & Replace**](semantic-search-replace.md): Updating a collection of function calls, which requires dealing with various formatting and semantic differences in the various function call sites. @@ -29,9 +29,9 @@ In the chats, you'll see a varity of coding tasks like generating new code, edit To better understand the chat transcripts, it's worth knowing that: - - Each time GPT-4 suggests a code change, `aider` automatically applies it to the source files. + - Each time the LLM suggests a code change, `aider` automatically applies it to the source files. - After applying the edits, `aider` commits them to git with a descriptive commit message. - - GPT-4 can only see and edit files which have been "added to the chat session". The user adds files either via the command line or the in-chat `/add` command. If GPT-4 asks to see specific files, `aider` asks the user for permission to add them to the chat. The transcripts contain notifications from `aider` whenever a file is added or dropped from the session. + - The LLM can only see and edit files which have been "added to the chat session". The user adds files either via the command line or the in-chat `/add` command. If the LLM asks to see specific files, `aider` asks the user for permission to add them to the chat. The transcripts contain notifications from `aider` whenever a file is added or dropped from the session. ## Transcript formatting @@ -41,7 +41,7 @@ To better understand the chat transcripts, it's worth knowing that: #### These are chat messages written by the user. -Chat responses from GPT-4 are in a blue font like this, and often include colorized "edit blocks" that specify edits to the code. +Chat responses from the LLM are in a blue font like this, and often include colorized "edit blocks" that specify edits to the code. Here's a sample edit block that switches from printing "hello" to "goodbye": ```python diff --git a/examples/add-test.md b/examples/add-test.md index 3151cd4b5..c3b79cbaf 100644 --- a/examples/add-test.md +++ b/examples/add-test.md @@ -1,17 +1,17 @@ # Create a "black box" test case -This transcript shows GPT-4 creating a black box test case, +This transcript shows aider creating a black box test case, **without being given access to the source code of the function being tested or any of the other code in the repo.** -Instead, GPT is operating entirely off a +Instead, aider is operating entirely off a [high level map of the repository based on ctags](https://aider.chat/docs/ctags.html), which provides details on all the symbols and function signatures in the repo. -Using only the meta-data in the map, GPT is able to figure out how to call the method to be tested, as well as how to instantiate multiple class objects that are required to prepare for the test. +Using only the meta-data in the map, the LLM is able to figure out how to call the method to be tested, as well as how to instantiate multiple class objects that are required to prepare for the test. -In detail, GPT is able to use the repo map to: +In detail, aider is able to use the repo map to: - Find the function signature of the `cmd_add()` function which the user wants a test case for. - Determine that it is a method of the `Command` class, so the test case will need to instantiate an instance to conduct the test. @@ -19,12 +19,12 @@ In detail, GPT is able to use the repo map to: - Figure out the arguments required to instantiate the `InputOutput` instance. - Decide that the `Coder` class looks complex enough to use a `MagickMock`. -The `cmd_add()` method is defined without type annotations, so GPT makes a +The `cmd_add()` method is defined without type annotations, so aider makes a reasonable but incorrect guess that it accepts a `list` of files. The user invokes the `/run` command to run the new unit test, which throws an error based on this misunderstanding. -GPT reviews the error message and fixes the call to `cmd_add()` to +The LLM reviews the error message and fixes the call to `cmd_add()` to correctly pass a `str` with a space-separated list of filenames instead. The test then passes on the next `/run`. diff --git a/examples/asciinema.md b/examples/asciinema.md index afb664f8c..a6940a6a6 100644 --- a/examples/asciinema.md +++ b/examples/asciinema.md @@ -1,5 +1,5 @@ -# Editing an asciinema cast file with aider and gpt-4 +# Editing an asciinema cast file with aider The user needs to make some tedious edits to the escape sequences in an `asciinema` screencast file. diff --git a/examples/census.md b/examples/census.md index ddefc1782..d734a6f56 100644 --- a/examples/census.md +++ b/examples/census.md @@ -4,7 +4,7 @@ Inspired by [Ethan Mollick's recent blog article about ChatGPT plugins](https://www.oneusefulthing.org/p/it-is-starting-to-get-strange), I used `aider` and GPT-4 to do data analysis of some US Census data. -I asked GPT-4 how to download census data and to suggest some +I asked aider how to download census data and to suggest some hypotheses to test. I asked it to write the code to get the data, test one of the hypotheses and provide a summary and plot of the results. diff --git a/examples/complex-change.md b/examples/complex-change.md index 94cf41ebb..d79079a3e 100644 --- a/examples/complex-change.md +++ b/examples/complex-change.md @@ -6,7 +6,7 @@ This is a chat transcript of a fairly complex code change. The user is trying to Some things to notice: - The initial changes don't work, so the user provides error messages and snippets from the `prompt_toolkit` docs to collaboratively debug and solve the problem. - The user edits a file outside the chat using their editor. `aider` notices these out-of-band changes and offers to commit them. - - At one point the user hits `^C` to interrupt the GPT-4 response, since it's not a useful response. The user gives clarifications and gets a better result on the next reply. + - At one point the user hits `^C` to interrupt the LLM response, since it's not a useful response. The user gives clarifications and gets a better result on the next reply.
diff --git a/examples/css-exercises.md b/examples/css-exercises.md index dfc0bd6fe..6197d6d2b 100644 --- a/examples/css-exercises.md +++ b/examples/css-exercises.md @@ -1,5 +1,5 @@ -# Complete a css exercise with aider and GPT-4 +# Complete a css exercise with aider In this chat transcript, the user asks about the open source [CSS Exercises repo](https://github.com/TheOdinProject/css-exercises) and completes an animation exercise. diff --git a/examples/hello-world-flask.md b/examples/hello-world-flask.md index 29112952f..95d72bdf3 100644 --- a/examples/hello-world-flask.md +++ b/examples/hello-world-flask.md @@ -1,5 +1,5 @@ -# Create a simple flask app with aider & gpt-4 +# Create a simple flask app with aider In this chat transcript, the user asks to make a simple flask app with various endpoints. diff --git a/examples/no-color.md b/examples/no-color.md index 8be3b8fc6..c018ebc55 100644 --- a/examples/no-color.md +++ b/examples/no-color.md @@ -2,13 +2,13 @@ # Honor the NO_COLOR environment variable The user pastes the specification of the NO_COLOR environment variable protocol -from [no-color.org](https://no-color.org) into the chat with GPT-4. +from [no-color.org](https://no-color.org) into the aider chat. -GPT figures out which files need to be edited, and asks permission to make changes. +Aider figures out which files need to be edited, and asks permission to make changes. It then disables all the color and pretty-printing functions if NO_COLOR is set, so the tool conforms to the specification. -The user then asks for a test case, which GPT adds as well. +The user then asks for a test case, which aider adds as well.
diff --git a/examples/pong.md b/examples/pong.md index 1da09dc57..6b89ed23e 100644 --- a/examples/pong.md +++ b/examples/pong.md @@ -1,5 +1,5 @@ -# Build pong with aider, gpt-4 and pygame. +# Build pong with aider and pygame. In this chat transcript, the user asks to make a pong game using pygame. After setting up a basic game, the user asks for some customization of the colors and game mechanics. diff --git a/examples/semantic-search-replace.md b/examples/semantic-search-replace.md index 212a7dff3..9b8e11bf2 100644 --- a/examples/semantic-search-replace.md +++ b/examples/semantic-search-replace.md @@ -1,4 +1,4 @@ -# Semantic search & replace code with aider and gpt-4 +# Semantic search & replace code with aider In this chat transcript, the user asks to modify a series of function calls. diff --git a/examples/update-docs.md b/examples/update-docs.md index ca956b5f8..94164845e 100644 --- a/examples/update-docs.md +++ b/examples/update-docs.md @@ -1,4 +1,4 @@ -# Automatically update docs with aider and gpt-4 +# Automatically update docs with aider In this chat transcript, the user asks to automatically update the Usage docs based on the latest version of the main() function in the code. From 30179c185d79ff1519f026d2b10f4661c4f34fea Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 13:14:28 -0700 Subject: [PATCH 012/102] copy --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2cb94de0e..a9e0e244e 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ $ aider --opus ## Features -* Chat with aider about your code by launching `aider` from the command line with set of source files to discuss and edit together. Aider lets the LLM see and edit the content of those files. +* Chat with aider about your code by running `aider ...` from the command line with set of source files to discuss and edit together. Aider lets the LLM see and edit the content of those files. * Aider can write and edit code in most popular languages: python, javascript, typescript, php, html, css, etc. * Aider works well with GPT-4o, Claude 3 Opus, GPT-3.5 and supports [connecting to almost any LLM](https://aider.chat/docs/llms.html). * Request new features, changes, improvements, or bug fixes to your code. Ask for new test cases, updated documentation or code refactors. @@ -73,7 +73,7 @@ $ aider --opus ## Usage -Run the `aider` tool by executing the following command with the source code files you want to edit. +Run `aider` with the source code files you want to edit. These files will be "added to the chat session", so that the LLM can see their contents and edit them according to your instructions. @@ -88,7 +88,7 @@ Aider will automatically share snippets from other, related files with the LLM so it can [understand the rest of your code base](https://aider.chat/docs/repomap.html). -You can also just launch `aider` anywhere in a git repo without naming +You can also just launch aider anywhere in a git repo without naming files on the command line. It will discover all the files in the repo. You can then add and remove individual files in the chat session with the `/add` and `/drop` chat commands described below. @@ -129,7 +129,7 @@ Aider can help the LLM figure out which files to edit all by itself, but the mos ## Example chat transcripts -[The example transcripts page](/examples/README.md) shows how you can chat with `aider` to write +[The example transcripts page](/examples/README.md) shows how you can chat with aider to write and edit code. ## Installation From 4e0f8765d9bce5b949ed940c1385028021c0fbce Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 13:15:07 -0700 Subject: [PATCH 013/102] copy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a9e0e244e..32e5ec7f0 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Aider can help the LLM figure out which files to edit all by itself, but the mos ## Example chat transcripts -[The example transcripts page](/examples/README.md) shows how you can chat with aider to write +[The example transcripts page](https://aider.chat/examples/) shows how you can chat with aider to write and edit code. ## Installation From 3cc0a3c2631548a4df43337b8f97d37f44005886 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 13:47:52 -0700 Subject: [PATCH 014/102] aider: Implemented function to parse file with tree-sitter and print syntax error line numbers. # Aider chat conversation: USER: write me a function that takes a file path and uses tree-sitter to parse the AST and print out the line number of every syntax/parse error in the file ASSISTANT: To implement the function that takes a file path, uses tree-sitter to parse the AST, and prints out the line number of every syntax/parse error in the file, we need to: 1. Install the `tree-sitter` library. 2. Write the function in `aider/linter.py`. Here is the *SEARCH/REPLACE* block for `aider/linter.py`: aider/linter.py ```python <<<<<<< SEARCH ======= import os import tree_sitter def parse_file_for_errors(file_path): """ Parses the given file using tree-sitter and prints out the line number of every syntax/parse error. """ # Load the language Language = tree_sitter.Language PARSER = tree_sitter.Parser() LANGUAGE = Language(os.path.join('build', 'my-languages.so'), 'python') PARSER.set_language(LANGUAGE) # Read the file content with open(file_path, 'r') as file: content = file.read() # Parse the content tree = PARSER.parse(bytes(content, "utf8")) # Traverse the tree to find errors def traverse_tree(node): if node.type == 'ERROR': print(f"Syntax error at line: {node.start_point[0] + 1}") for child in node.children: traverse_tree(child) traverse_tree(tree.root_node) >>>>>>> REPLACE ``` This code will: 1. Load the `tree-sitter` library and set up the parser for Python. 2. Read the content of the file specified by `file_path`. 3. Parse the content and traverse the AST to find and print the line numbers of any syntax errors. You can now run this function by calling `parse_file_for_errors('path/to/your/file.py')`. --- aider/linter.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 aider/linter.py diff --git a/aider/linter.py b/aider/linter.py new file mode 100644 index 000000000..c02beb030 --- /dev/null +++ b/aider/linter.py @@ -0,0 +1,28 @@ +import os +import tree_sitter + +def parse_file_for_errors(file_path): + """ + Parses the given file using tree-sitter and prints out the line number of every syntax/parse error. + """ + # Load the language + Language = tree_sitter.Language + PARSER = tree_sitter.Parser() + LANGUAGE = Language(os.path.join('build', 'my-languages.so'), 'python') + PARSER.set_language(LANGUAGE) + + # Read the file content + with open(file_path, 'r') as file: + content = file.read() + + # Parse the content + tree = PARSER.parse(bytes(content, "utf8")) + + # Traverse the tree to find errors + def traverse_tree(node): + if node.type == 'ERROR': + print(f"Syntax error at line: {node.start_point[0] + 1}") + for child in node.children: + traverse_tree(child) + + traverse_tree(tree.root_node) From c26833419dc5fa903100eabf21829f70892a608f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:00:41 -0700 Subject: [PATCH 015/102] Fixed tree-sitter FutureWarning and refactored parsing logic in linter. --- aider/linter.py | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index c02beb030..986a2a9a8 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,22 +1,24 @@ import os import tree_sitter +import sys +import warnings + +# tree_sitter is throwing a FutureWarning +warnings.simplefilter("ignore", category=FutureWarning) +from tree_sitter_languages import get_language, get_parser # noqa: E402 def parse_file_for_errors(file_path): - """ - Parses the given file using tree-sitter and prints out the line number of every syntax/parse error. - """ - # Load the language - Language = tree_sitter.Language - PARSER = tree_sitter.Parser() - LANGUAGE = Language(os.path.join('build', 'my-languages.so'), 'python') - PARSER.set_language(LANGUAGE) + + lang = "python" + + language = get_language(lang) + parser = get_parser(lang) # Read the file content with open(file_path, 'r') as file: content = file.read() - # Parse the content - tree = PARSER.parse(bytes(content, "utf8")) + tree = parser.parse(bytes(content, "utf8")) # Traverse the tree to find errors def traverse_tree(node): @@ -26,3 +28,18 @@ def parse_file_for_errors(file_path): traverse_tree(child) traverse_tree(tree.root_node) + +def main(): + """ + Main function to parse files provided as command line arguments. + """ + if len(sys.argv) < 2: + print("Usage: python linter.py ...") + sys.exit(1) + + for file_path in sys.argv[1:]: + print(f"Checking file: {file_path}") + parse_file_for_errors(file_path) + +if __name__ == "__main__": + main() From 0493680d571f41a2e478dd52437d8cbf24aab601 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:00:42 -0700 Subject: [PATCH 016/102] aider: Update the linter to check for both 'ERROR' and 'MISSING' nodes. # Aider chat conversation: USER: is node.type 'ERROR" a standard across all tree sitter languages? ASSISTANT: The `node.type == 'ERROR'` check is specific to the Tree-sitter library and is used to identify syntax errors in the parsed code. This is a standard feature across all languages supported by Tree-sitter. Would you like to proceed with any changes or enhancements to the `aider/linter.py` file? If so, please specify the changes you need. USER: is MISSING ever used to indicate a parsing/syntax problem ASSISTANT: Yes, in Tree-sitter, `MISSING` is also used to indicate a parsing or syntax problem. Specifically, it indicates that a required element is missing in the syntax tree. Would you like to update the `aider/linter.py` file to also check for `MISSING` nodes? If so, I can provide the necessary changes. USER: yes ASSISTANT: To update the `aider/linter.py` file to also check for `MISSING` nodes, we need to modify the `traverse_tree` function. Here are the necessary changes: aider/linter.py ```python <<<<<<< SEARCH if node.type == 'ERROR': print(f"Syntax error at line: {node.start_point[0] + 1}") ======= if node.type == 'ERROR' or node.is_missing: error_type = 'Syntax error' if node.type == 'ERROR' else 'Missing element' print(f"{error_type} at line: {node.start_point[0] + 1}") >>>>>>> REPLACE ``` This change will ensure that the linter checks for both `ERROR` and `MISSING` nodes and reports them accordingly. --- aider/linter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 986a2a9a8..5506f30df 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -22,8 +22,9 @@ def parse_file_for_errors(file_path): # Traverse the tree to find errors def traverse_tree(node): - if node.type == 'ERROR': - print(f"Syntax error at line: {node.start_point[0] + 1}") + if node.type == 'ERROR' or node.is_missing: + error_type = 'Syntax error' if node.type == 'ERROR' else 'Missing element' + print(f"{error_type} at line: {node.start_point[0] + 1}") for child in node.children: traverse_tree(child) From 86fdeb0597e5cacaf0336078fcab1445b587897e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:00:59 -0700 Subject: [PATCH 017/102] aider: Update `traverse_tree` to print error type, line number, and 3 lines of context before/after the error. # Aider chat conversation: USER: print out the line and the N=3 lines before/after it ASSISTANT: To print out the line with the error and the 3 lines before and after it, we need to modify the `traverse_tree` function in `aider/linter.py`. Here are the necessary changes: aider/linter.py ```python <<<<<<< SEARCH # Traverse the tree to find errors ======= # Traverse the tree to find errors and print context >>>>>>> REPLACE ``` aider/linter.py ```python <<<<<<< SEARCH print(f"{error_type} at line: {node.start_point[0] + 1}") ======= start_line = max(0, node.start_point[0] - 3) end_line = node.end_point[0] + 3 error_line = node.start_point[0] + 1 with open(file_path, 'r') as file: lines = file.readlines() print(f"{error_type} at line: {error_line}") print("Context:") for i in range(start_line, min(end_line, len(lines))): line_number = i + 1 prefix = ">> " if line_number == error_line else " " print(f"{prefix}{line_number}: {lines[i].rstrip()}") print("\n") >>>>>>> REPLACE ``` This will ensure that the linter prints the error type, the line number, and the context of 3 lines before and after the error. --- aider/linter.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 5506f30df..8fc19a79b 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -20,11 +20,24 @@ def parse_file_for_errors(file_path): tree = parser.parse(bytes(content, "utf8")) - # Traverse the tree to find errors + # Traverse the tree to find errors and print context def traverse_tree(node): if node.type == 'ERROR' or node.is_missing: error_type = 'Syntax error' if node.type == 'ERROR' else 'Missing element' - print(f"{error_type} at line: {node.start_point[0] + 1}") + start_line = max(0, node.start_point[0] - 3) + end_line = node.end_point[0] + 3 + error_line = node.start_point[0] + 1 + + with open(file_path, 'r') as file: + lines = file.readlines() + + print(f"{error_type} at line: {error_line}") + print("Context:") + for i in range(start_line, min(end_line, len(lines))): + line_number = i + 1 + prefix = ">> " if line_number == error_line else " " + print(f"{prefix}{line_number}: {lines[i].rstrip()}") + print("\n") for child in node.children: traverse_tree(child) From cb8a487c897caa4ca181de1a6e56eff3877e39e9 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:22:03 -0700 Subject: [PATCH 018/102] Display syntax errors with tree context --- aider/linter.py | 71 ++++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 8fc19a79b..e463a77a6 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -2,46 +2,61 @@ import os import tree_sitter import sys import warnings +from pathlib import Path + +from aider.dump import dump + +from grep_ast import TreeContext, filename_to_lang # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) from tree_sitter_languages import get_language, get_parser # noqa: E402 -def parse_file_for_errors(file_path): +def basic_lint(fname, code): - lang = "python" + lang = filename_to_lang(fname) language = get_language(lang) parser = get_parser(lang) - # Read the file content - with open(file_path, 'r') as file: - content = file.read() + tree = parser.parse(bytes(code, "utf-8")) - tree = parser.parse(bytes(content, "utf8")) + errors = traverse_tree(tree.root_node) + if not errors: + return - # Traverse the tree to find errors and print context - def traverse_tree(node): - if node.type == 'ERROR' or node.is_missing: - error_type = 'Syntax error' if node.type == 'ERROR' else 'Missing element' - start_line = max(0, node.start_point[0] - 3) - end_line = node.end_point[0] + 3 - error_line = node.start_point[0] + 1 + context = TreeContext( + fname, + code, + color=False, + line_number=False, + child_context=False, + last_line=False, + margin=0, + mark_lois=True, + loi_pad=5, + # header_max=30, + show_top_of_file_parent_scope=False, + ) + context.add_lines_of_interest(errors) + context.add_context() + output = "# Syntax Errors found on the lines marked with █\n" + output += fname + ":\n" + output += context.format() - with open(file_path, 'r') as file: - lines = file.readlines() + return output - print(f"{error_type} at line: {error_line}") - print("Context:") - for i in range(start_line, min(end_line, len(lines))): - line_number = i + 1 - prefix = ">> " if line_number == error_line else " " - print(f"{prefix}{line_number}: {lines[i].rstrip()}") - print("\n") - for child in node.children: - traverse_tree(child) +# Traverse the tree to find errors and print context +def traverse_tree(node): + errors = [] + if node.type == 'ERROR' or node.is_missing: + line_no = node.start_point[0] + errors.append(line_no) - traverse_tree(tree.root_node) + for child in node.children: + errors += traverse_tree(child) + + return errors def main(): """ @@ -52,8 +67,10 @@ def main(): sys.exit(1) for file_path in sys.argv[1:]: - print(f"Checking file: {file_path}") - parse_file_for_errors(file_path) + code = Path(file_path).read_text() + errors = basic_lint(file_path, code) + if errors: + print(errors) if __name__ == "__main__": main() From 494aa0140b27d226a9ad655ac8f9b3175bd07d91 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:27:53 -0700 Subject: [PATCH 019/102] Refactored the linter script to handle language detection and error traversal more efficiently. --- aider/linter.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index e463a77a6..0e2d89b75 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,20 +1,22 @@ import os -import tree_sitter import sys import warnings from pathlib import Path -from aider.dump import dump - +import tree_sitter from grep_ast import TreeContext, filename_to_lang +from aider.dump import dump + # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) from tree_sitter_languages import get_language, get_parser # noqa: E402 -def basic_lint(fname, code): +def basic_lint(fname, code): lang = filename_to_lang(fname) + if not lang: + return language = get_language(lang) parser = get_parser(lang) @@ -46,10 +48,11 @@ def basic_lint(fname, code): return output + # Traverse the tree to find errors and print context def traverse_tree(node): errors = [] - if node.type == 'ERROR' or node.is_missing: + if node.type == "ERROR" or node.is_missing: line_no = node.start_point[0] errors.append(line_no) @@ -58,6 +61,7 @@ def traverse_tree(node): return errors + def main(): """ Main function to parse files provided as command line arguments. @@ -72,5 +76,6 @@ def main(): if errors: print(errors) + if __name__ == "__main__": main() From a9fd02c74db5d2f7c401a41aeaebba91fe1995c6 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:27:54 -0700 Subject: [PATCH 020/102] aider: Remove unused imports and variable in `aider/linter.py` to pass pre-commit hooks. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: I ran this command: .git/hooks/pre-commit And got this output: [WARNING] Unstaged files detected. [INFO] Stashing unstaged files to /Users/gauthier/.cache/pre-commit/patch1715981257-12950. isort....................................................................Failed - hook id: isort - files were modified by this hook Fixing /Users/gauthier/Projects/aider/aider/linter.py black....................................................................Failed - hook id: black - files were modified by this hook reformatted aider/linter.py All done! ✨ 🍰 ✨ 1 file reformatted. flake8...................................................................Failed - hook id: flake8 - exit code: 1 aider/linter.py:1:1: F401 'os' imported but unused import os ^ aider/linter.py:6:1: F401 'tree_sitter' imported but unused import tree_sitter ^ aider/linter.py:9:1: F401 'aider.dump.dump' imported but unused from aider.dump import dump ^ aider/linter.py:21:5: F841 local variable 'language' is assigned to but never used language = get_language(lang) ^ [WARNING] Stashed changes conflicted with hook auto-fixes... Rolling back fixes... [INFO] Restored changes from /Users/gauthier/.cache/pre-commit/patch1715981257-12950. ASSISTANT: Please add `aider/linter.py` to the chat so I can make the necessary changes to fix the issues reported by the pre-commit hooks. USER: I added these files to the chat: aider/linter.py ASSISTANT: To fix the issues reported by the pre-commit hooks, we need to: 1. Remove unused imports: `os`, `tree_sitter`, and `dump`. 2. Remove the unused variable `language`. Here are the *SEARCH/REPLACE* blocks: aider/linter.py ```python <<<<<<< SEARCH import os import sys import warnings from pathlib import Path import tree_sitter from grep_ast import TreeContext, filename_to_lang from aider.dump import dump ======= import sys import warnings from pathlib import Path from grep_ast import TreeContext, filename_to_lang >>>>>>> REPLACE ``` aider/linter.py ```python <<<<<<< SEARCH language = get_language(lang) ======= >>>>>>> REPLACE ``` Now, the file should pass the pre-commit hooks. --- aider/linter.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 0e2d89b75..9258fe350 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,13 +1,9 @@ -import os import sys import warnings from pathlib import Path -import tree_sitter from grep_ast import TreeContext, filename_to_lang -from aider.dump import dump - # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) from tree_sitter_languages import get_language, get_parser # noqa: E402 @@ -18,7 +14,6 @@ def basic_lint(fname, code): if not lang: return - language = get_language(lang) parser = get_parser(lang) tree = parser.parse(bytes(code, "utf-8")) From 778dbcabb02592317273aaccdbb4d95c40a83309 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:30:58 -0700 Subject: [PATCH 021/102] aider: Remove the unused import `get_language` from `aider/linter.py`. # Aider chat conversation: USER: I ran this command: flake8 aider/linter.py And got this output: aider/linter.py:9:1: F401 'tree_sitter_languages.get_language' imported but unused ASSISTANT: Please add the file `aider/linter.py` to the chat so I can make the necessary changes. USER: I added these files to the chat: aider/linter.py ASSISTANT: To resolve the flake8 warning, we need to remove the unused import `get_language` from `aider/linter.py`. Here is the *SEARCH/REPLACE* block: aider/linter.py ```python <<<<<<< SEARCH from tree_sitter_languages import get_language, get_parser # noqa: E402 ======= from tree_sitter_languages import get_parser # noqa: E402 >>>>>>> REPLACE ``` --- aider/linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index 9258fe350..dddc564c1 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -6,7 +6,7 @@ from grep_ast import TreeContext, filename_to_lang # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) -from tree_sitter_languages import get_language, get_parser # noqa: E402 +from tree_sitter_languages import get_parser # noqa: E402 def basic_lint(fname, code): From e09d4d8e16d6dcdaaea391dab5c4d2864637d8b6 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:42:40 -0700 Subject: [PATCH 022/102] Upgraded deps to pickup grep-ast 0.3.0 --- requirements.txt | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index abf014eae..c33d01cf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,7 +60,7 @@ frozenlist==1.4.1 # via # aiohttp # aiosignal -fsspec==2024.3.1 +fsspec==2024.5.0 # via huggingface-hub gitdb==4.0.11 # via gitpython @@ -68,14 +68,14 @@ gitpython==3.1.43 # via # -r requirements.in # streamlit -google-ai-generativelanguage==0.6.2 +google-ai-generativelanguage==0.6.4 # via google-generativeai google-api-core[grpc]==2.19.0 # via # google-ai-generativelanguage # google-api-python-client # google-generativeai -google-api-python-client==2.128.0 +google-api-python-client==2.129.0 # via google-generativeai google-auth==2.29.0 # via @@ -86,7 +86,7 @@ google-auth==2.29.0 # google-generativeai google-auth-httplib2==0.2.0 # via google-api-python-client -google-generativeai==0.5.2 +google-generativeai==0.5.4 # via -r requirements.in googleapis-common-protos==1.63.0 # via @@ -94,7 +94,7 @@ googleapis-common-protos==1.63.0 # grpcio-status greenlet==3.0.3 # via playwright -grep-ast==0.2.4 +grep-ast==0.3.0 # via -r requirements.in grpcio==1.63.0 # via @@ -133,7 +133,7 @@ jsonschema==4.22.0 # altair jsonschema-specifications==2023.12.1 # via jsonschema -litellm==1.36.2 +litellm==1.37.14 # via -r requirements.in markdown-it-py==3.0.0 # via rich @@ -156,7 +156,7 @@ numpy==1.26.4 # pydeck # scipy # streamlit -openai==1.26.0 +openai==1.30.1 # via # -r requirements.in # litellm @@ -178,7 +178,7 @@ pillow==10.3.0 # via # -r requirements.in # streamlit -playwright==1.43.0 +playwright==1.44.0 # via -r requirements.in prompt-toolkit==3.0.43 # via -r requirements.in @@ -195,7 +195,7 @@ protobuf==4.25.3 # grpcio-status # proto-plus # streamlit -pyarrow==16.0.0 +pyarrow==16.1.0 # via streamlit pyasn1==0.6.0 # via @@ -211,7 +211,7 @@ pydantic==2.7.1 # openai pydantic-core==2.18.2 # via pydantic -pydeck==0.9.0 +pydeck==0.9.1 # via streamlit pyee==11.1.0 # via playwright @@ -235,7 +235,7 @@ referencing==0.35.1 # via # jsonschema # jsonschema-specifications -regex==2024.4.28 +regex==2024.5.15 # via tiktoken requests==2.31.0 # via @@ -275,7 +275,7 @@ streamlit==1.34.0 # via -r requirements.in tenacity==8.3.0 # via streamlit -tiktoken==0.6.0 +tiktoken==0.7.0 # via # -r requirements.in # litellm @@ -292,7 +292,7 @@ tqdm==4.66.4 # google-generativeai # huggingface-hub # openai -tree-sitter==0.21.3 +tree-sitter==0.22.2 # via tree-sitter-languages tree-sitter-languages==1.10.2 # via grep-ast @@ -315,5 +315,5 @@ wcwidth==0.2.13 # via prompt-toolkit yarl==1.9.4 # via aiohttp -zipp==3.18.1 +zipp==3.18.2 # via importlib-metadata From dd2ba6e3fc1f5be24329b52b02083952eff8a09a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 14:49:39 -0700 Subject: [PATCH 023/102] pin tree-sitter==0.21.3 --- dev-requirements.txt | 16 ++++++++-------- requirements.in | 3 +++ requirements.txt | 6 ++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 86199961b..fcbb97d26 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile dev-requirements.in +# pip-compile --output-file=dev-requirements.txt dev-requirements.in # alabaster==0.7.16 # via sphinx -babel==2.14.0 +babel==2.15.0 # via sphinx build==1.2.1 # via pip-tools @@ -48,7 +48,7 @@ imgcat==0.5.0 # via -r dev-requirements.in iniconfig==2.0.0 # via pytest -jinja2==3.1.3 +jinja2==3.1.4 # via sphinx kiwisolver==1.4.5 # via matplotlib @@ -58,7 +58,7 @@ markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 # via jinja2 -matplotlib==3.8.4 +matplotlib==3.9.0 # via -r dev-requirements.in mdurl==0.1.2 # via markdown-it-py @@ -85,7 +85,7 @@ pillow==10.3.0 # via matplotlib pip-tools==7.4.1 # via -r dev-requirements.in -platformdirs==4.2.1 +platformdirs==4.2.2 # via virtualenv pluggy==1.5.0 # via pytest @@ -93,9 +93,9 @@ pox==0.3.4 # via pathos ppft==1.7.6.8 # via pathos -pre-commit==3.7.0 +pre-commit==3.7.1 # via -r dev-requirements.in -pygments==2.17.2 +pygments==2.18.0 # via # rich # sphinx @@ -153,7 +153,7 @@ tzdata==2024.1 # via pandas urllib3==2.2.1 # via requests -virtualenv==20.26.1 +virtualenv==20.26.2 # via pre-commit wheel==0.43.0 # via pip-tools diff --git a/requirements.in b/requirements.in index bbf213d8a..778cb0cd4 100644 --- a/requirements.in +++ b/requirements.in @@ -27,3 +27,6 @@ pypandoc litellm google-generativeai streamlit + +# 0.22.2 seems to break tree-sitter-languages? +tree-sitter==0.21.3 diff --git a/requirements.txt b/requirements.txt index c33d01cf8..018e958a7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -292,8 +292,10 @@ tqdm==4.66.4 # google-generativeai # huggingface-hub # openai -tree-sitter==0.22.2 - # via tree-sitter-languages +tree-sitter==0.21.3 + # via + # -r requirements.in + # tree-sitter-languages tree-sitter-languages==1.10.2 # via grep-ast typing-extensions==4.11.0 From dc2850b53d394c6f549fee57c5980547a9c7a2cd Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 15:21:09 -0700 Subject: [PATCH 024/102] Added a Linter class for linting files and refactored linting logic in the main function. --- aider/linter.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index dddc564c1..ec6e50595 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,3 +1,4 @@ +import os import sys import warnings from pathlib import Path @@ -9,6 +10,23 @@ warnings.simplefilter("ignore", category=FutureWarning) from tree_sitter_languages import get_parser # noqa: E402 +class Linter: + def __init__(self, encoding="utf-8", root=None): + self.encoding = encoding + self.root = root + + def get_rel_fname(self, fname): + if self.root: + os.path.relpath(fname, self.root) + else: + return fname + + def lint(self, fname): + code = Path(fname).read_text() + + display_fname = self.get_rel_fname(fname) + return basic_lint(display_fname, code) + def basic_lint(fname, code): lang = filename_to_lang(fname) if not lang: @@ -65,9 +83,9 @@ def main(): print("Usage: python linter.py ...") sys.exit(1) + linter = Linter() for file_path in sys.argv[1:]: - code = Path(file_path).read_text() - errors = basic_lint(file_path, code) + errors = linter.lint(file_path) if errors: print(errors) From 4f81971a135892f3983381920497065311c17f44 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 15:37:05 -0700 Subject: [PATCH 025/102] Add support for running custom linters and improve error handling. --- aider/linter.py | 48 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index ec6e50595..e27a7d99d 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,4 +1,5 @@ import os +import subprocess import sys import warnings from pathlib import Path @@ -15,25 +16,56 @@ class Linter: self.encoding = encoding self.root = root + fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" + py_cmd = f"flake8 --select={fatal} --show-source" + + self.languages = dict(python=py_cmd) + + def set_linter(self, lang, cmd): + self.languages[lang] = cmd + def get_rel_fname(self, fname): if self.root: - os.path.relpath(fname, self.root) + return os.path.relpath(fname, self.root) else: return fname - def lint(self, fname): - code = Path(fname).read_text() + def run_cmd(self, cmd, rel_fname): + cmd += " " + rel_fname + cmd = cmd.split() + try: + _output = subprocess.check_output(cmd, cwd=self.root).decode() + return # zero exit status + except subprocess.CalledProcessError as err: + return err.output.decode() # non-zero exit status + + def lint(self, fname): + lang = filename_to_lang(fname) + if not lang: + return + + rel_fname = self.get_rel_fname(fname) + + cmd = self.languages[lang] + if cmd: + return self.run_cmd(cmd, rel_fname) + + # fall back to tree sitter / tree context linter + code = Path(fname).read_text(self.encoding) + + return basic_lint(rel_fname, code) - display_fname = self.get_rel_fname(fname) - return basic_lint(display_fname, code) def basic_lint(fname, code): + """ + Use tree-sitter to look for syntax errors, display them with tree context. + """ + lang = filename_to_lang(fname) if not lang: return parser = get_parser(lang) - tree = parser.parse(bytes(code, "utf-8")) errors = traverse_tree(tree.root_node) @@ -62,7 +94,7 @@ def basic_lint(fname, code): return output -# Traverse the tree to find errors and print context +# Traverse the tree to find errors def traverse_tree(node): errors = [] if node.type == "ERROR" or node.is_missing: @@ -83,7 +115,7 @@ def main(): print("Usage: python linter.py ...") sys.exit(1) - linter = Linter() + linter = Linter(root=os.getcwd()) for file_path in sys.argv[1:]: errors = linter.lint(file_path) if errors: From c7196b83909215728e766f039f25d4fb59cb6203 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 15:37:06 -0700 Subject: [PATCH 026/102] aider: Remove unused variable `_output` in the `run_cmd` method in `aider/linter.py`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: I ran this command: pre-commit run --files aider/linter.py And got this output: isort....................................................................Failed - hook id: isort - files were modified by this hook Fixing /Users/gauthier/Projects/aider/aider/linter.py black....................................................................Failed - hook id: black - files were modified by this hook reformatted aider/linter.py All done! ✨ 🍰 ✨ 1 file reformatted. flake8...................................................................Failed - hook id: flake8 - exit code: 1 aider/linter.py:37:13: F841 local variable '_output' is assigned to but never used _output = subprocess.check_output(cmd, cwd=self.root).decode() ^ ASSISTANT: Please add `aider/linter.py` to the chat so I can make the necessary changes to fix the flake8 issue. USER: I added these files to the chat: aider/linter.py ASSISTANT: To fix the flake8 issue, we need to remove the unused variable `_output` in the `run_cmd` method. Here is the *SEARCH/REPLACE* block: aider/linter.py ```python <<<<<<< SEARCH _output = subprocess.check_output(cmd, cwd=self.root).decode() return # zero exit status ======= subprocess.check_output(cmd, cwd=self.root).decode() return # zero exit status >>>>>>> REPLACE ``` This change will remove the assignment to the unused variable `_output` while keeping the call to `subprocess.check_output`. --- aider/linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index e27a7d99d..ae9b75b1f 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -34,7 +34,7 @@ class Linter: cmd += " " + rel_fname cmd = cmd.split() try: - _output = subprocess.check_output(cmd, cwd=self.root).decode() + subprocess.check_output(cmd, cwd=self.root).decode() return # zero exit status except subprocess.CalledProcessError as err: return err.output.decode() # non-zero exit status From fe65b7d11fa420a514419188a44716f4a2fca75e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 16:41:36 -0700 Subject: [PATCH 027/102] roughed in flake8 --- aider/linter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index ae9b75b1f..8523e5563 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -17,9 +17,11 @@ class Linter: self.root = root fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" - py_cmd = f"flake8 --select={fatal} --show-source" + py_cmd = f"flake8 --select={fatal} --show-source" # noqa: F841 - self.languages = dict(python=py_cmd) + self.languages = dict( + # python=py_cmd, + ) def set_linter(self, lang, cmd): self.languages[lang] = cmd @@ -46,7 +48,7 @@ class Linter: rel_fname = self.get_rel_fname(fname) - cmd = self.languages[lang] + cmd = self.languages.get(lang) if cmd: return self.run_cmd(cmd, rel_fname) From 4b0c38254e9e7a8c29d6e58b52a7f9c122f89015 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 16:58:04 -0700 Subject: [PATCH 028/102] added lint reflection --- aider/coders/base_coder.py | 27 ++++++++++++++++++++++++++- aider/linter.py | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 565da3c2e..e7c4f9860 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -20,6 +20,7 @@ from aider import __version__, models, prompts, utils from aider.commands import Commands from aider.history import ChatSummary from aider.io import InputOutput +from aider.linter import Linter from aider.litellm import litellm from aider.mdstream import MarkdownStream from aider.repo import GitRepo @@ -287,6 +288,8 @@ class Coder: self.verbose, ) + self.linter = Linter(root=self.root, encoding=io.encoding) + if max_chat_history_tokens is None: max_chat_history_tokens = self.main_model.max_chat_history_tokens self.summarizer = ChatSummary( @@ -721,8 +724,16 @@ class Coder: edited, edit_error = self.apply_updates() if edit_error: - self.update_cur_messages(set()) self.reflected_message = edit_error + self.update_cur_messages(set()) + return + + if edited: + lint_errors = self.lint_edited(edited) + if lint_errors: + self.reflected_message = lint_errors + self.update_cur_messages(set()) + return self.update_cur_messages(edited) @@ -744,6 +755,20 @@ class Coder: else: self.reflected_message = add_rel_files_message + def lint_edited(self, fnames): + res = "" + for fname in fnames: + errors = self.linter.lint(fname) + if errors: + res += "\n" + res += errors + res += "\n" + + if res: + self.io.tool_error(res) + + return res + def update_cur_messages(self, edited): if self.partial_response_content: self.cur_messages += [dict(role="assistant", content=self.partial_response_content)] diff --git a/aider/linter.py b/aider/linter.py index 8523e5563..75cb1d17c 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -89,7 +89,7 @@ def basic_lint(fname, code): ) context.add_lines_of_interest(errors) context.add_context() - output = "# Syntax Errors found on the lines marked with █\n" + output = "# Syntax Errors found on the lines marked with █\n\n" output += fname + ":\n" output += context.format() From 8f194c8313be56658c44f7cc4b01871fa01cabfc Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 19:11:48 -0700 Subject: [PATCH 029/102] Add reflection tracking and limit to Coder class. --- aider/coders/base_coder.py | 65 +++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index e7c4f9860..865cba1d2 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -55,7 +55,8 @@ class Coder: num_exhausted_context_windows = 0 num_malformed_responses = 0 last_keyboard_interrupt = None - max_apply_update_errors = 3 + num_reflections = 0 + max_reflections = 5 edit_format = None yield_stream = False @@ -487,10 +488,12 @@ class Coder: def run_stream(self, user_message): self.io.user_input(user_message) self.reflected_message = None + self.num_reflections = 0 yield from self.send_new_user_message(user_message) def run(self, with_message=None): while True: + self.num_reflections = 0 try: if with_message: new_user_message = with_message @@ -501,7 +504,14 @@ class Coder: while new_user_message: self.reflected_message = None list(self.send_new_user_message(new_user_message)) - new_user_message = self.reflected_message + if self.num_reflections < self.max_reflections: + self.num_reflections += 1 + new_user_message = self.reflected_message + else: + self.io.tool_error( + f"Only {self.max_reflections} reflections allowed, stopping." + ) + new_user_message = None if with_message: return self.partial_response_content @@ -722,9 +732,8 @@ class Coder: self.cur_messages += [dict(role="assistant", content=content)] return - edited, edit_error = self.apply_updates() - if edit_error: - self.reflected_message = edit_error + edited = self.apply_updates() + if self.reflected_message: self.update_cur_messages(set()) return @@ -1087,8 +1096,6 @@ class Coder: ) self.warning_given = True - apply_update_errors = 0 - def prepare_to_edit(self, edits): res = [] seen = dict() @@ -1122,37 +1129,29 @@ class Coder: edited = self.update_files() except ValueError as err: self.num_malformed_responses += 1 + err = err.args[0] - self.apply_update_errors += 1 - if self.apply_update_errors < self.max_apply_update_errors: - self.io.tool_error(f"Malformed response #{self.apply_update_errors}, retrying...") - self.io.tool_error("https://aider.chat/docs/faq.html#aider-isnt-editing-my-files") - self.io.tool_error(str(err), strip=False) - return None, err - else: - self.io.tool_error(f"Malformed response #{self.apply_update_errors}, aborting.") - self.io.tool_error("https://aider.chat/docs/faq.html#aider-isnt-editing-my-files") - self.io.tool_error(str(err), strip=False) - return False, None + + self.io.tool_error(f"The LLM did not conform to the edit format.") + self.io.tool_error( + "For more info see: https://aider.chat/docs/faq.html#aider-isnt-editing-my-files" + ) + self.io.tool_error(str(err), strip=False) + + self.reflected_message = str(err) + return except git.exc.GitCommandError as err: self.io.tool_error(str(err)) - return False, None + return except Exception as err: - print(err) - print() - traceback.print_exc() - self.apply_update_errors += 1 - if self.apply_update_errors < self.max_apply_update_errors: - self.io.tool_error(f"Update exception #{self.apply_update_errors}, retrying...") - self.io.tool_error(str(err), strip=False) - return None, str(err) - else: - self.io.tool_error(f"Update exception #{self.apply_update_errors}, aborting") - self.io.tool_error(str(err), strip=False) - return False, None + self.io.tool_error(f"Exception while updating files:") + self.io.tool_error(str(err), strip=False) - self.apply_update_errors = 0 + traceback.print_exc() + + self.reflected_message = str(err) + return for path in edited: if self.dry_run: @@ -1160,7 +1159,7 @@ class Coder: else: self.io.tool_output(f"Applied edit to {path}") - return edited, None + return edited def parse_partial_args(self): # dump(self.partial_response_function_call) From 0412b19f6b60e6f1d6c166df3e02b550e7d0daa3 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 19:11:49 -0700 Subject: [PATCH 030/102] aider: Fix flake8 errors by removing unnecessary f-string prefixes in `aider/coders/base_coder.py`. # Aider chat conversation: USER: I ran this command: pre-commit run --files aider/coders/base_coder.py And got this output: isort....................................................................Passed black....................................................................Passed flake8...................................................................Failed - hook id: flake8 - exit code: 1 aider/coders/base_coder.py:1135:32: F541 f-string is missing placeholders self.io.tool_error(f"The LLM did not conform to the edit format.") ^ aider/coders/base_coder.py:1148:32: F541 f-string is missing placeholders self.io.tool_error(f"Exception while updating files:") ^ ASSISTANT: Please add `aider/coders/base_coder.py` to the chat so I can propose the necessary changes to fix the flake8 errors. USER: I added these *read-write* files: aider/coders/base_coder.py ASSISTANT: Here are the changes to fix the flake8 errors related to the f-strings missing placeholders in `aider/coders/base_coder.py`: ```diff --- aider/coders/base_coder.py +++ aider/coders/base_coder.py @@ ... @@ - self.io.tool_error(f"The LLM did not conform to the edit format.") + self.io.tool_error("The LLM did not conform to the edit format.") @@ ... @@ - self.io.tool_error(f"Exception while updating files:") + self.io.tool_error("Exception while updating files:") ``` These changes remove the unnecessary f-string prefixes since there are no variables or expressions to be evaluated inside the strings. --- aider/coders/base_coder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 865cba1d2..b9d8c6df1 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1132,7 +1132,7 @@ class Coder: err = err.args[0] - self.io.tool_error(f"The LLM did not conform to the edit format.") + self.io.tool_error("The LLM did not conform to the edit format.") self.io.tool_error( "For more info see: https://aider.chat/docs/faq.html#aider-isnt-editing-my-files" ) @@ -1145,7 +1145,7 @@ class Coder: self.io.tool_error(str(err)) return except Exception as err: - self.io.tool_error(f"Exception while updating files:") + self.io.tool_error("Exception while updating files:") self.io.tool_error(str(err), strip=False) traceback.print_exc() From b26969ea9023fa6c327c2f165c395c127c90ae98 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Fri, 17 May 2024 19:22:17 -0700 Subject: [PATCH 031/102] Refactored linting to use absolute file path. --- aider/coders/base_coder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index b9d8c6df1..ccf7854f3 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -767,7 +767,7 @@ class Coder: def lint_edited(self, fnames): res = "" for fname in fnames: - errors = self.linter.lint(fname) + errors = self.linter.lint(self.abs_root_path(fname)) if errors: res += "\n" res += errors From 30c9c4c319c4cc83bd34a4977c8d2a6f51ad30a6 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 17:38:25 -0700 Subject: [PATCH 032/102] Refactored linter to support linting Python code with py_compile. --- aider/linter.py | 50 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 75cb1d17c..1bd13e8cd 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,9 +1,13 @@ import os +import traceback import subprocess import sys import warnings +import py_compile from pathlib import Path +from aider.dump import dump + from grep_ast import TreeContext, filename_to_lang # tree_sitter is throwing a FutureWarning @@ -20,7 +24,7 @@ class Linter: py_cmd = f"flake8 --select={fatal} --show-source" # noqa: F841 self.languages = dict( - # python=py_cmd, + python=self.py_lint, ) def set_linter(self, lang, cmd): @@ -47,16 +51,43 @@ class Linter: return rel_fname = self.get_rel_fname(fname) + code = Path(fname).read_text(self.encoding) cmd = self.languages.get(lang) + + if callable(cmd): + return cmd(fname, rel_fname, code) + if cmd: return self.run_cmd(cmd, rel_fname) - # fall back to tree sitter / tree context linter - code = Path(fname).read_text(self.encoding) - return basic_lint(rel_fname, code) + def py_lint(self, fname, rel_fname, code): + res = basic_lint(rel_fname, code) + if res: + return res + + return lint_pycompile(fname, code) + +def lint_pycompile(fname, code): + try: + #py_compile.compile(fname, doraise=True) + compile(code, fname, 'exec') + return + except Exception as err: + dump(dir(err)) + dump(err.text) + res = f"{type(err)}: {err}\n" + line_numbers = list(range(err.lineno - 1, err.end_lineno)) + + dump(line_numbers) + #last_call_stack = tb[-1] # Get the last call stack + #file_name = last_call_stack.filename + #line_number = last_call_stack.lineno + + res += tree_context(fname, code, line_numbers) + return res def basic_lint(fname, code): """ @@ -74,11 +105,14 @@ def basic_lint(fname, code): if not errors: return + return tree_context(fname, code, errors) + +def tree_context(fname, code, line_nums): context = TreeContext( fname, code, color=False, - line_number=False, + line_number=True, child_context=False, last_line=False, margin=0, @@ -87,9 +121,11 @@ def basic_lint(fname, code): # header_max=30, show_top_of_file_parent_scope=False, ) - context.add_lines_of_interest(errors) + line_nums = set(line_nums) + context.add_lines_of_interest(line_nums) context.add_context() - output = "# Syntax Errors found on the lines marked with █\n\n" + s = "s" if len(line_nums) > 1 else "" + output = f"# Fix the syntax error{s} found on the line{s} marked with █.\n\n" output += fname + ":\n" output += context.format() From f699241e12a95316344e5cdca7571b3912dc7575 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 17:38:26 -0700 Subject: [PATCH 033/102] aider: Change error message format to display the class name instead of the full type. # Aider chat conversation: USER: res = f"{type(err)}: {err}\n" is producing: ": unexpected indent (tmp.syntaxerror.py, line 4)" I want it to say "IndentationError: unexpected indent (tmp.syntaxerror.py, line 4)" ASSISTANT: To change the error message format from `: unexpected indent (tmp.syntaxerror.py, line 4)` to `IndentationError: unexpected indent (tmp.syntaxerror.py, line 4)`, we need to modify the line where the error message is being constructed. Here is the *SEARCH/REPLACE* block: aider/linter.py ```python <<<<<<< SEARCH res = f"{type(err)}: {err}\n" ======= res = f"{type(err).__name__}: {err}\n" >>>>>>> REPLACE ``` --- aider/linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index 1bd13e8cd..0f7962aae 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -78,7 +78,7 @@ def lint_pycompile(fname, code): except Exception as err: dump(dir(err)) dump(err.text) - res = f"{type(err)}: {err}\n" + res = f"{type(err).__name__}: {err}\n" line_numbers = list(range(err.lineno - 1, err.end_lineno)) dump(line_numbers) From 567c470c0b4b85ee490a66803ff434864bed1f75 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 17:41:58 -0700 Subject: [PATCH 034/102] Refactored error message and added a todo comment for printing out the last call stack. --- aider/linter.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 0f7962aae..1d69651d3 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -75,17 +75,17 @@ def lint_pycompile(fname, code): #py_compile.compile(fname, doraise=True) compile(code, fname, 'exec') return - except Exception as err: + except ValueError as err: dump(dir(err)) dump(err.text) res = f"{type(err).__name__}: {err}\n" line_numbers = list(range(err.lineno - 1, err.end_lineno)) dump(line_numbers) - #last_call_stack = tb[-1] # Get the last call stack - #file_name = last_call_stack.filename - #line_number = last_call_stack.lineno + # todo: print out the Traceback, but only the last call stack + + res += '\n' res += tree_context(fname, code, line_numbers) return res @@ -125,7 +125,7 @@ def tree_context(fname, code, line_nums): context.add_lines_of_interest(line_nums) context.add_context() s = "s" if len(line_nums) > 1 else "" - output = f"# Fix the syntax error{s} found on the line{s} marked with █.\n\n" + output = f"# Fix the error{s}, see relevant line{s} below marked with █.\n\n" output += fname + ":\n" output += context.format() From b600e179108e154db7f435132b2438294216853a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 17:42:04 -0700 Subject: [PATCH 035/102] Added traceback information to the linting output. --- aider/linter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 1d69651d3..5eeaa3e0a 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -13,6 +13,7 @@ from grep_ast import TreeContext, filename_to_lang # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) from tree_sitter_languages import get_parser # noqa: E402 +import traceback class Linter: @@ -72,7 +73,7 @@ class Linter: def lint_pycompile(fname, code): try: - #py_compile.compile(fname, doraise=True) + # py_compile.compile(fname, doraise=True) compile(code, fname, 'exec') return except ValueError as err: @@ -83,7 +84,10 @@ def lint_pycompile(fname, code): dump(line_numbers) - # todo: print out the Traceback, but only the last call stack + # Print out the Traceback, but only the last call stack + tb_lines = traceback.format_exception(type(err), err, err.__traceback__) + last_call_stack = ''.join(tb_lines[-2:]) + res += last_call_stack res += '\n' res += tree_context(fname, code, line_numbers) From 86facafd09dc6a6523b21279de0ec16702273288 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 17:57:35 -0700 Subject: [PATCH 036/102] works --- aider/linter.py | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 5eeaa3e0a..2a4530788 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,19 +1,21 @@ import os -import traceback +import py_compile # noqa: F401 import subprocess import sys +import traceback import warnings -import py_compile -from pathlib import Path +import traceback -from aider.dump import dump +from pathlib import Path from grep_ast import TreeContext, filename_to_lang +from aider.dump import dump # noqa: F401 + # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) + from tree_sitter_languages import get_parser # noqa: E402 -import traceback class Linter: @@ -69,30 +71,34 @@ class Linter: if res: return res - return lint_pycompile(fname, code) + return lint_python_compile(fname, code) -def lint_pycompile(fname, code): + +def lint_python_compile(fname, code): try: - # py_compile.compile(fname, doraise=True) - compile(code, fname, 'exec') + compile(code, fname, "exec") # USE TRACEBACK BELOW HERE return - except ValueError as err: - dump(dir(err)) - dump(err.text) - res = f"{type(err).__name__}: {err}\n" + except Exception as err: line_numbers = list(range(err.lineno - 1, err.end_lineno)) - dump(line_numbers) + tb_lines = traceback.format_exception(type(err), err, err.__traceback__) + last_file_i = 0 - # Print out the Traceback, but only the last call stack - tb_lines = traceback.format_exception(type(err), err, err.__traceback__) - last_call_stack = ''.join(tb_lines[-2:]) - res += last_call_stack + target = "# USE TRACEBACK" + target += " BELOW HERE" + for i in range(len(tb_lines)): + if target in tb_lines[i]: + last_file_i = i + break - res += '\n' + tb_lines = tb_lines[:1] + tb_lines[last_file_i + 1 :] + + res = "".join(tb_lines) + res += "\n" res += tree_context(fname, code, line_numbers) return res + def basic_lint(fname, code): """ Use tree-sitter to look for syntax errors, display them with tree context. @@ -111,6 +117,7 @@ def basic_lint(fname, code): return tree_context(fname, code, errors) + def tree_context(fname, code, line_nums): context = TreeContext( fname, From bc3ed8657e89d473f55b2d0b76d68871c35f2fa9 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 17:59:40 -0700 Subject: [PATCH 037/102] stick with compile() --- aider/linter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 2a4530788..98a0ebc15 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -1,16 +1,13 @@ import os -import py_compile # noqa: F401 import subprocess import sys import traceback import warnings -import traceback - from pathlib import Path from grep_ast import TreeContext, filename_to_lang -from aider.dump import dump # noqa: F401 +from aider.dump import dump # noqa: F401 # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) From 22736b63d07d458df18be0f61cab48b6f799ff68 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:05:43 -0700 Subject: [PATCH 038/102] noop --- requirements.in | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.in b/requirements.in index 778cb0cd4..6f3eb5ba4 100644 --- a/requirements.in +++ b/requirements.in @@ -12,7 +12,6 @@ numpy scipy backoff pathspec -networkx<3.3 # 3.3 no longer works on python 3.9 diskcache grep_ast packaging @@ -28,5 +27,8 @@ litellm google-generativeai streamlit -# 0.22.2 seems to break tree-sitter-languages? +# v3.3 no longer works on python 3.9 +networkx<3.3 + +# v0.22.2 seems to break tree-sitter-languages? tree-sitter==0.21.3 From 5d8ed278aace8d4580883794193afa23f7d75248 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:13:08 -0700 Subject: [PATCH 039/102] aider: Add get_dirty_files() method to return all dirty files staged or in the working directory. # Aider chat conversation: USER: add a get_dirty_files() method which returns all the files which are dirty (not committed) either staged or in the working dir. ASSISTANT: To add the `get_dirty_files()` method, we need to: 1. Define the new method in the `GitRepo` class. 2. Implement the logic to return all dirty files, both staged and in the working directory. Here is the *SEARCH/REPLACE* block: aider/repo.py ```python <<<<<<< SEARCH def is_dirty(self, path=None): ======= def get_dirty_files(self): """ Returns a list of all files which are dirty (not committed), either staged or in the working directory. """ dirty_files = set() # Get staged files staged_files = self.repo.git.diff('--name-only', '--cached').splitlines() dirty_files.update(staged_files) # Get unstaged files unstaged_files = self.repo.git.diff('--name-only').splitlines() dirty_files.update(unstaged_files) return list(dirty_files) >>>>>>> REPLACE ``` aider/repo.py ```python <<<<<<< SEARCH return self.repo.is_dirty(path=path) ======= return self.repo.is_dirty(path=path) >>>>>>> REPLACE ``` --- aider/repo.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/aider/repo.py b/aider/repo.py index 4538c16a3..0133d15dc 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -242,7 +242,21 @@ class GitRepo: res = Path(self.root) / path return utils.safe_abs_path(res) - def is_dirty(self, path=None): + def get_dirty_files(self): + """ + Returns a list of all files which are dirty (not committed), either staged or in the working directory. + """ + dirty_files = set() + + # Get staged files + staged_files = self.repo.git.diff('--name-only', '--cached').splitlines() + dirty_files.update(staged_files) + + # Get unstaged files + unstaged_files = self.repo.git.diff('--name-only').splitlines() + dirty_files.update(unstaged_files) + + return list(dirty_files) if path and not self.path_in_repo(path): return True From abbe5416a19ba37305189c3b84b1d6791fe20278 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:30:50 -0700 Subject: [PATCH 040/102] Refactored GitRepo class to use consistent quotation marks for git commands. --- aider/repo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aider/repo.py b/aider/repo.py index 0133d15dc..4815d681f 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -249,14 +249,16 @@ class GitRepo: dirty_files = set() # Get staged files - staged_files = self.repo.git.diff('--name-only', '--cached').splitlines() + staged_files = self.repo.git.diff("--name-only", "--cached").splitlines() dirty_files.update(staged_files) # Get unstaged files - unstaged_files = self.repo.git.diff('--name-only').splitlines() + unstaged_files = self.repo.git.diff("--name-only").splitlines() dirty_files.update(unstaged_files) return list(dirty_files) + + def is_dirty(self, path=None): if path and not self.path_in_repo(path): return True From 7b8e60324990d23d3cae26f6dd61b8531fe7f434 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:30:51 -0700 Subject: [PATCH 041/102] aider: Shortened the docstring in `aider/repo.py` to comply with the 100 characters line length limit. # Aider chat conversation: USER: I ran this command: pre-commit run --files aider/repo.py And got this output: isort....................................................................Passed black....................................................................Passed flake8...................................................................Failed - hook id: flake8 - exit code: 1 aider/repo.py:247:101: E501 line too long (111 > 100 characters) """ Returns a list of all files which are dirty (not committed), either staged or in the working directory. """ ^ ASSISTANT: Please add the file `aider/repo.py` to the chat so I can make the necessary changes. USER: I added these files to the chat: aider/repo.py ASSISTANT: To fix the flake8 error, we need to shorten the docstring to comply with the line length limit of 100 characters. Here is the *SEARCH/REPLACE* block: aider/repo.py ```python <<<<<<< SEARCH """ Returns a list of all files which are dirty (not committed), either staged or in the working directory. """ ======= """ Returns a list of all files which are dirty (not committed), either staged or in the working directory. """ >>>>>>> REPLACE ``` --- aider/repo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aider/repo.py b/aider/repo.py index 4815d681f..666a8f084 100644 --- a/aider/repo.py +++ b/aider/repo.py @@ -244,7 +244,8 @@ class GitRepo: def get_dirty_files(self): """ - Returns a list of all files which are dirty (not committed), either staged or in the working directory. + Returns a list of all files which are dirty (not committed), either staged or in the working + directory. """ dirty_files = set() From 04084883e85559766bc9f8fb46a92ff90830b15c Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:35:33 -0700 Subject: [PATCH 042/102] Added a new command to run the linter on dirty files, fix problems, and then commit. --- aider/args.py | 6 ++++++ aider/commands.py | 24 ++++++++++++++++++++++++ aider/linter.py | 5 ++++- aider/main.py | 4 ++++ 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/aider/args.py b/aider/args.py index 3b42d074d..9ff40f61a 100644 --- a/aider/args.py +++ b/aider/args.py @@ -311,6 +311,12 @@ def get_parser(default_config_files, git_root): help="Commit all pending changes with a suitable commit message, then exit", default=False, ) + group.add_argument( + "--lint", + action="store_true", + help="Run the linter on all dirty files, fix problems and then commit.", + default=False, + ) ########## group = parser.add_argument_group("Other Settings") diff --git a/aider/commands.py b/aider/commands.py index c592a5d7f..19ffa9b70 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -153,6 +153,30 @@ class Commands: commit_message = args.strip() self.coder.repo.commit(message=commit_message) + def cmd_lint(self, args): + "Run the linter on all dirty files, fix problems and then commit" + + if not self.coder.repo: + self.io.tool_error("No git repository found.") + return + + if not self.coder.repo.is_dirty(): + self.io.tool_error("No more changes to commit.") + return + + fnames = self.coder.repo.get_dirty_files() + + for fname in fnames: + errors = self.coder.linter.lint(fname) + if errors: + self.io.tool_error(errors) + + abs_file_path = self.coder.abs_root_path(fname) + self.coder.abs_fnames.add(abs_file_path) + self.coder.run(errors) + + self.cmd_commit("") + def cmd_clear(self, args): "Clear the chat history" diff --git a/aider/linter.py b/aider/linter.py index 98a0ebc15..42670fc58 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -24,7 +24,8 @@ class Linter: py_cmd = f"flake8 --select={fatal} --show-source" # noqa: F841 self.languages = dict( - python=self.py_lint, + # python=self.py_lint, + python="pre-commit run --files" ) def set_linter(self, lang, cmd): @@ -41,8 +42,10 @@ class Linter: cmd = cmd.split() try: subprocess.check_output(cmd, cwd=self.root).decode() + print("zero") return # zero exit status except subprocess.CalledProcessError as err: + print("non-zero") return err.output.decode() # non-zero exit status def lint(self, fname): diff --git a/aider/main.py b/aider/main.py index 2074cf803..34aca61ca 100644 --- a/aider/main.py +++ b/aider/main.py @@ -355,6 +355,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.commands.cmd_commit("") return + if args.lint: + coder.commands.cmd_lint("") + return + if args.show_repo_map: repo_map = coder.get_repo_map() if repo_map: From 2e6c7a13b2ba52d3d96afbf5dc1638e4e504c622 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:57:14 -0700 Subject: [PATCH 043/102] Add detailed error message when non-zero exit status occurs in Linter. --- aider/linter.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 42670fc58..f94d37cc2 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -42,11 +42,14 @@ class Linter: cmd = cmd.split() try: subprocess.check_output(cmd, cwd=self.root).decode() - print("zero") return # zero exit status except subprocess.CalledProcessError as err: - print("non-zero") - return err.output.decode() # non-zero exit status + errors = err.output.decode() # non-zero exit status + + res = "# Running: {cmd]\n" + res += "If the output below indicates errors or problems, fix them.\n" + res += "But if the command fixed all the issues itself, don't take further action.\n\n" + res += errors def lint(self, fname): lang = filename_to_lang(fname) From 5959e7990af8979b73e7aecb2606e6f45b0ac29f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:59:37 -0700 Subject: [PATCH 044/102] Return the linting results in the Linter class. --- aider/linter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aider/linter.py b/aider/linter.py index f94d37cc2..7f0d13dac 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -40,6 +40,7 @@ class Linter: def run_cmd(self, cmd, rel_fname): cmd += " " + rel_fname cmd = cmd.split() + try: subprocess.check_output(cmd, cwd=self.root).decode() return # zero exit status @@ -51,6 +52,8 @@ class Linter: res += "But if the command fixed all the issues itself, don't take further action.\n\n" res += errors + return res + def lint(self, fname): lang = filename_to_lang(fname) if not lang: From e516f838f5a9e1f1c7f0acae7d30297731ef8de3 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 18:59:39 -0700 Subject: [PATCH 045/102] aider: Fix import sorting in `aider/linter.py` to resolve `isort` failure. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: # Running: {cmd] If the output below indicates errors or problems, fix them. But if the command fixed all the issues itself, don't take further action. isort....................................................................Failed - hook id: isort - files were modified by this hook Fixing /Users/gauthier/Projects/aider/aider/linter.py black....................................................................Passed flake8...................................................................Passed USER: # Running: {cmd] If the output below indicates errors or problems, fix them. But if the command fixed all the issues itself, don't take further action. isort....................................................................Failed - hook id: isort - files were modified by this hook Fixing /Users/gauthier/Projects/aider/aider/linter.py black....................................................................Passed flake8...................................................................Passed ASSISTANT: To fix the `isort` issues, we need to sort the imports in `aider/linter.py`. Here is the *SEARCH/REPLACE* block to apply the necessary changes: aider/linter.py ```python <<<<<<< SEARCH import os import subprocess import sys import traceback import warnings from pathlib import Path from grep_ast import TreeContext, filename_to_lang from aider.dump import dump # noqa: F401 # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) from tree_sitter_languages import get_parser # noqa: E402 ======= import os import subprocess import sys import traceback import warnings from pathlib import Path from grep_ast import TreeContext, filename_to_lang from tree_sitter_languages import get_parser # noqa: E402 from aider.dump import dump # noqa: F401 # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) >>>>>>> REPLACE ``` --- aider/linter.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 7f0d13dac..ed323449f 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -6,14 +6,13 @@ import warnings from pathlib import Path from grep_ast import TreeContext, filename_to_lang +from tree_sitter_languages import get_parser # noqa: E402 from aider.dump import dump # noqa: F401 # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) -from tree_sitter_languages import get_parser # noqa: E402 - class Linter: def __init__(self, encoding="utf-8", root=None): From f43b352949ebb6a0e1769f0117890d02a395a88f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:00:43 -0700 Subject: [PATCH 046/102] Fix string interpolation typo in linter output message. --- aider/linter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index ed323449f..ae7da0253 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -46,9 +46,11 @@ class Linter: except subprocess.CalledProcessError as err: errors = err.output.decode() # non-zero exit status - res = "# Running: {cmd]\n" + res = f"# Running: {cmd}\n" res += "If the output below indicates errors or problems, fix them.\n" - res += "But if the command fixed all the issues itself, don't take further action.\n\n" + res += ( + "But if the command says it fixed all the issues itself, don't take further action.\n\n" + ) res += errors return res From 901d8a1a2a47cc8660adf6d5531333bf5b256c97 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:14:19 -0700 Subject: [PATCH 047/102] Set linter command as a string and update the result message format. --- aider/linter.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index ae7da0253..fd0b130b1 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -24,7 +24,7 @@ class Linter: self.languages = dict( # python=self.py_lint, - python="pre-commit run --files" + python="/Users/gauthier/Projects/aider/tmp.sh" ) def set_linter(self, lang, cmd): @@ -46,11 +46,8 @@ class Linter: except subprocess.CalledProcessError as err: errors = err.output.decode() # non-zero exit status - res = f"# Running: {cmd}\n" - res += "If the output below indicates errors or problems, fix them.\n" - res += ( - "But if the command says it fixed all the issues itself, don't take further action.\n\n" - ) + cmd = " ".join(cmd) + res = f"# Running: {cmd}\n\n" res += errors return res From ba46cb6b6c4f32b953fda3794efd48a643b21044 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:15:47 -0700 Subject: [PATCH 048/102] Remove unused import and fix FutureWarning in linter. --- aider/linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index fd0b130b1..102f4d6a5 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -8,7 +8,7 @@ from pathlib import Path from grep_ast import TreeContext, filename_to_lang from tree_sitter_languages import get_parser # noqa: E402 -from aider.dump import dump # noqa: F401 +from aider.dump import dump # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) From c494f507c5969c9b42b67a9e15f05e3c73e1633e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:15:49 -0700 Subject: [PATCH 049/102] aider: Removed unused import statement for `dump` from `aider/linter.py`. # Aider chat conversation: USER: # Running: /Users/gauthier/Projects/aider/tmp.sh aider/linter.py aider/linter.py aider/linter.py:11:1: F401 'aider.dump.dump' imported but unused from aider.dump import dump ^ ASSISTANT: To resolve the unused import warning, we need to remove the import statement for `dump` from `aider/linter.py`. Here is the *SEARCH/REPLACE* block: aider/linter.py ```python <<<<<<< SEARCH from aider.dump import dump ======= >>>>>>> REPLACE ``` --- aider/linter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 102f4d6a5..d055b30ce 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -8,8 +8,6 @@ from pathlib import Path from grep_ast import TreeContext, filename_to_lang from tree_sitter_languages import get_parser # noqa: E402 -from aider.dump import dump - # tree_sitter is throwing a FutureWarning warnings.simplefilter("ignore", category=FutureWarning) From 1d519cdf1eb7db07066fca705ae6bf1d78a2fde0 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:16:24 -0700 Subject: [PATCH 050/102] Removed unused import from wholefile_coder.py. --- aider/coders/wholefile_coder.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aider/coders/wholefile_coder.py b/aider/coders/wholefile_coder.py index 4b244f637..a4644200d 100644 --- a/aider/coders/wholefile_coder.py +++ b/aider/coders/wholefile_coder.py @@ -1,5 +1,3 @@ -from pathlib import Path - from aider import diffs from ..dump import dump # noqa: F401 From 4bbeb9de6ff1081e1aa4aa8e9f5895116e4366c6 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:16:25 -0700 Subject: [PATCH 051/102] aider: Import 'Path' from 'pathlib' to fix undefined name errors. # Aider chat conversation: USER: # Running: /Users/gauthier/Projects/aider/tmp.sh aider/coders/wholefile_coder.py aider/coders/wholefile_coder.py aider/coders/wholefile_coder.py:72:62: F821 undefined name 'Path' if fname and fname not in chat_files and Path(fname).name in chat_files: ^ aider/coders/wholefile_coder.py:73:33: F821 undefined name 'Path' fname = Path(fname).name ^ aider/coders/wholefile_coder.py:102:30: F821 undefined name 'Path' full_path = (Path(self.root) / fname).absolute() ^ aider/coders/wholefile_coder.py:132:12: F821 undefined name 'Path' if Path(full_path).exists(): ^ ASSISTANT: To fix the `undefined name 'Path'` errors, we need to import `Path` from the `pathlib` module at the beginning of the file. Here are the necessary changes: aider/coders/wholefile_coder.py python <<<<<<< SEARCH from aider import diffs ======= from pathlib import Path from aider import diffs >>>>>>> REPLACE --- aider/coders/wholefile_coder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aider/coders/wholefile_coder.py b/aider/coders/wholefile_coder.py index a4644200d..4b244f637 100644 --- a/aider/coders/wholefile_coder.py +++ b/aider/coders/wholefile_coder.py @@ -1,3 +1,5 @@ +from pathlib import Path + from aider import diffs from ..dump import dump # noqa: F401 From 6b6f9e9ed46568ff5cc59daa9dd9644d9e5b077f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:17:38 -0700 Subject: [PATCH 052/102] Added conditional check to only commit if there are dirty files. --- aider/commands.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aider/commands.py b/aider/commands.py index 19ffa9b70..74bb6263d 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -166,6 +166,9 @@ class Commands: fnames = self.coder.repo.get_dirty_files() + if fnames: + self.cmd_commit("") + for fname in fnames: errors = self.coder.linter.lint(fname) if errors: @@ -175,7 +178,8 @@ class Commands: self.coder.abs_fnames.add(abs_file_path) self.coder.run(errors) - self.cmd_commit("") + if self.repo.is_dirty(): + self.cmd_commit("") def cmd_clear(self, args): "Clear the chat history" From 66f53db9272815c4d6ad7b19f0f317eceb3e40fc Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:17:56 -0700 Subject: [PATCH 053/102] Fixed a bug where the method was checking the wrong object for dirtiness. --- aider/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/commands.py b/aider/commands.py index 74bb6263d..8e8ab25e8 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -178,7 +178,7 @@ class Commands: self.coder.abs_fnames.add(abs_file_path) self.coder.run(errors) - if self.repo.is_dirty(): + if self.coder.repo.is_dirty(): self.cmd_commit("") def cmd_clear(self, args): From 58050078852481a8cd2aeb8f16b1a80fcf495ebd Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:18:38 -0700 Subject: [PATCH 054/102] Removed unused import statement from wholefile_coder.py. --- aider/coders/wholefile_coder.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aider/coders/wholefile_coder.py b/aider/coders/wholefile_coder.py index 4b244f637..a4644200d 100644 --- a/aider/coders/wholefile_coder.py +++ b/aider/coders/wholefile_coder.py @@ -1,5 +1,3 @@ -from pathlib import Path - from aider import diffs from ..dump import dump # noqa: F401 From 8a2ce8ec0ec2b9075796624e9408899650c937bc Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:18:43 -0700 Subject: [PATCH 055/102] aider: Import 'Path' from 'pathlib' to fix undefined name errors. # Aider chat conversation: USER: # Running: /Users/gauthier/Projects/aider/tmp.sh aider/coders/wholefile_coder.py aider/coders/wholefile_coder.py aider/coders/wholefile_coder.py:72:62: F821 undefined name 'Path' if fname and fname not in chat_files and Path(fname).name in chat_files: ^ aider/coders/wholefile_coder.py:73:33: F821 undefined name 'Path' fname = Path(fname).name ^ aider/coders/wholefile_coder.py:102:30: F821 undefined name 'Path' full_path = (Path(self.root) / fname).absolute() ^ aider/coders/wholefile_coder.py:132:12: F821 undefined name 'Path' if Path(full_path).exists(): ^ ASSISTANT: To fix the `undefined name 'Path'` errors, we need to import `Path` from the `pathlib` module at the beginning of the file. Here are the necessary changes: aider/coders/wholefile_coder.py python <<<<<<< SEARCH from aider import diffs ======= from pathlib import Path from aider import diffs >>>>>>> REPLACE --- aider/coders/wholefile_coder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aider/coders/wholefile_coder.py b/aider/coders/wholefile_coder.py index a4644200d..4b244f637 100644 --- a/aider/coders/wholefile_coder.py +++ b/aider/coders/wholefile_coder.py @@ -1,3 +1,5 @@ +from pathlib import Path + from aider import diffs from ..dump import dump # noqa: F401 From ee2e6333801689e1acf819b0f73188cf6e5889c5 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:22:49 -0700 Subject: [PATCH 056/102] aider: Modify `run_cmd` method to capture both stdout and stderr together using `subprocess.Popen`. # Aider chat conversation: USER: can `check_output` intermingle stdout and stdin like they would be in a terminal? ASSISTANT: The `subprocess.check_output` function captures the output of the command and returns it as a string. It does not intermingle stdout and stderr like they would be in a terminal. If you need to capture both stdout and stderr together, you can use `subprocess.Popen` with `stdout=subprocess.PIPE` and `stderr=subprocess.STDOUT`. Would you like me to modify the `run_cmd` method to use `subprocess.Popen` to capture both stdout and stderr together? USER: yes ASSISTANT: Here are the changes to modify the `run_cmd` method to use `subprocess.Popen` to capture both stdout and stderr together: aider/linter.py ```python <<<<<<< SEARCH try: subprocess.check_output(cmd, cwd=self.root).decode() return # zero exit status except subprocess.CalledProcessError as err: errors = err.output.decode() # non-zero exit status ======= process = subprocess.Popen(cmd, cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, _ = process.communicate() errors = stdout.decode() if process.returncode == 0: return # zero exit status >>>>>>> REPLACE ``` This change ensures that both stdout and stderr are captured together, similar to how they would be intermingled in a terminal. --- aider/linter.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index d055b30ce..cc247b6c3 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -38,11 +38,13 @@ class Linter: cmd += " " + rel_fname cmd = cmd.split() - try: - subprocess.check_output(cmd, cwd=self.root).decode() + process = subprocess.Popen( + cmd, cwd=self.root, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + stdout, _ = process.communicate() + errors = stdout.decode() + if process.returncode == 0: return # zero exit status - except subprocess.CalledProcessError as err: - errors = err.output.decode() # non-zero exit status cmd = " ".join(cmd) res = f"# Running: {cmd}\n\n" From 828a5264b0f9ee6f99b4fb67a1cf7a45780806b3 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:28:59 -0700 Subject: [PATCH 057/102] Updated linter to display relevant error lines and context. --- aider/linter.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index cc247b6c3..3a62fad72 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -21,8 +21,9 @@ class Linter: py_cmd = f"flake8 --select={fatal} --show-source" # noqa: F841 self.languages = dict( - # python=self.py_lint, - python="/Users/gauthier/Projects/aider/tmp.sh" + python=self.py_lint, + #python="/Users/gauthier/Projects/aider/tmp.sh" + #python="flake8 --show-source" ) def set_linter(self, lang, cmd): @@ -97,7 +98,8 @@ def lint_python_compile(fname, code): tb_lines = tb_lines[:1] + tb_lines[last_file_i + 1 :] - res = "".join(tb_lines) + res = "# Fix this error:\n\n" + res += "".join(tb_lines) res += "\n" res += tree_context(fname, code, line_numbers) return res @@ -140,7 +142,7 @@ def tree_context(fname, code, line_nums): context.add_lines_of_interest(line_nums) context.add_context() s = "s" if len(line_nums) > 1 else "" - output = f"# Fix the error{s}, see relevant line{s} below marked with █.\n\n" + output = f"## See relevant line{s} below marked with █.\n\n" output += fname + ":\n" output += context.format() From bc06d8623ce2b526034869e442ec6357d524c232 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:30:32 -0700 Subject: [PATCH 058/102] Updated lint command to commit after fixing problems. --- aider/args.py | 4 ++-- aider/commands.py | 2 +- aider/main.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aider/args.py b/aider/args.py index 9ff40f61a..e7da13913 100644 --- a/aider/args.py +++ b/aider/args.py @@ -312,9 +312,9 @@ def get_parser(default_config_files, git_root): default=False, ) group.add_argument( - "--lint", + "--lint-and-commit", action="store_true", - help="Run the linter on all dirty files, fix problems and then commit.", + help="Commit, run the linter on all dirty files, fix problems and commit again", default=False, ) diff --git a/aider/commands.py b/aider/commands.py index 8e8ab25e8..09e22679a 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -154,7 +154,7 @@ class Commands: self.coder.repo.commit(message=commit_message) def cmd_lint(self, args): - "Run the linter on all dirty files, fix problems and then commit" + "Commit, run the linter on all dirty files, fix problems and commit again" if not self.coder.repo: self.io.tool_error("No git repository found.") diff --git a/aider/main.py b/aider/main.py index 34aca61ca..360749ddd 100644 --- a/aider/main.py +++ b/aider/main.py @@ -355,7 +355,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.commands.cmd_commit("") return - if args.lint: + if args.lint_and_commit: coder.commands.cmd_lint("") return From ef286f0de63de092efb7a32912946fa7dcf01a3b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:35:36 -0700 Subject: [PATCH 059/102] Refactored the Linter class to use a dynamic command for Python linting and added a placeholder function for finding filenames and line numbers in text. --- aider/linter.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 3a62fad72..c9d9de5ad 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -21,9 +21,10 @@ class Linter: py_cmd = f"flake8 --select={fatal} --show-source" # noqa: F841 self.languages = dict( - python=self.py_lint, - #python="/Users/gauthier/Projects/aider/tmp.sh" - #python="flake8 --show-source" + #python=self.py_lint, + #python="/Users/gauthier/Projects/aider/tmp.sh", + #python="flake8 --show-source", + python=py_cmd, ) def set_linter(self, lang, cmd): @@ -161,6 +162,11 @@ def traverse_tree(node): return errors +def find_filenames_and_linenums(text, fnames): + # TODO: + # search text for all occurrances of :\d+ and make a list of them + # where is one of the filenames in the list `fnames` + pass def main(): """ From 9e0f20bb9ad04bd6354c3ea40f33eea96e56ce3b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:35:37 -0700 Subject: [PATCH 060/102] aider: Refactored the `find_filenames_and_linenums` function to search for occurrences of `:\d+` in the text and return a list of matches. # Aider chat conversation: USER: do the todo ASSISTANT: To implement the TODO, we need to: 1. Search the text for all occurrences of `:\d+` where `` is one of the filenames in the list `fnames`. 2. Make a list of these occurrences. Here is the *SEARCH/REPLACE* block: aider/linter.py ```python <<<<<<< SEARCH def find_filenames_and_linenums(text, fnames): # TODO: # search text for all occurrances of :\d+ and make a list of them # where is one of the filenames in the list `fnames` pass ======= import re def find_filenames_and_linenums(text, fnames): """ Search text for all occurrences of :\d+ and make a list of them where is one of the filenames in the list `fnames`. """ pattern = re.compile(r'(\b(?:' + '|'.join(re.escape(fname) for fname in fnames) + r'):\d+\b)') matches = pattern.findall(text) return matches >>>>>>> REPLACE ``` --- aider/linter.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index c9d9de5ad..385d87a03 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -162,11 +162,16 @@ def traverse_tree(node): return errors +import re + def find_filenames_and_linenums(text, fnames): - # TODO: - # search text for all occurrances of :\d+ and make a list of them - # where is one of the filenames in the list `fnames` - pass + """ + Search text for all occurrences of :\d+ and make a list of them + where is one of the filenames in the list `fnames`. + """ + pattern = re.compile(r'(\b(?:' + '|'.join(re.escape(fname) for fname in fnames) + r'):\d+\b)') + matches = pattern.findall(text) + return matches def main(): """ From d8f3d155e6ebd26779561acf732aab5547ee8754 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:36:23 -0700 Subject: [PATCH 061/102] aider: Refactored the code to turn the matches into a list of `(filename, linenum)` pairs. # Aider chat conversation: USER: turn the matches into a list of (filename, linenum) where linenum is int ASSISTANT: Here is the *SEARCH/REPLACE* block to turn the matches into a list of `(filename, linenum)` where `linenum` is an integer: aider/linter.py ```python <<<<<<< SEARCH return matches ======= result = [] for match in matches: fname, linenum = match.rsplit(':', 1) result.append((fname, int(linenum))) return result >>>>>>> REPLACE ``` --- aider/linter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index 385d87a03..20304a508 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -171,7 +171,11 @@ def find_filenames_and_linenums(text, fnames): """ pattern = re.compile(r'(\b(?:' + '|'.join(re.escape(fname) for fname in fnames) + r'):\d+\b)') matches = pattern.findall(text) - return matches + result = [] + for match in matches: + fname, linenum = match.rsplit(':', 1) + result.append((fname, int(linenum))) + return result def main(): """ From 2e72aabcf47f4659e25176c8887473a9738edc52 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:36:41 -0700 Subject: [PATCH 062/102] aider: Accumulated a set of line numbers for each filename in the `find_filenames_and_linenums` function. # Aider chat conversation: USER: for each filename accumulate a set of the linenums it has ASSISTANT: To accumulate a set of line numbers for each filename, we need to modify the `find_filenames_and_linenums` function. Here is the *SEARCH/REPLACE* block: aider/linter.py ```python <<<<<<< SEARCH result = [] ======= result = {} >>>>>>> REPLACE ``` aider/linter.py ```python <<<<<<< SEARCH result.append((fname, int(linenum))) ======= if fname not in result: result[fname] = set() result[fname].add(int(linenum)) >>>>>>> REPLACE ``` --- aider/linter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 20304a508..0f4018524 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -171,10 +171,12 @@ def find_filenames_and_linenums(text, fnames): """ pattern = re.compile(r'(\b(?:' + '|'.join(re.escape(fname) for fname in fnames) + r'):\d+\b)') matches = pattern.findall(text) - result = [] + result = {} for match in matches: fname, linenum = match.rsplit(':', 1) - result.append((fname, int(linenum))) + if fname not in result: + result[fname] = set() + result[fname].add(int(linenum)) return result def main(): From d8a5108f9e093efc24af3a0d952727eaa04d8271 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:40:02 -0700 Subject: [PATCH 063/102] Added functionality to include tree context in linter results. --- aider/linter.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 0f4018524..b1aa3336d 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -36,7 +36,7 @@ class Linter: else: return fname - def run_cmd(self, cmd, rel_fname): + def run_cmd(self, cmd, rel_fname, code): cmd += " " + rel_fname cmd = cmd.split() @@ -52,6 +52,11 @@ class Linter: res = f"# Running: {cmd}\n\n" res += errors + filenames_linenums = find_filenames_and_linenums(errors, [rel_fname]) + if filenames_linenums: + filename, linenums = filenames_linenums.items().next() + res += tree_context(rel_fname, code, linenums) + return res def lint(self, fname): @@ -68,7 +73,7 @@ class Linter: return cmd(fname, rel_fname, code) if cmd: - return self.run_cmd(cmd, rel_fname) + return self.run_cmd(cmd, rel_fname, code) return basic_lint(rel_fname, code) From f69da2cd86e6df9d8ffdd2c611d404be86043c3e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:40:04 -0700 Subject: [PATCH 064/102] aider: Fix 'dict_items' object has no attribute 'next' error by using 'next(iter(...))' to iterate over dictionary items in Python 3. # Aider chat conversation: USER: Traceback (most recent call last): File "", line 198, in _run_module_as_main File "", line 88, in _run_code File "/Users/gauthier/Projects/aider/aider/linter.py", line 203, in main() File "/Users/gauthier/Projects/aider/aider/linter.py", line 197, in main errors = linter.lint(file_path) ^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gauthier/Projects/aider/aider/linter.py", line 76, in lint return self.run_cmd(cmd, rel_fname, code) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gauthier/Projects/aider/aider/linter.py", line 57, in run_cmd filename, linenums = filenames_linenums.items().next() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'dict_items' object has no attribute 'next' ASSISTANT: To fix the `AttributeError: 'dict_items' object has no attribute 'next'`, we need to update the code to use the correct method for iterating over dictionary items in Python 3. The `next()` method should be replaced with `next(iter(...))`. Here is the *SEARCH/REPLACE* block: aider/linter.py ```python <<<<<<< SEARCH if filenames_linenums: filename, linenums = filenames_linenums.items().next() res += tree_context(rel_fname, code, linenums) ======= if filenames_linenums: filename, linenums = next(iter(filenames_linenums.items())) res += tree_context(rel_fname, code, linenums) >>>>>>> REPLACE ``` --- aider/linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index b1aa3336d..65cca49ca 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -54,7 +54,7 @@ class Linter: filenames_linenums = find_filenames_and_linenums(errors, [rel_fname]) if filenames_linenums: - filename, linenums = filenames_linenums.items().next() + filename, linenums = next(iter(filenames_linenums.items())) res += tree_context(rel_fname, code, linenums) return res From 65495a60a935c0770d1fb95008714467a8c9e00e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:43:53 -0700 Subject: [PATCH 065/102] Added error fixing instructions and adjusted line numbers for better context in linter output. --- aider/linter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aider/linter.py b/aider/linter.py index 65cca49ca..4cf379766 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -50,11 +50,14 @@ class Linter: cmd = " ".join(cmd) res = f"# Running: {cmd}\n\n" + res += "## Fix these errors:\n\n" res += errors filenames_linenums = find_filenames_and_linenums(errors, [rel_fname]) if filenames_linenums: filename, linenums = next(iter(filenames_linenums.items())) + linenums = [num-1 for num in linenums] + res += "\n" res += tree_context(rel_fname, code, linenums) return res From b0821bb7057fe0a5d6cfdede7444201451f7c3d9 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:49:39 -0700 Subject: [PATCH 066/102] Refactored linter to include additional flake8 command for linting. --- aider/linter.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 4cf379766..0d9c64a2a 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -17,14 +17,10 @@ class Linter: self.encoding = encoding self.root = root - fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" - py_cmd = f"flake8 --select={fatal} --show-source" # noqa: F841 - self.languages = dict( - #python=self.py_lint, + python=self.py_lint, #python="/Users/gauthier/Projects/aider/tmp.sh", #python="flake8 --show-source", - python=py_cmd, ) def set_linter(self, lang, cmd): @@ -85,7 +81,14 @@ class Linter: if res: return res - return lint_python_compile(fname, code) + res = lint_python_compile(fname, code) + if res: + return res + + fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" + flake8 = f"flake8a --select={fatal} --show-source" + + return self.run_cmd(flake8, rel_fname, code) def lint_python_compile(fname, code): From f2b197a69fd6e78e1669050c911a7a1e79641159 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:50:00 -0700 Subject: [PATCH 067/102] Handled FileNotFoundError exception when running flake8 linter. --- aider/linter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index 0d9c64a2a..702f88b76 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -88,7 +88,10 @@ class Linter: fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" flake8 = f"flake8a --select={fatal} --show-source" - return self.run_cmd(flake8, rel_fname, code) + try: + return self.run_cmd(flake8, rel_fname, code) + except FileNotFoundError: + pass def lint_python_compile(fname, code): From e4c1653b2ee079eb46afd348266dff999775ec08 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:50:11 -0700 Subject: [PATCH 068/102] Fix typo in flake8 command in Linter class. --- aider/linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index 702f88b76..6ee42f2c6 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -86,7 +86,7 @@ class Linter: return res fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" - flake8 = f"flake8a --select={fatal} --show-source" + flake8 = f"flake8 --select={fatal} --show-source" try: return self.run_cmd(flake8, rel_fname, code) From 6f557ace06e9a3e34c9eccf185fc2a04e480654f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 19:55:08 -0700 Subject: [PATCH 069/102] Refactored py_lint method to accumulate linting results instead of returning immediately. --- aider/linter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 6ee42f2c6..323478b43 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -77,22 +77,26 @@ class Linter: return basic_lint(rel_fname, code) def py_lint(self, fname, rel_fname, code): + result = '' res = basic_lint(rel_fname, code) if res: - return res + result += res res = lint_python_compile(fname, code) if res: - return res + result += res fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" flake8 = f"flake8 --select={fatal} --show-source" try: - return self.run_cmd(flake8, rel_fname, code) + res = self.run_cmd(flake8, rel_fname, code) except FileNotFoundError: pass + if res: + result += res + return result def lint_python_compile(fname, code): try: @@ -149,7 +153,7 @@ def tree_context(fname, code, line_nums): last_line=False, margin=0, mark_lois=True, - loi_pad=5, + loi_pad=3, # header_max=30, show_top_of_file_parent_scope=False, ) From 24e98a4ac2b2b8c47be2ae8e6d43981054b59b9f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:44:06 -0700 Subject: [PATCH 070/102] Refactored linter methods to return `LintResult` objects. --- aider/linter.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 323478b43..71bef5c80 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -45,18 +45,16 @@ class Linter: return # zero exit status cmd = " ".join(cmd) - res = f"# Running: {cmd}\n\n" - res += "## Fix these errors:\n\n" + res = f"## Running: {cmd}\n\n" res += errors + linenums = [] filenames_linenums = find_filenames_and_linenums(errors, [rel_fname]) if filenames_linenums: filename, linenums = next(iter(filenames_linenums.items())) linenums = [num-1 for num in linenums] - res += "\n" - res += tree_context(rel_fname, code, linenums) - return res + return LintResult(text=res, lines=linenums) def lint(self, fname): lang = filename_to_lang(fname) @@ -69,12 +67,21 @@ class Linter: cmd = self.languages.get(lang) if callable(cmd): - return cmd(fname, rel_fname, code) + linkres = cmd(fname, rel_fname, code) + elif cmd: + linkres = self.run_cmd(cmd, rel_fname, code) + else: + linkres = basic_lint(rel_fname, code) - if cmd: - return self.run_cmd(cmd, rel_fname, code) + if not linkres: + return - return basic_lint(rel_fname, code) + res = '# Fix any errors below\n\n' + res += linkres.text + res += '\n' + res += tree_context(fname, code, linkres.lines) + + return res def py_lint(self, fname, rel_fname, code): result = '' @@ -117,11 +124,8 @@ def lint_python_compile(fname, code): tb_lines = tb_lines[:1] + tb_lines[last_file_i + 1 :] - res = "# Fix this error:\n\n" - res += "".join(tb_lines) - res += "\n" - res += tree_context(fname, code, line_numbers) - return res + res = "".join(tb_lines) + return LintResult(text = res, lines = line_numbers) def basic_lint(fname, code): @@ -140,7 +144,7 @@ def basic_lint(fname, code): if not errors: return - return tree_context(fname, code, errors) + return LintResult(text = '', lines = errors) def tree_context(fname, code, line_nums): From 84edf3039c28387bd6a63a33bc6f79c70f1de6fc Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:44:07 -0700 Subject: [PATCH 071/102] aider: Add LintResult class to linter.py. # Aider chat conversation: USER: create LinkResult ASSISTANT: To create the `LintResult` class, we need to add it to the `aider/linter.py` file. Here is the *SEARCH/REPLACE* block to add the `LintResult` class: aider/linter.py ```python <<<<<<< SEARCH from pathlib import Path ======= from pathlib import Path from dataclasses import dataclass >>>>>>> REPLACE ``` aider/linter.py ```python <<<<<<< SEARCH def lint_python_compile(fname, code): ======= @dataclass class LintResult: text: str lines: list def lint_python_compile(fname, code): >>>>>>> REPLACE ``` --- aider/linter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/aider/linter.py b/aider/linter.py index 71bef5c80..8fbfb76c7 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -4,6 +4,7 @@ import sys import traceback import warnings from pathlib import Path +from dataclasses import dataclass from grep_ast import TreeContext, filename_to_lang from tree_sitter_languages import get_parser # noqa: E402 @@ -105,6 +106,12 @@ class Linter: return result +@dataclass +class LintResult: + text: str + lines: list + + def lint_python_compile(fname, code): try: compile(code, fname, "exec") # USE TRACEBACK BELOW HERE From 553f39a5291ce09a3d94659e69160c06729a5fca Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:47:46 -0700 Subject: [PATCH 072/102] Refactored linter to aggregate lint results and return a unified LintResult object. --- aider/linter.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index 8fbfb76c7..d42020dfe 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -86,32 +86,35 @@ class Linter: def py_lint(self, fname, rel_fname, code): result = '' - res = basic_lint(rel_fname, code) - if res: - result += res - - res = lint_python_compile(fname, code) - if res: - result += res + basic_res = basic_lint(rel_fname, code) + compile_res = lint_python_compile(fname, code) fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" flake8 = f"flake8 --select={fatal} --show-source" try: - res = self.run_cmd(flake8, rel_fname, code) + flake_res = self.run_cmd(flake8, rel_fname, code) except FileNotFoundError: - pass - if res: - result += res + flake_res = None - return result + text = '' + lines = set() + for res in [basic_res, compile_res, flake_res]: + if not res: + continue + if text: + text += '\n' + text += res.text + lines.update(res.lines) + + return LintResult(text, lines) @dataclass class LintResult: text: str lines: list - + def lint_python_compile(fname, code): try: compile(code, fname, "exec") # USE TRACEBACK BELOW HERE From 1a3ae1fb60c269fa13e646f165632d13012b910a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:48:36 -0700 Subject: [PATCH 073/102] Refactored Linter class to only return LintResult if text is not empty. --- aider/linter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index d42020dfe..e14b08430 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -107,7 +107,8 @@ class Linter: text += res.text lines.update(res.lines) - return LintResult(text, lines) + if text: + return LintResult(text, lines) @dataclass class LintResult: From 828e14fd3d560f5dc0f47c6395ffe09f4737bf60 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:48:53 -0700 Subject: [PATCH 074/102] Updated Linter to return LintResult if text or lines are present. --- aider/linter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/linter.py b/aider/linter.py index e14b08430..af7135652 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -107,7 +107,7 @@ class Linter: text += res.text lines.update(res.lines) - if text: + if text or lines: return LintResult(text, lines) @dataclass From a50f01a00489891fdc8b213b790171703394ebcf Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:49:10 -0700 Subject: [PATCH 075/102] Removed unnecessary import of Path from pathlib. --- aider/coders/wholefile_coder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aider/coders/wholefile_coder.py b/aider/coders/wholefile_coder.py index 4b244f637..8e9b25e1f 100644 --- a/aider/coders/wholefile_coder.py +++ b/aider/coders/wholefile_coder.py @@ -1,4 +1,3 @@ -from pathlib import Path from aider import diffs From 6fac50a3a13fe0ca4f42f10fbc3b8643aaf0e739 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:49:17 -0700 Subject: [PATCH 076/102] aider: Fix undefined name 'Path' errors by importing the `Path` class from the `pathlib` module. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: # Fix any errors below ## Running: flake8 --select=E9,F821,F823,F831,F406,F407,F701,F702,F704,F706 --show-source aider/coders/wholefile_coder.py aider/coders/wholefile_coder.py:73:62: F821 undefined name 'Path' if fname and fname not in chat_files and Path(fname).name in chat_files: ^ aider/coders/wholefile_coder.py:74:33: F821 undefined name 'Path' fname = Path(fname).name ^ aider/coders/wholefile_coder.py:103:30: F821 undefined name 'Path' full_path = (Path(self.root) / fname).absolute() ^ aider/coders/wholefile_coder.py:133:12: F821 undefined name 'Path' if Path(full_path).exists(): ^ ## See relevant lines below marked with █. aider/coders/wholefile_coder.py: ...⋮... 2│from aider import diffs 3│ ...⋮... 9│class WholeFileCoder(Coder): 10│ edit_format = "whole" 11│ ...⋮... 30│ def get_edits(self, mode="update"): 31│ content = self.partial_response_content 32│ ...⋮... 44│ for i, line in enumerate(lines): 45│ if line.startswith(self.fence[0]) or line.startswith(self.fence[1]): 46│ if fname is not None: 47│ # ending an existing block 48│ saw_fname = None 49│ 50│ full_path = self.abs_root_path(fname) 51│ 52│ if mode == "diff": 53│ output += self.do_live_diff(full_path, new_lines, True) 54│ else: 55│ edits.append((fname, fname_source, new_lines)) 56│ ...⋮... 63│ if i > 0: 64│ fname_source = "block" ...⋮... 70│ # Did gpt prepend a bogus dir? It especially likes to 71│ # include the path/to prefix from the one-shot example in 72│ # the prompt. 73█ if fname and fname not in chat_files and Path(fname).name in chat_files: 74█ fname = Path(fname).name 75│ if not fname: # blank line? or ``` was on first line i==0 76│ if saw_fname: 77│ fname = saw_fname ...⋮... 100│ if mode == "diff": 101│ if fname is not None: 102│ # ending an existing block 103█ full_path = (Path(self.root) / fname).absolute() 104│ output += self.do_live_diff(full_path, new_lines, False) 105│ return "\n".join(output) 106│ ...⋮... 130│ self.io.write_text(full_path, new_lines) 131│ 132│ def do_live_diff(self, full_path, new_lines, final): 133█ if Path(full_path).exists(): 134│ orig_lines = self.io.read_text(full_path).splitlines(keepends=True) 135│ 136│ show_diff = diffs.diff_partial_update( 137│ orig_lines, 138│ new_lines, 139│ final=final, 140│ ).splitlines() 141│ output = show_diff 142│ else: ...⋮... ASSISTANT: To fix the errors, we need to import the `Path` class from the `pathlib` module. Here are the necessary changes: aider/coders/wholefile_coder.py python <<<<<<< SEARCH from aider import diffs ======= from aider import diffs from pathlib import Path >>>>>>> REPLACE aider/coders/wholefile_coder.py python <<<<<<< SEARCH if fname and fname not in chat_files and Path(fname).name in chat_files: fname = Path(fname).name ======= if fname and fname not in chat_files and Path(fname).name in chat_files: fname = Path(fname).name >>>>>>> REPLACE aider/coders/wholefile_coder.py python <<<<<<< SEARCH full_path = (Path(self.root) / fname).absolute() ======= full_path = (Path(self.root) / fname).absolute() >>>>>>> REPLACE aider/coders/wholefile_coder.py python <<<<<<< SEARCH if Path(full_path).exists(): ======= if Path(full_path).exists(): >>>>>>> REPLACE --- aider/coders/wholefile_coder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aider/coders/wholefile_coder.py b/aider/coders/wholefile_coder.py index 8e9b25e1f..b9ecc60b5 100644 --- a/aider/coders/wholefile_coder.py +++ b/aider/coders/wholefile_coder.py @@ -1,5 +1,6 @@ from aider import diffs +from pathlib import Path from ..dump import dump # noqa: F401 from .base_coder import Coder From 06bda2d730835a13b191f87fc8d4b27083f93728 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:52:21 -0700 Subject: [PATCH 077/102] aider: Added a `--lint-cmd` argument to allow users to specify a lint command multiple times. # Aider chat conversation: USER: add a --lint-cmd arg that takes a string like "python python-linter --with --args". allow the user to specify this args multiple times ASSISTANT: To add a `--lint-cmd` argument that takes a string and allows the user to specify this argument multiple times, we need to: 1. Add the new argument to the parser. 2. Ensure it can be specified multiple times. Here is the *SEARCH/REPLACE* block for `aider/args.py`: aider/args.py ```python <<<<<<< SEARCH ) ======= ) ########## group = parser.add_argument_group("Lint Settings") group.add_argument( "--lint-cmd", action="append", metavar="LINT_CMD", help="Specify a lint command to run. Can be specified multiple times.", ) >>>>>>> REPLACE ``` --- aider/args.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/aider/args.py b/aider/args.py index e7da13913..1d7d3fc51 100644 --- a/aider/args.py +++ b/aider/args.py @@ -14,6 +14,15 @@ def get_parser(default_config_files, git_root): config_file_parser_class=configargparse.YAMLConfigFileParser, auto_env_var_prefix="AIDER_", ) + + ########## + group = parser.add_argument_group("Lint Settings") + group.add_argument( + "--lint-cmd", + action="append", + metavar="LINT_CMD", + help="Specify a lint command to run. Can be specified multiple times.", + ) ########## group = parser.add_argument_group("Main") From 73919037595fff7277404a1e62af962c1333db65 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 20:57:01 -0700 Subject: [PATCH 078/102] Removed redundant lint command option and added support for specifying lint commands for different languages. --- aider/args.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/aider/args.py b/aider/args.py index 1d7d3fc51..018e42ca6 100644 --- a/aider/args.py +++ b/aider/args.py @@ -14,15 +14,6 @@ def get_parser(default_config_files, git_root): config_file_parser_class=configargparse.YAMLConfigFileParser, auto_env_var_prefix="AIDER_", ) - - ########## - group = parser.add_argument_group("Lint Settings") - group.add_argument( - "--lint-cmd", - action="append", - metavar="LINT_CMD", - help="Specify a lint command to run. Can be specified multiple times.", - ) ########## group = parser.add_argument_group("Main") @@ -321,11 +312,17 @@ def get_parser(default_config_files, git_root): default=False, ) group.add_argument( - "--lint-and-commit", + "--lint", action="store_true", help="Commit, run the linter on all dirty files, fix problems and commit again", default=False, ) + group.add_argument( + "--lint-cmd", + action="append", + help='Specify lint commands to run for different languages, eg: "python: flake8 --select=..." (can be used multiple times)', + ) + ########## group = parser.add_argument_group("Other Settings") From 5a6e72a64b83c22d6cb63004afa01e7ed8565a6f Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 21:00:45 -0700 Subject: [PATCH 079/102] Updated linting functionality to only run linting without committing. --- aider/linter.py | 2 -- aider/main.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/aider/linter.py b/aider/linter.py index af7135652..95f436c4f 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -20,8 +20,6 @@ class Linter: self.languages = dict( python=self.py_lint, - #python="/Users/gauthier/Projects/aider/tmp.sh", - #python="flake8 --show-source", ) def set_linter(self, lang, cmd): diff --git a/aider/main.py b/aider/main.py index 360749ddd..34aca61ca 100644 --- a/aider/main.py +++ b/aider/main.py @@ -355,7 +355,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.commands.cmd_commit("") return - if args.lint_and_commit: + if args.lint: coder.commands.cmd_lint("") return From 38de35e924fb83f49c366d1e45870aeb12ffe38e Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 21:04:47 -0700 Subject: [PATCH 080/102] Set linters based on provided commands in args.lint_cmd. --- aider/main.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/aider/main.py b/aider/main.py index 34aca61ca..473c2c0b1 100644 --- a/aider/main.py +++ b/aider/main.py @@ -343,6 +343,19 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.show_announcements() + for lint_cmd in args.lint_cmd: + pieces = lint_cmd.split(':') + lang = pieces[0] + cmd = lint_cmd[len(lang)+1:] + + lang = lang.strip() + cmd = cmd.strip() + + if lang and cmd: + coder.linter.set_linter(lang, cmd) + else: + io.tool_error(f"Unable to parse --lang-cmd {lang_cmd}") + if args.show_prompts: coder.cur_messages += [ dict(role="user", content="Hello!"), From fec1a646d5ed010a61e2cb9f3df5bfc416e4610b Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 21:06:30 -0700 Subject: [PATCH 081/102] Refactored linting process to handle FileNotFoundError exceptions. --- aider/commands.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/aider/commands.py b/aider/commands.py index 09e22679a..10585e53c 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -170,7 +170,13 @@ class Commands: self.cmd_commit("") for fname in fnames: - errors = self.coder.linter.lint(fname) + try: + errors = self.coder.linter.lint(fname) + except FileNotFoundError as err: + self.io.tool_error(f"Unable to lint {fname}") + self.io.tool_error(err) + continue + if errors: self.io.tool_error(errors) From d79a629db0a1d51289a2a8d0a8e2c3e96f7ed4b9 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 21:06:45 -0700 Subject: [PATCH 082/102] Fix tool error message to display the correct error information. --- aider/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/commands.py b/aider/commands.py index 10585e53c..7abd0a831 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -174,7 +174,7 @@ class Commands: errors = self.coder.linter.lint(fname) except FileNotFoundError as err: self.io.tool_error(f"Unable to lint {fname}") - self.io.tool_error(err) + self.io.tool_error(str(err)) continue if errors: From 6474139d04d339014c353b5fd70b92e449fd779c Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sat, 18 May 2024 21:13:19 -0700 Subject: [PATCH 083/102] Lint files before fixing errors and commit changes if necessary. --- aider/args.py | 1 + aider/commands.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/aider/args.py b/aider/args.py index 018e42ca6..0b8ed74d1 100644 --- a/aider/args.py +++ b/aider/args.py @@ -321,6 +321,7 @@ def get_parser(default_config_files, git_root): "--lint-cmd", action="append", help='Specify lint commands to run for different languages, eg: "python: flake8 --select=..." (can be used multiple times)', + default=[], ) diff --git a/aider/commands.py b/aider/commands.py index 7abd0a831..076d5dcf2 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -165,26 +165,28 @@ class Commands: return fnames = self.coder.repo.get_dirty_files() - - if fnames: - self.cmd_commit("") - + linted = False for fname in fnames: try: errors = self.coder.linter.lint(fname) + linted = True except FileNotFoundError as err: self.io.tool_error(f"Unable to lint {fname}") self.io.tool_error(str(err)) continue if errors: + # Commit everything before we start fixing lint errors + if self.coder.repo.is_dirty(): + self.cmd_commit("") + self.io.tool_error(errors) abs_file_path = self.coder.abs_root_path(fname) self.coder.abs_fnames.add(abs_file_path) self.coder.run(errors) - if self.coder.repo.is_dirty(): + if linted and self.coder.repo.is_dirty(): self.cmd_commit("") def cmd_clear(self, args): From 73dd873f862e730237dda4857e95e4c3e01fdce2 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 06:23:20 -0700 Subject: [PATCH 084/102] Add option to enable/disable automatic linting after changes. --- aider/args.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aider/args.py b/aider/args.py index 0b8ed74d1..cb25daf5e 100644 --- a/aider/args.py +++ b/aider/args.py @@ -14,6 +14,12 @@ def get_parser(default_config_files, git_root): config_file_parser_class=configargparse.YAMLConfigFileParser, auto_env_var_prefix="AIDER_", ) + group.add_argument( + "--auto-lint", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable automatic linting after changes (default: True)", + ) ########## group = parser.add_argument_group("Main") From 76038e69d7f20c49aec844dedd66115e60b42499 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 06:23:27 -0700 Subject: [PATCH 085/102] Added an argument for enabling/disabling automatic linting after changes. --- aider/args.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aider/args.py b/aider/args.py index cb25daf5e..3766f845f 100644 --- a/aider/args.py +++ b/aider/args.py @@ -14,14 +14,13 @@ def get_parser(default_config_files, git_root): config_file_parser_class=configargparse.YAMLConfigFileParser, auto_env_var_prefix="AIDER_", ) + ########## group.add_argument( "--auto-lint", action=argparse.BooleanOptionalAction, default=True, help="Enable/disable automatic linting after changes (default: True)", ) - - ########## group = parser.add_argument_group("Main") group.add_argument( "files", From 398a1300dc5a0bf94c49c825999b05829bc17c49 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 06:23:28 -0700 Subject: [PATCH 086/102] aider: Added --auto-lint/--no-auto-lint switch to the parser in aider/args.py. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: add a --auto-lint/--no-auto-lint switch ASSISTANT: To add the `--auto-lint/--no-auto-lint` switch, we need to: 1. Add the new argument to the parser in `aider/args.py`. Here is the *SEARCH/REPLACE* block: aider/args.py ```python <<<<<<< SEARCH ) ======= ) group.add_argument( "--auto-lint", action=argparse.BooleanOptionalAction, default=True, help="Enable/disable automatic linting after changes (default: True)", ) >>>>>>> REPLACE ``` USER: # Fix any errors below ## Running: flake8 --select=E9,F821,F823,F831,F406,F407,F701,F702,F704,F706 --show-source aider/args.py aider/args.py:17:5: F821 undefined name 'group' group.add_argument( ^ ## See relevant line below marked with █. /Users/gauthier/Projects/aider/aider/args.py: ...⋮... 9│def get_parser(default_config_files, git_root): 10│ parser = configargparse.ArgumentParser( 11│ description="aider is GPT powered coding in your terminal", 12│ add_config_file_help=True, 13│ default_config_files=default_config_files, 14│ config_file_parser_class=configargparse.YAMLConfigFileParser, 15│ auto_env_var_prefix="AIDER_", 16│ ) 17█ group.add_argument( 18│ "--auto-lint", 19│ action=argparse.BooleanOptionalAction, 20│ default=True, 21│ help="Enable/disable automatic linting after changes (default: True)", ...⋮... ASSISTANT: To fix the error, we need to move the `--auto-lint` argument addition to the correct location within the `get_parser` function, after the `group` variable is defined. Here is the corrected *SEARCH/REPLACE* block: aider/args.py ```python <<<<<<< SEARCH group.add_argument( "--auto-lint", action=argparse.BooleanOptionalAction, default=True, help="Enable/disable automatic linting after changes (default: True)", ) ########## ======= ########## group.add_argument( "--auto-lint", action=argparse.BooleanOptionalAction, default=True, help="Enable/disable automatic linting after changes (default: True)", ) >>>>>>> REPLACE ``` USER: # Fix any errors below ## Running: flake8 --select=E9,F821,F823,F831,F406,F407,F701,F702,F704,F706 --show-source aider/args.py aider/args.py:18:5: F821 undefined name 'group' group.add_argument( ^ ## See relevant line below marked with █. /Users/gauthier/Projects/aider/aider/args.py: ...⋮... 9│def get_parser(default_config_files, git_root): 10│ parser = configargparse.ArgumentParser( 11│ description="aider is GPT powered coding in your terminal", 12│ add_config_file_help=True, 13│ default_config_files=default_config_files, 14│ config_file_parser_class=configargparse.YAMLConfigFileParser, 15│ auto_env_var_prefix="AIDER_", 16│ ) 17│ ########## 18█ group.add_argument( 19│ "--auto-lint", 20│ action=argparse.BooleanOptionalAction, 21│ default=True, 22│ help="Enable/disable automatic linting after changes (default: True)", ...⋮... ASSISTANT: The error is occurring because the `group` variable is not defined before it is used. We need to place the `--auto-lint` argument addition after the `group` variable is defined. Here is the corrected *SEARCH/REPLACE* block: aider/args.py ```python <<<<<<< SEARCH ########## group.add_argument( "--auto-lint", action=argparse.BooleanOptionalAction, default=True, help="Enable/disable automatic linting after changes (default: True)", ) ======= group = parser.add_argument_group("Main") >>>>>>> REPLACE ``` aider/args.py ```python <<<<<<< SEARCH group.add_argument( ======= group.add_argument( "--auto-lint", action=argparse.BooleanOptionalAction, default=True, help="Enable/disable automatic linting after changes (default: True)", ) group.add_argument( >>>>>>> REPLACE ``` --- aider/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aider/args.py b/aider/args.py index 3766f845f..1689621a2 100644 --- a/aider/args.py +++ b/aider/args.py @@ -14,14 +14,14 @@ def get_parser(default_config_files, git_root): config_file_parser_class=configargparse.YAMLConfigFileParser, auto_env_var_prefix="AIDER_", ) - ########## + group = parser.add_argument_group("Main") + group = parser.add_argument_group("Main") group.add_argument( "--auto-lint", action=argparse.BooleanOptionalAction, default=True, help="Enable/disable automatic linting after changes (default: True)", ) - group = parser.add_argument_group("Main") group.add_argument( "files", metavar="FILE", From 9ee332f5d9adfb05292b33f2d65c15d1aed7e012 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 07:34:19 -0700 Subject: [PATCH 087/102] Added options for automatic linting and testing after changes. --- aider/args.py | 40 +++++++++++++++++++++++++++--------- aider/coders/base_coder.py | 42 +++++++++++++++++++++++++++++++++++--- aider/commands.py | 2 +- aider/linter.py | 40 +++++++++++++++++++++--------------- aider/main.py | 26 +++++++++++------------ 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/aider/args.py b/aider/args.py index 1689621a2..a528c78d0 100644 --- a/aider/args.py +++ b/aider/args.py @@ -15,13 +15,6 @@ def get_parser(default_config_files, git_root): auto_env_var_prefix="AIDER_", ) group = parser.add_argument_group("Main") - group = parser.add_argument_group("Main") - group.add_argument( - "--auto-lint", - action=argparse.BooleanOptionalAction, - default=True, - help="Enable/disable automatic linting after changes (default: True)", - ) group.add_argument( "files", metavar="FILE", @@ -310,6 +303,7 @@ def get_parser(default_config_files, git_root): default=False, help="Perform a dry run without modifying files (default: False)", ) + group = parser.add_argument_group("Fixing and committing") group.add_argument( "--commit", action="store_true", @@ -319,16 +313,42 @@ def get_parser(default_config_files, git_root): group.add_argument( "--lint", action="store_true", - help="Commit, run the linter on all dirty files, fix problems and commit again", + help="Run the linter on all dirty files, fix problems and commit", default=False, ) group.add_argument( "--lint-cmd", action="append", - help='Specify lint commands to run for different languages, eg: "python: flake8 --select=..." (can be used multiple times)', + help=( + 'Specify lint commands to run for different languages, eg: "python: flake8' + ' --select=..." (can be used multiple times)' + ), default=[], ) - + group.add_argument( + "--auto-lint", + action=argparse.BooleanOptionalAction, + default=True, + help="Enable/disable automatic linting after changes (default: True)", + ) + group.add_argument( + "--test-cmd", + action="append", + help="Specify command to run tests", + default=[], + ) + group.add_argument( + "--auto-test", + action=argparse.BooleanOptionalAction, + default=False, + help="Enable/disable automatic testing after changes (default: False)", + ) + group.add_argument( + "--test", + action="store_true", + help="Run tests and fix problems found", + default=False, + ) ########## group = parser.add_argument_group("Other Settings") diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index ccf7854f3..6a1991222 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -59,6 +59,9 @@ class Coder: max_reflections = 5 edit_format = None yield_stream = False + auto_lint = True + auto_test = False + test_cmd = None @classmethod def create( @@ -195,6 +198,10 @@ class Coder: done_messages=None, max_chat_history_tokens=None, restore_chat_history=False, + auto_lint=True, + auto_test=False, + lint_cmds=None, + test_cmd=None, ): if not fnames: fnames = [] @@ -289,8 +296,6 @@ class Coder: self.verbose, ) - self.linter = Linter(root=self.root, encoding=io.encoding) - if max_chat_history_tokens is None: max_chat_history_tokens = self.main_model.max_chat_history_tokens self.summarizer = ChatSummary( @@ -307,6 +312,14 @@ class Coder: self.done_messages = utils.split_chat_history_markdown(history_md) self.summarize_start() + # Linting and testing + self.linter = Linter(root=self.root, encoding=io.encoding) + self.auto_lint = auto_lint + self.setup_lint_cmds(lint_cmds) + + self.auto_test = auto_test + self.test_cmd = test_cmd + # validate the functions jsonschema if self.functions: for function in self.functions: @@ -316,6 +329,22 @@ class Coder: self.io.tool_output("JSON Schema:") self.io.tool_output(json.dumps(self.functions, indent=4)) + def setup_lint_cmds(self, lint_cmds): + for lint_cmd in lint_cmds: + pieces = lint_cmd.split(":") + lang = pieces[0] + cmd = lint_cmd[len(lang) + 1 :] + + lang = lang.strip() + cmd = cmd.strip() + + if lang and cmd: + self.linter.set_linter(lang, cmd) + else: + self.io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"') + self.io.tool_error(f'The arg should be "language: cmd --args ..."') + self.io.tool_error('For example: --lint-cmd "python: flake8 --select=E9"') + def show_announcements(self): for line in self.get_announcements(): self.io.tool_output(line) @@ -737,13 +766,20 @@ class Coder: self.update_cur_messages(set()) return - if edited: + if edited and self.auto_lint: lint_errors = self.lint_edited(edited) if lint_errors: self.reflected_message = lint_errors self.update_cur_messages(set()) return + if edited and self.auto_test: + test_errors = self.commands.cmd_test(self.test_cmd) + if test_errors: + self.reflected_message = test_errors + self.update_cur_messages(set()) + return + self.update_cur_messages(edited) if edited: diff --git a/aider/commands.py b/aider/commands.py index 076d5dcf2..95f9b67b8 100644 --- a/aider/commands.py +++ b/aider/commands.py @@ -168,7 +168,7 @@ class Commands: linted = False for fname in fnames: try: - errors = self.coder.linter.lint(fname) + errors = self.coder.linter.lint(fname, cmd=args) linted = True except FileNotFoundError as err: self.io.tool_error(f"Unable to lint {fname}") diff --git a/aider/linter.py b/aider/linter.py index 95f436c4f..a18ff963d 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -3,8 +3,8 @@ import subprocess import sys import traceback import warnings -from pathlib import Path from dataclasses import dataclass +from pathlib import Path from grep_ast import TreeContext, filename_to_lang from tree_sitter_languages import get_parser # noqa: E402 @@ -51,19 +51,21 @@ class Linter: filenames_linenums = find_filenames_and_linenums(errors, [rel_fname]) if filenames_linenums: filename, linenums = next(iter(filenames_linenums.items())) - linenums = [num-1 for num in linenums] + linenums = [num - 1 for num in linenums] return LintResult(text=res, lines=linenums) - def lint(self, fname): - lang = filename_to_lang(fname) - if not lang: - return - + def lint(self, fname, cmd=None): rel_fname = self.get_rel_fname(fname) code = Path(fname).read_text(self.encoding) - cmd = self.languages.get(lang) + if cmd: + cmd = cmd.strip() + if not cmd: + lang = filename_to_lang(fname) + if not lang: + return + cmd = self.languages.get(lang) if callable(cmd): linkres = cmd(fname, rel_fname, code) @@ -75,15 +77,15 @@ class Linter: if not linkres: return - res = '# Fix any errors below\n\n' + res = "# Fix any errors below, if possible.\n\n" res += linkres.text - res += '\n' + res += "\n" res += tree_context(fname, code, linkres.lines) return res def py_lint(self, fname, rel_fname, code): - result = '' + result = "" basic_res = basic_lint(rel_fname, code) compile_res = lint_python_compile(fname, code) @@ -95,19 +97,20 @@ class Linter: except FileNotFoundError: flake_res = None - text = '' + text = "" lines = set() for res in [basic_res, compile_res, flake_res]: if not res: continue if text: - text += '\n' + text += "\n" text += res.text lines.update(res.lines) if text or lines: return LintResult(text, lines) + @dataclass class LintResult: text: str @@ -134,7 +137,7 @@ def lint_python_compile(fname, code): tb_lines = tb_lines[:1] + tb_lines[last_file_i + 1 :] res = "".join(tb_lines) - return LintResult(text = res, lines = line_numbers) + return LintResult(text=res, lines=line_numbers) def basic_lint(fname, code): @@ -153,7 +156,7 @@ def basic_lint(fname, code): if not errors: return - return LintResult(text = '', lines = errors) + return LintResult(text="", lines=errors) def tree_context(fname, code, line_nums): @@ -193,23 +196,26 @@ def traverse_tree(node): return errors + import re + def find_filenames_and_linenums(text, fnames): """ Search text for all occurrences of :\d+ and make a list of them where is one of the filenames in the list `fnames`. """ - pattern = re.compile(r'(\b(?:' + '|'.join(re.escape(fname) for fname in fnames) + r'):\d+\b)') + pattern = re.compile(r"(\b(?:" + "|".join(re.escape(fname) for fname in fnames) + r"):\d+\b)") matches = pattern.findall(text) result = {} for match in matches: - fname, linenum = match.rsplit(':', 1) + fname, linenum = match.rsplit(":", 1) if fname not in result: result[fname] = set() result[fname].add(int(linenum)) return result + def main(): """ Main function to parse files provided as command line arguments. diff --git a/aider/main.py b/aider/main.py index 473c2c0b1..e09275209 100644 --- a/aider/main.py +++ b/aider/main.py @@ -332,6 +332,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F aider_ignore_file=args.aiderignore, max_chat_history_tokens=args.max_chat_history_tokens, restore_chat_history=args.restore_chat_history, + auto_lint=args.auto_lint, + auto_test=args.auto_test, + lint_cmds=args.lint_cmd, + test_cmd=args.test_cmd, ) except ValueError as err: @@ -343,19 +347,6 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.show_announcements() - for lint_cmd in args.lint_cmd: - pieces = lint_cmd.split(':') - lang = pieces[0] - cmd = lint_cmd[len(lang)+1:] - - lang = lang.strip() - cmd = cmd.strip() - - if lang and cmd: - coder.linter.set_linter(lang, cmd) - else: - io.tool_error(f"Unable to parse --lang-cmd {lang_cmd}") - if args.show_prompts: coder.cur_messages += [ dict(role="user", content="Hello!"), @@ -372,6 +363,15 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F coder.commands.cmd_lint("") return + if args.test: + if not args.test_cmd: + io.tool_error("No --test-cmd provided.") + return 1 + test_errors = coder.commands.cmd_test(args.test_cmd) + if test_errors: + coder.run(test_errors) + return + if args.show_repo_map: repo_map = coder.get_repo_map() if repo_map: From 990dda1122a0d87b0f6da7ee1edffa6b6cc783e1 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 07:35:27 -0700 Subject: [PATCH 088/102] Added empty lines for better readability. --- aider/coders/base_coder.py | 1 + aider/linter.py | 1 + 2 files changed, 2 insertions(+) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 6a1991222..6ccc7c40c 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -39,6 +39,7 @@ class ExhaustedContextWindow(Exception): pass + def wrap_fence(name): return f"<{name}>", f"" diff --git a/aider/linter.py b/aider/linter.py index a18ff963d..4ca1ee17d 100644 --- a/aider/linter.py +++ b/aider/linter.py @@ -13,6 +13,7 @@ from tree_sitter_languages import get_parser # noqa: E402 warnings.simplefilter("ignore", category=FutureWarning) + class Linter: def __init__(self, encoding="utf-8", root=None): self.encoding = encoding From 9bd78d7ff331095416f8ab7d51ef9066e26dad62 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 07:40:41 -0700 Subject: [PATCH 089/102] Refactored lint command parsing and setup logic. --- aider/coders/base_coder.py | 16 ++-------------- aider/main.py | 28 +++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 6ccc7c40c..18f78d633 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -331,20 +331,8 @@ class Coder: self.io.tool_output(json.dumps(self.functions, indent=4)) def setup_lint_cmds(self, lint_cmds): - for lint_cmd in lint_cmds: - pieces = lint_cmd.split(":") - lang = pieces[0] - cmd = lint_cmd[len(lang) + 1 :] - - lang = lang.strip() - cmd = cmd.strip() - - if lang and cmd: - self.linter.set_linter(lang, cmd) - else: - self.io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"') - self.io.tool_error(f'The arg should be "language: cmd --args ..."') - self.io.tool_error('For example: --lint-cmd "python: flake8 --select=E9"') + for lang,cmd in lint_cmds.items(): + self.linter.set_linter(lang, cmd) def show_announcements(self): for line in self.get_announcements(): diff --git a/aider/main.py b/aider/main.py index e09275209..ce51c7515 100644 --- a/aider/main.py +++ b/aider/main.py @@ -177,6 +177,28 @@ def launch_gui(args): # sys.argv = ['streamlit', 'run', '--'] + args +def parse_lint_cmds(lint_cmds, io): + err = False + res = dict() + for lint_cmd in lint_cmds: + pieces = lint_cmd.split(":") + lang = pieces[0] + cmd = lint_cmd[len(lang) + 1 :] + + lang = lang.strip() + cmd = cmd.strip() + + if lang and cmd: + res[lang] = cmd + else: + io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"') + io.tool_error(f'The arg should be "language: cmd --args ..."') + io.tool_error('For example: --lint-cmd "python: flake8 --select=E9"') + err = True + if err: + return + return res + def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): if argv is None: argv = sys.argv[1:] @@ -306,6 +328,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F main_model = models.Model(args.model, weak_model=args.weak_model) + lint_cmds = parse_lint_cmds(args.lint_cmd, io) + if lint_cmds is None: + return 1 + if args.show_model_warnings: models.sanity_check_models(io, main_model) @@ -334,7 +360,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F restore_chat_history=args.restore_chat_history, auto_lint=args.auto_lint, auto_test=args.auto_test, - lint_cmds=args.lint_cmd, + lint_cmds=lint_cmds, test_cmd=args.test_cmd, ) From 25e44f584b430d43dd5198c66548bb72c733ad5d Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 07:40:48 -0700 Subject: [PATCH 090/102] aider: Fixed extra blank lines and added missing whitespace after a comma. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: # Fix any errors below, if possible. ## Running: flake8 aider/coders/base_coder.py aider/coders/base_coder.py:43:1: E303 too many blank lines (3) aider/coders/base_coder.py:334:17: E231 missing whitespace after ',' ## See relevant lines below marked with █. aider/coders/base_coder.py: ...⋮... 40│ 41│ 42│ 43█def wrap_fence(name): 44│ return f"<{name}>", f"" 45│ 46│ 47│class Coder: 48│ abs_fnames = None ...⋮... 331│ self.io.tool_output(json.dumps(self.functions, indent=4)) 332│ 333│ def setup_lint_cmds(self, lint_cmds): 334█ for lang,cmd in lint_cmds.items(): 335│ self.linter.set_linter(lang, cmd) 336│ 337│ def show_announcements(self): ...⋮... ASSISTANT: Here are the necessary changes to fix the flake8 errors: 1. Remove the extra blank lines before the `wrap_fence` function. 2. Add a missing whitespace after the comma in the `setup_lint_cmds` function. aider/coders/base_coder.py ```python <<<<<<< SEARCH class ExhaustedContextWindow(Exception): pass def wrap_fence(name): ======= class ExhaustedContextWindow(Exception): pass def wrap_fence(name): >>>>>>> REPLACE ``` aider/coders/base_coder.py ```python <<<<<<< SEARCH for lang,cmd in lint_cmds.items(): ======= for lang, cmd in lint_cmds.items(): >>>>>>> REPLACE ``` --- aider/coders/base_coder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 18f78d633..d24c1e56b 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -39,7 +39,6 @@ class ExhaustedContextWindow(Exception): pass - def wrap_fence(name): return f"<{name}>", f"" @@ -331,7 +330,7 @@ class Coder: self.io.tool_output(json.dumps(self.functions, indent=4)) def setup_lint_cmds(self, lint_cmds): - for lang,cmd in lint_cmds.items(): + for lang, cmd in lint_cmds.items(): self.linter.set_linter(lang, cmd) def show_announcements(self): From afd827b9d9114d0dc5e4da04f575ac93283bae7a Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 07:40:54 -0700 Subject: [PATCH 091/102] aider: Fixed f-string error and added missing blank line before the `main` function. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Aider chat conversation: USER: # Fix any errors below, if possible. ## Running: flake8 aider/main.py aider/main.py:195:27: F541 f-string is missing placeholders aider/main.py:202:1: E302 expected 2 blank lines, found 1 ## See relevant lines below marked with █. aider/main.py: ...⋮... 180│def parse_lint_cmds(lint_cmds, io): 181│ err = False 182│ res = dict() 183│ for lint_cmd in lint_cmds: 184│ pieces = lint_cmd.split(":") ...⋮... 191│ if lang and cmd: 192│ res[lang] = cmd 193│ else: 194│ io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"') 195█ io.tool_error(f'The arg should be "language: cmd --args ..."') 196│ io.tool_error('For example: --lint-cmd "python: flake8 --select=E9"') 197│ err = True 198│ if err: 199│ return 200│ return res 201│ 202█def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): 203│ if argv is None: 204│ argv = sys.argv[1:] 205│ ...⋮... ASSISTANT: Here are the necessary changes to fix the flake8 errors: 1. Correct the f-string to not use placeholders when not needed. 2. Add the missing blank line before the `main` function. aider/main.py ```python <<<<<<< SEARCH io.tool_error(f'The arg should be "language: cmd --args ..."') ======= io.tool_error('The arg should be "language: cmd --args ..."') >>>>>>> REPLACE ``` aider/main.py ```python <<<<<<< SEARCH return res ======= return res >>>>>>> REPLACE ``` --- aider/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aider/main.py b/aider/main.py index ce51c7515..ad4c55c3d 100644 --- a/aider/main.py +++ b/aider/main.py @@ -192,13 +192,14 @@ def parse_lint_cmds(lint_cmds, io): res[lang] = cmd else: io.tool_error(f'Unable to parse --lint-cmd "{lint_cmd}"') - io.tool_error(f'The arg should be "language: cmd --args ..."') + io.tool_error('The arg should be "language: cmd --args ..."') io.tool_error('For example: --lint-cmd "python: flake8 --select=E9"') err = True if err: return return res + def main(argv=None, input=None, output=None, force_git_root=None, return_coder=False): if argv is None: argv = sys.argv[1:] From 0b18f3b4efd03d94ca2a1a0155336f40f7c652bc Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 07:42:39 -0700 Subject: [PATCH 092/102] Add check to skip setting up lint commands if none are provided. --- aider/coders/base_coder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index d24c1e56b..7a4eb0fa3 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -330,6 +330,8 @@ class Coder: self.io.tool_output(json.dumps(self.functions, indent=4)) def setup_lint_cmds(self, lint_cmds): + if not lint_cmds: + return for lang, cmd in lint_cmds.items(): self.linter.set_linter(lang, cmd) From 41c678da0995562cfb1c2d0d59c17d490c0e2713 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 07:56:13 -0700 Subject: [PATCH 093/102] +litellm.suppress_debug_info --- aider/litellm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aider/litellm.py b/aider/litellm.py index ff6d2d528..f6faa403b 100644 --- a/aider/litellm.py +++ b/aider/litellm.py @@ -8,4 +8,6 @@ os.environ["OR_APP_NAME"] = "Aider" import litellm # noqa: E402 +litellm.suppress_debug_info = True + __all__ = [litellm] From cb6e7cabb66f29880b8794f50e83bd8fb1f469b3 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 08:13:20 -0700 Subject: [PATCH 094/102] Added lint/test_outcome --- aider/coders/base_coder.py | 13 +++++++++++-- requirements.in | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 7a4eb0fa3..f31251f2a 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -62,6 +62,8 @@ class Coder: auto_lint = True auto_test = False test_cmd = None + lint_outcome = None + test_outcome = None @classmethod def create( @@ -506,13 +508,18 @@ class Coder: def run_stream(self, user_message): self.io.user_input(user_message) + self.init_before_message() + yield from self.send_new_user_message(user_message) + + def init_before_message(self): self.reflected_message = None self.num_reflections = 0 - yield from self.send_new_user_message(user_message) + self.lint_outcome = None + self.test_outcome = None def run(self, with_message=None): while True: - self.num_reflections = 0 + self.init_before_message() try: if with_message: new_user_message = with_message @@ -758,6 +765,7 @@ class Coder: if edited and self.auto_lint: lint_errors = self.lint_edited(edited) + self.lint_outcome = not lint_errors if lint_errors: self.reflected_message = lint_errors self.update_cur_messages(set()) @@ -765,6 +773,7 @@ class Coder: if edited and self.auto_test: test_errors = self.commands.cmd_test(self.test_cmd) + self.test_outcome = not test_errors if test_errors: self.reflected_message = test_errors self.update_cur_messages(set()) diff --git a/requirements.in b/requirements.in index 6f3eb5ba4..1da3cbafa 100644 --- a/requirements.in +++ b/requirements.in @@ -26,6 +26,7 @@ pypandoc litellm google-generativeai streamlit +flake8 # v3.3 no longer works on python 3.9 networkx<3.3 From ec7af731e03df83f65b3f97d7247dfb0645a4966 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 10:26:06 -0700 Subject: [PATCH 095/102] confirm auto lint and test --- aider/coders/base_coder.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index f31251f2a..d85182357 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -767,17 +767,21 @@ class Coder: lint_errors = self.lint_edited(edited) self.lint_outcome = not lint_errors if lint_errors: - self.reflected_message = lint_errors - self.update_cur_messages(set()) - return + ok = self.io.confirm_ask("Attempt to fix lint errors?") + if ok: + self.reflected_message = lint_errors + self.update_cur_messages(set()) + return if edited and self.auto_test: test_errors = self.commands.cmd_test(self.test_cmd) self.test_outcome = not test_errors if test_errors: - self.reflected_message = test_errors - self.update_cur_messages(set()) - return + ok = self.io.confirm_ask("Attempt to fix test errors?") + if ok: + self.reflected_message = test_errors + self.update_cur_messages(set()) + return self.update_cur_messages(edited) From 5641483a3b34828731a76deb6e8bdce47ae0cee8 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 10:31:36 -0700 Subject: [PATCH 096/102] S/R will replace all matches --- aider/coders/editblock_prompts.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aider/coders/editblock_prompts.py b/aider/coders/editblock_prompts.py index 08b985089..5f3a7b1fb 100644 --- a/aider/coders/editblock_prompts.py +++ b/aider/coders/editblock_prompts.py @@ -124,6 +124,9 @@ Every *SEARCH/REPLACE block* must use this format: Every *SEARCH* section must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc. +*SEARCH/REPLACE* blocks will replace *all* matching occurrences. +Include enough lines to ensure unique matches if needed. + Include *ALL* the code being searched and replaced! Only *SEARCH/REPLACE* files that the user says you can edit. From ca18220b7715ba8657c9adbf799411c343eb7af4 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 14:19:06 -0700 Subject: [PATCH 097/102] num_with_malformed_responses --- benchmark/benchmark.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index a729e0c19..6261e00f9 100755 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -317,6 +317,7 @@ def summarize_results(dirname): res.test_timeouts = 0 res.exhausted_context_windows = 0 res.num_malformed_responses = 0 + res.num_with_malformed_responses = 0 res.syntax_errors = 0 res.indentation_errors = 0 res.lazy_comments = 0 @@ -342,6 +343,8 @@ def summarize_results(dirname): res.user_asks += results.get("num_user_asks", 0) res.exhausted_context_windows += results.get("num_exhausted_context_windows", 0) res.num_malformed_responses += results.get("num_malformed_responses", 0) + if results.get("num_malformed_responses"): + res.num_with_malformed_responses += 1 res.lazy_comments += results.get("lazy_comments", 0) res.syntax_errors += results.get("syntax_errors", 0) @@ -392,11 +395,12 @@ def summarize_results(dirname): for i in range(tries): print(f" pass_rate_{i+1}: {percents[i]:.1f}") - pct_well_formed = 1.0 - res.num_malformed_responses / res.completed_tests + pct_well_formed = 1.0 - res.num_with_malformed_responses / res.completed_tests print(f" percent_cases_well_formed: {pct_well_formed*100:.1f}") show("error_outputs") show("num_malformed_responses") + show("num_with_malformed_responses") show("user_asks") show("lazy_comments") show("syntax_errors") From 642be2a8f0757e330cda33f23294a727c5c80119 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 14:22:29 -0700 Subject: [PATCH 098/102] S/R uniq prompt --- aider/coders/editblock_prompts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/coders/editblock_prompts.py b/aider/coders/editblock_prompts.py index 5f3a7b1fb..f327da171 100644 --- a/aider/coders/editblock_prompts.py +++ b/aider/coders/editblock_prompts.py @@ -125,7 +125,7 @@ Every *SEARCH/REPLACE block* must use this format: Every *SEARCH* section must *EXACTLY MATCH* the existing source code, character for character, including all comments, docstrings, etc. *SEARCH/REPLACE* blocks will replace *all* matching occurrences. -Include enough lines to ensure unique matches if needed. +Include enough lines to make the SEARCH blocks unique. Include *ALL* the code being searched and replaced! From 038806f6947f7e8c4c9fc9f7746fb7acf53416a1 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 14:24:06 -0700 Subject: [PATCH 099/102] max_reflex=3, auto fix lint/test --- aider/coders/base_coder.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index d85182357..243d05d8d 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -56,7 +56,7 @@ class Coder: num_malformed_responses = 0 last_keyboard_interrupt = None num_reflections = 0 - max_reflections = 5 + max_reflections = 3 edit_format = None yield_stream = False auto_lint = True @@ -767,21 +767,21 @@ class Coder: lint_errors = self.lint_edited(edited) self.lint_outcome = not lint_errors if lint_errors: - ok = self.io.confirm_ask("Attempt to fix lint errors?") - if ok: - self.reflected_message = lint_errors - self.update_cur_messages(set()) - return + # ok = self.io.confirm_ask("Attempt to fix lint errors?") + # if ok: + self.reflected_message = lint_errors + self.update_cur_messages(set()) + return if edited and self.auto_test: test_errors = self.commands.cmd_test(self.test_cmd) self.test_outcome = not test_errors if test_errors: - ok = self.io.confirm_ask("Attempt to fix test errors?") - if ok: - self.reflected_message = test_errors - self.update_cur_messages(set()) - return + # ok = self.io.confirm_ask("Attempt to fix test errors?") + # if ok: + self.reflected_message = test_errors + self.update_cur_messages(set()) + return self.update_cur_messages(edited) From 5745657991325ef801589637d6f07c2e2bbeaccb Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 15:02:16 -0700 Subject: [PATCH 100/102] ask to fix lint/test --- aider/coders/base_coder.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 243d05d8d..9042ed562 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -516,6 +516,7 @@ class Coder: self.num_reflections = 0 self.lint_outcome = None self.test_outcome = None + self.edit_outcome = None def run(self, with_message=None): while True: @@ -760,28 +761,31 @@ class Coder: edited = self.apply_updates() if self.reflected_message: + self.edit_outcome = False self.update_cur_messages(set()) return + if edited: + self.edit_outcome = True if edited and self.auto_lint: lint_errors = self.lint_edited(edited) self.lint_outcome = not lint_errors if lint_errors: - # ok = self.io.confirm_ask("Attempt to fix lint errors?") - # if ok: - self.reflected_message = lint_errors - self.update_cur_messages(set()) - return + ok = self.io.confirm_ask("Attempt to fix lint errors?") + if ok: + self.reflected_message = lint_errors + self.update_cur_messages(set()) + return if edited and self.auto_test: test_errors = self.commands.cmd_test(self.test_cmd) self.test_outcome = not test_errors if test_errors: - # ok = self.io.confirm_ask("Attempt to fix test errors?") - # if ok: - self.reflected_message = test_errors - self.update_cur_messages(set()) - return + ok = self.io.confirm_ask("Attempt to fix test errors?") + if ok: + self.reflected_message = test_errors + self.update_cur_messages(set()) + return self.update_cur_messages(edited) From 04539feccbcf5ee71d1d0278feca0e894f6d6898 Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 15:10:57 -0700 Subject: [PATCH 101/102] bump deps to get grep-ast 0.3.1 --- aider/coders/base_coder.py | 1 + requirements.txt | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 9042ed562..f5cf80051 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1179,6 +1179,7 @@ class Coder: self.io.tool_error( "For more info see: https://aider.chat/docs/faq.html#aider-isnt-editing-my-files" ) + self.io.tool_error() self.io.tool_error(str(err), strip=False) self.reflected_message = str(err) diff --git a/requirements.txt b/requirements.txt index 018e958a7..90a340ccb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,6 +56,8 @@ distro==1.9.0 # via openai filelock==3.14.0 # via huggingface-hub +flake8==7.0.0 + # via -r requirements.in frozenlist==1.4.1 # via # aiohttp @@ -94,7 +96,7 @@ googleapis-common-protos==1.63.0 # grpcio-status greenlet==3.0.3 # via playwright -grep-ast==0.3.0 +grep-ast==0.3.1 # via -r requirements.in grpcio==1.63.0 # via @@ -133,12 +135,14 @@ jsonschema==4.22.0 # altair jsonschema-specifications==2023.12.1 # via jsonschema -litellm==1.37.14 +litellm==1.37.16 # via -r requirements.in markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 # via jinja2 +mccabe==0.7.0 + # via flake8 mdurl==0.1.2 # via markdown-it-py multidict==6.0.5 @@ -203,6 +207,8 @@ pyasn1==0.6.0 # rsa pyasn1-modules==0.4.0 # via google-auth +pycodestyle==2.11.1 + # via flake8 pycparser==2.22 # via cffi pydantic==2.7.1 @@ -215,6 +221,8 @@ pydeck==0.9.1 # via streamlit pyee==11.1.0 # via playwright +pyflakes==3.2.0 + # via flake8 pygments==2.18.0 # via rich pypandoc==1.13 From deb13c060c66519480ce0c7a989515a4de1beded Mon Sep 17 00:00:00 2001 From: Paul Gauthier Date: Sun, 19 May 2024 15:14:22 -0700 Subject: [PATCH 102/102] copy --- docs/commands.md | 5 ++--- docs/leaderboards/index.md | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 35cd9d7be..9f28d03c9 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -37,6 +37,5 @@ The interactive prompt is built with [prompt-toolkit](https://github.com/prompt- - `Ctrl-P` : Move up to the previous history entry. - `Ctrl-R` : Reverse search in command history. -Note: aider currently exits vi normal mode after a single command, (maybe something to do with the esc keybinding?). Feel free to investigate and make a PR if you would like to see it fully supported. - -Prompt toolkit also does not provide clear documentation on the bindings they support - maybe you can take aider and help them out with that and we can then link to the authoritative docs. +Note: aider currently exits vi normal mode after a single command, (maybe something to do with the esc keybinding?). +Feel free to investigate and make a PR if you would like to see it fully supported. diff --git a/docs/leaderboards/index.md b/docs/leaderboards/index.md index d33cc6a54..7fb8cf290 100644 --- a/docs/leaderboards/index.md +++ b/docs/leaderboards/index.md @@ -2,7 +2,7 @@ highlight_image: /assets/leaderboard.jpg --- -# GPT-4o takes #1 & #2 on the Aider LLM leaderboards +# Aider LLM Leaderboards Aider works best with LLMs which are good at *editing* code, not just good at writing code. @@ -14,7 +14,7 @@ The leaderboards below report the results from a number of popular LLMs. While [aider can connect to almost any LLM](/docs/llms.html), it works best with models that score well on the benchmarks. -## GPT-4o +## GPT-4o takes the #1 & #2 spots GPT-4o tops the aider LLM code editing leaderboard at 72.9%, versus 68.4% for Opus. GPT-4o takes second on aider's refactoring leaderboard with 62.9%, versus Opus at 72.3%.