From 8c0612afc1c95ad763ec0d2db5907dd5b2bf9956 Mon Sep 17 00:00:00 2001 From: gauravgupta654 Date: Fri, 1 May 2026 21:36:37 +0530 Subject: [PATCH 1/3] Fix DockArea.py import for ChatbotGUI --- src/frontEnd/DockArea.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/frontEnd/DockArea.py b/src/frontEnd/DockArea.py index 32d0682fb..a63c87379 100755 --- a/src/frontEnd/DockArea.py +++ b/src/frontEnd/DockArea.py @@ -12,7 +12,6 @@ from PyQt5.QtWidgets import QLineEdit, QLabel, QPushButton, QVBoxLayout, QHBoxLayout from PyQt5.QtCore import Qt import os -from frontEnd.Chatbot import create_chatbot_dock from converter.pspiceToKicad import PspiceConverter from converter.ltspiceToKicad import LTspiceConverter from converter.LtspiceLibConverter import LTspiceLibConverter @@ -607,17 +606,3 @@ def closeDock(self): self.temp = self.obj_appconfig.current_project['ProjectName'] for dockwidget in self.obj_appconfig.dock_dict[self.temp]: dockwidget.close() - - def chatbotEditor(self): - """ - Creates the eSim Copilot (Chatbot) dock. - """ - global count - - self.chatbot_dock = create_chatbot_dock(self) - - self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.chatbot_dock) - - self.chatbot_dock.setVisible(True) - self.chatbot_dock.raise_() - From 0cb75bf3768dab7d8e8bc865268930e173c53e4b Mon Sep 17 00:00:00 2001 From: gauravgupta654 Date: Mon, 25 May 2026 00:12:53 +0530 Subject: [PATCH 2/3] Added chatbot changes --- chatbot_files.zip | Bin 0 -> 79086 bytes chatbot_updates.zip | Bin 0 -> 42049 bytes library/config/.esim/workspace.txt | 1 + scratch3.py | 19 + src/chatbot/__init__.py | 8 +- src/chatbot/chatbot_core.py | 701 ----------------------------- src/chatbot/chatbot_thread.py | 86 ++-- src/chatbot/error_solutions.py | 106 ----- src/chatbot/image_handler.py | 247 ---------- src/chatbot/knowledge_base.py | 144 ------ src/chatbot/ollama_runner.py | 192 -------- src/chatbot/stt_handler.py | 92 ---- src/frontEnd/Application.py | 9 + src/frontEnd/Chatbot.py | 260 ++++++----- 14 files changed, 225 insertions(+), 1640 deletions(-) create mode 100644 chatbot_files.zip create mode 100644 chatbot_updates.zip create mode 100644 library/config/.esim/workspace.txt create mode 100644 scratch3.py delete mode 100644 src/chatbot/chatbot_core.py delete mode 100644 src/chatbot/error_solutions.py delete mode 100644 src/chatbot/image_handler.py delete mode 100644 src/chatbot/knowledge_base.py delete mode 100644 src/chatbot/ollama_runner.py delete mode 100644 src/chatbot/stt_handler.py diff --git a/chatbot_files.zip b/chatbot_files.zip new file mode 100644 index 0000000000000000000000000000000000000000..b9e6e91fd7b64e9fb2ec2b2a4226dbb37dd46d2d GIT binary patch literal 79086 zcmV)bK&ih_O9KQH0000808x~&T+XDDX!0EZ07GT~02u%P0ApxjbYgFGTw`cqbYgFG zUvy}4WnpA4aCy}|+m0K_b?*Z74`mJ#l3B7fSM8cM4UA?-(nOaT&x}^y%nH5DW|3@9 zv#Yl+Lvdv-_9;(63>eRWP z)9qjxizH(ry?-x|UlVS=rozAGS@AQU#EI~ES`5yDWidQkB;1<@(cD}SVdyQr`}byv zShBPAFS7&Zm)Vg>I8y_Dc}a5yEM9+;T?F&U3-8~%e{YL@_}%yLUv?U2fq;u@e5$Vh zSpJK9$&&Tx{yheNnv5~${=!YTFXmBz`(s|p(nJn&FV5Y=FAh(hA3l11%*O08$$`TB zt5OnG@(N&J)Ndi|57H|MyddlrQBql8`{=w2Yy0xic;STakrtD%()&v;WLzplt(B%EcI^SB9DAQKT*a2%oJ9c=0Gh3l`S}n z^5ujlOw3@tYaXR^aVe%eL>7ytDFe>JYqSbr;J!Hgm3#8y@X4|J`N<178pMw=wV!Uv zA}fS$U_BKPg*t zptlh?N5BE~K*pR$IOF8BAXUF#%vufD0;a)a2T#KSH)BihZ4dAF+0VxnT3PEh;aA+* zDsLQEPUQ1NF^*4Md_brU)QK ztkxZ?Pj0-Eg!_f}KBk#Ci)5qYx+nKMo(9k+86{mrv4u ziL|&J-NIuJBG&7`_|AZJo_G2c+RAgq3BFuPjFfee~3?!^iH)%j3&| zzI$2sQGtN`gcR-($`a9jZA>Uq-m7zmG|_kC3Clbq!GNjUG|OCM22l1r zQrT&i6bf4_+3sA3dC0pcF&E_0oLe3ZDAgcJGvF%@-W`WEST_<8?>e?#EqO0>+&Xdz zHC|stFr5P9Aa$ji=>iuHwZXzg zOlV))0A{dXX$(_A))jvP7#~K%|9tc$aFsk(Ht`b1#DE_807Ar!#49Zz0>5w%od;kA z@L14m;co+2pL+@TNr0b(8xo48{lGnuI*|uzWIP6D$-4I8nb5;IBiq>}#F)RFvIp0! zBP=p>x*%VmiNZ0SgjS9~=S}d+YW=D^@zMpr=??wwpnQ*Y-wr{WEKx-c`7#f^3|xsJ zuOPvG^BdN!&YVYL$<1sw95Q=OekrWn}QhYbdVxoEKx54@z_-nMP7f|+&j zAf9O4kg@2HtOIQJ9u%YMt0)#u8gd@@b}GiRRUBKzq%I4_&RmK(@L9SF&?BAlOv>t? z-@a_JJD|`{&)nCi=bvAIzIiA2*!7T`ya@jVNyCpIrGM#Ckm0{#Mz;n2JEeaE&3)Y^ z%~`JT-xYlqG}EJ*6S_X3>mI=s3C*7w01L6AxeGaCwV)}!L}@061DzKVLy25QgY^y) z0>6^*fot$%Epq&jJ4)pT?2lkCz_XE`DFS&9qyfsD9Jv>-PcDz1x|dJSk1w8{KK~rZ zwC(I1Snq>M=s_Bim}%(#nyY5TerXL4;~e}l*|>mZb29ZtdSRFDB>bBkLTx@RD|kd- z0jvf?@@6xT%2;cg5ZT2dN_lCc_0e=BAO7_pAoj?^X@Q2OgqLRm!lwXzn6>j&)M){6 zCdq_WAEd+BVoI>&(WqpR8({SBVjedfpguQA!JpX2)L>Lgw(|ZxKlIX6ZTNzKzowOX zDvzp}2uKPsidq{arLI($T8&8P%HzE zt37|frh%U|PojVa;Coq?l;@NdG#l1`=>DQvyLwZ;jy7v4J-bzdBuTFsb|?3QKf8U@ z{7Y+-Xo|bd=r(P1)AkznsCS0WH~#MT8>XU$0NfgOU^ee~F(xm2_@hRiE%pqggF=-X zz!f|}neX%==0ME{o-#x-GOd?V3Un$^J5M0AyDozQ%`^jcT(|_mB?!IRf^C8z&q-}3 zm87`pezW4yu5&Q-0c0}Tn{;iK+7JwGTB!wU$?RS%JxX#x#Lg!96wc%?*F&y(%SWd# z&YmCt%6)eHWyNRSf?zg7Sq6{d0=$o7<%p`QiksDwyiT;Np9+=LebG(o&=|PUDcOdL)DwvA2xz%Z&&# z5H|t~E+_#dUmpXZ6Fl06lcgwx$ruB4eGt)n{x%Lh84oSFhvtQ%2eASv=BdGN8q8)q z!J}dmo~<~KBzVi7}lnyt_mo9kDsMC5m7b8GDno>$<{G1 z3^Z^q9szlLsGQk{zyBx3b`KtEY*_ERo#8G6j|(Z3LWIVS&Rz}Jce~qEt(tjhVR0bJ zT14p1X%$J(4t91eXuHGx259>Y(3H}6Kr(8eefsb-3)>C1?`f$&84KJOh+X z+0u)0ssutP21iSwr{;*8z>|l{GxXe3fDIg$D0W@}X~UJbPSH7vf#i5(8J1w-;PM#5 zj|m1)=D?1`asmRA!c7cJ_Fq7nOqLR@3W0ca6SEjfTVXpw3jZ%bn+>vcdiz_EgyME#B^`Vt(5FCEr# zek}q!U55~_4n}WuxKBI6i|8$t1pR8bKeF5^Fy1)KAx|8`_(iYt3dWDLOKuJuc7$n8 zf~-hnF(;Ug4Ct@IyVTVq##lV*T~nsPYym;YOEtUKuP>(Li8N%OlDAs8QnR2Mb?br^ zZQQ4jM+Kt_X^p~juv5@{W`kjZu;4WUid5NsXo4_GuvFjX7X5VDGd2;;7G!6iV^#YI)W2OQOkq z9)!jNP*EDFj-CWsu_iTtZ4C~^l7Tv3#y`5A7RzL

