From a9b7dbaaca9b4c4a619200ea66ff748074e00a7b Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 9 Mar 2026 08:21:33 +0000 Subject: [PATCH] [31988f42] Lead-Engine: Produktivsetzung und Anfrage per Teams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementiert: * **End-to-End Test-Button pro Lead:** Ein neuer Button "🧪 Test-Versand (an floke.com@gmail.com)" wurde in der Lead-Detailansicht hinzugefügt, um spezifische Leads sicher zu testen. * **Verbesserte E-Mail-Generierung:** * Der LLM-Prompt wurde optimiert, um redundante Termin-Vorschläge und Betreffzeilen im generierten E-Mail-Text zu vermeiden. * Der E-Mail-Body wurde umstrukturiert für eine klarere und leserlichere Integration des LLM-generierten Textes und der dynamischen Terminvorschläge. * **HTML-Signatur mit Inline-Bildern:** * Ein Skript zum Extrahieren von HTML-Signaturen und eingebetteten Bildern aus -Dateien wurde erstellt und ausgeführt. * Die -Funktion wurde überarbeitet, um die neue HTML-Signatur und alle zugehörigen Bilder dynamisch als Inline-Anhänge zu versenden. * **Bugfixes und verbesserte Diagnosefähigkeit:** * Der für wurde durch Verschieben der Funktion in den globalen Bereich behoben. * Die im Kalender-Abruf wurde durch die explizite Übergabe der Zeitzoneninformation an die Graph API korrigiert. * Fehlende Uhrzeit in Teams-Nachrichten behoben. * Umfassendes Logging wurde in kritischen Funktionen (, , ) implementiert, um die Diagnosefähigkeit bei zukünftigen Problemen zu verbessern. --- .dev_session/SESSION_INFO | 2 +- lead-engine/app.py | 33 +++ lead-engine/generate_reply.py | 10 +- lead-engine/trading_twins/image001.png | Bin 0 -> 4236 bytes lead-engine/trading_twins/image002.png | Bin 0 -> 1199 bytes lead-engine/trading_twins/image003.png | Bin 0 -> 1284 bytes lead-engine/trading_twins/image004.png | Bin 0 -> 2029 bytes lead-engine/trading_twins/image005.png | Bin 0 -> 12699 bytes lead-engine/trading_twins/manager.py | 245 ++++++++++++------ lead-engine/trading_twins/signature.html | 90 ++++++- .../trading_twins/teams_notification.py | 4 +- scripts/extract_signature_assets.py | 85 ++++++ 12 files changed, 375 insertions(+), 94 deletions(-) create mode 100644 lead-engine/trading_twins/image001.png create mode 100644 lead-engine/trading_twins/image002.png create mode 100644 lead-engine/trading_twins/image003.png create mode 100644 lead-engine/trading_twins/image004.png create mode 100644 lead-engine/trading_twins/image005.png create mode 100644 scripts/extract_signature_assets.py diff --git a/.dev_session/SESSION_INFO b/.dev_session/SESSION_INFO index 3c6e3258..a25fed5c 100644 --- a/.dev_session/SESSION_INFO +++ b/.dev_session/SESSION_INFO @@ -1 +1 @@ -{"task_id": "30388f42-8544-8088-bc48-e59e9b973e91", "token": "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", "readme_path": null, "session_start_time": "2026-03-08T14:55:15.337017"} \ No newline at end of file +{"task_id": "31988f42-8544-80fc-8bc1-dcc57acdfd4f", "token": "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", "readme_path": null, "session_start_time": "2026-03-09T06:47:23.911130"} \ No newline at end of file diff --git a/lead-engine/app.py b/lead-engine/app.py index 9a1836ca..3fdb955c 100644 --- a/lead-engine/app.py +++ b/lead-engine/app.py @@ -50,6 +50,21 @@ st.title("🚀 Lead Engine: TradingTwins") # Sidebar Actions st.sidebar.header("Actions") +if st.sidebar.button("🚀 Trigger Test-Lead (Teams)"): + try: + import requests + # The feedback server runs on port 8004 inside the same container + response = requests.get("http://localhost:8004/test_lead") + if response.status_code == 202: + st.sidebar.success("Test lead triggered successfully!") + st.toast("Check Teams for the notification.") + else: + st.sidebar.error(f"Error: {response.status_code} - {response.text}") + except Exception as e: + st.sidebar.error(f"Failed to trigger test: {e}") + +st.sidebar.divider() + if st.sidebar.button("1. Ingest Emails (Mock)"): from ingest import ingest_mock_leads init_db() @@ -246,6 +261,24 @@ if not df.empty: # Always display the draft from the database if it exists if row.get('response_draft'): st.text_area("Email Entwurf", value=row['response_draft'], height=400) + + if st.button("🧪 Test-Versand (an floke.com@gmail.com)", key=f"test_send_{row['id']}"): + try: + import requests + payload = { + "company_name": row['company_name'], + "contact_name": row['contact_name'], + "opener": row['response_draft'] + } + response = requests.post("http://localhost:8004/test_specific_lead", json=payload) + if response.status_code == 202: + st.success("Specific test lead triggered!") + st.toast("Check Teams for the notification.") + else: + st.error(f"Error: {response.status_code} - {response.text}") + except Exception as e: + st.error(f"Failed to trigger test: {e}") + st.button("📋 Copy to Clipboard", key=f"copy_{row['id']}", on_click=lambda: st.write("Copy functionality simulated")) else: st.info("Sync with Company Explorer first to generate a response.") diff --git a/lead-engine/generate_reply.py b/lead-engine/generate_reply.py index de5caffc..855b7902 100644 --- a/lead-engine/generate_reply.py +++ b/lead-engine/generate_reply.py @@ -194,14 +194,12 @@ def generate_email_draft(lead_data, company_data, booking_link="[IHR BUCHUNGSLIN 2. EINSTIEG: Nutze den inhaltlichen Kern von: "{ce_opener}". 3. DER ÜBERGANG: Verknüpfe dies mit der Anfrage zu {purpose}. Erkläre, dass manuelle Prozesse bei {qualitative_area} angesichts der Dokumentationspflichten und des Fachkräftemangels zum Risiko werden. 4. DIE LÖSUNG: Schlage die Kombination aus {solution['solution_text']} als integriertes Konzept vor, um das Team in Reinigung, Service und Patientenansprache spürbar zu entlasten. - 5. ROI: Sprich kurz die Amortisation (18-24 Monate) an – als Argument für den wirtschaftlichen Entscheider. - 6. CTA: Schlag konkret den {suggested_date} vor. Alternativ: {booking_link} + - ROI: Sprich kurz die Amortisation (18-24 Monate) an – als Argument für den wirtschaftlichen Entscheider. + 6. CTA: Schließe die E-Mail ab und leite zu den nächsten Schritten über, ohne direkt Termine vorzuschlagen oder nach Links zu fragen. STIL: Senior, lösungsorientiert, direkt. Keine unnötigen Füllwörter. FORMAT: - Betreff: [Prägnant, z.B. Automatisierungskonzept für {company_name}] - [E-Mail Text] """ @@ -214,7 +212,9 @@ def generate_email_draft(lead_data, company_data, booking_link="[IHR BUCHUNGSLIN response = requests.post(url, headers=headers, json=payload) response.raise_for_status() result = response.json() - return result['candidates'][0]['content']['parts'][0]['text'] + # Remove the placeholder from the LLM-generated text + cleaned_text = result['candidates'][0]['content']['parts'][0]['text'].replace(booking_link, '').strip() + return cleaned_text except Exception as e: return f"Error generating draft: {str(e)}" diff --git a/lead-engine/trading_twins/image001.png b/lead-engine/trading_twins/image001.png new file mode 100644 index 0000000000000000000000000000000000000000..f54abafd5cf7d49ace8159ef47c20c72529f6441 GIT binary patch literal 4236 zcmaJ_c{tQ-`={(%vSvAqAzRFhvCcGO8Ac;!ELkHNW@s>GjIrlfvUeZOGL6o;vMI$^Zu^uci#7VUHA8Sp8L5!_x-t-zn&Bu+$ljmX+AbKHbD!F znH_8GVF{dvi}jb&t;}F8QgpO4{S3vM9*U{X<>d|206-y79WX>6tfi-^1%+$F;1C_a zj}OQSNF#W|?aa)7gkznMzzcLb6%Gc6hK7PdwLuh`4_FHZgMlGXFchlEvd|0)51`}2 zGy{SZe|j(@26@p)R627ImFaNN7qbS#~kt#YY`Ac#|L;3e_~1h z!J7O}EZmev#M3D>dkV$>X9I06Q0SDP3lu5_yV&r+i&s%ic4-o%xR_eE>PYv=Ky<8#}aMf*jJm0kuTbhdEYyKhz&88K8- zU7eiatH-aQq2cmh-FOUDn10XuTfIa)gUB}->D_A+rFKy7O>OC4L~>HfL@8h z4G9%(m#b2fNBB&{ZnDn^6@HRXIeAr~=flyqUtf7QpdFNZNJrcBYFx;+Io)4cdGI~{ zj;=*ZcaB10<|g+SGedCxj(#Ang_R0G5MV-RyGWmPZ-2TrM%8EPvpy!Wf5XCHjBjx1 z>8g`LH)?K1RIfr)ohikInz6JV7#H6AmMIx_sI4WXeO|bKKB?M|%tAlY>Op2|ADR}6 zwOO_rvB5-lflM3ix@t)hnciwar?9T^ho%a)q5H*L(t(I7xyqj%_eOd0D>N z#ow0EJqR#sN!;PaJh}FvO7Em*d4bdekQw46S4}m6UAZHroix)ra<8tM5Lh&)u+Nhm zq|$poO>a@7T>7_)bS;mn^!9T~Tr*ZpEytj;W(>Ivg{G$P^MBm1Eb=&2G*TXO6&V>q z=DC~44{JRHld(HrB3mnt;{M1r4;K+`eb}Kjm09A0q+gVsI=4ohOkJA2Ss30VQ!7;~ z(tP^x^n<)GlM!Bggrn&j+e<>ZPTXDit_q&4btQf}O?Fhu?v%#m4+9Tj?8w{G)cc~Y zd-m3#Jd5!bZstSOyhjTP^Z0%1p;FoA)T6mFB9Ry=rMNetE1*$vjKq;Z=da>`?#D{d zZz3uMlEs8@Ht_Cm8HV0Nmonu)`^3mOs>99XWAF222;nj)dAOVtO(jE^F!Tkg+jb4)zdI5eUSE2 zK!!U`_A2AP)ve<*qz8jx3xU_H-XH1<&x{j}C_tH;G%L7UAHn;YVUD=&G6n+0=~lX8 zO0HFEnP*V*o)re1fr-X%z*$|2j^nEXuVf>Bo$@++ds>Fy30}PQ@O4h&$5;a;yw|SS zOm3!WT!5XU=_>9PV#~#7rMp~8xv+6huzPLHYwEn_<7~piz;|_2tMz0BXROxt4qU7F z#ce^362|hi8-A%`fEvLs5W@ZX`p6mN*%YCID-B9ZQ3Kj%7q+jid!*N2I4qFRxjv?F z4z|i}!G}yjDnFO*lKe2}it|?+psCE}6GzTGbz5pjxj*d`X!MgeSmMZ4H3o;l37$vF2X(TSE2TqTtURvHBd2U?-Qzv$g->lIw z*VtNf7@-*NBVM*Al9Ql}{h1Co@zE%`DB-{faqW9`8Jy^z86a#$v#eIq@?~B2;&AZ2 zSk(>(AhyY~^Jh_o`rfgni1_3BBtQcGZ_1@<+vNF>iZr^n9yH|Ae}ecO&Cbw}-{6N11+6 zUp#ZH3&P0o_1$yR!j|T;I(nVd}BH5SqO{9%@ZM2-38O9@1iI56L z>%c}y_F6D4Ds$%KDiNK1d?3g*f8Elhe>MDf^2a$u{5z=!i)-yf#_~tPz)pSh5RMsb z%{1}{q&mH+FlbRVT6^D8Q7p765CMPGppuL`6MfjW<(NXHx@|nFqR_)}VZ14*AoWs3 zpG-GQg@5ES_RRZt?(Amo&7QjW3c#Ueh7&@8GTP@H8-}*O_%raY2_E7)^Ut}4E7mR| zC++64Og|1zfp8F{D`K-*Znv2q_vYpwPVs;VLx(AYXWM7vND-%)#f&mqBZ_IgCi`d$ zJA3o;oB9RmO#PxLw{@4z*;!pE$7mt8wpXChNYQcuKB+Bi^yZg}M!g6g4V{h*KJ3UZ zA6wF-&A`dmt)u12$#FI%ZG2BWCnWUVz+v%b$7IAer8e#0Jv}N8P<@>n zuGJbZDJr^pw}c0NYYTJYT7^}dY7bfOuH2V(F?(q&UnS)nN7N#+?rciBim!}xo_(5= z0Xr>Sx|iZGdv!EaWaCX8&z6{xU^I10)C)d-=C@}?x=Uzo zM7)$ZmQz+$5FR14b9(4gqM@cI;zB2Z$+0%C^Sa^_##CxuFW;kV0*w=Ya$8x5$8_#p zKQ5l*28}j*tN4l7P+wQZBJd~k>reE zYk>-)ZN-dp8DE56$J>Vzt5fMY3xVfFB?aHEkrXY&xJ^_2HAk z-j7As&eXSs%Q)Z`%umjJm9hiRCt1q7@LjfCXgzlP683?e>C4d;W?9~+84hqH^@M)j zU4btOm`@zVk0$2e6WeO-!S_k;_4`4>wd>|j8%-*^PY+4-Z!mfX8f}Z3q;=5z9U64* z)|F#t_Xtmz)yscGHIoDj5Nkl)vK}#)=kN2G@+B_>4%-@Fm=(S3Al;_?Yf-{wN`n2n zT|P167H56oVqhJOq0zi}X0c?F#^Ps0>-{KI^-tdoYV}K;nCZ1D4o_f-S`LUIftQx! zzo3q|eqVgLe??8S*VcRC0y?)w3dmoXW>pZ?xuKyiv*i8YtUheY5OY*K3k$x?w;ic6 zn|)LJvbVXg%T1x7Md{vR)k%>f5xKApgz*)0A#p=--fw2j9UpnBuPl)G)Oi(KGRV=h znwhXC)LQj*DW~N_eii;0_-nV}A6tbz0r^w)2oNE#1T{WilNEZ?SSXp-QTWE^LbW=w zE0Fh=)FEVDyauN)Bk3BAHQ%)f@U3qiJ5GmlIZH+9wR#S%#ezqZUX4u9)VGIeecmn8 zK@@Lg(YRET(Qm=q;lHStbgN8*G!Ukn=JBy1JC!(KN`(K*q3bKVJk5QUwgrQ+C+s|$ z9dNZCZqU(`N@ZjTS7kf(kpcgL)NTKrV(DeDjS~3ubzw=RD-Y^kZ{rlcc0!*4;kBI> z0PJO)M=f0}>!o#ps&;Lj!Rc3NA6*pr_hXI~B@!(A7G8OOIVm$~gnX+y_j`|6S>iZE zSJmTgf?u3&aJpuA70*2SJ%fhr=%scF$lo1ZF86=Cvz0LLG^cIZvo`_YR=!V3bMFd| z=-4W`Rd;E3M#wf|Ih2csS+SgGu({6EwmDVclcRykx8rH`aVx0o=#rQW^c5cK6phdM z9z}G=imQ~}orG1o+ZU(161DOCyYR2|-DWf6d^{;9pA2M` zi4=5wFMYyu*}bpkkvx9i)paBxr`uRx)h%OKQ)Xi=2-n>^w+y8MWy(U``Z8FtZbP3za`Oo$I~x0@D-H+0vBqRb?E1 zy@*>gaJ8Lixh#?g0iq0Z`$IOGnSr6PiUKs6nMFP|_wdARZ-nm$;S~+&DANC+RW$zYDq4;;+ zz=>Sm^alo%Sic3Tz~+^R0U(M|+TcT~OrE}J$=B6(L=K^xquSdzrND@K9w;D$`8A$B zp)C!aXQ1Wd9h*<~51ya#vfKd?4BJ5K;pQppTqg1@9vQQstuV!|?WD-oT=EmyQ8B(N zi?Ghe%WdXK#SI^n%a242wJ2qAUtCU9;?Z^w33FFNo*lRz_PG4wvU*~Uf?f@&+9x=# zvvF2^Vna%;T~+=?zDBss)E#m=rvG%y89#>Xg7EFJ&7`nCeyE}0QoNgC&ie(IFQp<2 zy4FTXQhLv7s_(>o{?2_5z{%*X3h|eChP^8Hr0wQ&#Z%gyZEbDxul*&QotUu+X~9KW^D&BC%mCT=8>ABRCm?EPK;++~$^y_PGiqjn8zj8U(5zisDsf4sZd z+d?SS7`J4iA%Un5q)#|_AnuVEf)5ZOfrJDf#0SHJB%mfJ_&~&T{%*9xhsh;(zd!kW zKHtyxPi~v)ovLI(ETtF~2y&V< z0gF&lX5YUC2Qke5s#+>L<@_)&8yX>b7{b*|#Ky3}eXc3VQ_ukwIHBqZ{K2=EaG)v) zd@Pcu@@5)Ns^XjlN9RUL^4yfnDfqtE!Jx|{0S!75aJ6aO=G_Fos>`FjcTM7872-@K z@TaEA`2t8A76dd!P+^&(C=lZag&L4$I?Bag0}RDPNGe8BOqgbPI?mH9XgxTJXer}- zQOLI9p;H2%bR3f>$!fJqR9V8XCPwG{+t%a&?7s-Xi< zQK}d-P69`6pQfRi`TUx&Znuhs%0{}9Nzw#GYMPhZD%y66@R^Nu(ROLpgk%xg#*8JS zo{aBmlTrPiAIJkDH@slTGaA&LoRGlL4WXzC&%|gh#%2bxRGg*hEH%Umu?!a#vZ+*@ zW7wfKH>cZm=Udrv8^p zA~U4d+5dF9eTBxy+pf(nI;>4U)X~UWXtqUE1 zyJ35!Co=38-z)w)6nfl!`K5Y!)BJ@c>CT&=>4V>=!{xx;gV?e^^wEji5fIpTXiw<3 zJ8WS~-|?Qt&w+*7hKCy)M_4nIV^1f86WhRrj`@qrAMZR0Zn(keA8)O^C01(3l<;|R z=jPMRce-A=Vad2vVAAM7!7<~zxTqEy~VAcf3oeX&l*Rf;FM5&vUR?eyv-!9FWyL=>;2}@S=hG} b#JVW#Z2s_pGgHwk-Y+DV84=D8z5C(6bN7d= literal 0 HcmV?d00001 diff --git a/lead-engine/trading_twins/image003.png b/lead-engine/trading_twins/image003.png new file mode 100644 index 0000000000000000000000000000000000000000..b1c2ba8c1cbfd62317dddb36d441caf355f6fb74 GIT binary patch literal 1284 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2v2cX7$XFh>{3jAFJg2T)jk)8oi3#0-$aN1{?c|g2d$P)DnfH z)bz|eTc!8A_bVx6rr0WloBA5~7C5J7WO`H;r3P2|g(O#HCtIc{+1n}DR9FEG$W1Lt zRH(?!$t$+1uvG$^YXxM3g!Ppaz)DK8ZIvL7itr6kaLzAERWLNrGtf;oFfdRsx7159 zFiB1}G_$nOQ7|$vGSxRQ*EcZIH8ip^w6HQXR)7K}pp8W-X;wilZczJxJX@uVl9B=| zef{$Ca=mh6z5JqdeM3u2OP~SzMn<|o6}rWhc_oPzx_QOQFcVx-i<65o3raHc^AtcP zCMM;Vme?u*T?_F>Zh@~a+~B-oXkY<@O}`?sK;KZ$KtDGZbaOU>>GjMjE=kNwPKD{sMCdiatJli8C^fMpzbGU> zKL-|r0U7xv`NbLe1qw!HdWH%b;hA|U`Q^o$ps1t_c>E;4xfgYKF~4xpqNF9X_yc& z`GA=4vD9W$;=E4jK!WVjv*Dd)=b^+#q20>?D?PO){p+xA30gH zxgA3;WGodwD89iYDf7ATlAtv%^OY8S)8c4KD1RUvbE(Tsz+}$-2jY^`_j*6n`?Djp z?ECGRH*fCs*kmI(kvm}O|E*>jE&^wmI{tR8N{BPK!5sVT{*Sp2wllTWcDvbZV=rKN zm#X%va{H+w(P%c`4H@k*{GmC#G7YlJ4&D9I{WGi4xFFdz_F$+1zaQI^R}wk_UpVjl zWHH@i_-;p%*!?E&Z(EhJ62#sZ<@CN-EPrxBN7A@AnABQ1Y(3!a{-{m99d8CGyf())#HR*t5v|D2td zQ!W2I3HL9aZ@K#NGU+#syup)O<-#hMt&T1GT5)!A|I52C=d(XdKfB}n;eVxBmZcHO z`<4|?U(+XDG`Ur4Pt``v#hX@#S4?lV=W{(j{o0Civ!%Y&u+}Ghog#gvv*ibC4YvV< X>V+KdgAz+OgUTgOS3j3^P6@t`Y8 zv51rwgoQ1FMT4!#E>#fL6%JrQ&MM17s{-Pqs63)rfh9X2*#6<^oSE;N@0>! zou)uV^j+LwAXqKN5*2=F8Z06$BvO`^B!f}9rw7elCnO1!7=h4q%4C&Rs1woWd4=Tr z+%}6&n>QhnMD(|!qJl$dVqAmKK!6D_WB>rr_%IU%I5HW?g9Te@5CCym0G|aw3=k56 z0wKtzEnIZcqXtb7hD)Ri{>YVxo=6aCA&Zron#xRNGjWZa1;Q{)60jhML0T}hdKH1_ z7%J`NMFt6`m1z`eLV>GjbBstloW$nicbpT_}t93O8fAmD(Y z6oB~xPy$OJAHFwF%oBSrV*OQG0#V7ZMZXHt?*i8A53xeA215v36N%%=i&YSsh!eOr z5m(b79uuUw?oy~wJXO18?tI>cE5S610~jjR;7Zzjf`y9r2#5#bVK#^g7%;+y7?6O& zX21eaz<@Y>E>9o;WC<`vU%;ckOWfNcC3nam)Qeh#5SPp23V0j_hVfwr0CFG(0&&P? z0)zlE6yR}T0i8ukWX;vg?`mjqi!98!*LPh(F5dMOrXrh3Lv~HC=Z_~T6iW+#iC3hq z@7bQDpd4g_`LWF9v!<}T?1wv)$WjElVcyY|cighM%m6h>OcGPhKd`;_3TTJtI@YK0Cu&fnxP_2Jlgji0WZ&aAu~vxKMY3}Y6=49ic7 z#!d3v@EDWd?vn35FGbv143iGw_Vy*!bv~6{rhB^OropWjb68EWD~bo|U%$a1l9}<68uk^BfrdPwKn!QRpv&w)Y-qyb|{{rfZvYj?N+Fg<%6;Z|cn355M@Sw>R-j(Suc2 zUtYL)RBj(oF};`1@ZXE}o*(TmMmMU?RaN;pf5cDVazp#gEiE?}dmp`liL>{ZR!%6w|+ z@#^ZN2h%fpEn(^j{_n3nAucOc-Y(+fZMOCSL#;U#Q%B_}Z`F{|Fm24ym`=b^AAGQ; zZRhTgb)62TK+2MvB@Uy{y|z^hmwoH?boYzG5x#09xm!_z4Hw&eX$1b|AJt$u`qqeBuzHK@$pf=sm z>g&9${!)LD&fTyR+U0hL85j^UWNEzBPU|>nDTce-V`+(cE34yoEhd)7X1U}}q_^iw z&PIO!&}@8`IdkJ>#pS@5_3paW_xp<-j*h!F=yFc1Gb|rj-V&h*Ee++Tt)F#A#Z{bMv*YOs=_jJ$D^G6* z8gJ)}u;17@O#X7_Nb8TE_jouJdhTtdIz;TBUfHotxFm0BXI5o4wS`)xA9MU`WZIN- zwnx%U4eNaEr}>udYffE93*wrb-t}1A;8>;F&U&h5Hf7*T3Y9Wox%TCB=04us57^%) KL}K*bm-&BiBSeP) literal 0 HcmV?d00001 diff --git a/lead-engine/trading_twins/image005.png b/lead-engine/trading_twins/image005.png new file mode 100644 index 0000000000000000000000000000000000000000..da9d5bc2d55b55b7d831692a26136f87c8d7101a GIT binary patch literal 12699 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 q{EEj str: + """Formats a datetime object to 'Heute HH:MM', 'Morgen HH:MM', or 'DD.MM. HH:MM'.""" + now = datetime.now(TZ_BERLIN).date() + if dt.date() == now: + return dt.strftime("Heute %H:%M Uhr") + elif dt.date() == (now + timedelta(days=1)): + return dt.strftime("Morgen %H:%M Uhr") + else: + return dt.strftime("%d.%m. %H:%M Uhr") + # --- Setup --- TZ_BERLIN = ZoneInfo("Europe/Berlin") DB_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "trading_twins.db") @@ -48,32 +68,31 @@ def get_access_token(client_id, client_secret, tenant_id): return result.get('access_token') def get_availability(target_email, app_creds): - print(f"DEBUG: Requesting availability for {target_email}") + logging.info(f"Requesting availability for {target_email}") token = get_access_token(*app_creds) if not token: - print("DEBUG: Failed to acquire access token.") + logging.error("Failed to acquire access token for calendar.") return None headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json", "Prefer": 'outlook.timezone="Europe/Berlin"'} start_time = datetime.now(TZ_BERLIN).replace(hour=0, minute=0, second=0, microsecond=0) end_time = start_time + timedelta(days=3) # Use 15-minute intervals for finer granularity - payload = {"schedules": [target_email], "startTime": {"dateTime": start_time.isoformat()}, "endTime": {"dateTime": end_time.isoformat()}, "availabilityViewInterval": 15} + payload = {"schedules": [target_email], "startTime": {"dateTime": start_time.isoformat(), "timeZone": str(TZ_BERLIN)}, "endTime": {"dateTime": end_time.isoformat(), "timeZone": str(TZ_BERLIN)}, "availabilityViewInterval": 15} try: url = f"{GRAPH_API_ENDPOINT}/users/{target_email}/calendar/getSchedule" r = requests.post(url, headers=headers, json=payload) - print(f"DEBUG: API Status Code: {r.status_code}") + logging.info(f"Graph API getSchedule status code: {r.status_code}") if r.status_code == 200: view = r.json()['value'][0].get('availabilityView', '') - print(f"DEBUG: Availability View received (Length: {len(view)})") + logging.info(f"Availability View received (Length: {len(view)})") return start_time, view, 15 else: - print(f"DEBUG: API Error Response: {r.text}") + logging.error(f"Graph API Error Response: {r.text}") except Exception as e: - print(f"DEBUG: Exception during API call: {e}") - pass + logging.error(f"Exception during Graph API call: {e}") return None def find_slots(start, view, interval): @@ -135,6 +154,22 @@ def trigger_test_lead(background_tasks: BackgroundTasks): background_tasks.add_task(process_lead, req_id, "Testfirma GmbH", "Wir haben Ihre Anfrage erhalten.", TEST_RECEIVER_EMAIL, "Max Mustermann") return {"status": "Test lead triggered", "id": req_id} +@app.post("/test_specific_lead", status_code=202) +def trigger_specific_test_lead(payload: TestLeadPayload, background_tasks: BackgroundTasks): + """Triggers a lead process with specific data but sends email to the TEST_RECEIVER_EMAIL.""" + req_id = f"test_specific_{int(time.time())}" + # Key difference: Use data from payload, but force the receiver email + background_tasks.add_task( + process_lead, + request_id=req_id, + company=payload.company_name, + opener=payload.opener, + receiver=TEST_RECEIVER_EMAIL, # <--- FORCED TEST EMAIL + name=payload.contact_name + ) + return {"status": "Specific test lead triggered", "id": req_id} + + @app.get("/stop/{job_uuid}") def stop(job_uuid: str): db = SessionLocal(); job = db.query(ProposalJob).filter(ProposalJob.job_uuid == job_uuid).first() @@ -157,80 +192,144 @@ def book_slot(job_uuid: str, ts: int): db.close(); return Response("Fehler bei Kalender.", 500) # --- Workflow Logic --- -def send_email(subject, body, to_email, signature, banner_path=None): +def send_email(subject, body, to_email): + """ + Sends an email using Microsoft Graph API, attaching a dynamically generated + HTML signature with multiple inline images. + """ + logging.info(f"Preparing to send email to {to_email} with subject: '{subject}'") + + # 1. Read the signature file + try: + with open(SIGNATURE_FILE_PATH, 'r', encoding='utf-8') as f: + signature_html = f.read() + except Exception as e: + logging.error(f"Could not read signature file: {e}") + signature_html = "" # Fallback to no signature + + # 2. Find and prepare all signature images as attachments attachments = [] - if banner_path and os.path.exists(banner_path): - with open(banner_path, "rb") as f: - content_bytes = f.read() - content_b64 = base64.b64encode(content_bytes).decode("utf-8") - attachments.append({ - "@odata.type": "#microsoft.graph.fileAttachment", - "name": "RoboPlanetBannerWebinarEinladung.png", - "contentBytes": content_b64, - "isInline": True, - "contentId": "banner_image" - }) + image_dir = os.path.dirname(SIGNATURE_FILE_PATH) + image_files = [f for f in os.listdir(image_dir) if f.startswith('image') and f.endswith('.png')] + + for filename in image_files: + try: + with open(os.path.join(image_dir, filename), "rb") as f: + content_bytes = f.read() + content_b64 = base64.b64encode(content_bytes).decode("utf-8") + + attachments.append({ + "@odata.type": "#microsoft.graph.fileAttachment", + "name": filename, + "contentBytes": content_b64, + "isInline": True, + "contentId": filename + }) + except Exception as e: + logging.error(f"Could not process image {filename}: {e}") + + # 3. Get access token catchall = os.getenv("EMAIL_CATCHALL"); to_email = catchall if catchall else to_email token = get_access_token(AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID) - if not token: return + if not token: + logging.error("Failed to get access token for sending email.") + return + + # 4. Construct and send the email headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} - payload = {"message": {"subject": subject, "body": {"contentType": "HTML", "content": body + signature}, "toRecipients": [{"emailAddress": {"address": to_email}}]}, "saveToSentItems": "true"} - if attachments: payload["message"]["attachments"] = attachments - requests.post(f"{GRAPH_API_ENDPOINT}/users/{SENDER_EMAIL}/sendMail", headers=headers, json=payload) + full_body = body + signature_html + payload = { + "message": { + "subject": subject, + "body": {"contentType": "HTML", "content": full_body}, + "toRecipients": [{"emailAddress": {"address": to_email}}] + }, + "saveToSentItems": "true" + } + if attachments: + payload["message"]["attachments"] = attachments + + response = requests.post(f"{GRAPH_API_ENDPOINT}/users/{SENDER_EMAIL}/sendMail", headers=headers, json=payload) + logging.info(f"Send mail API response status: {response.status_code}") + if response.status_code not in [200, 202]: + logging.error(f"Error sending mail: {response.text}") + def process_lead(request_id, company, opener, receiver, name): + logging.info(f"--- Starting process_lead for request_id: {request_id} ---") db = SessionLocal() - job = ProposalJob(job_uuid=request_id, customer_email=receiver, customer_company=company, customer_name=name, status="pending") - db.add(job); db.commit() - - cal_data = get_availability("e.melcer@robo-planet.de", (CAL_APPID, CAL_SECRET, CAL_TENNANT_ID)) - suggestions = find_slots(*cal_data) if cal_data else [] - - # --- FALLBACK LOGIC --- - if not suggestions: - print("WARNING: No slots found via API. Creating fallback slots.") - now = datetime.now(TZ_BERLIN) - # Tomorrow 10:00 - tomorrow = (now + timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0) - # Day after tomorrow 14:00 - overmorrow = (now + timedelta(days=2)).replace(hour=14, minute=0, second=0, microsecond=0) - suggestions = [tomorrow, overmorrow] - # -------------------- - - for s in suggestions: db.add(ProposedSlot(job_id=job.id, start_time=s, end_time=s+timedelta(minutes=15))) - db.commit() - - send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES) - # Using the more detailed card from teams_notification.py - from .teams_notification import send_approval_card - send_approval_card(job_uuid=request_id, customer_name=company, time_string=send_time.strftime("%H:%M"), webhook_url=TEAMS_WEBHOOK_URL, api_base_url=FEEDBACK_SERVER_BASE_URL) - - send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES) - while datetime.now(TZ_BERLIN) < send_time: - db.refresh(job) - if job.status in ["cancelled", "send_now"]: break - time.sleep(5) - - if job.status == "cancelled": db.close(); return - - booking_html = "" - try: - with open(SIGNATURE_FILE_PATH, 'r') as f: sig = f.read() - except: sig = "" + job = ProposalJob(job_uuid=request_id, customer_email=receiver, customer_company=company, customer_name=name, status="pending") + db.add(job) + db.commit() + logging.info(f"Job {request_id} created and saved to DB.") + + cal_data = get_availability("e.melcer@robo-planet.de", (CAL_APPID, CAL_SECRET, CAL_TENNANT_ID)) + suggestions = find_slots(*cal_data) if cal_data else [] + + if not suggestions: + logging.warning(f"No slots found via API for job {request_id}. Creating fallback slots.") + now = datetime.now(TZ_BERLIN) + tomorrow = (now + timedelta(days=1)).replace(hour=10, minute=0, second=0, microsecond=0) + overmorrow = (now + timedelta(days=2)).replace(hour=14, minute=0, second=0, microsecond=0) + suggestions = [tomorrow, overmorrow] + + logging.info(f"Found/created {len(suggestions)} slot suggestions for job {request_id}.") + for s in suggestions: + db.add(ProposedSlot(job_id=job.id, start_time=s, end_time=s + timedelta(minutes=15))) + db.commit() + + send_time = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES) + logging.info(f"Sending Teams approval card for job {request_id}.") + from .teams_notification import send_approval_card + send_approval_card(job_uuid=request_id, customer_name=company, time_string=send_time.strftime("%H:%M"), webhook_url=TEAMS_WEBHOOK_URL, api_base_url=FEEDBACK_SERVER_BASE_URL) + + logging.info(f"Waiting for response or timeout until {send_time.strftime('%H:%M:%S')} for job {request_id}") + wait_until = datetime.now(TZ_BERLIN) + timedelta(minutes=DEFAULT_WAIT_MINUTES) + while datetime.now(TZ_BERLIN) < wait_until: + db.refresh(job) + if job.status in ["cancelled", "send_now"]: + logging.info(f"Status for job {request_id} changed to '{job.status}'. Exiting wait loop.") + break + time.sleep(5) + + db.refresh(job) + if job.status == "cancelled": + logging.info(f"Job {request_id} was cancelled. No email will be sent.") + return + + logging.info(f"Timeout reached or 'Send Now' clicked for job {request_id}. Proceeding to send email.") + booking_html = "" + + try: + with open(SIGNATURE_FILE_PATH, 'r') as f: + sig = f.read() + except: + sig = "" + + email_body = f""" +

