o
     iB                     @   sF  d Z ddlZddlZddlZddlZddlZddlZddlZddl	Zddl
m
Z
mZmZ ddlmZ dZdZdZdZd	Zd
ZdZdZdZedZdZdZdd Zdd Zdd Zdd Zdd Zdd Z dd Z!dd  Z"d!d" Z#d#d$ Z$d%d& Z%d'd( Z&d)d* Z'd+d, Z(d-d. Z)d/d0 Z*d1d2 Z+d3d4 Z,d5d6 Z-e.d7kre-  dS dS )8u   
MRAV Call Watcher — Phase 2C
Watches calls.jsonl for completed calls, extracts structured items via Claude,
routes each item to the correct tool, sends Telegram confirmation.
Runs via system cron every 2 minutes.
    N)datetimetimezone	timedelta)ZoneInfoz0/home/isthekid/.openclaw/voice-calls/calls.jsonlzB/home/isthekid/.openclaw/workspace/scripts/call-watcher-state.jsonz&/home/isthekid/.openclaw/openclaw.jsonz0/home/isthekid/.openclaw/voice-calls/extractionsz./home/isthekid/.openclaw/secrets/anthropic.envz3/home/isthekid/.openclaw/workspace/action/data.jsonz)/home/isthekid/.openclaw/workspace/memoryzsrvdeskops@gmail.comprimaryzAmerica/New_Yorkzclaude-sonnet-4-6uW  You are a structured data extractor. Below is a transcript of a phone call between Adner and his voice assistant MRAV.

Extract all actionable items into JSON.

Rules:
- Every item must have a unique id (format: "item-001", "item-002", etc.)
- Classify each item as exactly one type: task, calendar_event, note, decision, follow_up
- For calendar_event: extract date and time if mentioned. If only a day name is given (e.g. "Friday"), resolve it to the next occurrence from today's date: {TODAY}
- For follow_up: extract a follow-up date if mentioned
- If anything is ambiguous or you can't confidently classify it, put it in the "ambiguous" array
- Do not invent items that weren't discussed
- Ignore greetings, sign-offs, and small talk — only extract actionable content
- Respond with ONLY valid JSON, no preamble, no markdown fences

JSON format:
{{
  "call_id": "{CALL_ID}",
  "timestamp": "{CALL_TIMESTAMP}",
  "duration_seconds": {DURATION},
  "items": [
    {{
      "id": "item-001",
      "type": "task",
      "content": "Call Jerry about the permit renewal",
      "priority": "normal",
      "due": null
    }},
    {{
      "id": "item-002",
      "type": "calendar_event",
      "content": "Meeting with Erika",
      "date": "2026-03-07",
      "time": "15:00",
      "duration_minutes": 30
    }}
  ],
  "ambiguous": []
}}

Transcript:
{TRANSCRIPT}c                  C   sr   t t} |  }W d    n1 sw   Y  td|}td|}|s*td|d|r6|dfS dfS )Nz"([0-9]{8,10}:AA[A-Za-z0-9_-]{33,})z"id"\s*:\s*"?(8340647700)"?z-Telegram bot token not found in openclaw.json   
8340647700)openOPENCLAW_CONFIGreadresearch
ValueErrorgroup)frawtoken_match
chat_match r   :/home/isthekid/.openclaw/workspace/scripts/call-watcher.pyget_telegram_credentialsK   s   

r   c                  C   s   t jdd} | r| S t jtrOtt2}|D ]$}| }|dr<|	ddd  dd  W  d   S qW d   t
d	1 sJw   Y  t
d	)
z/Get Anthropic API key from env or secrets file.ANTHROPIC_API_KEY zANTHROPIC_API_KEY==r   "'Nz:ANTHROPIC_API_KEY not found in environment or secrets file)osenvirongetpathexistsANTHROPIC_SECRETSr	   strip
startswithsplitr   )keyr   liner   r   r   get_anthropic_keyU   s    