R!$Y_;Ws@~~e0p(le2g0ED&#IC_NzK#;Ei8VO`C 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

E(+Y zT)$+m-#}qU8t)dhniMhAw~@3|=uyv6YcjwzX%DF7(?Jd7k#AJ@_Oa~}Wa;#Y7p9rd z>Qrd9drKd;-fHK1Uf=_60OUq}62$h3ONGU^)+Xe+XDfWe(iK7BC2JCdXYmUUmfI^l z&i>jnn@UzSorVlfPeB^UKFB||B$z_bu>3SR@gVVCN0#ZRNgJ%b<15Y;ni;!`3J53_ z^#W}5CO!+O=0nAw7I*kk>;W4rNR+>2PMv|yL*)!;e97ArM&i}TjrAxUxv9aPh$=f7 z)cr4csy99s#S`e=uX#UznxBA7S4^yHhY1%i$d`CY@P(`6zsLWAuW2h9mtDLR4@vw9B>LRD2ycAf>3%<>}&TU}`+ox+{f+?Ocs2`_d}y zOA(G3*-#tkUGIc6TF9Ap!k!~KxD9`isc6WUKsnK>9o2fklxj`gN}8qSoTFGFn4pGY z)8lwuV#8>E0`}LE9E8Y?vYG{?5V)Vp4Q$`wW7hyo6;-_v9+N-wT6hy9Bmf+P`!PtH z%lyQ9_4`Ddmqdz^Ns{yAwhO=a;p699EY60NIdI0$=b0qIQs~spd}~BL0^e_#;L(VL zD1jrO9*sG@paJf}eA3W05(;asG!7zyGXWJ~fO}jfKidHioo(Q_v9j!7J&E{4P?9(k z2{OmoZ?;=~{K1$a$qaQ)aW$QbrY!{15}|I5Xr=tn%`vwVJK?xQz*Ul+gYsidSgS(U zVOarl!OE8f`8v*Zbf1+k(c7fxD8mY(w`H_CNp9iXrvqn;iqTV=VKmWDs}s3I$vS1P zF=WmdNp_+qZ=KC%ZHPK!Ee?YS6&)2t(&c5mY$!Hqr1Izb`5z{e0b zElfDn7FGQW3i5inj9Iwz4e7mFs=ygjg_R^CHF1m__vWj6nLDSVpAo4~Zj*E*YGKYC zBTK|2wM!xuktjZByy?o8IGM83PMq)b5&Gs~_AsMLkQHvK$U*-a3nvm|TL>>h+M~ zp$M*wPjDd;^R3jP$=7YheJbja+`?GQT7oLamIOsPfos?@PhE>0_>+$xpLJ(&hdAHz`A0dQ?(fb~F+NSurC$8KqG@Go?N`R$nUS3-E=A1lpb%(x4@R1llUQnulM^{DlAICElp;Y!TrtWr zU9y1mY;vN)cIKHW>(Sv^+>YpJOT6KV5m;p&bMPAa!MftrQZawJf;clJsnWj981V^k}{Mm6oy^pN6D53MCF{wS5gidygtCgiAKU z1sdU!0PXI80`z(j(;)gRKZb6}eDu1Dbwium1;X+=2T16=R z2d8%nQL^dapL66X!sXcK(@G4o8WE*T*m{Q~v>B#9D4O@V3s^vH{&|UWeQal>>7Bn* zN}17>kCDi%hR`6pI5&morx3VFto$?Ig*nMeQtrIP+9StSf{PM_kqF>;#uK6v^=>nj zbwpKTDn`L5Q~Me%t3ce1X~J!a)f&a6Pt}hM0m^7eq?O)iznfS{L{*1(6qM}7CXfj;_6?N#QRPpH4{ca=Qoe?J8f&xZJJqt!Q3 zS0g4;t2*Ux)z42RI&;BxP6iBKj>CEN&D6enh}#ix^gwU@h@vJ|1*L8MZmtZG0LcJ* zn>r$5_OHx$Dr$EII(XiI!SSAS-hzlm3^f&2OQT33XPPAI;wxUyB}Mb*Rw` z4H8ceqHki(2&1gg3kS3)3Orn+8Dgbo;_nkmw)Q#4%mFn6gPa@?y!IOn$wDK7BuQmF z*+(#56HG%KiNMU1T3uF5F_C~eJri4{PDPa?@#;@`D|DGi5|lr+J!!MbT6=N%^;yDP zQNZlj*xA^eQ_?~inW(5-rA*6YlhOX_POxc(HQsZ_BU)`TSJ-ghu8_Ts;fIx?R-J7;+*f?}gGJ-EYKmCM@ z-8?Gx5Z)Y~m)wV+d*ObE&)PfofK!YIsC0nIr>4@=i6O=J%W^>Lxs0oOPw$=77AefJ2;HL8YfxMS2-c1r#!S}w3^63 zQcpK;n5$Eq-@+Ur(4j6Di(mR0$~Pdp#fC>&__9R3!bphqnkWr-vbQYWa{5Ji25B7-%hn+mbAMlw7fQQCUO60;6-WHn#+ zRwpdAOb<+@jcqJlUf3KO!5*%37%FWzp5+X}aJIevbNcP{Q2xXW{Kc{~A>!jg!f^Sy zQXLB4YCT;e_?1QNt;YAJa|e3=^>dJ`04@xo6!shX97J=cdp9 z*;IKWioJ`rp{n+3)(%W;x<6m(mMC3ljA=3KQHJmFFR0SeG2pQd}Lr{CT z02i<`rgr{FhRozb=xKil3YelZcOu$;b&ibNdJ;0l{{osoWxqaiARS50=Osaz1EU_D zlc1|aw9U?=CqPgNr-KtmMn%!QQ#eWF0a+~=ULguh zm`Q`_2a1<`abLf%V+;&~B!HHO0^7(u_yguk+5|&Qdf*%wmE>*p8 zP$DuK_9-|hBxqyst`d@5YkFi(ih`YqM~Hr*%a}M46o}&jsylNr5tCxo>F@RnuZR=l z!bE8DbZF8Xpr&U&lY`wSy8r|A3*c>&O^pa5Eb`>2)*v%;9_A%n!jWtiY!*!7_19k~ zzMufpAZedjRB$c|a!K-=-!uz?r{XQixfC(#9>3rbwPZy)dHPAM+HOrpGdb8ZsQ0EP z2;r`N!E>Y%k5u9_l{iG=D_Z*mj^+{|d@A!N?af52*GiQ6tVBi+l8v2BNO&NS$|kjx zq(td7pgJ5(BA%X>FwS7oOZc^o1(zb27;mizO0I02KF#+82R&rEgC5%015nLLl_d?5 zISgL#TrpoUt9p4JT=Uc|9ca2tQc?|Y@CsFnIXGM|i;1I)zfGX8r%06J)tj1tn+*YQ z2Pi@dbOI=kkZo5D>0D|$s$5nrBmoW=(HXZ`mDk)?Baw`xE7g#pUOOZzS#+d)TjfZn zpD@3Yh= z^9v6N>!|!F*-@CQb^U>;whjBOI-c2OFR$7+0!Vc}3l+$D=I~W>{XA5?w)@_8yoc`N zOhlIU?N!atE7AGjiK_g}yaM}Sn%TFPK7B~4J|@wa1&m^1!&ELCQRd(j5?sLICz zs=A{*aYtpZ9q%XKEcE23DaYYsq|&P-+oWZ~oHYm4reSGXlVG^NiX1GFQZqTxY2Y-N z2@lK0;EPBw@^(VvMBE4)KAK!~fl{5WxG~x(U<~<3Nt-_~R4Q8A7QIDl@72*Y?j*xo zJ~uW!Xl%Y_x^HP(xbV@?s>NHhc+0KZR$Kdut$oWKrPiU<)`P{?gLl4DY8|~kcHh>r zX!%H7wRwv+?=NOPj^B!ZoW7O56JGU=6@6p(UC_Wfp*!(X=g|cdxXlgTf~)U~4Shy$zgY6U^ecDCH&SlxxjyzqLua9D zxYXb;Sp92yx2<7~MW*H_KIH7ue4%nf%Y(M<%c-LC;Db(2x%;_tci-ZLM+VN-u*Msj z?Q0E4=qYz}m%Do)v&p{Jgq)r=h3AJdzJzv_wxno^ADUYH?23Ui(<*S{Tg4kH@%y> zo?1LovI|RtMSJfxwrp`Oy!_6A#XU=&4~EO`_9b`0{anHN+yjUEkCzx&`)^!*Up>h) z$lCN68F|Znqiw;l#4O4Md(S;%?|p~+-Td46vM}&Z?Crom>Tk<;bhjl?6|E zaR0yWF3iM=f>dtrEH}44vgkX^kCEPK`4Z_(mao=$=<=yd+=O>tW{!H$udM?|UF@$9 zyxa|6{;gv#grB&~N4pK5bQnn5V?MTr{iOfkF(3PB9|P%6eLRFJM`&n+hd@j_^$?z- zXP(WraK=Ed*3y~>QB|$PlUVokYIwFK)tmHx0iZYe5F#;XP5NptnnW|`P+qix-eg0p zXrp@5PQtBpc~yb;KKlmIO7*s5oucBZT8U^Ws+XK;F2MCx;&pn-mERG}r8SGaPAtu4 zqF@bTLNR$ywx2u9rF>WT>MltrI(%{_zPfQ&|yB$WnhrV<{vBwX>i( zKqQkrD7$`ahN{+#Taj^PC(o=S|{cX=;ycuvFlIw$*Jj}9@r}i3!S1`eZvrRtB+7eWpG z(=`8lCIOlPZZ2F;q6G0aRDG`|jwpTop*x{+MBHibLgb>*2WU*(S^DCqn(B`{^okJ) zq}@33)I+T~y(M&OZXx_FlBQc{JO~t&OG+={zk;vi(B&Nv0#q61b7cA)**-@W_=Owd zYPo6qk*#dnUbec(tS$d(YG}OKa--#D=Z((A)be!6b?B=7L9?fT+~w963aF`U7p_{?^m?XYaq><YYq|Xe|EW^vc)>Yw)$&BoF|ROd4W}8#vKU=MB)+}p35_3}*R?S{3os@`OFcBY z9sQC-YX=QX<0AVdjNC{!^8Wx(O9KQH000080DzRSTw)Ax7PJ5W0Mq~g04x9i0Apxj zbYgFGTwh;sd1GN?Xk}ktTwh;lZfSI1UoK;Cd30!RZY?u0GcIs>WBbg@%f$c$I(s>@ z41n}w1|+};r9blkiKz_13`Gq33`LCTjG9bUVqB@gnYjwi8HpuH`6UVgiOJcC>8V_P znoPIYB^G=4F<|$LkeT-r}&y%}*)KNwq6thq|j+5J-GrW@Kdi$i&3R z@_~bao41j@h!rRb08mQ<1QY-O00;mOl(AeV#xprtWdHz5(*Xbq0000(Xkm0>Z*(qj zdF{PxZyQ;bAo#t2`VY5gWRY}AH1)Jnw%l;qa#~Y<%bD4=N)IbY2E|B`5v+)yWwlm~ z`Y_YL0<+y{bk7X7(X%zXX!IZ01$uzpPd)!6|G@N5*z>qI;=UqC%F0Le7PHDCBhG!F zd+s^sp2sLnreZJ-@_v$cfC_7f-tUmwn9lV}j+Fm_e?Ni+!` zN5LdHh4;mmXHk|Hy(xbj<>3?_JV}FTs46%fq{(FR5GEep98aPlb@M2m&2z5K9sZ*v z4b|9Rzm29;0$Q3~{5C(1PU8T(eH{H+XqEe79+kylRvyjMEWxtB3NH@dhcR~caT=T+ z#(9{Q50V*umz52Ycodz^Q<{Yi%m_bF6&DXA_%O)B9J;99tKUEWnS79zUz4o(`fZlP z#gF+s8WunLK^E?A|NLk0U<4&PG0denI1tS^Z|?J)Qp501oFB#oHqU~5JPl5xfe^Pu zocuP}f3n@(-~v$`J zlMnyk{`d2r^$w4Zp7$Ofy@7GU#N%i>-zHJqQa{2EvmhSMVWYL0f8K1b!EA(CG;IR@ z_MRRczkT`U*S#l4&kpVKMa57;k)kndnk2pGARt9 zq3qG~FRCyb$)nMz-JtRhzc}n2 z{^{FeD1X_+a!ni^l$3u%{sN`xFO;XhScCdPf6v4Ita*jF@#6FtXkI8_hi76C#G)UH zjqdtRSN!Vpdm;n67eViW$wVY05dq#$fP?`GWx0sw)7~KeurD^d+j||XY_}^WK*oR~ zW+IG}`RQ1Ul2ioog_wpx3^k4B6A`8oE8{neIbvQ0iZP{}2D^{v9Y( zTORvxPtfo4FghLQ`~74xya($xNz(mW8^K1f8QKl(QUhEqe!}BuDjpt*hp=G)7m&n|_#`!MY%l!oni1_i?>{6+H@uYifI}-S4Mb zkvPmJ(9LX309~0+=nP`ChDU(ggD4%$BSe}EmZ4)7ewgRMU<~a6QO6DNFTekrzr;e> zEDX-z*I)jF0KOmMCap49T?EEpmc*e68W*fZW~E3sj`kW2E}XP;>gCE9jHFwYbDTwjYX zvye6q?*D#}iaZg)U;vwjDGn0V%$$zmFU>LS6WCmDfBov{1rXae51-TiKjA{nU*7t# zx%24By)OU;x;m;O^f+kOwP*HziZmT(N;`_U^jPU9)%C+&i4* zpZ{zajzllNm;rM1`tyE&61GOjR33EOl5ZI%z#kt7b3slBNPHr;-Z4}f=JPaG3^b1= z51=|Azr%cd(Aem9KWT^o91ao}4;o$m4ILcF-w*ER>4TH?#W->?%loN_C39Lu^F-7n@I7^ zy^D`T`Ogjj=*6}DiW}J=ma?XOtT{_S1`xM~-QghIGe*y&Wq1%jB1;_e4NYCKRj{!m8d{5zs{?Yl*RXA0-LAdihCd(~6p4ILk za)n>s5D4&11cPLf#=S3Y-^RoA_U$!sJIVo<2k`AT0}x_=vj!5t7?=yd1vqqo;T0!P z_AGnXnx~U?$7aZtq^Hp6C`y60W{Jq={VarIC(h|>9_4d9 zfHDz)pqB-skn19D?127sfc2ULgRs^7CGwTO45qVt&35_pK0Te}#={5n@Dv~79C7!a z%AttXbPl}%E_9YANcn*~OfZ59IVernZ2ox`C(eUmV5O*=>)X045O+XcjJjH9^R*cVOQ&d|&z ze%+<7TljUGzHZ}JAa}^~`)YwosIty!n763?HCe#ZEHUIU&XDTerUwekpq zX-duWsw7O8`0v#{hofv()njs+jR%*qhga%=bPHM?)+FtUF<8qdPb|b6DSXcJ0-NOq zaZwPreu?s*!=OO?yN!=;-$oX&*Te6h|WOAs9)4A>Q!5O_q)HCJg3PwBjt3V7Uzz^c0h6^L88j??R zpte3KeL77?Uh!yeZ||PQ5_f46UgAWm!-Lw#HKN(YgMxIk{zWpoX$di(?JbgFwj6;% zR zj7g!RAS+g-Ab8MfUR@#rSIQsF3Op#bH6uE%S4o?KXyR;Gxkq8`@%4VyqT38Uz_v|V z)|5;Fm$Rju&q;CyW_~h58Q@13Ts}tD!8@55dn_=+>;p!NaB)3#WDfPc+awEb5to&3;5OCK9){CfF# zKJCYVCcSY!owWM9+dUi#j%N*|G2r`yY3Lg!h;u9{ia?+)dni?iK}PCCW`6i)N)(NV zh!xVPJnIw!A&PT7zO*WuTtS-q-t{&`1xjB~q1Z`LK5#TUn@TO-6^{C43B}H4tzySe zq;^NUqmfP9wk6FI>Wf!{gGwi#vffa&+>fTGB1;Dc4cJ1#KCSm1yy&^d>%4X;(dVim zjDp2Q!H0F4%l(aAB)Rxo>3cldBU1c}J60dOn)lBDR69kbmOW2ql9jzIP)t&4V>;`o zcJ*6*FX08B(mLWaf-nFS1=y?UOc5XK0SaP^NQy*foJq-~^5k~lNf_f{Ltq{Gy*bZE z>wC?1yE6=p2S3!V*u37Z*#2I2#e3N%->*20cKK=wcVyQ=ZY5fTz(|7xa_3Sl!BtTY z?GT)2zUtqPLA11)4~d5TF8&Ajn@2eoMRN)iSf{hR@!-3^|EvG;-~Nik6dThj_PPGp z<)^yc&vr(;#>VD1=VK5Gs#Ugh|728Mp}yTw`rQik?Uwb*y!SGDX6tWpV_f-noVh@_ z1^LbJK_z~v>F@V#UfQTkSG#Y@D@#O)Ung_HEc`$J6aN*L#a6!Z_N;krssTJ(ntbTx z$ypfp2C$&H#Z*LbZZZ`>FUq5A@1A}+j^B~C*Y?;YsE!!FB*P0cmb%}_8jH< zjkDmII^t+#6h`gJY4kpf*Pw@k$s7c@H({P$+@ffO*%n$EfcUZPX2T^-i!~{rP1vZ&y5pnN7}|Ui%M*Ey=3v zxkOslpa}J4xwcpK=)>;aZns-Q0=O8j^+m>K+!@>U2<& z>}z%qHyZZKH^ZX+65{8K{2kXlf^!(O{!{*CRlBap|r?CCluFe`2@x`I^|d zv0p3$Ez+kM>y?8TI%IW5R+xTBHA3c*#OOJxJII=i)nNvwsVir72|MhDAxPK-3E$ZX zc1*s~m0)!>z#a*`WOlUaPMkGOWQ$clS8-cuh7EZ;Nd;-l`9BCKRFp_sU6F%4T#I=&=ALAYO82hLeA}v zA+d6QN#eXoWuybvq@Ds9#8;!QYZlUAZ+ZkAgs-{o%l8jw>XfnyL88PcX@SM=yL3htl zUBOB5?*PRJN%jRRRO-~6D^rsC-7zw(3FLzPFqC8#q#(f$=2@Ohd-HTcHV0}G48|c{ zDZsN1|7b~MA15T<;|u(%9!rN6e2icDZYY|yns-X;FldpiNKeWOIjlkxn+QVP$Kktn z0g1Ibkgz!s@?=H__e|dEkCinTv`t0d*|H7~X^m^q`5|`n=$%FJ5XkHBG;HBl-yIGu zFIjDsogyV?V_&oyw}9E%9*r7nqH$|;xH}k*8g0$0qCp&20U3u(QCNJ+pL z7WU9~xJR)4z1^VOHDOCD_8wWW7lpdAfx+(P-sWCWNLrEgPuIiHHBc(++Uf^0kNhapeuDp~8 zyK9lU1ySndm)b&mZM! z*(!W4HKpv4HcIj`8YI&xTGuLXqtrtLg zKK-iQk^TroifFm$J?dpkC&?DAo46&yX#`{rRsu9wV`MRrRhuGev;*CN{dsU8 zUgsTXzV{W)8o!$+!w66{%>d$;2=EB#NSaKZ%jeb%mEAcooQONciwB@NMAbRAS0sPvX`mY3r4*W!+JnRbR$9UJ5lyxKACu{PQ&~&)4lwQm;%9Pp@HU;> zcH{a`B>%MJSXA}Z5TgcRW$&`eVyF4ZuRoc7GVFcw^poeG95>rn=!V)@X~4XgXu#4^ zLjX46)r&706$NO=>IAfdw6Z8%QHl>g^W81+1btcaaZ28)d17rw+_k0YYSF%=pC)Ob zJ!rM9+FW8Ie>tPw;*za>XKUFM3rUt|4=~s;4Mxam(=R&heXavG4aDnBw=2L;hz+xJ zIB`4lKy)j+Qvj_W*i{ig7UszJc^W@4D|pxTlt%InE1KzBgnH*B{IgN{Fhfg6dkeVT2R)aiG<&o6ng+}tzw4FmL+!?;N`F2{X-!KMqDVZ|y zJ_^tOupGQREaszekjnvmMdNA7U>t=%q|3KS*>PVCqd`uhCCrjxE1=J4QJCR$mz3bV zM*nIZAJ+2;cN1&e@ntoQOn8xZugxB_&Kycolh~YM{9exyC!ya zyEYgsIlkWM!t32_=QY%Fj1tuGIE4N+&+wh~HLscNd!nXJr?q%F+dnbB?>VH|e8xg`)=Pn|Y`EPJAj_jqiTfP<=d-G&o;rKM8Rux7*47M)#e1hMVe2UvJfhjh>diR%oi+${{ZrsA|g? z~Yg=1e_v-4vYnv<_nstO-{O?{h5#YH5QE&jV zTxXEPaX64{7RIoc<%?FDDzHhchVcb0DM9)iPz*6ai!Y`VTM2<{{Bjn?&(KiN;ovl& zT13#=SV(gD_^u+kywdJlDK1$8w@H7d6wI&?4cQG3gb zW&v?8e-~y8no?Ku2SLv7qpI{*=2@CQ;Wo-#6G4U zNtXFiGRejv%*_0!%$1|PYaUDr9Mb!)YF_eiMvIMQ4rgtgC7!6ZD&dav6oXB+jIj@a z_W2I*$M9TlQo-3+wY6CRbTg;;rp>|a0tZ)3s_|BRbJI(zb$pe^w?cQ4s6QQSK4ABw z`G9VBZ*#b}`q|&@?gst6nzO%HgyoKZFD|J%YN%4N`OND%87%8Z*Mr==KBf0KoP>G! z2t#?MQ`QqKp;95?K>Aazrcc2LrX325IsrUj#mLhr&c*+XFJ!~6I^$su$7g?@hb@5m z`eE-a5|POq&%VzKJgQcve2vyRu&qrJ3Fr~($BiYf(`(}E`riVoy$H~qb`Ye)h7V|o z6XzBdUU2M)h3Agkt?ar~xxs~gG%;trG&_df)J`W&p+Tc2#QDuZ*h1WFDgTxF$rS zbPg1r6Q6+3kH;{XEr#hleP)I(l7KoAn(hsag&l(`Ok~O99&pSSuh@0~YP46d5`%E! z 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%0AAAp6CM-{}i%Q(Ecq4~QVp8N2D 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 N2LbVA0{!~*e*l9pd&mF) literal 0 HcmV?d00001 diff --git a/chatbot_updates.zip b/chatbot_updates.zip new file mode 100644 index 0000000000000000000000000000000000000000..a7ae59528c2de959e6cfa58e700dd8d12e23cd99 GIT binary patch literal 42049 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%0D;ygoB{2}V#IgU6wpHd7ZG@Qus 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 IMgKYbe;&A66aWAK literal 0 HcmV?d00001 diff --git a/library/config/.esim/workspace.txt b/library/config/.esim/workspace.txt new file mode 100644 index 000000000..49a209513 --- /dev/null +++ b/library/config/.esim/workspace.txt @@ -0,0 +1 @@ +0 C:\Users\Dell\eSim-Workspace \ No newline at end of file diff --git a/scratch3.py b/scratch3.py new file mode 100644 index 000000000..b2849a088 --- /dev/null +++ b/scratch3.py @@ -0,0 +1,19 @@ +import sys, base64 +from PyQt5.QtWidgets import QApplication, QMainWindow +from PyQt5.QtCore import QUrl + +app = QApplication(sys.argv) +from src.frontEnd.Chatbot import ChatbotGUI + +gui = ChatbotGUI() + +img_key = "test_key" +with open("images/chatbot.png", "rb") as f: + raw = f.read() +b64 = base64.b64encode(raw).decode('utf-8') +gui._images_store = {img_key: [("chatbot.png", b64)]} + +url = QUrl(f"imageview://{img_key}/0") +print("Emitting link click...") +gui._handle_link_click(url) +print("Done!") diff --git a/src/chatbot/__init__.py b/src/chatbot/__init__.py index 2157cc829..01fe96212 100644 --- a/src/chatbot/__init__.py +++ b/src/chatbot/__init__.py @@ -2,10 +2,4 @@ eSim Chatbot Package """ -from .chatbot_core import handle_input, ESIMCopilotWrapper, analyze_schematic - -__all__ = [ - 'handle_input', - 'ESIMCopilotWrapper', - 'analyze_schematic' -] +__all__ = [] diff --git a/src/chatbot/chatbot_core.py b/src/chatbot/chatbot_core.py deleted file mode 100644 index 24b8eef09..000000000 --- a/src/chatbot/chatbot_core.py +++ /dev/null @@ -1,701 +0,0 @@ -# chatbot_core.py - -import os -import re -import json -from typing import Dict, Any, Tuple, List -from .error_solutions import get_error_solution -from .image_handler import analyze_and_extract -from .ollama_runner import run_ollama -from .knowledge_base import search_knowledge -from .ollama_runner import get_embedding - -# ==================== ESIM WORKFLOW KNOWLEDGE ==================== - -ESIM_WORKFLOWS = """ -=== COMMON ESIM WORKFLOWS === - -HOW TO ADD GROUND: -1. In KiCad schematic, press 'A' key (Add Component) -2. Type "GND" in the search box -3. Select ground symbol from "power" library -4. Click to place it on schematic -5. Press 'W' to add wire and connect to circuit -6. Save (Ctrl+S) β†’ eSim: Simulation β†’ Convert KiCad to NgSpice - -HOW TO ADD ANY COMPONENT: -1. In KiCad schematic, press 'A' key -2. Type component name (e.g., "Q2N3904", "1N4148", "uA741") -3. Select from appropriate library (eSim_Devices, eSim_Subckt, etc.) -4. Place on schematic and connect with wires -5. Save β†’ Convert KiCad to NgSpice - -HOW TO FIX MISSING SPICE MODELS (3 Methods): - -Method 1 - Direct Netlist Edit (FASTEST, but temporary): -1. eSim: Tools β†’ Spice Editor (or Ctrl+E) -2. Open your_project.cir.out file -3. Scroll to bottom (before .end line) -4. Add model definition: - BJT: .model Q2N3904 NPN(Bf=200 Is=1e-14 Vaf=100) - Diode: .model 1N4148 D(Is=1e-14 Rs=1) - Zener: .model DZ5V1 D(Is=1e-14 Bv=5.1 Ibv=5m) -5. Save (Ctrl+S) β†’ Run Simulation -NOTE: This gets overwritten when you "Convert KiCad to NgSpice" again - -Method 2 - Component Properties (PERMANENT): -1. Open KiCad schematic (double-click .proj in Project Explorer) -2. Find the component that uses the missing model (e.g., transistor Q1) -3. Right-click on it β†’ Properties (or press E when hovering over it) -4. Click "Edit Spice Model" button in the Properties dialog -5. In the Spice Model field, paste the model definition: - .model Q2N3904 NPN(Bf=200 Is=1e-14 Vaf=100) -6. Click OK β†’ Save schematic (Ctrl+S) -7. eSim: Simulation β†’ Convert KiCad to NgSpice -NOTE: This permanently associates the model with the component - -Method 3 - Include Library: -1. Spice Editor β†’ Open .cir.out -2. Add at top: .include /usr/share/ngspice/models/bjt.lib -3. Save β†’ Simulate - -HOW TO FIX MISSING SUBCIRCUITS: -1. Spice Editor β†’ Open .cir.out -2. Add before .end: - .subckt OPAMP_IDEAL inp inn out vdd vss - Rin inp inn 1Meg - E1 out 0 inp inn 100000 - Rout out 0 75 - .ends -3. Save β†’ Simulate -OR: Replace with eSim library opamp (uA741, LM324) - -HOW TO FIX FLOATING NODES: -1. Open KiCad schematic -2. Find the unconnected pin/node -3. Either connect it with wire (press W) or delete component -4. For sense points: Add Rleak node 0 1Meg -5. Save β†’ Convert to NgSpice - -KICAD SHORTCUTS: -A = Add component -W = Add wire -M = Move item -R = Rotate item -C = Copy item -Delete = Remove item -Ctrl+S = Save - -ESIM MENU PATHS: -Convert to NgSpice: Simulation β†’ Convert KiCad to NgSpice -Run Simulation: Simulation β†’ Simulate -Spice Editor: Tools β†’ Spice Editor (Ctrl+E) -Model Editor: Tools β†’ Model Editor -Open KiCad: Double-click .proj file in Project Explorer - -FILE LOCATIONS: -Project folder: ~/eSim-Workspace// -Netlist: .cir.out -Schematic: .proj -""" - -LAST_BOT_REPLY: str = "" -LAST_IMAGE_CONTEXT: Dict[str, Any] = {} -LAST_NETLIST_ISSUES: Dict[str, Any] = {} - - -def get_history() -> Dict[str, Any]: - return LAST_IMAGE_CONTEXT - - -def clear_history() -> None: - global LAST_IMAGE_CONTEXT, LAST_NETLIST_ISSUES - LAST_IMAGE_CONTEXT = {} - LAST_NETLIST_ISSUES = {} - -# ==================== ESIM ERROR LOGIC ==================== - -def answer_with_rag_fallback(user_input: str) -> str: - """ - Try to answer using eSim manuals (RAG). - If nothing relevant is found, fallback to Ollama. - """ - - rag_context = search_knowledge(user_input) - - if rag_context.strip(): - prompt = f""" -You are eSim Copilot. - -Use ONLY the following official eSim documentation -to answer the question. Do NOT invent information. - -{rag_context} - -Question: -{user_input} - -Answer clearly and step-by-step. -""" - return run_ollama(prompt) - - # Fallback: general LLM answer - prompt = f""" -Answer the following question clearly: - -{user_input} -""" - return run_ollama(prompt) - -def detect_esim_errors(image_context: Dict[str, Any], user_input: str) -> str: - """ - Display errors from hybrid analysis with SMART FILTERING to remove hallucinations. - """ - if not image_context: - return "" - - analysis = image_context.get("circuit_analysis", {}) - raw_errors = analysis.get("design_errors", []) - warnings = analysis.get("design_warnings", []) - - # === SMART FILTERING === - components_str = str(image_context.get("components", [])).lower() - summary_str = str(image_context.get("vision_summary", "")).lower() - context_text = components_str + summary_str - - filtered_errors: List[str] = [] - for err in raw_errors: - err_lower = err.lower() - - if "ground" in err_lower and ( - "gnd" in context_text or "ground" in context_text or " 0 " in context_text - ): - continue - - if "floating" in err_lower and ( - "vin" in err_lower or "vout" in err_lower or "label" in err_lower - ): - continue - - filtered_errors.append(err) - - output: List[str] = [] - - if filtered_errors: - output.append("**🚨 CRITICAL ERRORS:**") - for err in filtered_errors: - output.append(f"❌ {err}") - - if warnings: - output.append("\n**⚠️ WARNINGS:**") - for warn in warnings: - output.append(f"⚠️ {warn}") - - text = user_input.lower() - if "singular matrix" in text: - output.append("\n**πŸ”§ FIX:** Add 1GΞ© resistors to all nodes β†’ GND") - if "timestep" in text: - output.append("\n**πŸ”§ FIX:** Reduce timestep or add 0.1Ξ© series R") - - if not output: - return "**βœ… No errors detected**" - - return "\n".join(output) - - -# ==================== UTILITIES ==================== - -VALID_EXTS = (".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".gif") - - -def _is_image_file(path: str) -> bool: - if not path: - return False - clean = re.sub(r"\[Image:\s*(.*?)\]", r"\1", path).strip() - return clean.lower().endswith(VALID_EXTS) - - -def _is_image_query(user_input: str) -> bool: - if not user_input: - return False - if "[Image:" in user_input: - return True - if "|" in user_input: - parts = user_input.split("|", 1) - if len(parts) == 2 and _is_image_file(parts[1]): - return True - return _is_image_file(user_input) - - -def _parse_image_query(user_input: str) -> Tuple[str, str]: - user_input = user_input.strip() - - match = re.search(r"\[Image:\s*(.*?)\]", user_input) - if match: - return user_input.replace(match.group(0), "").strip(), match.group(1).strip() - - if "|" in user_input: - q, p = [x.strip() for x in user_input.split("|", 1)] - if _is_image_file(p): - return q, p - if _is_image_file(q): - return p, q - - if _is_image_file(user_input): - return "", user_input - - return user_input, "" - - -def clean_response_raw(raw: str) -> str: - cleaned = re.sub(r"<\|.*?\|>", "", raw.strip()) - cleaned = re.sub(r"\[Context:.*?\]", "", cleaned, flags=re.DOTALL) - cleaned = re.sub(r"\[FACT .*?\]", "", cleaned, flags=re.MULTILINE) - cleaned = re.sub( - r"\[ESIM_NETLIST_START\].*?\[ESIM_NETLIST_END\]", "", cleaned, flags=re.DOTALL - ) - return cleaned.strip() - - -def _history_to_text(history: List[Dict[str, str]] | None, max_turns: int = 6) -> str: - """Convert history to readable text with MORE context (6 turns).""" - if not history: - return "" - recent = history[-max_turns:] - lines: List[str] = [] - for i, t in enumerate(recent, 1): - u = (t.get("user") or "").strip() - b = (t.get("bot") or "").strip() - if u: - lines.append(f"[Turn {i}] User: {u}") - if b: - if len(b) > 300: - b = b[:300] + "..." - lines.append(f"[Turn {i}] Assistant: {b}") - return "\n".join(lines).strip() - - -def _is_follow_up_question(user_input: str, history: List[Dict[str, str]] | None) -> bool: - """ - Detect if this is a follow-up question that needs history context. - Returns True if question lacks standalone context. - """ - if not history: - return False - - user_lower = user_input.lower().strip() - words = user_lower.split() - - - if len(words) <= 7: - return True - - pronouns = ["it", "that", "this", "those", "these", "they", "them"] - if any(pronoun in words for pronoun in pronouns): - return True - - continuations = [ - "what next", "next step", "after that", "and then", "then what", - "what about", "how about", "what if", "but why", "why not" - ] - if any(phrase in user_lower for phrase in continuations): - return True - - question_starters = ["why", "how", "where", "when", "what", "which"] - if words[0] in question_starters and len(words) <= 5: - return True - - return False -import numpy as np - -def is_semantic_topic_switch( - user_input: str, - history: list, - threshold: float = 0.30 -) -> bool: - """ - Detect topic switch using embedding similarity. - Returns True if new question is unrelated to previous assistant reply. - """ - - if not history: - return False - - last_assistant_msg = None - for item in reversed(history): - if item.get("role") == "assistant": - last_assistant_msg = item.get("content") - break - - if not last_assistant_msg: - return False - - try: - emb_new = get_embedding(user_input) - emb_prev = get_embedding(last_assistant_msg) - - if not emb_new or not emb_prev: - return False - - emb_new = np.array(emb_new) - emb_prev = np.array(emb_prev) - - similarity = np.dot(emb_new, emb_prev) / ( - np.linalg.norm(emb_new) * np.linalg.norm(emb_prev) - ) - - print(f"[COPILOT] Semantic similarity = {similarity:.3f}") - - return similarity < threshold - - except Exception as e: - print(f"[COPILOT] Topic switch check failed: {e}") - return False - -# ==================== QUESTION CLASSIFICATION ==================== - -def classify_question_type(user_input: str, has_image_context: bool, - history: List[Dict[str, str]] | None = None) -> str: - """ - Classify question type for smart routing. - Returns: 'greeting', 'simple', 'esim', 'image_query', 'follow_up_image', - 'follow_up', 'netlist' - """ - user_lower = user_input.lower() - - if "[ESIM_NETLIST_START]" in user_input: - return "netlist" - - if _is_image_query(user_input): - return "image_query" - - if has_image_context: - follow_phrases = [ - "this circuit", "that circuit", "in this schematic", - "components here", "what is the value", "how many", - "the circuit", "this schematic","what","can","how" - ] - if any(p in user_lower for p in follow_phrases): - return "follow_up_image" - - greetings = ["hello", "hi", "hey", "howdy", "greetings"] - user_words = user_lower.strip().split() - if len(user_words) <= 3 and any(g in user_words for g in greetings): - return "greeting" - - is_followup = _is_follow_up_question(user_input, history) - if is_semantic_topic_switch(user_input, history): - print("[COPILOT] Topic switch detected (semantic)") - is_followup = False - - if not is_followup: - history.clear() - LAST_IMAGE_CONTEXT = None - - esim_keywords = [ - "esim", "kicad", "ngspice", "spice", "simulation", "netlist", - "schematic", "convert", "gnd", "ground", ".model", ".subckt", - "singular matrix", "floating", "timestep", "convergence" - ] - if any(keyword in user_lower for keyword in esim_keywords): - return "esim" - - error_keywords = [ - "error", "fix", "problem", "issue", "warning", "missing", - "not working", "failed", "crash" - ] - if any(keyword in user_lower for keyword in error_keywords): - return "esim" - - return "simple" - - -# ==================== HANDLERS ==================== - -def handle_greeting() -> str: - return ( - "Hello! I'm eSim Copilot. I can help you with:\n" - "β€’ Circuit analysis and netlist debugging\n" - "β€’ Electronics concepts and SPICE simulation\n" - "β€’ Component selection and circuit design\n\n" - "What would you like to know?" - ) - - -def handle_simple_question(user_input: str) -> str: - """ - Handles standalone questions. - Uses RAG first, then falls back to Ollama. - keep in mind that your a copilot of eSim an EDA tool - """ - return answer_with_rag_fallback(user_input) - - -def handle_follow_up(user_input: str, - image_context: Dict[str, Any], - history: List[Dict[str, str]] | None = None) -> str: - """ - Handle follow-up questions that depend on conversation history. - This handler PRIORITIZES history over RAG. - """ - history_text = _history_to_text(history, max_turns=6) - - if not history_text: - return "I need more context. Could you provide more details about your question?" - - rag_context = "" - user_lower = user_input.lower() - if any(kw in user_lower for kw in ["model", "spice", "ground", "error", "netlist"]): - rag_context = search_knowledge(user_input, n_results=2) - - prompt = ( - "You are an eSim expert assistant. The user is asking a follow-up question.\n\n" - "=== CONVERSATION HISTORY (MOST IMPORTANT) ===\n" - f"{history_text}\n" - "=============================================\n\n" - f"=== CURRENT USER QUESTION (FOLLOW-UP) ===\n{user_input}\n\n" - ) - - if rag_context: - prompt += f"=== REFERENCE MANUAL (if needed) ===\n{rag_context}\n\n" - - if image_context: - prompt += ( - f"=== CURRENT CIRCUIT CONTEXT ===\n" - f"Type: {image_context.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" - f"Components: {image_context.get('components', [])}\n\n" - ) - - prompt += ( - "CRITICAL INSTRUCTIONS:\n" - "1. The user's question refers to the CONVERSATION HISTORY above.\n" - "2. Identify what 'it', 'that', 'this', or 'next step' refers to by reading the history.\n" - "3. Answer based on the conversation context first, then use manual/workflows if needed.\n" - "4. If the user asks 'why', explain based on what was just discussed.\n" - "5. If the user asks 'what next' or 'next step', continue from the last instruction.\n" - "6. Be specific and reference what you're talking about (e.g., 'In the previous step, I mentioned...').\n" - "7. Keep answer concise (max 150 words).\n\n" - "Answer:" - ) - - return run_ollama(prompt, mode="default") - - -def handle_esim_question(user_input: str, - image_context: Dict[str, Any], - history: List[Dict[str, str]] | None = None) -> str: - """ - Handle eSim-specific questions with RAG + conversation history. - """ - user_lower = user_input.lower() - - sol = get_error_solution(user_input) - if sol and sol.get("description") != "General schematic error": - fixes = "\n".join(f"- {f}" for f in sol.get("fixes", [])) - cmd = sol.get("eSim_command", "") - answer = ( - f"**Detected issue:** {sol['description']}\n" - f"**Severity:** {sol.get('severity', 'unknown')}\n\n" - f"**Recommended fixes:**\n{fixes}\n\n" - ) - if cmd: - answer += f"**eSim action:** {cmd}\n" - return answer_with_rag_fallback(user_input) - - history_text = _history_to_text(history, max_turns=6) - - rag_context = search_knowledge(user_input, n_results=5) - - image_context_str = "" - if image_context: - image_context_str = ( - f"\n=== CURRENT CIRCUIT ===\n" - f"Type: {image_context.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" - f"Components: {image_context.get('components', [])}\n" - f"Values: {image_context.get('values', {})}\n" - ) - - prompt = ( - "You are an eSim expert. Answer using the workflows, manual, and conversation history.\n\n" - f"{ESIM_WORKFLOWS}\n\n" - f"=== MANUAL CONTEXT ===\n{rag_context}\n" - f"{image_context_str}\n" - ) - - if history_text: - prompt += f"=== CONVERSATION HISTORY ===\n{history_text}\n\n" - - prompt += ( - f"USER QUESTION: {user_input}\n\n" - "INSTRUCTIONS:\n" - "1. If the question refers to previous conversation, use the history.\n" - "2. Use exact menu paths and shortcuts from the workflows when relevant.\n" - "3. If the manual context does not contain the answer, say you need to check the manual.\n" - "4. Keep the answer concise (max 150 words).\n\n" - "Answer:" - ) - - return run_ollama(prompt, mode="default") - - -def handle_image_query(user_input: str) -> Tuple[str, Dict[str, Any]]: - """ - Handle image analysis queries. - Returns: (response_text, image_context_dict) - """ - question, image_path = _parse_image_query(user_input) - image_path = image_path.strip("'\"").strip() - - if not image_path or not os.path.exists(image_path): - return f"Error: Image not found: {image_path}", {} - - extraction = analyze_and_extract(image_path) - - if extraction.get("error"): - return f"Analysis Failed: {extraction['error']}", {} - - if not question: - error_report = detect_esim_errors(extraction, "") - - summary = ( - "**Image Analysis Complete**\n" - f"**Type:** {extraction.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" - f"**Components:** {extraction.get('component_counts', {})}\n" - f"**Description:** {extraction.get('vision_summary', '')}\n\n" - ) - - if extraction.get("components"): - summary += f"**Detected Components:** {', '.join(extraction['components'])}\n" - - if extraction.get("values"): - summary += "**Component Values:**\n" - for comp, val in extraction["values"].items(): - summary += f" β€’ {comp}: {val}\n" - - summary += ( - "\n**Note:** Vision analysis may have errors. Use 'Analyze netlist' for precise results.\n" - ) - - if "🚨" in error_report or "⚠️" in error_report: - summary += f"\n{error_report}" - - return summary, extraction - - return handle_follow_up_image_question(question, extraction), extraction - - -def handle_follow_up_image_question(user_input: str, - image_context: Dict[str, Any]) -> str: - """ - Answer questions about an analyzed image using ONLY extracted data. - """ - image_context_str = ( - f"**Circuit Type:** {image_context.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}\n" - f"**Components Detected:** {image_context.get('components', [])}\n" - f"**Component Values:** {image_context.get('values', {})}\n" - f"**Component Counts:** {image_context.get('component_counts', {})}\n" - f"**Description:** {image_context.get('vision_summary', '')}\n" - ) - - prompt = ( - "You are analyzing a circuit schematic. Answer using ONLY the circuit data below.\n\n" - "=== ANALYZED CIRCUIT DATA ===\n" - f"{image_context_str}\n" - "==============================\n\n" - f"USER QUESTION: {user_input}\n\n" - "STRICT INSTRUCTIONS:\n" - "1. Answer ONLY using the circuit data above - DO NOT use external knowledge.\n" - "2. For counts: use 'Component Counts'.\n" - "3. For values: use 'Component Values'.\n" - "4. For lists: use 'Components Detected'.\n" - "5. If data is missing, answer: 'The image analysis did not detect that information.'\n" - "6. Keep answer brief (2-3 sentences).\n\n" - "Answer:" - ) - - return run_ollama(prompt, mode="default") - - -def handle_netlist_analysis(user_input: str) -> str: - """ - Handle netlist analysis prompts (FACT-based prompt from GUI). - """ - raw_reply = run_ollama(user_input) - return clean_response_raw(raw_reply) - - -# ==================== MAIN ROUTER ==================== - -def handle_input(user_input: str, - history: List[Dict[str, str]] | None = None) -> str: - """ - Main router. Accepts optional conversation history for follow-up understanding. - """ - global LAST_IMAGE_CONTEXT, LAST_BOT_REPLY - - user_input = (user_input or "").strip() - if not user_input: - return "Please enter a query." - - if "[ESIM_NETLIST_START]" in user_input: - raw_reply = run_ollama(user_input) - cleaned = clean_response_raw(raw_reply) - LAST_BOT_REPLY = cleaned - return cleaned - - question_type = classify_question_type( - user_input, bool(LAST_IMAGE_CONTEXT), history - ) - print(f"[COPILOT] Question type: {question_type}") - - try: - if question_type == "netlist": - response = handle_netlist_analysis(user_input) - - elif question_type == "greeting": - response = handle_greeting() - - elif question_type == "image_query": - response, LAST_IMAGE_CONTEXT = handle_image_query(user_input) - - elif question_type == "follow_up_image": - response = handle_follow_up_image_question(user_input, LAST_IMAGE_CONTEXT) - - elif question_type == "simple": - response = handle_simple_question(user_input) - - elif question_type == "follow_up" and history: - response = handle_follow_up(user_input, LAST_IMAGE_CONTEXT, history) - else: - response = handle_simple_question(user_input) - - LAST_BOT_REPLY = response - return response - - except Exception as e: - error_msg = f"Error processing question: {str(e)}" - print(f"[COPILOT ERROR] {error_msg}") - return error_msg - - -# ==================== WRAPPER ==================== - -class ESIMCopilotWrapper: - def __init__(self) -> None: - self.history: List[Dict[str, str]] = [] - - def handle_input(self, user_input: str) -> str: - reply = handle_input(user_input, self.history) - self.history.append({"user": user_input, "bot": reply}) - if len(self.history) > 12: - self.history = self.history[-12:] - return reply - - def analyze_schematic(self, query: str) -> str: - return self.handle_input(query) - -_GLOBAL_WRAPPER = ESIMCopilotWrapper() - - -def analyze_schematic(query: str) -> str: - return _GLOBAL_WRAPPER.handle_input(query) diff --git a/src/chatbot/chatbot_thread.py b/src/chatbot/chatbot_thread.py index f134d4f57..4b3c33931 100644 --- a/src/chatbot/chatbot_thread.py +++ b/src/chatbot/chatbot_thread.py @@ -28,7 +28,7 @@ # llava internally resizes images to 336Γ—336 anyway. # Downscaling large images before sending saves encoding time and reduces # the number of tokens the model spends on the image. -_MAX_IMAGE_DIM = 512 # pixels on longest side β€” fast, good quality +_MAX_IMAGE_DIM = 336 # llava's native patch size β€” no benefit sending larger def _downscale_image_bytes(raw_bytes: bytes) -> bytes: @@ -51,7 +51,7 @@ def _downscale_image_bytes(raw_bytes: bytes) -> bytes: if img.mode not in ("RGB", "L"): img = img.convert("RGB") buf = _io.BytesIO() - img.save(buf, format="JPEG", quality=85) + img.save(buf, format="JPEG", quality=70) return buf.getvalue() except Exception: return raw_bytes # fall back to original on any error @@ -139,6 +139,22 @@ def detect_topic_switch(prev_text: str, curr_text: str) -> bool: # ── Model / service background workers ─────────────────────────────────────── +def _ensure_ollama_running(worker) -> bool: + """Helper to start Ollama server if it isn't running. Returns True if ready.""" + if not is_ollama_running(): + worker.status_signal.emit("Starting Ollama server β€” please wait…") + started = start_ollama(stop_flag=lambda: worker._stop_requested) + if not started: + if not worker._stop_requested: + worker.response_signal.emit( + "❌ Could not start Ollama automatically.\n" + "Please open a terminal and run: ollama serve" + ) + return False + worker.status_signal.emit("Ollama started!") + time.sleep(1) + return True + class OllamaStatusWorker(QThread): result_signal = pyqtSignal(bool) @@ -167,9 +183,9 @@ def run(self): # Keep the vision model cache warm so image sends don't block _refresh_model_cache() - self.result_signal.emit(names if names else ['qwen2.5-coder:3b']) + self.result_signal.emit(names if names else ['qwen2.5-coder:1.5b']) except Exception: - self.result_signal.emit(['qwen2.5-coder:3b']) + self.result_signal.emit(['qwen2.5-coder:1.5b']) # ── Smart token budget ─────────────────────────────────────────────────────── @@ -275,7 +291,7 @@ class OllamaWorker(QThread): response_signal = pyqtSignal(str) status_signal = pyqtSignal(str) - def __init__(self, chat_history, model="qwen2.5-coder:3b", + def __init__(self, chat_history, model="qwen2.5-coder:1.5b", temperature=0.25, num_predict=1024): super().__init__() self.chat_history = chat_history @@ -289,19 +305,9 @@ def stop(self): def run(self): try: - if not is_ollama_running(): - self.status_signal.emit("Starting Ollama server β€” please wait…") - started = start_ollama(stop_flag=lambda: self._stop_requested) - if not started: - if self._stop_requested: - return # user cancelled cleanly - self.response_signal.emit( - "❌ Could not start Ollama automatically.\n" - "Please open a terminal and run: ollama serve" - ) - return - self.status_signal.emit("Ollama started! Getting response…") - time.sleep(1) + if not _ensure_ollama_running(self): + return + self.status_signal.emit("Ollama is ready! Getting response…") # Keep last 10 history lines (5 turns). # Sending 20 lines fills most of the context window before the @@ -444,23 +450,14 @@ def _pick_best_vision_model(preferred: str = "") -> str: def _build_schematic_vision_prompt(extra_prompt: str, image_count: int) -> str: """ Build the prompt sent to the vision model alongside the image(s). - If the user typed a question, that question drives the response. - If no question was given, request a general circuit analysis. + Kept short to minimize prompt token processing time. """ - n = "this schematic" if image_count == 1 else f"these {image_count} schematics" if extra_prompt and extra_prompt.strip(): - # User asked something specific - make that the primary request. - return ( - f"Looking at {n}: {extra_prompt.strip()}\n\n" - "Base your answer on what is actually visible in the image." - ) + return extra_prompt.strip() else: - # No question given - do a general analysis. return ( - f"Please analyse {n}. " - "Identify the circuit's function, list all visible components with their " - "reference designators and values, name the nets and signal rails, " - "and flag any potential design issues you can see." + "Analyse this circuit image. Identify components, connections, " + "and its function. Flag any design issues." ) @@ -501,11 +498,13 @@ def _chat_once(self, model_name: str, prompt: str, image_bytes_list): ], stream=True, options={ - "temperature": 0.15, - # llava: ~576 tokens/image patch + ~200 prompt + 512 predict. - # 3072 gives comfortable headroom without the overhead of 4096. - "num_ctx": 3072, - "num_predict": 512, + "temperature": 0.1, + # Smaller context = faster KV-cache allocation on CPU. + # 2048 is enough for image patches + short prompt + response. + "num_ctx": 2048, + # Cap output to ~256 tokens for much faster responses. + # Most useful circuit analysis fits well within this budget. + "num_predict": 256, "repeat_penalty": 1.05, "keep_alive": "10m", } @@ -531,19 +530,8 @@ def _chat_once(self, model_name: str, prompt: str, image_bytes_list): def run(self): try: - if not is_ollama_running(): - self.status_signal.emit("Starting Ollama server β€” please wait…") - started = start_ollama(stop_flag=lambda: self._stop_requested) - if not started: - if self._stop_requested: - return - self.response_signal.emit( - "❌ Could not start Ollama automatically.\n" - "Please open a terminal and run: ollama serve" - ) - return - self.status_signal.emit("Ollama started!") - time.sleep(1) + if not _ensure_ollama_running(self): + return if not self.image_paths: self.response_signal.emit("❌ No image paths provided.") diff --git a/src/chatbot/error_solutions.py b/src/chatbot/error_solutions.py deleted file mode 100644 index 615a3d63c..000000000 --- a/src/chatbot/error_solutions.py +++ /dev/null @@ -1,106 +0,0 @@ -# error_solutions.py -from typing import Dict,Any - -ERROR_SOLUTIONS = { - "no ground": { - "description": "Missing ground reference (Node 0)", - "severity": "critical", - "fixes": [ - "Add GND symbol (0) to schematic", - "Ensure all nodes have DC path to ground", - "Add 1GΞ© resistors from floating nodes to GND for simulation stability", - "Use GND symbol from eSim power library" - ], - "eSim_command": "Add 'GND' symbol from 'power' library" - }, - - "floating pins": { - "description": "Unconnected component pins", - "severity": "moderate", - "fixes": [ - "Connect all unused pins to appropriate nets", - "For unused inputs: tie to VCC or GND through resistors", - "For unused outputs: leave unconnected but label properly" - ], - "eSim_command": "Use 'Place Wire' tool to connect pins" - }, - - "disconnected wires": { - "description": "Wires not properly connected to pins", - "severity": "critical", - "fixes": [ - "Zoom in and check wire endpoints touch pins", - "Use junction dots at wire intersections", - "Re-route wires to ensure proper connections" - ], - "eSim_command": "Press 'J' to add junction dots" - }, - - "missing spice model": { - "description": "Component lacks SPICE model definition", - "severity": "critical", - "fixes": [ - "Add .lib statement: .lib /usr/share/esim/models.lib", - "Check IC availability in Components/ICs.pdf", - "Use eSim library components only", - "Create custom model using Model Editor" - ], - "eSim_command": "Add '.lib /usr/share/esim/models.lib' in schematic" - }, - - "singular matrix": { - "description": "Simulation convergence error", - "severity": "critical", - "fixes": [ - "Add 1GΞ© resistors from ALL nodes β†’ GND", - "Add .options gmin=1e-12 reltol=0.01", - "Use .nodeset for initial voltages", - "Add 0.1Ξ© series resistors to voltage sources" - ], - "eSim_command": "Add '.options gmin=1e-12 reltol=0.01' in .cir file" - }, - - "missing component values": { - "description": "Components without specified values", - "severity": "moderate", - "fixes": [ - "Double-click components to edit values", - "Set R, C, L values before simulation", - "For ICs: specify model number", - "For sources: set voltage/current values" - ], - "eSim_command": "Double-click component β†’ Edit Properties β†’ Set Value" - }, - - "no load after rectifier": { - "description": "Rectifier output has no load capacitor", - "severity": "warning", - "fixes": [ - "Add filter capacitor after rectifier (100-1000ΞΌF)", - "Add load resistor to establish DC operating point", - "Add voltage regulator for stable output" - ], - "eSim_command": "Add capacitor between rectifier output and GND" - } -} - -def get_error_solution(error_message: str) -> Dict[str, Any]: - """Get detailed solution for specific error.""" - error_lower = error_message.lower() - - for error_key, solution in ERROR_SOLUTIONS.items(): - if error_key in error_lower: - return solution - - # Default solution for unknown errors - return { - "description": "General schematic error", - "severity": "unknown", - "fixes": [ - "Check all connections are proper", - "Verify component values are set", - "Ensure ground symbol is present", - "Check for duplicate component IDs" - ], - "eSim_command": "Run Design Rule Check (DRC) in KiCad" - } diff --git a/src/chatbot/image_handler.py b/src/chatbot/image_handler.py deleted file mode 100644 index cd8744791..000000000 --- a/src/chatbot/image_handler.py +++ /dev/null @@ -1,247 +0,0 @@ -import os -import json -import base64 -import io -import time -from typing import Dict, Any -from PIL import Image -MAX_IMAGE_BYTES = int(0.5*1024 * 1024) -from .ollama_runner import run_ollama_vision - -# === IMPORT PADDLE OCR === -try: - from paddleocr import PaddleOCR - import logging - logging.getLogger("ppocr").setLevel(logging.ERROR) - - # CRITICAL FIX: Disabled MKLDNN and Angle Classification to prevent VM Crashes - ocr_engine = PaddleOCR( - use_angle_cls=False, # <--- MUST BE FALSE TO STOP SIGABRT - lang='en', - use_gpu=False, # Force CPU - enable_mkldnn=False, # <--- MUST BE FALSE FOR PADDLE v3 COMPATIBILITY - use_mp=False, # Disable multiprocessing - show_log=False - ) - HAS_PADDLE = True - print("[INIT] PaddleOCR initialized (Safe Mode).") -except Exception as e: - HAS_PADDLE = False - print(f"[INIT] PaddleOCR init failed: {e}") - print("[INIT] Vision analysis unavailable. Text and netlist analysis still work.") - - -def encode_image(image_path: str) -> str: - """Convert image to base64 string.""" - with open(image_path, "rb") as image_file: - return base64.b64encode(image_file.read()).decode("utf-8") - - -def optimize_image_for_vision(image_path: str) -> bytes: - """ - Resize large images to reduce vision model processing time. - Target: Max 1920x1080 while maintaining aspect ratio. - """ - try: - img = Image.open(image_path) - - if img.mode not in ('RGB', 'L'): - img = img.convert('RGB') - - max_width = 1920 - max_height = 1080 - - if img.width > max_width or img.height > max_height: - # Calculate scaling factor - scale = min(max_width / img.width, max_height / img.height) - new_size = (int(img.width * scale), int(img.height * scale)) - img = img.resize(new_size, Image.Resampling.LANCZOS) - print(f"[IMAGE] Resized from {img.width}x{img.height} to {new_size[0]}x{new_size[1]}") - - # Convert to bytes (PNG format prevents compression artifacts on text) - buffer = io.BytesIO() - img.save(buffer, format='PNG', optimize=True, quality=85) - return buffer.getvalue() - - except Exception as e: - print(f"[IMAGE] Optimization failed: {e}, using original") - with open(image_path, 'rb') as f: - return f.read() - - -def extract_text_with_paddle(image_path: str) -> str: - """Extract text using PaddleOCR (Handles rotated/vertical text excellently).""" - if not HAS_PADDLE: - return "" - try: - result = ocr_engine.ocr(image_path, cls=True) - detected_texts = [] - if result and result[0]: - for line in result[0]: - text = line[1][0] - conf = line[1][1] - - if conf > 0.6: - detected_texts.append(text) - - full_text = " ".join(detected_texts) - return full_text - - except Exception as e: - print(f"[OCR] PaddleOCR Failed: {e}") - return "" - -def analyze_and_extract(image_path: str) -> Dict[str, Any]: - """ - Analyze schematic with image optimization, PaddleOCR text injection, and timeout handling. - Rejects images larger than 0.5 MB. - """ - if not os.path.exists(image_path): - return { - "error": "Image file not found", - "vision_summary": "", - "component_counts": {}, - "circuit_analysis": { - "circuit_type": "Unknown", - "design_errors": [], - "design_warnings": [] - }, - "components": [], - "values": {} - } - - try: - file_size = os.path.getsize(image_path) - except OSError as e: - return { - "error": f"Could not read image size: {e}", - "vision_summary": "", - "component_counts": {}, - "circuit_analysis": { - "circuit_type": "Unknown", - "design_errors": [], - "design_warnings": [] - }, - "components": [], - "values": {} - } - - if file_size > MAX_IMAGE_BYTES: - size_mb = round(file_size / (1024 * 1024), 2) - return { - "error": f"Image too large ({size_mb} MB). Max allowed size is 0.5 MB.", - "vision_summary": "", - "component_counts": {}, - "circuit_analysis": { - "circuit_type": "Unknown", - "design_errors": ["Image file size exceeded 0.5 MB limit"], - "design_warnings": [] - }, - "components": [], - "values": {} - } - - # === OPTIMIZE IMAGE BEFORE SENDING === - print(f"[VISION] Processing image: {os.path.basename(image_path)}") - image_bytes = optimize_image_for_vision(image_path) - - # === EXTRACT OCR TEXT (CRITICAL STEP) === - ocr_text = extract_text_with_paddle(image_path) - - if ocr_text: - clean_ocr = ocr_text.strip() - print(f"[VISION] PaddleOCR Hints injected: {clean_ocr[:100]}...") - else: - clean_ocr = "No readable text detected." - - # === PROMPT WITH CONTEXT === - prompt = f""" -ANALYZE THIS ELECTRONICS SCHEMATIC IMAGE. - -CONTEXT FROM OCR SCAN (Text detected in image): -"{clean_ocr}" - -INSTRUCTIONS: -1. Use the OCR text to identify component labels (e.g., if you see "D1" text, there is a Diode, R1,R2,R3... for resistor). -2. Look for rotated text labels near symbols. -3. Identify the circuit topology. - -VERY IMPORTANT INSTRUCTIONS: -1. DON'T OVERCALCULATE MODEL COUNT LIKE MODEL COUNT + OCR COUNT -2. IF THERE IS ANY VALUE NOT PRESENT FOR ANY COMPONENT JUST ADD A QUESTION MARK IN FRONT OF IT - -OUTPUT RULES: -1. Return ONLY valid JSON. -2. Structure: - - -RESPOND WITH JSON ONLY. -""" - - max_retries = 2 - for attempt in range(max_retries): - try: - print(f"[VISION] Attempt {attempt + 1}/{max_retries}...") - - response_text = run_ollama_vision(prompt, image_bytes) - - cleaned_json = response_text.replace("```json", "").replace("```", "").strip() - - if "{" in cleaned_json and "}" in cleaned_json: - start = cleaned_json.index("{") - end = cleaned_json.rindex("}") + 1 - cleaned_json = cleaned_json[start:end] - - data = json.loads(cleaned_json) - - required_keys = ["vision_summary", "component_counts", "circuit_analysis", "components", "values"] - for key in required_keys: - if key not in data: - raise ValueError(f"Missing required key: {key}") - - if not isinstance(data.get("circuit_analysis"), dict): - data["circuit_analysis"] = {"circuit_type": "Unknown", "design_errors": [], "design_warnings": []} - - if "design_errors" not in data["circuit_analysis"]: - data["circuit_analysis"]["design_errors"] = [] - - if not data.get("component_counts") or all(v == 0 for v in data.get("component_counts", {}).values()): - counts = {"R": 0, "C": 0, "U": 0, "Q": 0, "D": 0, "L": 0, "Misc": 0} - for comp in data.get("components", []): - if isinstance(comp, str) and len(comp) > 0: - comp_type = comp[0].upper() - if comp_type in counts: - counts[comp_type] += 1 - elif "DIODE" in comp.upper() or comp.startswith("D"): - counts["D"] = counts.get("D", 0) + 1 - data["component_counts"] = counts - - if data.get("components"): - data["components"] = list(dict.fromkeys(data["components"])) - - print(f"[VISION] Success: {data.get('circuit_analysis', {}).get('circuit_type', 'Unknown')}") - return data - - except Exception as e: - print(f"[VISION] Attempt {attempt + 1} failed: {str(e)}") - if attempt == max_retries - 1: - return { - "error": f"Vision analysis failed: {str(e)}", - "vision_summary": "Unable to analyze circuit image", - "component_counts": {}, - "circuit_analysis": { - "circuit_type": "Unknown", - "design_errors": ["Analysis timed out or failed"], - "design_warnings": [] - }, - "components": [], - "values": {} - } - else: - import time - time.sleep(2) - - -def analyze_image(image_path: str, question: str | None = None, preprocess: bool = True) -> str: - """Helper for manual testing.""" - return str(analyze_and_extract(image_path)) \ No newline at end of file diff --git a/src/chatbot/knowledge_base.py b/src/chatbot/knowledge_base.py deleted file mode 100644 index 14ea4cc17..000000000 --- a/src/chatbot/knowledge_base.py +++ /dev/null @@ -1,144 +0,0 @@ -import os -import chromadb -from .ollama_runner import get_embedding - -# ==================== DATABASE SETUP ==================== - -def _default_db_path() -> str: - xdg_data_home = os.environ.get("XDG_DATA_HOME", "").strip() - if not xdg_data_home: - xdg_data_home = os.path.join(os.path.expanduser("~"), ".local", "share") - return os.path.join(xdg_data_home, "esim-copilot", "chroma") - -db_path = os.environ.get("ESIM_COPILOT_DB_PATH", "").strip() or _default_db_path() -os.makedirs(db_path, exist_ok=True) -chroma_client = chromadb.PersistentClient(path=db_path) - -collection = chroma_client.get_or_create_collection(name="esim_manuals") - -# ==================== INGESTION ==================== -def ingest_pdfs(manuals_directory: str) -> None: - """ - Read the single master text file and index it. - Call this once from src/ingest.py. - """ - if not os.path.exists(manuals_directory): - print("Directory not found.") - return - - # Clear existing DB to ensure no duplicates from old files - print("Clearing old database...") - try: - chroma_client.delete_collection("esim_manuals") - global collection - collection = chroma_client.get_or_create_collection(name="esim_manuals") - except Exception as e: - print(f"Warning clearing DB: {e}") - - # Look for .txt files only - files = [f for f in os.listdir(manuals_directory) if f.lower().endswith(".txt")] - - if not files: - print("❌ No .txt files found to ingest!") - return - - for filename in files: - path = os.path.join(manuals_directory, filename) - print(f"\nπŸ“„ Processing Master File: {filename}") - - try: - with open(path, "r", encoding="utf-8") as f: - text = f.read() - - raw_sections = text.split("\n\n") - - documents, embeddings, metadatas, ids = [], [], [], [] - - chunk_counter = 0 - for section in raw_sections: - section = section.strip() - if len(section) < 50: - continue - - # Further split large sections by double newlines if needed - sub_chunks = [c.strip() for c in section.split("\n\n") if len(c) > 50] - - for chunk in sub_chunks: - embed = get_embedding(chunk) - if embed: - documents.append(chunk) - embeddings.append(embed) - metadatas.append({"source": filename, "type": "master_ref"}) - ids.append(f"{filename}_{chunk_counter}") - chunk_counter += 1 - - if documents: - collection.add( - documents=documents, - embeddings=embeddings, - metadatas=metadatas, - ids=ids, - ) - print(f" βœ… Indexed {len(documents)} chunks from {filename}") - else: - print(f" ⚠️ No valid chunks found in {filename}") - - except Exception as e: - print(f" ❌ Failed to process {filename}: {e}") - - -# ==================== SEARCH ==================== - -# Relevance threshold: ChromaDB returns distances (L2 or cosine). -# Lower distance = more similar. Filter out chunks with distance > threshold. -RELEVANCE_THRESHOLD = float(os.environ.get("ESIM_RAG_RELEVANCE_THRESHOLD", "1.0")) - - -def search_knowledge(query: str, n_results: int = 4) -> str: - """ - Semantic search with relevance threshold to reduce hallucination. - Filters out chunks with distance > RELEVANCE_THRESHOLD. - """ - try: - query_embed = get_embedding(query) - if not query_embed: - return "" - - results = collection.query( - query_embeddings=[query_embed], - n_results=n_results, - include=["documents", "distances"], - ) - - docs_list = results.get("documents", [[]]) - distances_list = results.get("distances", [[]]) - - if not docs_list or not docs_list[0]: - return "" - - docs = docs_list[0] - distances = distances_list[0] if distances_list else [] - - # Filter by relevance threshold (lower distance = more similar) - if distances and len(distances) == len(docs): - filtered = [ - (doc, d) for doc, d in zip(docs, distances) - if d <= RELEVANCE_THRESHOLD - ] - if filtered: - selected_chunks = [doc for doc, _ in filtered] - else: - return "" - else: - selected_chunks = docs - - context_text = "\n\n...\n\n".join(selected_chunks) - if len(context_text) > 3500: - context_text = context_text[:3500] - - header = "=== ESIM OFFICIAL DOCUMENTATION ===\n" - return f"{header}{context_text}\n===================================\n" - - except Exception as e: - print(f"RAG Error: {e}") - return "" diff --git a/src/chatbot/ollama_runner.py b/src/chatbot/ollama_runner.py deleted file mode 100644 index ae754bd0b..000000000 --- a/src/chatbot/ollama_runner.py +++ /dev/null @@ -1,192 +0,0 @@ -import os -import ollama -import json -import time - -# ==================== CLIENT ==================== - -ollama_client = ollama.Client( - host="http://localhost:11434", - timeout=300.0, -) - -# ==================== SETTINGS ==================== - -_SETTINGS_DIR = os.path.join( - os.path.expanduser("~"), ".local", "share", "esim-copilot" -) -_SETTINGS_PATH = os.path.join(_SETTINGS_DIR, "settings.json") - -_DEFAULT_TEXT_MODEL = "qwen2.5:3b" -_DEFAULT_VISION_MODEL = "minicpm-v:latest" -EMBED_MODEL = "nomic-embed-text" - - -def load_model_settings() -> dict: - """Load persisted model preferences from disk.""" - try: - with open(_SETTINGS_PATH, "r", encoding="utf-8") as f: - return json.load(f) - except Exception: - return {} - - -def save_model_settings(text_model: str, vision_model: str) -> None: - """Persist model preferences to disk.""" - os.makedirs(_SETTINGS_DIR, exist_ok=True) - try: - with open(_SETTINGS_PATH, "w", encoding="utf-8") as f: - json.dump({"text_model": text_model, "vision_model": vision_model}, f, indent=2) - except Exception as e: - print(f"[SETTINGS] Failed to save: {e}") - - -def list_available_models() -> list: - """Query Ollama for installed models. Returns list of model name strings.""" - try: - resp = ollama_client.list() - names = [m["name"] for m in resp.get("models", [])] - return names if names else [_DEFAULT_TEXT_MODEL, _DEFAULT_VISION_MODEL] - except Exception: - return [_DEFAULT_TEXT_MODEL, _DEFAULT_VISION_MODEL, EMBED_MODEL] - - -# Load settings and initialise model dicts -_settings = load_model_settings() - -VISION_MODELS = {"primary": _settings.get("vision_model", _DEFAULT_VISION_MODEL)} -TEXT_MODELS = {"default": _settings.get("text_model", _DEFAULT_TEXT_MODEL)} - - -def reload_model_settings() -> None: - """Re-read settings from disk and update running dicts (called after save).""" - s = load_model_settings() - VISION_MODELS["primary"] = s.get("vision_model", _DEFAULT_VISION_MODEL) - TEXT_MODELS["default"] = s.get("text_model", _DEFAULT_TEXT_MODEL) - - -# ==================== VISION ==================== - -def run_ollama_vision(prompt: str, image_input) -> str: - """Call vision model with Chain-of-Thought for better accuracy.""" - model = VISION_MODELS["primary"] - - try: - import base64 - - image_b64 = "" - if isinstance(image_input, bytes): - image_b64 = base64.b64encode(image_input).decode("utf-8") - elif isinstance(image_input, str) and os.path.isfile(image_input): - with open(image_input, "rb") as f: - image_b64 = base64.b64encode(f.read()).decode("utf-8") - elif isinstance(image_input, str) and len(image_input) > 100: - image_b64 = image_input - else: - raise ValueError("Invalid image input format") - - system_prompt = ( - "You are an expert Electronics Engineer using eSim.\n" - "Analyze the schematic image carefully.\n\n" - "STEP 1: THINKING PROCESS\n" - "- List visible components (e.g., 'I see 4 diodes in a bridge...').\n" - "- Trace connections (e.g., 'Resistor R1 is in series...').\n" - "- Check against the OCR text provided.\n\n" - "STEP 2: JSON OUTPUT\n" - "After your analysis, output a SINGLE JSON object wrapped in ```json ... ```.\n" - "Structure:\n" - "{\n" - ' "vision_summary": "Summary string",\n' - ' "component_counts": {"R": 0, "C": 0, "D": 0, "Q": 0, "U": 0},\n' - ' "circuit_analysis": {\n' - ' "circuit_type": "Rectifier/Amplifier/etc",\n' - ' "design_errors": [],\n' - ' "design_warnings": []\n' - ' },\n' - ' "components": ["R1", "D1"],\n' - ' "values": {"R1": "1k"}\n' - "}\n" - ) - - resp = ollama_client.chat( - model=model, - messages=[ - {"role": "system", "content": system_prompt}, - { - "role": "user", - "content": prompt, - "images": [image_b64], - }, - ], - options={ - "temperature": 0.0, - "num_ctx": 8192, - "num_predict": 1024, - }, - ) - - content = resp["message"]["content"] - - import re - json_match = re.search(r'```json\s*(\{.*?\})\s*```', content, re.DOTALL) - if json_match: - return json_match.group(1) - - start = content.find('{') - end = content.rfind('}') + 1 - if start != -1 and end != -1: - return content[start:end] - - return "{}" - - except Exception as e: - print(f"[VISION ERROR] {e}") - return json.dumps({ - "vision_summary": f"Vision failed: {str(e)[:50]}", - "component_counts": {}, - "circuit_analysis": {"circuit_type": "Error", "design_errors": [], "design_warnings": []}, - "components": [], - "values": {}, - }) - - -# ==================== TEXT ==================== - -def run_ollama(prompt: str, mode: str = "default") -> str: - """Run text model with focused parameters.""" - model = TEXT_MODELS.get(mode, TEXT_MODELS["default"]) - - try: - resp = ollama_client.chat( - model=model, - messages=[ - { - "role": "system", - "content": "You are an eSim and electronics expert. Be concise, accurate, and practical.", - }, - {"role": "user", "content": prompt}, - ], - options={ - "temperature": 0.05, - "num_ctx": 2048, - "num_predict": 400, - "top_p": 0.9, - "repeat_penalty": 1.1, - }, - ) - return resp["message"]["content"].strip() - - except Exception as e: - return f"[Error] {str(e)}" - - -# ==================== EMBEDDINGS ==================== - -def get_embedding(text: str): - """Get text embeddings for RAG.""" - try: - r = ollama_client.embeddings(model=EMBED_MODEL, prompt=text) - return r["embedding"] - except Exception as e: - print(f"[EMBED ERROR] {e}") - return None diff --git a/src/chatbot/stt_handler.py b/src/chatbot/stt_handler.py deleted file mode 100644 index f2d536066..000000000 --- a/src/chatbot/stt_handler.py +++ /dev/null @@ -1,92 +0,0 @@ -import os -import json -import queue -import time - -try: - import sounddevice as sd - from vosk import Model, KaldiRecognizer - _HAS_STT = True -except Exception: - sd = None - Model = None - KaldiRecognizer = None - _HAS_STT = False - -_MODEL = None - -DEFAULT_VOSK_DIR = os.path.join( - os.path.expanduser("~"), ".local", "share", - "esim-copilot", "vosk-model-small-en-us-0.15", -) - -def _get_model(): - global _MODEL - if not _HAS_STT: - raise RuntimeError( - "Speech-to-text is not available (missing vosk/sounddevice)." - ) - model_path = os.environ.get("VOSK_MODEL_PATH", "").strip() - if not model_path: - model_path = DEFAULT_VOSK_DIR - if not os.path.isdir(model_path): - raise RuntimeError( - f"Vosk model path not found. Set VOSK_MODEL_PATH or install at: {model_path}" - ) - if _MODEL is None: - _MODEL = Model(model_path) - return _MODEL - -def listen_to_mic(should_stop=lambda: False, max_silence_sec=3, samplerate=16000, phrase_limit_sec=8) -> str: - """ - Offline STT using Vosk. - Returns recognized text, or "" if cancelled / timed out. - """ - if not _HAS_STT: - raise RuntimeError("Speech-to-text is not installed or failed to load.") - q = queue.Queue() - rec = KaldiRecognizer(_get_model(), samplerate) - - started = False - t0 = time.time() - t_speech = None - - def callback(indata, frames, time_info, status): - q.put(bytes(indata)) - - with sd.RawInputStream( - samplerate=samplerate, - channels=1, - dtype="int16", - blocksize=8000, - callback=callback, - ): - while True: - if should_stop(): - return "" - - now = time.time() - - # Stop after silence - if not started and (now - t0) >= max_silence_sec: - return "" - - if started and t_speech and (now - t_speech) >= phrase_limit_sec: - break - - try: - data = q.get(timeout=0.2) - except queue.Empty: - continue - - if rec.AcceptWaveform(data): - text = json.loads(rec.Result()).get("text", "").strip() - if text: - return text - else: - partial = json.loads(rec.PartialResult()).get("partial", "").strip() - if partial and not started: - started = True - t_speech = now - - return json.loads(rec.FinalResult()).get("text", "").strip() diff --git a/src/frontEnd/Application.py b/src/frontEnd/Application.py index 20a529731..14cf662bd 100644 --- a/src/frontEnd/Application.py +++ b/src/frontEnd/Application.py @@ -130,6 +130,7 @@ def initchatbot(self): self.chatbot_dock.visibilityChanged.connect( lambda _: self._reposition_chatbot_icon() ) + self.chatbot_dock.installEventFilter(self) # ── Floating icon button (bottom-right corner) ────────────────── self.chatboticon = QtWidgets.QPushButton( @@ -191,6 +192,14 @@ def _reposition_chatbot_icon(self): self.chatboticon.move(x, bottom_y) self.chatboticon.raise_() # Always keep on top + def eventFilter(self, obj, event): + """ + Detect resize events on the dock widget so the icon stays aligned. + """ + if obj == self.chatbot_dock and event.type() == QtCore.QEvent.Resize: + self._reposition_chatbot_icon() + return super().eventFilter(obj, event) + def resizeEvent(self, event): """ Adjust chatbot icon button position during window resize. diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index 1a3e75701..9ebc5fc60 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -217,11 +217,15 @@ def _image_thumbnail_html(b64_str: str, filename: str) -> str: def _user_bubble(text, timestamp): safe = _escape_text_preserve_breaks(text) + b64_text = base64.b64encode(text.encode('utf-8')).decode('utf-8') return ( '' '' '
' '' + '' '' - f'' '
' + f'✏️' + '' f'{safe}' '
You  Β·  {timestamp}
' '
' @@ -243,45 +247,34 @@ def _approx_token_count(text: str) -> int: return max(1, len(text) // 4) -def _bot_bubble(text, timestamp, response_idx): +def _bot_bubble(text, timestamp, response_idx=None): + """Render a bot response bubble. If response_idx is given, include Retry/Copy links.""" rendered = _render_markdown(text) - copy_href = f'copy:///{response_idx}' - retry_href = f'retry:///{response_idx}' - token_est = _approx_token_count(text) - - return ( - '' - '' - '
' - '' - '' - '
' - f'{rendered}' - '
' - '' - f'' - f'' - '
' - f'eSim AI  Β·  {timestamp}  Β·  ~{token_est} tokens' - f'↻ Retry' - f'  ' - f'Copy
' - '
' - '
' - ) + if response_idx is not None: + copy_href = f'copy:///{response_idx}' + retry_href = f'retry:///{response_idx}' + token_est = _approx_token_count(text) + footer = ( + '' + '' + f'' + f'' + '
' + f'eSim AI  Β·  {timestamp}  Β·  ~{token_est} tokens' + f'↻ Retry' + f'  ' + f'Copy
' + '' + ) + else: + footer = ( + f'' + f'eSim AI  Β·  {timestamp}' + ) -def _bot_bubble_simple(text, timestamp): - rendered = _render_markdown(text) return ( '' '' - f'' + f'{footer}' '
' @@ -296,8 +289,7 @@ def _bot_bubble_simple(text, timestamp): '">' f'{rendered}' '
' - f'eSim AI  Β·  {timestamp}
' '' '' @@ -408,6 +400,10 @@ def _is_image_file(path: str) -> bool: # ── Smart input field ───────────────────────────────────────────────────────── class _HistoryLineEdit(QLineEdit): + """Input field with command history (↑/↓) and clipboard image paste (Ctrl+V).""" + + image_pasted = pyqtSignal(str) # emits temp file path of pasted image + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._sent_history = [] @@ -419,6 +415,29 @@ def add_to_history(self, text): self._hist_idx = -1 def keyPressEvent(self, event: QKeyEvent): + # ── Ctrl+V: check for clipboard image before default paste ──── + if event.key() == Qt.Key_V and event.modifiers() & Qt.ControlModifier: + clipboard = QApplication.clipboard() + mime = clipboard.mimeData() + if mime and mime.hasImage(): + image = clipboard.image() + if not image.isNull(): + import tempfile + tmp_dir = os.path.join( + os.path.expanduser('~'), '.esim', 'clipboard_images' + ) + os.makedirs(tmp_dir, exist_ok=True) + tmp_path = os.path.join( + tmp_dir, + f"paste_{datetime.now().strftime('%Y%m%d_%H%M%S')}.png" + ) + image.save(tmp_path, "PNG") + self.image_pasted.emit(tmp_path) + return + # Fall through to default paste for text + super().keyPressEvent(event) + return + if event.key() == Qt.Key_Up and self._sent_history: if self._hist_idx == -1: self._draft = self.text() @@ -514,7 +533,7 @@ def __init__(self, session: dict, parent=None): if line.startswith("User:"): html += _user_bubble(line[5:].strip(), "") elif line.startswith("Bot:"): - html += _bot_bubble_simple(line[4:].strip(), "") + html += _bot_bubble(line[4:].strip(), "") browser.setHtml(html if html else "

No messages

") QTimer.singleShot(120, lambda: browser.verticalScrollBar().setValue(browser.verticalScrollBar().maximum())) root.addWidget(browser) @@ -1334,10 +1353,11 @@ def __init__(self): self.user_input = _HistoryLineEdit( self, placeholderText="Message eSim AI… (↑↓ for history)" ) + self.user_input.setMinimumHeight(42) self.user_input.setStyleSheet(""" QLineEdit { - font-size:13px; padding:9px 14px; - border:1.5px solid #e0e0e0; border-radius:22px; + font-size:14px; padding:10px 18px; + border:1.5px solid #e0e0e0; border-radius:21px; background:#f7f7f7; color:#1a1a2e; } QLineEdit:focus { @@ -1346,15 +1366,20 @@ def __init__(self): } """) self.user_input.returnPressed.connect(self.ask_ollama) + self.user_input.image_pasted.connect( + lambda path: self._stage_image_paths([path]) + ) input_layout.addWidget(self.user_input) - self.send_button = QPushButton("Send") - self.send_button.setFixedHeight(38) + self.send_button = QPushButton("➀") + self.send_button.setFixedSize(40, 40) + self.send_button.setToolTip("Send Message") self.send_button.setStyleSheet(""" QPushButton { - font-size:13px; font-weight:600; padding:5px 20px; + font-size:18px; font-weight:600; background-color:#0095f6; color:white; - border:none; border-radius:19px; + border:none; border-radius:20px; + padding-left: 2px; } QPushButton:hover { background-color:#0082d8; } """) @@ -1374,18 +1399,7 @@ def __init__(self): self.stop_button.hide() input_layout.addWidget(self.stop_button) - self.clear_button = QPushButton("Clear") - self.clear_button.setFixedHeight(38) - self.clear_button.setStyleSheet(""" - QPushButton { - font-size:13px; padding:5px 14px; - background-color:#f0f0f0; color:#666; - border:none; border-radius:19px; - } - QPushButton:hover { background-color:#ffe0e0; color:#cc0000; } - """) - self.clear_button.clicked.connect(self.clear_session) - input_layout.addWidget(self.clear_button) + chat_layout.addLayout(input_layout) @@ -2051,6 +2065,19 @@ def _handle_link_click(self, url): return self._retry_response(idx) + elif scheme == 'edit': + if not parts: + return + try: + import base64 + b64_text = parts[-1] + text = base64.b64decode(b64_text).decode('utf-8') + self._editing_prompt_text = text + self.user_input.setText(text) + self.user_input.setFocus() + except Exception: + pass + elif scheme == 'clear': self.clear_session() @@ -2153,35 +2180,49 @@ def _clear_staged_images(self): self._staged_images.clear() self._refresh_staging_strip() - def _warn_or_switch_to_vision_model(self) -> bool: + def _auto_switch_model(self, keywords, preferred_names, label): """ - Ensure a vision-capable model is selected before sending images. - - Returns True if it is safe to proceed (a vision model is active), - or False if no vision model is installed and the request should be - blocked. Sending images to a text-only model causes it to fabricate - completely wrong answers because it cannot actually see the image. + Shared helper for auto-switching models. + Returns the index of the matched model, or -1 if none found. """ current = self.model_combo.currentText() - vision_keywords = ["llava", "bakllava", "vision", "moondream", "qwen2-vl", "minicpm-v"] - # Already on a vision model β€” good to go. - if any(k in current.lower() for k in vision_keywords): - return True + # Already on a matching model β€” no switch needed. + if any(k in current.lower() for k in keywords): + return self.model_combo.currentIndex() + + # Try preferred model names first (exact match). + for preferred in preferred_names: + idx = self.model_combo.findText(preferred) + if idx >= 0: + self.model_combo.setCurrentIndex(idx) + self.chat_display.append(_system_bubble( + f"πŸ”„ Auto-switched to {label} model: {preferred}" + )) + self._scroll_to_bottom() + return idx - # Try to auto-switch to any vision model the user has installed. - preferred_order = ["moondream", "llava:7b", "llava", "bakllava", "llava:13b"] + # Fallback: any model containing one of the keywords. for i in range(self.model_combo.count()): name = self.model_combo.itemText(i) - if any(k in name.lower() for k in vision_keywords): + if any(k in name.lower() for k in keywords): self.model_combo.setCurrentIndex(i) self.chat_display.append(_system_bubble( - f"Switched to vision model: {name}" + f"πŸ”„ Auto-switched to {label} model: {name}" )) self._scroll_to_bottom() - return True + return i + + return -1 - # No vision model found β€” block the request and explain clearly. + def _warn_or_switch_to_vision_model(self) -> bool: + """Ensure a vision model is active before sending images.""" + vision_kw = ["llava", "bakllava", "vision", "moondream", "qwen2-vl", "minicpm-v"] + preferred = ["llava:latest", "llava", "llava:7b", "llava:13b", "bakllava", "moondream"] + idx = self._auto_switch_model(vision_kw, preferred, "vision") + if idx >= 0: + return True + # No vision model found β€” block and explain. self.chat_display.append(_system_bubble( "⚠️ No vision model installed. Image analysis is not possible with the " "current model β€” a text-only model cannot see images and will give " @@ -2193,6 +2234,10 @@ def _warn_or_switch_to_vision_model(self) -> bool: self._scroll_to_bottom() return False + def _switch_to_text_model(self): + """Auto-switch to qwen2.5 for text queries.""" + self._auto_switch_model(["qwen2.5"], [], "text") + # ── Mic ────────────────────────────────────────────────────────── def _on_temp_changed(self, value: int): @@ -2451,19 +2496,23 @@ def _on_models_fetched(self, model_names: list): for name in model_names: self.model_combo.addItem(name) - preferred_order = [ - 'qwen2.5-coder:3b', - 'llava:13b', - 'llava:7b', - 'llava', - 'bakllava', - ] + # Try to default to any qwen2.5 variant chosen_idx = -1 - for preferred in preferred_order: - idx = self.model_combo.findText(preferred) - if idx >= 0: - chosen_idx = idx + for i in range(self.model_combo.count()): + name = self.model_combo.itemText(i) + if "qwen2.5" in name.lower(): + chosen_idx = i break + + # If no qwen2.5, try some fallback preferred models + if chosen_idx == -1: + preferred_fallbacks = ['llava:13b', 'llava:7b', 'llava', 'bakllava'] + for preferred in preferred_fallbacks: + idx = self.model_combo.findText(preferred) + if idx >= 0: + chosen_idx = idx + break + if chosen_idx >= 0: self.model_combo.setCurrentIndex(chosen_idx) @@ -2482,7 +2531,7 @@ def _start_thinking(self): self._staging_area.setEnabled(False) self.send_button.hide() self.stop_button.show() - self.clear_button.setEnabled(False) + self._show_typing_bubble() def _stop_thinking(self): @@ -2495,7 +2544,7 @@ def _stop_thinking(self): self._staging_area.setEnabled(True) self.stop_button.hide() self.send_button.show() - self.clear_button.setEnabled(True) + def _scroll_to_bottom(self): self.chat_display.verticalScrollBar().setValue( @@ -2629,6 +2678,17 @@ def ask_ollama(self): # so no rebuild is needed β€” just clear the read-only flag. self._viewing_past_session = False + editing_text = getattr(self, '_editing_prompt_text', None) + if editing_text: + # We are editing an existing prompt. Find it in history and truncate. + for i in range(len(self.chat_history) - 1, -1, -1): + msg = self.chat_history[i] + if msg == f"User: {editing_text}" or msg.endswith(f"\n{editing_text}"): + self.chat_history = self.chat_history[:i] + self._rebuild_chat_html_from_history() + break + self._editing_prompt_text = None + ts = _get_time() if staged_paths: @@ -2718,8 +2778,16 @@ def ask_ollama(self): self.worker.start() return - self._current_session_kind = "text" self._check_topic_switch(user_text) + + # The user explicitly requested that any text-only search should + # switch to the Qwen model. Since Qwen cannot process images, + # we must drop any previous image context and use the text worker. + self._last_image_paths.clear() + + self._current_session_kind = "text" + self._switch_to_text_model() + self.chat_history = (self.chat_history + [f"User: {user_text}"])[-20:] self.chat_display.append(_user_bubble(user_text, ts)) self._scroll_to_bottom() @@ -2730,19 +2798,7 @@ def ask_ollama(self): self._retry_history = list(self.chat_history) self._start_thinking() - # If the user is following up on an image session, re-send the last - # images so the model has visual context for its answer. - followup_image_paths = [ - p for p in self._last_image_paths if os.path.exists(p) - ] - if followup_image_paths and self._current_session_kind in ("image", "text"): - self.worker = OllamaVisionWorker( - image_paths=followup_image_paths, - extra_prompt=user_text, - model=self.model_combo.currentText(), - ) - else: - self.worker = OllamaWorker( + self.worker = OllamaWorker( self.chat_history, model=self.model_combo.currentText(), temperature=self._temperature, From b0b080761c40c4a7876a3be43dcf063af0554288 Mon Sep 17 00:00:00 2001 From: gauravgupta654 Date: Mon, 25 May 2026 01:30:28 +0530 Subject: [PATCH 3/3] Updated changes --- src/chatbot/chatbot_thread.py | 72 +++++------ src/frontEnd/Chatbot.py | 237 ++++++++++++++++------------------ src/frontEnd/pathmagic.py | 16 ++- 3 files changed, 156 insertions(+), 169 deletions(-) diff --git a/src/chatbot/chatbot_thread.py b/src/chatbot/chatbot_thread.py index 4b3c33931..c8cce620b 100644 --- a/src/chatbot/chatbot_thread.py +++ b/src/chatbot/chatbot_thread.py @@ -58,16 +58,7 @@ def _downscale_image_bytes(raw_bytes: bytes) -> bytes: # ── Connectivity / runtime helpers ─────────────────────────────────────────── - -def _check_internet(host="8.8.8.8", port=53, timeout=2): - try: - socket.setdefaulttimeout(timeout) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((host, port)) - sock.close() - return True - except Exception: - return False +# REMOVED: _check_internet() β€” dead code, never called anywhere in the codebase def get_stt_backend() -> str: @@ -162,30 +153,38 @@ def run(self): self.result_signal.emit(is_ollama_running()) +# EXTRACTED: shared model-name parser used by both ModelFetchWorker and _refresh_model_cache +def _fetch_model_names() -> list: + """Call ollama.list() and return a flat list of model name strings.""" + models_data = ollama.list() + raw = (models_data.get('models', []) + if isinstance(models_data, dict) + else getattr(models_data, 'models', [])) + + names = [] + for m in raw: + name = (m.get('name') or m.get('model', '') + if isinstance(m, dict) + else getattr(m, 'model', str(m))) + if name: + names.append(name) + return names + + class ModelFetchWorker(QThread): result_signal = pyqtSignal(list) def run(self): try: - models_data = ollama.list() - raw = (models_data.get('models', []) - if isinstance(models_data, dict) - else getattr(models_data, 'models', [])) - - names = [] - for m in raw: - name = (m.get('name') or m.get('model', '') - if isinstance(m, dict) - else getattr(m, 'model', str(m))) - if name: - names.append(name) + # MERGED: uses shared _fetch_model_names() instead of inline duplicate + names = _fetch_model_names() # Keep the vision model cache warm so image sends don't block _refresh_model_cache() - self.result_signal.emit(names if names else ['qwen2.5-coder:1.5b']) + self.result_signal.emit(names if names else []) except Exception: - self.result_signal.emit(['qwen2.5-coder:1.5b']) + self.result_signal.emit([]) # ── Smart token budget ─────────────────────────────────────────────────────── @@ -291,7 +290,7 @@ class OllamaWorker(QThread): response_signal = pyqtSignal(str) status_signal = pyqtSignal(str) - def __init__(self, chat_history, model="qwen2.5-coder:1.5b", + def __init__(self, chat_history, model="", temperature=0.25, num_predict=1024): super().__init__() self.chat_history = chat_history @@ -367,13 +366,17 @@ def run(self): # ── Vision model helpers ────────────────────────────────────────────────────── +# EXTRACTED: single source of truth for vision-model keywords. +# Imported by Chatbot.py so both files share the same list. +VISION_MODEL_KEYWORDS = ["llava", "bakllava", "vision", "moondream", "minicpm-v", "qwen2-vl"] + + def _is_vision_model(model_name: str) -> bool: if not model_name: return False m = model_name.lower() - return any(k in m for k in [ - "llava", "bakllava", "vision", "moondream", "minicpm-v", "qwen2-vl" - ]) + # MERGED: uses shared VISION_MODEL_KEYWORDS constant + return any(k in m for k in VISION_MODEL_KEYWORDS) # QThread reads/writes don't produce a data race. _cache_lock = threading.Lock() _installed_models_cache: list = [] @@ -383,17 +386,8 @@ def _is_vision_model(model_name: str) -> bool: def _refresh_model_cache(): global _installed_models_cache, _installed_models_cache_valid try: - models_data = ollama.list() - raw = (models_data.get('models', []) - if isinstance(models_data, dict) - else getattr(models_data, 'models', [])) - names = [] - for m in raw: - name = (m.get('name') or m.get('model', '') - if isinstance(m, dict) - else getattr(m, 'model', str(m))) - if name: - names.append(name) + # MERGED: uses shared _fetch_model_names() instead of inline duplicate + names = _fetch_model_names() with _cache_lock: _installed_models_cache = names _installed_models_cache_valid = True diff --git a/src/frontEnd/Chatbot.py b/src/frontEnd/Chatbot.py index 9ebc5fc60..2339681de 100644 --- a/src/frontEnd/Chatbot.py +++ b/src/frontEnd/Chatbot.py @@ -1,7 +1,29 @@ -from chatbot.chatbot_thread import ( +import os +import sys + +# Import pathmagic first to ensure 'src' is in sys.path before any local imports +try: + import pathmagic # noqa:F401 +except ImportError: + try: + from frontEnd import pathmagic # noqa:F401 + except ImportError: + # Fallback: manually add the src directory relative to this file + current_dir = os.path.dirname(os.path.abspath(__file__)) + src_dir = os.path.abspath(os.path.join(current_dir, '..')) + if src_dir not in sys.path: + sys.path.insert(0, src_dir) + +if os.name == 'nt': + init_path = '' +else: + init_path = '../../' + +from chatbot.chatbot_thread import ( # type: ignore OllamaWorker, OllamaVisionWorker, MicWorker, OllamaStatusWorker, ModelFetchWorker, - detect_topic_switch, get_stt_backend + detect_topic_switch, get_stt_backend, + VISION_MODEL_KEYWORDS, # EXTRACTED: shared constant, avoids duplicate keyword list ) from PyQt5.QtWidgets import ( QWidget, QHBoxLayout, QTextBrowser, QVBoxLayout, @@ -14,18 +36,10 @@ from configuration.Appconfig import Appconfig from datetime import datetime import re -import os import json import uuid import base64 -if os.name == 'nt': - from frontEnd import pathmagic # noqa:F401 - init_path = '' -else: - import pathmagic # noqa:F401 - init_path = '../../' - # ── Storage paths ───────────────────────────────────────────────────────────── _ESIM_DIR = os.path.join(os.path.expanduser('~'), '.esim') _HISTORY_FILE = os.path.join(_ESIM_DIR, 'chatbot_history.json') @@ -1071,9 +1085,6 @@ def __init__(self): self._save_debounce_timer.setSingleShot(True) self._save_debounce_timer.timeout.connect(self._flush_save) - self._thinking_timer = QTimer(self) - self._thinking_timer.timeout.connect(self._animate_thinking) - self._typing_anim_timer = QTimer(self) self._typing_anim_timer.timeout.connect(self._animate_typing_bubble) @@ -1836,19 +1847,8 @@ def _new_chat(self): # clear_session() β€” that method deletes the session file, which # would erase the chat we just saved above. self.chat_display.setHtml(WELCOME_MESSAGE) - self.chat_history = [] - self._retry_history = [] - self._bot_responses = {} - self._response_counter = 0 - self._last_user_text = "" - self._viewing_past_session = False - self._clear_staged_images() - self._images_store = {} - self._last_image_paths = [] - self._current_session_kind = "text" - self._session_title_override = None - self._current_session_id = str(uuid.uuid4()) - self._session_created_at = datetime.now().strftime("%Y-%m-%d %H:%M") + # MERGED: combined all state resetting into one reusable helper + self._reset_session_state() try: if os.path.exists(_HISTORY_FILE): @@ -1857,9 +1857,6 @@ def _new_chat(self): pass self._sidebar.populate() - self._current_session_kind = "text" - self._session_title_override = None - self._sidebar.populate() def _on_session_deleted(self, deleted_id: str): if deleted_id == self._current_session_id or self._viewing_past_session: @@ -1870,18 +1867,8 @@ def _on_session_deleted(self, deleted_id: str): self._save_debounce_timer.stop() self._save_pending = False - self._current_session_id = str(uuid.uuid4()) - self._session_created_at = datetime.now().strftime("%Y-%m-%d %H:%M") - self._current_session_kind = "text" - self._session_title_override = None - self._viewing_past_session = False - self.chat_history = [] - self._retry_history = [] - self._bot_responses = {} - self._response_counter = 0 - self._last_user_text = "" - self._images_store = {} - self._last_image_paths = [] + # MERGED: combined all state resetting into one reusable helper + self._reset_session_state() try: if os.path.exists(_HISTORY_FILE): os.remove(_HISTORY_FILE) @@ -1947,8 +1934,6 @@ def _on_status_result(self, running: bool): """) self._was_ollama_offline = True - # ── Typing bubble ───────────────────────────────────────────────── - # ── Typing bubble (window-switch safe) ────────────────────────── # # (_typing_start_pos) and used it to select-and-replace the animated @@ -2217,9 +2202,9 @@ def _auto_switch_model(self, keywords, preferred_names, label): def _warn_or_switch_to_vision_model(self) -> bool: """Ensure a vision model is active before sending images.""" - vision_kw = ["llava", "bakllava", "vision", "moondream", "qwen2-vl", "minicpm-v"] + # MERGED: uses shared VISION_MODEL_KEYWORDS from chatbot_thread preferred = ["llava:latest", "llava", "llava:7b", "llava:13b", "bakllava", "moondream"] - idx = self._auto_switch_model(vision_kw, preferred, "vision") + idx = self._auto_switch_model(VISION_MODEL_KEYWORDS, preferred, "vision") if idx >= 0: return True # No vision model found β€” block and explain. @@ -2359,15 +2344,8 @@ def analyse_netlist(self, netlist_path: str): self._last_user_text = prompt self._start_thinking() - self.worker = OllamaWorker( - self.chat_history, - model=self.model_combo.currentText(), - temperature=self._temperature, - num_predict=self._num_predict, - ) - self.worker.response_signal.connect(self.display_response) - self.worker.status_signal.connect(self._on_status_update) - self.worker.start() + # EXTRACTED: helper method to launch OllamaWorker + self._launch_text_worker(self.chat_history) # ── Topic switch ───────────────────────────────────────────────── @@ -2493,6 +2471,17 @@ def _populate_models(self): def _on_models_fetched(self, model_names: list): self.model_combo.clear() + + if not model_names: + # No models found β€” Ollama may be offline or has no models pulled. + self.model_combo.addItem("No models found") + self.model_combo.setEnabled(False) + self.status_label.setText( + "⚠️ No Ollama models found. Run 'ollama pull qwen2.5-coder' " + "in a terminal to install one." + ) + return + for name in model_names: self.model_combo.addItem(name) @@ -2513,6 +2502,10 @@ def _on_models_fetched(self, model_names: list): chosen_idx = idx break + # If still nothing matched, just use the first available model + if chosen_idx == -1 and self.model_combo.count() > 0: + chosen_idx = 0 + if chosen_idx >= 0: self.model_combo.setCurrentIndex(chosen_idx) @@ -2520,9 +2513,6 @@ def _on_models_fetched(self, model_names: list): # ── Thinking / retry / regenerate ──────────────────────────────── - def _animate_thinking(self): - pass - def _start_thinking(self): self._is_generating = True self.user_input.setEnabled(False) @@ -2551,6 +2541,45 @@ def _scroll_to_bottom(self): self.chat_display.verticalScrollBar().maximum() ) + def _reset_session_state(self): + """EXTRACTED: Common session state resets to avoid code duplication across handlers.""" + self.chat_history = [] + self._retry_history = [] + self._bot_responses = {} + self._response_counter = 0 + self._last_user_text = "" + self._viewing_past_session = False + self._clear_staged_images() + self._images_store = {} + self._last_image_paths = [] + self._current_session_kind = "text" + self._session_title_override = None + self._current_session_id = str(uuid.uuid4()) + self._session_created_at = datetime.now().strftime("%Y-%m-%d %H:%M") + + def _launch_text_worker(self, chat_history): + """EXTRACTED: Launch OllamaWorker with correct configuration and signal mappings.""" + self.worker = OllamaWorker( + chat_history, + model=self.model_combo.currentText(), + temperature=self._temperature, + num_predict=self._num_predict, + ) + self.worker.response_signal.connect(self.display_response) + self.worker.status_signal.connect(self._on_status_update) + self.worker.start() + + def _launch_vision_worker(self, image_paths, extra_prompt): + """EXTRACTED: Launch OllamaVisionWorker with correct configuration and signal mappings.""" + self.worker = OllamaVisionWorker( + image_paths=image_paths, + extra_prompt=extra_prompt, + model=self.model_combo.currentText(), + ) + self.worker.response_signal.connect(self.display_response) + self.worker.status_signal.connect(self._on_status_update) + self.worker.start() + def _stop_generating(self): if hasattr(self, 'worker') and self.worker.isRunning(): self.worker.stop() @@ -2604,26 +2633,13 @@ def _retry_response(self, response_idx: int): followup_paths = [p for p in self._last_image_paths if os.path.exists(p)] if followup_paths and "[Image analysis request:" in last_user: prompt = last_user.split("\n", 1)[-1].strip() if "\n" in last_user else "" - self.worker = OllamaVisionWorker( - image_paths=followup_paths, - extra_prompt=prompt, - model=self.model_combo.currentText(), - ) + # EXTRACTED: helper method to launch OllamaVisionWorker + self._launch_vision_worker(followup_paths, prompt) else: - self.worker = OllamaWorker( - self._retry_history, - model=self.model_combo.currentText(), - temperature=self._temperature, - num_predict=self._num_predict, - ) - self.worker.response_signal.connect(self.display_response) - self.worker.status_signal.connect(self._on_status_update) - self.worker.start() + # EXTRACTED: helper method to launch OllamaWorker + self._launch_text_worker(self._retry_history) - def _retry_last(self): - """Legacy shim kept so any external callers don't break.""" - if self.chat_history: - self._retry_response(self._response_counter - 1) + # REMOVED: _retry_last() β€” legacy shim with no callers found in codebase def _regenerate_last_response(self): if not self.chat_history: @@ -2644,15 +2660,8 @@ def _regenerate_last_response(self): self._rebuild_chat_html_from_history() self._start_thinking() - self.worker = OllamaWorker( - self._retry_history, - model=self.model_combo.currentText(), - temperature=self._temperature, - num_predict=self._num_predict, - ) - self.worker.response_signal.connect(self.display_response) - self.worker.status_signal.connect(self._on_status_update) - self.worker.start() + # EXTRACTED: helper method to launch OllamaWorker + self._launch_text_worker(self._retry_history) def _on_status_update(self, msg: str): self.status_label.setText(msg) @@ -2673,6 +2682,16 @@ def ask_ollama(self): if self._is_generating: return + # Guard: prevent sending when no valid model is available + selected = self.model_combo.currentText() + if not selected or selected == "No models found" or selected == "Loading models…": + self.status_label.setText( + "⚠️ No model available. Make sure Ollama is running " + "and you have pulled a model." + ) + self._populate_models() + return + if self._viewing_past_session: # chat_history was already synced when the session was loaded, # so no rebuild is needed β€” just clear the read-only flag. @@ -2768,14 +2787,8 @@ def ask_ollama(self): self._clear_staged_images() self._start_thinking() - self.worker = OllamaVisionWorker( - image_paths=staged_paths, - extra_prompt=vision_extra_prompt, - model=self.model_combo.currentText(), - ) - self.worker.response_signal.connect(self.display_response) - self.worker.status_signal.connect(self._on_status_update) - self.worker.start() + # EXTRACTED: helper method to launch OllamaVisionWorker + self._launch_vision_worker(staged_paths, vision_extra_prompt) return self._check_topic_switch(user_text) @@ -2798,15 +2811,8 @@ def ask_ollama(self): self._retry_history = list(self.chat_history) self._start_thinking() - self.worker = OllamaWorker( - self.chat_history, - model=self.model_combo.currentText(), - temperature=self._temperature, - num_predict=self._num_predict, - ) - self.worker.response_signal.connect(self.display_response) - self.worker.status_signal.connect(self._on_status_update) - self.worker.start() + # EXTRACTED: helper method to launch OllamaWorker + self._launch_text_worker(self.chat_history) # ── Window / response / clear ──────────────────────────────────── @@ -2862,22 +2868,8 @@ def clear_session(self): pass self.chat_display.setHtml(WELCOME_MESSAGE) - self.chat_history = [] - self._retry_history = [] - self._bot_responses = {} - self._response_counter = 0 - self._last_user_text = "" - self._viewing_past_session = False - self._clear_staged_images() - self._images_store = {} - self._last_image_paths = [] - self._viewing_past_session = False - self._current_session_kind = "text" - self._session_title_override = None - - # Assign a fresh session ID so the next conversation starts clean - self._current_session_id = str(uuid.uuid4()) - self._session_created_at = datetime.now().strftime("%Y-%m-%d %H:%M") + # MERGED: combined all state resetting into one reusable helper + self._reset_session_state() try: if os.path.exists(_HISTORY_FILE): @@ -2903,15 +2895,8 @@ def debug_ollama(self): self._scroll_to_bottom() self._retry_history = list(self.chat_history) self._start_thinking() - self.worker = OllamaWorker( - self.chat_history, - model=self.model_combo.currentText(), - temperature=self._temperature, - num_predict=self._num_predict, - ) - self.worker.response_signal.connect(self.display_response) - self.worker.status_signal.connect(self._on_status_update) - self.worker.start() + # EXTRACTED: helper method to launch OllamaWorker + self._launch_text_worker(self.chat_history) self.user_input.clear() def debug_error(self, log): diff --git a/src/frontEnd/pathmagic.py b/src/frontEnd/pathmagic.py index 5f0d712c3..30e1516a2 100755 --- a/src/frontEnd/pathmagic.py +++ b/src/frontEnd/pathmagic.py @@ -1,7 +1,15 @@ import os import sys -# Setting PYTHONPATH -cwd = os.getcwd() -(setPath, fronEnd) = os.path.split(cwd) -sys.path.append(setPath) +# Calculate absolute path to the 'src' directory relative to this file +# __file__ is src/frontEnd/pathmagic.py, its parent is 'src/frontEnd', and its grandparent is 'src' +current_dir = os.path.dirname(os.path.abspath(__file__)) +src_dir = os.path.abspath(os.path.join(current_dir, '..')) + +if src_dir not in sys.path: + sys.path.insert(0, src_dir) + +# Also add the project root directory to sys.path +root_dir = os.path.abspath(os.path.join(src_dir, '..')) +if root_dir not in sys.path: + sys.path.append(root_dir)