o
    ih                  
   @   s@  d Z ddlZddlZddlZddlmZ ejdejeje	 ddl
mZmZmZmZ ddlmZ ddlmZ ddlmZ ddlmZmZ dd	lmZ dd
lmZ ddlmZ eeej drfej j!ne"d#ej j!Z$ej%j&ddd ej'e(eej)ej*de+ej%d e, gd e-dZ.ee/Z0eej Z1ee1Ze0j2dd Z3dZ4e.5d e.5de4 d e.5d e.5dee"ddgddrdnd   e.5d d!e6fd"d#Z7d$ed!e6fd%d&Z8i d'd(d) d*d+d) d,d-d) d.d/d) d0d1d) d2d3d) d4d5d) d6d7d) d8d9d) d:d;d) d<d=d) d>d?d) d@dAd) dBdCd) dDdEd) dFdGd) dHdId) i dJdKd) dLdMd) dNdOd) dPdQd) dRdSd) dTdUd) dVdWd) dXdYd) dZd[d) d\d]d) d^d_d) d`dad) dbdcd) ddded) dfdgd) dhdid) djdkd) i dldmd) dndod) dpdqd) drdsd) dtdud) dvdwd) dxdyd) dzd{d) d|d}d) d~dd) ddd) ddd) ddd) ddd) ddd) ddd) ddd) Z9dddZ:dd Z;dd Z<dd Z=de>d!efddZ?dddZ@ded!e6fddZAded!efddZBe0jCddgddd ZDe0jCddgddd8 ZEe0jCddgddd ZFe0Cddd ZGe0Cddd ZHe/dkre.5d zeI  W n" eJeKfy ZL ze.MdeL  eNd W Y dZL[LndZL[Lww e.5de$  e.5dej   e.5dejO  ddlPZPddlQZRzBePjSg dddT ZUeUVdD ]%ZWeWT ZWeWreWeeX kre.5deW  ePjYddeWgdd qe.5d eRZd¡ W n ePj[y   Y nw ddl\m]Z] e]j^Z_ddń Z`e`e]_^e.5dơ ea  e.5dǡ e0jYddddˍ dS dS )u  
WhatsApp Dashboard Agent — Flask webhook server.
Receives Twilio WhatsApp messages, parses via Claude API,
updates dashboard-data.json, and responds via WhatsApp.

Includes: background scheduler for proactive nudges, data refresh,
weekly reports, and 'on this day' insights.
    N)datetime)Flaskrequestsend_from_directoryjsonify)MessagingResponse)RequestValidator)Config)parse_messageconversation_memory)DashboardManager)DashboardScheduler)ingest_health_connectparentpathlibTparentsexist_okz1%(asctime)s [%(levelname)s] %(name)s: %(message)szwhatsapp-agent.log)levelformathandlerszwhatsapp-agentc                 C   s>   | j rd| j v stjdrd| jd< d| jd< d| jd< | S )	zTPrevent browser caching for JSON/API responses so dashboard always shows fresh data.jsonz/api/#no-cache, no-store, must-revalidateCache-Controlno-cachePragma0Expires)content_typer   path
startswithheaders)response r#   app.pyadd_no_cache_headers4   s
   