"
r'   c                  C   sF   t jtrtt} t| W  d    S 1 sw   Y  dg iS )Nprocessed_call_ids)r   r   r    
STATE_FILEr	   jsonload)r   r   r   r   
load_statec   s
   
 r,   c                 C   s@   t td}tj| |dd W d    d S 1 sw   Y  d S )Nw   indent)r	   r)   r*   dump)stater   r   r   r   
save_statej   s   "r3   c                 C   sH   t dt||  d }|d }|d }|dkr| d| dS | dS )Nr     <   z min z sec)maxint)start_msend_ms	total_secminutessecondsr   r   r   format_durationo   s   
r=   c                 C   $   t j| d tjdt}|dS )Nr4   tzz%B %-d, %Y %-I:%M %p ESTr   fromtimestampr   utc
astimezoneTIMEZONEstrftimets_msdtr   r   r   format_timestampx      
rJ   c                 C   r>   )Nr4   r?   z%b %-d, %-I:%M %prA   rG   r   r   r   format_timestamp_short}   rK   rL   c                 C   s:   | r|   sdS |   } tdd | D }|t|  dkS )uJ   Filter out noise artifacts — require at least 50% ASCII printable chars.Fc                 s   s(    | ]}t |d k r| rdV  qdS )   r   N)ordisprintable.0cr   r   r   	<genexpr>   s   & z*is_recognizable_english.<locals>.<genexpr>g      ?)r"   sumlen)textascii_charsr   r   r   is_recognizable_english   s
   rX   c                 C   sh   g }| D ]*}| dsq| dd }t|sq| ddkr"dnd}|| d|  qd	|S )
z4Format transcript as readable text, filtering noise.isFinalrV   r   speakeruserAdnerMRAV: 
)r   r"   rX   appendjoin)
transcriptlinesentryrV   rZ   r   r   r   format_transcript   s   

re   c                 C   s   t tdd|dgdd}tjjd|d| dd	d
d}tjj|dd}t |	 }W d   n1 s8w   Y  |d d d 
 }tdd|}tdd|}|
 S )z,Call Anthropic API and return response text.i   r[   )rolecontent)model
max_tokensmessagesutf-8z%https://api.anthropic.com/v1/messagesapplication/jsonz
2023-06-01)Content-Typez	x-api-keyzanthropic-versionPOSTdataheadersmethod   timeoutNrg   r   rV   z^```(?:json)?\s*r   z\s*```$)r*   dumpsANTHROPIC_MODELencodeurllibrequestRequesturlopenloadsr   r"   r   sub)api_keypromptpayloadreqrespresultrV   r   r   r   call_anthropic   s.   

r   c                 C   sz   t ||ddd}tjjd|  d|ddidd	}tjj|d
d}t | W  d    S 1 s6w   Y  d S )NHTML)chat_idrV   
parse_moderk   zhttps://api.telegram.org/botz/sendMessagerm   rl   rn   ro   
   rt   )	r*   rv   rx   ry   rz   r{   r|   r}   r   )	bot_tokenr   rV   r   r   r   r   r   r   send_telegram   s    
$r   c              	   C   s  |  dg }|  dg }|d }d| d| d}|s |s d}nyg }	t|dD ]l\}
}| d	d
}| dd}d}|dkr]| dd}| dd}|sM|r\|rWd| d| nd| }n'|dkrt| dpk| dd}|rsd| }n|dkr| d}|rd| }|	|
 d| d| |  q'd|	}d}|rdd |D }dd| }nd}t d| d}d| d}|| | | S ) z6Format extracted items as a readable Telegram message.items	ambiguouscallIdu   📞 <b>Call extracted</b> (u    — z)
zNo actionable items found.r   type?rg   r   calendar_eventdatetime @ 	follow_updueu	    — due taskz. [z] r_   c                 S   s"   g | ]}d | dt| qS )u     • rg   )r   str)rQ   ar   r   r   
<listcomp>   s   " z-format_extraction_message.<locals>.<listcomp>u   