Hallo {name},

+{opener} +

Ich freue mich auf den Austausch und schlage Ihnen hierfür konkrete Termine vor:

+
    +{booking_html} +
+

Mit freundlichen Grüßen,

+""" + + send_email(f"Ihr Kontakt mit RoboPlanet - {company}", email_body, receiver) + job.status = "sent" + db.commit() + logging.info(f"--- Finished process_lead for request_id: {request_id} ---") + + except Exception as e: + logging.error(f"FATAL error in process_lead for request_id {request_id}: {e}", exc_info=True) + finally: + db.close() - # THIS IS THE CORRECTED EMAIL BODY - email_body = f""" -

Hallo {name},

-

{opener}

-

Hätten Sie an einem dieser Termine Zeit für ein kurzes Gespräch?

- {booking_html} - """ - - send_email(f"Ihr Kontakt mit RoboPlanet - {company}", email_body, receiver, sig, BANNER_FILE_PATH) - job.status = "sent"; db.commit(); db.close() if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8004) diff --git a/lead-engine/trading_twins/signature.html b/lead-engine/trading_twins/signature.html index 9bc9a3f1..066482c2 100644 --- a/lead-engine/trading_twins/signature.html +++ b/lead-engine/trading_twins/signature.html @@ -1,13 +1,77 @@ -Freundliche Grüße
-Elizabeta Melcer
-Inside Sales Managerin
-Wackler Logo
-RoboPlanet GmbH
-Schatzbogen 39, 81829 München
-T: +49 89 420490-402 | M: +49 175 8334071
-e.melcer@robo-planet.de | www.robo-planet.de
-LinkedIn Instagram Newsletteranmeldung
-Sitz der Gesellschaft München | Geschäftsführung: Axel Banoth
-Registergericht AG München, HRB 296113 | USt.-IdNr. DE400464410
-Hinweispflichten zum Datenschutz
-RoboPlanet Webinar Einladung \ No newline at end of file + + + + + + +
+

Freundliche Grüße
+
+
Elizabeta Melcer
+
Inside Sales Managerin

+
+

 

+ + + + + + + + + + + + + +
+

Wackler Logo

+
+

RoboPlanet GmbH
+
Schatzbogen 39, 81829 München
+
T: ++49 89 420490-402 +| M: ++49 175 8334071
+e.melcer@robo-planet.de
 | +www.robo-planet.de +

+
+

 

+ + + + + + +
+

LinkedIn Instagram Newsletteranmeldung

+
+

 

+ + + + + + +
+

Sitz der Gesellschaft München | Geschäftsführung: Axel Banoth
+
Registergericht AG München, HRB 296113 | USt.-IdNr. DE400464410
+Hinweispflichten
 zum + Datenschutz +

+
+

 

+ + + + + + +
+

RoboPlanetBannerWebinarEinladung.png

+
+

 

+ + + diff --git a/lead-engine/trading_twins/teams_notification.py b/lead-engine/trading_twins/teams_notification.py index 0cc1fddd..4f86beb5 100644 --- a/lead-engine/trading_twins/teams_notification.py +++ b/lead-engine/trading_twins/teams_notification.py @@ -33,7 +33,7 @@ def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT }, { "type": "TextBlock", - "text": "Wenn Du bis {time_string} Uhr NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.", + "text": f"Wenn Du bis {time_string} Uhr NICHT reagierst, wird die generierte E-Mail automatisch ausgesendet.", "isSubtle": True, "wrap": True } @@ -60,5 +60,5 @@ def send_approval_card(job_uuid, customer_name, time_string, webhook_url=DEFAULT response.raise_for_status() return True except Exception as e: - print(f"Fehler beim Senden an Teams: {e}") + logging.error(f"Fehler beim Senden an Teams: {e}") return False diff --git a/scripts/extract_signature_assets.py b/scripts/extract_signature_assets.py new file mode 100644 index 00000000..495a9e75 --- /dev/null +++ b/scripts/extract_signature_assets.py @@ -0,0 +1,85 @@ +import email +from email.message import Message +import os +import re + +# Define paths +eml_file_path = '/app/docs/FYI .eml' +output_dir = '/app/lead-engine/trading_twins/' +signature_file_path = os.path.join(output_dir, 'signature.html') + +def extract_assets(): + """ + Parses an .eml file to extract the HTML signature and its embedded images. + The images are saved to disk, and the HTML is cleaned up to use simple + Content-ID (cid) references for use with the Microsoft Graph API. + """ + if not os.path.exists(eml_file_path): + print(f"Error: EML file not found at {eml_file_path}") + return + + with open(eml_file_path, 'r', errors='ignore') as f: + msg = email.message_from_file(f) + + html_content = "" + images = {} + + for part in msg.walk(): + content_type = part.get_content_type() + content_disposition = str(part.get("Content-Disposition")) + + if content_type == 'text/html' and "attachment" not in content_disposition: + payload = part.get_payload(decode=True) + charset = part.get_content_charset() or 'Windows-1252' + try: + html_content = payload.decode(charset) + except (UnicodeDecodeError, AttributeError): + html_content = payload.decode('latin1') + + + if content_type.startswith('image/') and "attachment" not in content_disposition: + content_id = part.get('Content-ID', '').strip('<>') + filename = part.get_filename() + if filename and content_id: + images[filename] = { + "data": part.get_payload(decode=True), + "original_cid": content_id + } + + if not html_content: + print("Error: Could not find HTML part in the EML file.") + return + + # Isolate the signature part of the HTML + signature_start = html_content.find('Freundliche Gr') + if signature_start != -1: + # Step back to the start of the table containing the greeting + table_start = html_content.rfind('