r%   z2.4.0-scale-visionz<============================================================z  LUKE vz starting upz;  Features: media-handler, conversation-memory, personalityz  Context file: claude_parser_BILL_CONTEXT)fromlistloadedmissingreturnc                 C   s6   t tj}| jdd}| jdd}||| j|S )z-Verify the request actually came from Twilio.zhttp://zhttps://zX-Twilio-Signature )	r   r	   TWILIO_AUTH_TOKENurlreplacer!   getvalidateform)req	validatorr.   	signaturer#   r#   r$   validate_twilio_requestH   s   
r6   phonec                 C   s.   ddl }|dd| }|ddtj}||kS )z+Check if the sender is Bill's phone number.r   Nz[^0-9]r,   )resubr	   BILL_PHONE_NUMBER)r7   r8   cleanexpectedr#   r#   r$   is_authorizedQ   s   r=   briefingc                   C      t  S N	schedulertrigger_morning_briefingr#   r#   r#   r$   <lambda>]       rD   morningc                   C   r?   r@   rA   r#   r#   r#   r$   rD   ^   rE   weeklyc                   C   r?   r@   rB   trigger_weekly_reportr#   r#   r#   r$   rD   _   rE   zweekly reportc                   C   r?   r@   rH   r#   r#   r#   r$   rD   `   rE   reportc                   C   r?   r@   rH   r#   r#   r#   r$   rD   a   rE   zon this dayc                   C   r?   r@   rB   get_on_this_dayr#   r#   r#   r$   rD   b   rE   otdc                   C   r?   r@   rK   r#   r#   r#   r$   rD   c   rE   historyc                   C   r?   r@   rK   r#   r#   r#   r$   rD   d   rE   healthc                   C   r?   r@   rB   trigger_health_ingestr#   r#   r#   r$   rD   e   rE   ingestc                   C   r?   r@   rP   r#   r#   r#   r$   rD   f   rE   ourac                   C   r?   r@   rB   trigger_oura_ingestr#   r#   r#   r$   rD   g   rE   z	oura ringc                   C   r?   r@   rT   r#   r#   r#   r$   rD   h   rE   emailc                   C      t  S r@   _get_email_summaryr#   r#   r#   r$   rD   i       inboxc                   C   rW   r@   rX   r#   r#   r#   r$   rD   j   rZ   gmailc                   C   rW   r@   rX   r#   r#   r#   r$   rD   k   rZ   slatec                   C   rW   r@   _get_todays_slater#   r#   r#   r$   rD   l   rZ   gamesc                   C   rW   r@   r^   r#   r#   r#   r$   rD   m   rZ   zgames todayc                   C   rW   r@   r^   r#   r#   r#   r$   rD   n   rZ   tonightc                   C   rW   r@   r^   r#   r#   r#   r$   rD   o   rZ   zwhats onc                   C   rW   r@   r^   r#   r#   r#   r$   rD   p   rZ   z	what's onc                   C   rW   r@   r^   r#   r#   r#   r$   rD   q   rZ   z
whats goodc                   C   rW   r@   _get_best_targetsr#   r#   r#   r$   rD   r   rZ   zwhat's goodc                   C   rW   r@   rb   r#   r#   r#   r$   rD   s   rZ   zwhats good todayc                   C   rW   r@   rb   r#   r#   r#   r$   rD   t   rZ   zwhat's good todayc                   C   rW   r@   rb   r#   r#   r#   r$   rD   u   rZ   zwhats good tonightc                   C   rW   r@   rb   r#   r#   r#   r$   rD   v   rZ   zwhat's good tonightc                   C   rW   r@   rb   r#   r#   r#   r$   rD   w   rZ   z
cro anchorc                   C   r?   r@   dmupdate_cro_anchorr#   r#   r#   r$   rD   y   rE   zreset anchorc                   C   r?   r@   rd   r#   r#   r#   r$   rD   z   rE   anchorc                   C   r?   r@   rd   r#   r#   r#   r$   rD   {   rE   z	reset croc                   C   r?   r@   rd   r#   r#   r#   r$   rD   |   rE   znow playingc                   C      t dS Nnow_playing_spotifyr#   r#   r#   r$   rD   ~   rE   zwhat's playingc                   C   rh   ri   rk   r#   r#   r#   r$   rD      rE   zwhats playingc                   C   rh   ri   rk   r#   r#   r#   r$   rD      rE   zrecently playedc                   C   rh   Nrecentrk   r#   r#   r#   r$   rD      rE   rn   c                   C   rh   rm   rk   r#   r#   r#   r$   rD      rE   pausec                   C   rh   Nro   rk   r#   r#   r#   r$   rD      rE   z
stop musicc                   C   rh   rp   rk   r#   r#   r#   r$   rD      rE   resumec                   C   rh   )Nrq   rk   r#   r#   r#   r$   rD      rE   skipc                   C   rh   Nrr   rk   r#   r#   r#   r$   rD      rE   nextc                   C   rh   rs   rk   r#   r#   r#   r$   rD      rE   z	next songc                   C   rh   rs   rk   r#   r#   r#   r$   rD      rE   ztop artistsc                   C   rh   )Ntop_artistsrk   r#   r#   r#   r$   rD      rE   z
top tracksc                   C   rh   N
top_tracksrk   r#   r#   r#   r$   rD      rE   z	top songsc                   C   rh   rv   rk   r#   r#   r#   r$   rD      rE   z	new musicc                   C   rh   Nnew_releasesrk   r#   r#   r#   r$   rD      rE   znew releasesc                   C   rh   rx   rk   r#   r#   r#   r$   rD      rE   z	new dropsc                   C   rh   rx   rk   r#   r#   r#   r$   rD      rE   z	whats newc                   C   rh   rx   rk   r#   r#   r#   r$   rD      rE   z
what's newc                   C   rh   rx   rk   r#   r#   r#   r$   rD      rE   znew music fridayc                   C   rh   rx   rk   r#   r#   r#   r$   rD      rE   c              
   C   s$  zsddl }| dkr| W S | dkr| W S | dkr | W S | dkr)| W S | dkr2| W S | dkr;| W S | d	krD| W S | d