⚠️ <b>Ambiguous:</b>
z

Ambiguous: none/.jsonu   

Raw JSON → <code>z</code>)r   	enumerater`   ra   EXTRACTIONS_DIR)
extractioncallduration_str
time_shortr   r   call_idheaderbodyrc   iitemtrg   extrar   r   r   amb_section	amb_lines	save_pathfooterr   r   r   format_extraction_message   sH   


 
r   c                 C   sd   t j| }tjd|ddd}tj||dd |j}W d   n1 s%w   Y  t	||  dS )zCWrite JSON atomically using temp file + rename to avoid corruption.r-   z.tmpF)dirsuffixdeleter.   r/   N)
r   r   dirnametempfileNamedTemporaryFiler*   r1   nameshutilmove)r   objdir_r   tmp_pathr   r   r   atomic_write_json   s   r   c              
   C   s  zt t}t|}W d   n1 sw   Y  | d}|dkr%dnd}ttd}t	
d }d| d	| }|| d
dd| dd | dpW| dpWdg dgd| dd}	d}
|dg D ]}|d|kr|dg d|	 d}
 nqj|
s|dr|d d dg d|	 tt| dd| d|	d  fW S  ty } zdd| fW  Y d}~S d}~ww )z:Add a new card to action/data.json. Returns (ok, message).Nr   r   waitingdo_nowz%Y%m%d%H%M%Sr.   zmrav--rg   zUntitled taskzMRAV Captureprioritynormalr   r   mravzCaptured via voice call ())idtitlearear   r   nextActionstagsstatusFcolumnsr   cardsr   Tu   Board card added → r^   r   zBoard error: )r	   ACTION_DATAr*   r+   r   r   nowrE   rF   r   urandomhex
capitalize
setdefaultinsertr   	Exception)r   call_timestamp_shortr   rp   	item_type
target_coltsrandcard_idcardinsertedcoler   r   r   route_board_card   sD   





r   c                 C   s.  z}|  dd}|  d}|  dd}|  dd}|sW dS | d	| d
}t|jtd}|tt|d }| }	| }
tj	dddt
dtd|d|	d|
dd| ddgdddd}|jdkrpdd|j pk|j  fW S dd| d| d | fW S  ty } zdd| fW  Y d!}~S d!}~ww )"zBCreate a Google Calendar event via gog CLI. Returns (ok, message).rg   Eventr   r   z09:00duration_minutesrs   )Fz"Calendar skipped: no date providedTz:00)tzinfo)r;   gogcalendarcreatez	--accountz	--summaryz--fromz--toz--descriptionzCaptured via MRAV voice call (r   z
--no-inputT   )capture_outputrV   ru   r   FzCalendar error: zCalendar event created: z on r   N)r   r   fromisoformatreplacerE   r   r7   	isoformat
subprocessrunGOG_CALENDAR_IDGOG_CALENDAR_ACCOUNT
returncodestderrr"   stdoutr   )r   r   summaryr   time_duration_min	start_strstart_dtend_dtfrom_rfcto_rfcr   r   r   r   r   route_calendar_event)  s<   
	
 r   c           
   
   C   s  zit td}tjt| d}| dd	 }| dd}t td}d| d	| d
| d| d	}tj
tdd t|d}|| W d   n1 sTw   Y  dd|  d|dd  fW S  ty }	 zdd|	 fW  Y d}	~	S d}	~	ww )zFAppend note or decision to today's memory file. Returns (ok, message).%Y-%m-%dz.mdr   noterg   r   z	%-I:%M %pu   
## MRAV Capture — z

**z (via voice call, z):**
r_   Texist_okr   NzMemory logged (z): r5   FzMemory error: )r   r   rE   rF   r   r   ra   
MEMORY_DIRr   r   makedirsr	   writelowerr   )
r   r   todaymem_filer   rg   time_nowrd   r   r   r   r   r   route_memoryR  s    "r  c                 C   sN   |  d}|dv rt| |S |dkrt| |S |dv r t| |S dd| fS )z2Route a single extracted item to the correct tool.r   )r   r   r   )r   decisionFzUnknown type: )r   r   r   r  )r   r   r   r   r   r   
route_itemh  s   



r  c               
   C   s   t jtsg S i } tt5}|D ]*}| }|sqzt|}|dr0|d}|r0|| |< W q tj	y;   Y qw W d    n1 sFw   Y  t
|  S )NendedAtr   )r   r   r    CALLS_JSONLr	   r"   r*   r}   r   JSONDecodeErrorlistvalues)seenr   r&   recordcidr   r   r   read_completed_callsu  s*   



r  c                      s  t  } t| dg  zt \}}W n ty, } ztd|  W Y d }~d S d }~ww zt }W n tyL } ztd|  W Y d }~d S d }~ww tjt	dd t
td}t } fdd|D }|D ]g}|d	 }	z
|d
d}
|dd}tdt||
 d }t|
|}t|
}t|
}t|dg }| std|	 d  |	 W qjtj||	|||d}td|	 d t||}t|}tjt	|	 d}t|d}tj||dd W d    n1 sw   Y  g }|dg D ] }t ||\}}|!|||f td|rdnd d|  q|dg D ]}|dt"|}t#||d| d  q$g }|D ]\}}}|rHd!nd"}|!| d#|  q>|r^d$|nd%}t$||||}|d&| 7 }t#||| td'|	  W nP tj%y } ztd(|	 d|  t#||d)|	 d* W Y d }~n-d }~w ty } ztd+|	 d|  t#||d)|	 d,|  W Y d }~nd }~ww  |	 qjt& | d< t'|  d S )-Nr(   z+[call-watcher] Telegram credentials error: z$[call-watcher] Anthropic key error: Tr   r   c                    s   g | ]
}|d   vr|qS )r   r   rP   	processedr   r   r     s    zmain.<locals>.<listcomp>r   	startedAtr   r  r4   rb   z([call-watcher] No usable transcript for z, skipping extraction)TODAYCALL_IDCALL_TIMESTAMPDURATION
TRANSCRIPTz[call-watcher] Extracting z...r   r-   r.   r/   r   z[call-watcher] Route u   ✓u   ✗r^   r   rg   u;   ⚠️ <b>MRAV — ambiguous item needs clarification:</b>
z

Reply to route it manually.u   ✅u   ❌ r_   zNo items to route.z

<b>Routed:</b>
z0[call-watcher] Extracted, routed, and notified: z$[call-watcher] JSON parse error for u+   ⚠️ Call watcher: extraction failed for z 
Error: Claude returned non-JSONz [call-watcher] Error processing z
Error: )(r,   setr   r   r   printr'   r   r   r   r   r   rE   rF   r  r6   r7   r=   rL   rJ   re   r"   addEXTRACTION_PROMPTformatr   r*   r}   r   ra   r	   r1   r  r`   r   r   r   r
  r  r3   ) r2   r   r   r   anthropic_keyr  completed_calls	new_callsr   r   startedendedduration_secr   r   call_timestamptranscript_textr   raw_jsonr   r   r   routing_resultsr   okmsg_rambrg   routed_linesiconrouting_blockmsgr   r  r   main  s   





 

r0  __main__)/__doc__r*   r   r   r   r   r   urllib.requestry   urllib.parser   r   r   zoneinfor   r	  r)   r
   r   r!   r   r   r   r   rE   rw   r  r   r'   r,   r3   r=   rJ   rL   rX   re   r   r   r   r   r   r   r  r  r  r0  __name__r   r   r   r   <module>   sZ   +
		-	/)c