krM| W S | dkrY|rY|	|W S | dkre|re|
|W S | dkrq|rq||W S W dS  ty } zdt|dd  dW  Y d}~S d}~ww )zQuick command: Spotify control.r   Nrj   rn   ro   rq   rr   ru   rw   ry   playqueueplaylistu_   Not sure what to do with Spotify. Try 'now playing', 'pause', 'skip', or 'play [song]' — LukezSpotify error: d   	    — Luke)spotify_modulerj   recently_playedro   rq   rr   ru   rw   ry   play_searchadd_to_queueplay_playlist	Exceptionstr)actionquerysper#   r#   r$   rl      s:   







$rl   c               
   C   Z   zddl m}  |  }| W S  ty, } zdt|dd  dW  Y d}~S d}~ww )z(Quick command: get today's sports slate.r   get_analystSports module error: Nr}   r~   )sports_bettingr   get_todays_slater   r   r   analystr   r#   r#   r$   r_         
$r_   c               
   C   r   )zCQuick command: get only the games that pass Bill's strategy filter.r   r   r   Nr}   r~   )r   r   get_best_targetsr   r   r   r#   r#   r$   rc      r   rc   c               
   C   s   z%ddl m}  |  }|jsW dS | }|dr#d|dd W S W dS  tyB } zd	t|d
d  W  Y d
}~S d
}~ww )z*Quick command: get Gmail briefing summary.r   get_gmail_briefinguR   Gmail not connected yet. Run 'python setup_google_auth.py' to hook me up. — Luke	availablezEMAIL SUMMARY

summary_textzNo new emailszCouldn't fetch email data.zGmail error: Nr}   )gmail_briefingr   r   generate_briefingr0   r   r   )r   r\   datar   r#   r#   r$   rY      s   
"rY   parsedc              
   C   s  |  dd}|  dd}|  di }|  dd}zb|dkrs|dkr,tt|d	 W S |d
kr:tt|d	 W S |dkrMtt|d t|d W S |dkrYt|d W S |dkrp| d	}|durjt|}t|W S |W S |dkr|dkrtj	| dd| dd| dd| dd| dd| ddW S |dkrtj
| dd| dd| d d| d!dd"W S |d#krtj| d$d%| d&d| d'd| d(d| ddd)W S |d*krtj| d+d,| d-dd.W S |W S |d/kr"|dkrtj| d0d| dd1W S |d#krtj| d2dd3W S |W S |d4krz|dkr1t W S |d
kr;t W S |dkrEt W S |dkrY| d5d6}tjt|d7W S |d#krct W S |dkrmt W S |d8krwt W S |W S |d9krzAd:d;lm} | }| d<d=}	| d>d}
|	d?kr| W W S |	d@kr| dA}||W W S |
r||
W W S | W W S  ty } ztjdB| dCdD |W  Y d}~W S d}~ww |dEkr| dFdG}| d4d}t||W S |dHkrq| dHd}dIdJ dKdJ dLdJ dMdJ dNdJ dOdJ dPdJ dQ}| |}|rfz| }|r.|W W S dR| dSW W S  tye } z#tjdR| dT| dCdD dU| dVt|ddW  dXW  Y d}~W S d}~ww t dY|  |W S |dZkry|W S |W S  t!y } ztd[|  d\| d]W  Y d}~S d}~w ty } ztjd^| dCdD d_t|ddW  W  Y d}~S d}~ww )`z8Execute the parsed intent and return a response message.intentunknownfieldvaluesresponse_textzDone.updateweightvaluebodyFatbloodPressuresystolic	diastolicfocustextcrypto_anchorNaddscheduledater,   timetitleUntitledlocationnoteend_date)
event_dater   r   r   r   r   signalscataiwhysource)r   r   r   r   supplementstimingrF   namedosebrand)r   r   r   r   r   viomecategoryavoidfood)r   r   removetitle_fragment)r   r   name_fragment)r   r   
days_ahead   )r   summarybettingr   r   typeanalysisteamtargetsr]   sportzBetting handler error: Texc_infospotifyr   rj   modulec                   S   r?   r@   rT   r#   r#   r#   r$   rD   R  rE   z execute_action.<locals>.<lambda>c                   S   rW   r@   rX   r#   r#   r#   r$   rD   S  rZ   c                   S   r?   r@   rA   r#   r#   r#   r$   rD   T  rE   c                   S   r?   r@   rP   r#   r#   r#   r$   rD   U  rE   c                   S   rW   r@   r^   r#   r#   r#   r$   rD   V  rZ   c                   S   r?   r@   rH   r#   r#   r#   r$   rD   W  rE   c                   S   r?   r@   rK   r#   r#   r#   r$   rD   X  rE   )rS   rV   r>   rO   r]   rG   rM   zModule 'u   ' returned no data. — Luke
' failed: zHit a snag pulling z data: r}   r~   zUnknown module requested: chatzMissing value for action: zMissing data for that update: z'. Could you try again with more detail?zAction execution failed: zSomething went wrong: )"r0   re   update_weightfloatupdate_body_fatupdate_blood_pressureintupdate_focusrf   	add_event
add_signaladd_supplementadd_viome_foodremove_eventremove_supplementquery_weightquery_body_fatquery_blood_pressurequery_schedulequery_supplementsquery_signalsquery_summaryr   r   r   r   generate_analysisr   loggererrorrl   r   warningKeyError)r   r   r   r   r"   pricedaysr   r   bet_typer   r   r   r   r   r   MODULE_DISPATCHhandlerresultr#   r#   r$   execute_action   s  
{















^




Q




















	,
"r   r,   c                 C   s  ddl }ddl}t|D ]}| jd| }| jd| d}td| d| d|  | d	rzt	j
}t	j}	|j|||	fd
d}
|
  |
j}tdt|d dd ddlm}m}m} |||}|dd}|dkr|drtd ||t}|rt|d t|| d| dW   S |dkr|drtdt|d  d ||t}|rt|d t|| d| dW   S |dv r	 W  d S td!| d" W n ty } ztjd#| d$d% W Y d}~nd}~ww qd&| v pd'| v }|std(|  qzt	j
}t	j}	|j|||	fd
d}|  |jd)d*}tj|d+t d, d-}t |d.}|!|j W d   n	1 sZw   Y  t|jd }td/| d0|dd ddl"}|#|st$d1|  W  d2S t%|t}t	j&j'd3 t d4 }|j(d$d$d5 ddl)}|*||d6  |j+|d$d7 d| d}|rt|d8 t|| |W   S  ty } ztjd9| d$d% d:t,|dd;  d<W  Y d}~  S d}~ww dS )=u   
    Handle incoming WhatsApp media attachments.
    - Images → scale screenshot detection (iHealth, etc.) via Claude Vision
    - Zip files → Health Connect ingestion
    Returns a response string, or None if the media wasn't handled.
    r   NMediaUrlMediaContentTyper,   zMedia attachment z: type=z, url=zimage/   )authtimeoutzDownloaded image (i   z.0fz KB))parse_health_screenshotapply_scale_dataapply_bp_datar   r   scale	weight_lbz$Scale screenshot detected and parsedz[Sent scale screenshot]   Got it! 📊

   

Dashboard updated. — Lukeblood_pressurereadingsu   BP screenshot detected — z	 readingsz [Sent blood pressure screenshot]u   Got it! 💓

)r   r   uj   I can see that's a health screenshot but couldn't read the numbers clearly. Try sending it again? — LukezImage type 'u.   ' — not a health screenshot, passing throughzImage processing failed: Tr   zipzoctet-streamzSkipping unhandled media: zclawd-whatsapp-)prefixhealth_connect_z%Y%m%d_%H%M%S.zipwbzDownloaded media to z (zFile is not a valid zip: uu   That file doesn't look like a valid zip. Send me a Health Connect export zip and I'll update your dashboard. — LukerO   z%Y-%m-%dr   zhealth_connect_export.zip)ignore_errorsz[Sent Health Connect zip file]zMedia processing failed: z5I received your file but hit an error processing it: r}   u    . Try sending it again? — Luke)-requeststempfileranger2   r0   r   infolowerr    r	   TWILIO_ACCOUNT_SIDr-   raise_for_statuscontentlenscale_visionr   r   r   re   r   add_user_messageadd_system_eventr   r   mkdtemposr   joinr   nowstrftimeopenwritezipfile
is_zipfiler   r   DASHBOARD_DATA_PATHr   mkdirshutilcopy2rmtreer   )r3   	num_mediasenderhttp_requestsr	  i	media_urlr   
twilio_sidtwilio_tokenimg_resp
image_datar   r   r   	extracted	data_typer   r   is_zipr"   temp_dirzip_pathfsize_kbr  r   archive_dirr  
result_msgr#   r#   r$   _handle_media_attachmentw  s   




 


(r4  bodyc                 C   sL   |    }d|v rd|v p%d|v p%|dkp%|dkp%d|v o%d|v o%d|v S )	z>Detect if the message body is a Health Connect file reference.zhealth connectr  exportzhealth connect.zipzhealth_connect.ziprO   connectr  )r  strip)r5  br#   r#   r$   _is_health_connect_mention  s   r:  r#  c              
   C   s   t d z3t }|r-d|vr-t d|  | r&t| d t| | d| dW S t d|  W 	 dS  tyV } zt jd| d	d
 W Y d}~	 dS d}~ww )u   
    Handle Health Connect file references detected in message body.
    Uses scheduler's comprehensive search: Drive API (72h) → multiple
    filesystem paths → health-imports → dashboard dir.
    uS   Health Connect file detected in message text — triggering ingestion via schedulerzNo Health Connectz!Health Connect ingestion result: z&[Sent Health Connect zip via WhatsApp]r   r   z*Scheduler ingest returned nothing useful: z!Health Connect ingestion failed: Tr   Nu  I see you're sharing a Health Connect export — nice! WhatsApp doesn't pass zip files through to me though. Your export should sync to Google Drive automatically — once it lands, just text me 'health' and I'll grab it. Or AirDrop it to ~/clawd/health-imports/ on your Mac. — Luke)	r   r  rB   rQ   r   r  r  r   r   )r#  r   r   r#   r#   r$   _handle_health_connect_text  s$   
r;  z/webhook/whatsappPOST)methodsc               
   C   sB  t jdkstdddkrttstd dS tj	dd} tj	dd
 }tj	d	d
}td|  d| d|  |d
krZtjD ]}|drYtd| dtj|   qDt| sttd|   t }|d t|dfS ttj	d	d}|dkrtt|| d}|rt }|| t|dfS |rt|rt| }t }|| t|dfS |st }|d t|dfS ddl}|d| 
 }	|	rzt|	d}
t|
}W n ttfy } zd}W Y d}~nd}~ww t| | t | | t }|| td|dd   t|dfS | 
 }|t!v rzt!|  }W n/ ty] } z"tj"d| d| dd  d!| d"t|dd#  d$}W Y d}~nd}~ww |sgd!| d%}t| | t | | t }|| td| d&t|dd   t|dfS zt# }W n) ty } zt"d'|  t }|d( t|dfW  Y d}~S d}~ww t$||| d}td)|	d* d+|	d, d-|	d. d/ |	d.d0kr|	d*d1krt }||	d2d3 t|dfS t%|}t }|| td4|dd5   t|dfS )6z.Handle incoming WhatsApp messages from Twilio.z	127.0.0.1VALIDATE_TWILIOtrueu.   Invalid Twilio signature — rejecting request)	Forbiddeni  Fromr,   BodyNumMediar   zMessage from z: body='z', NumMedia=Mediaz  Twilio media field: =zUnauthorized sender: zLuke only responds to Bill.   r   )r#  u?   Empty message — send me anything and I'll handle it. — LukeNz3^(?:cro\s+anchor|reset\s+anchor|anchor)\s+([\d.]+)$   u;   Couldn't parse that price. Try: 'cro anchor 0.085' — LukezCRO anchor command with price: P   zQuick command 'r   Tr   z	Command 'z' hit an error: x   r~   u1   ' returned no output. Check server logs. — Lukez' executed: zFailed to read dashboard data: z4Couldn't read dashboard data. Check the server logs.zParsed intent: r   z / r   z (confidence: 
confidence)lowr   r   zCould you rephrase that?zResponse sent: r}   )&r	   
FLASK_HOSTr  getenvr6   r   r   r   r2   r0   r8  r  r    r=   r   messager   r   r4  r:  r;  r8   matchr  r   groupre   rf   
ValueErrorr   r   r  add_assistant_messageQUICK_COMMANDSr   readr
   r   )r#  r5  num_media_rawkeyrespr"  media_result	hc_result_re_appcro_anchor_matchanchor_pricer   r   
body_lowerdashboard_datar   r#   r#   r$   whatsapp_webhook)  s   








*
"
0 
r`  z/healthGETc            
      C   s.  i } zddl m} | | d< W n ty   d| d< Y nw zddlm} | }|j| d< W n ty:   d| d< Y nw zddlm} | }|j| d< W n tyX   d| d< Y nw d}zdd	lm	} | j}W n	 typ   Y nw d}z
dd
l
}	|	 }W n	 ty   Y nw dtj| ||t  ddfS )z5Health check endpoint with Google integration status.r   )is_configuredauth_configuredF)get_connectordriver   r\   )
get_clientNok)statusrB   googlerS   r   	timestamprF  )google_authrb  r   gdrive_healthrd  r   r   r   oura_apirf  r   is_availablerB   runningr   r  	isoformat)
google_statusrb  rd  gdriver   r\   oura_statusrf  spotify_statusr   r#   r#   r$   rO     sX   
z/api/briefingc                  C   s$   t  } | r
t| S tddidfS )z/Return current briefing data for the dashboard.r   zNo briefing data available yeti  )rB   get_briefing_datar   )r>   r#   r#   r$   api_briefing  s   rv  z/<path:filename>c                 C   s6   t t| }| drd|jd< d|jd< d|jd< |S )z+Serve dashboard static files from ~/clawd/.z.jsonr   r   r   r   r   r   )r   DASHBOARD_DIRendswithr!   )filenamerX  r#   r#   r$   serve_dashboard  s   




rz  /c                   C   s
   t tdS )z-Serve dashboard-standalone.html as the index.zdashboard-standalone.html)r   rw  r#   r#   r#   r$   serve_index  s   
r|  __main__zCStarting WhatsApp Dashboard Agent + Dashboard Server + Scheduler...zConfig error: rG  zDashboard dir: zDashboard data: zClaude model: )lsofz-tiz:8080)r   
z+Killing leftover process on port 8080: PID killz-9)capture_outputz#Waiting for port 8080 to release...   )BaseWSGIServerc                 O   s2   dd l }d|jj_d| _t| g|R i | d S )Nr   T)http.serverserver
HTTPServerallow_reuse_address_original_init)selfargskwargshttpr#   r#   r$   _patched_init  s   
r  zListening on 0.0.0.0:8080z[Background scheduler started (morning nudge, evening check-in, weekly report, data refresh)z0.0.0.0i  F)hostportdebugr@   )r,   )b__doc__sysr  loggingr   r   insertdirnameabspath__file__flaskr   r   r   r   twilio.twiml.messaging_responser   twilio.request_validatorr   configr	   r&   r
   r   dashboard_managerr   rB   r   health_connectr   r   hasattrr  r   
__import__Pathrw  LOG_DIRr  basicConfiggetattr	LOG_LEVELINFOFileHandlerStreamHandler	getLoggerr   __name__appre   after_requestr%   _VERSIONr  boolr6   r=   rT  rl   r_   rc   rY   dictr   r4  r:  r;  router`  rO   rv  rz  r|  r1   rR  FileNotFoundErrorr   r   exitCLAUDE_MODEL
subprocessr   _timecheck_outputr8  pidssplitpidgetpidrunsleepCalledProcessErrorwerkzeug.servingr  __init__r  r  startr#   r#   r#   r$   <module>   s  	*


	

(
		
 "#$%&'()*+,-./012345
9 

 
  $
q
-








