
    GiHH                    h   d Z ddlm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
mZmZ ddlmZmZ ddlZddl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   ejB                  d      Z"ejF                  jI                  dd      jK                         dk(  Z& e'ejF                  jI                  dd            Z( e'ejF                  jI                  dd            Z) e'ejF                  jI                  dd            Z* e+ejF                  jI                  dd            Z, e+ejF                  jI                  dd            Z- e ed            Z.d(d)dZ/d*dZ0dZ1d+d Z2d,d!Z3d(d-d"Z4	 	 	 	 	 d.	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d/d#Z5d0d$Z6 ejn                  d%ejp                        Z9d1d&Z:d2d'Z;y)3u  
Task Extractor(階段 7-B + 階段 9 主動模式):自動從訊息偵測「派工意圖」並抽出結構化任務。

被叫到的時機:
  - line_bridge 在訊息存好之後丟 background task 過來
  - messages.py(網頁版發訊息)同樣的點

設計:
  1. 只看內容 + 群裡的人員清單(避免拿到無關 user)
  2. 用 Haiku 4.5(快、便宜,夠準),不用 Sonnet
  3. 輸出 JSON:{is_task, confidence, assignee_name, title, due, priority, reasoning}
  4. 信心 < 0.5 整段 ignore
  5. 信心 0.5–0.65 → 「主動建議」soft push:「這要記成任務嗎?」(不真建)
  6. 信心 ≥ 0.65 → 寫 agent_activity + EXTRACTOR_LIVE=true 才真建任務 + 通知

階段 9 加上的「不洗版」防護:
  - 同頻道 60 分鐘最多 PROACTIVE_MAX_PER_HOUR 次主動行為
  - 23:00–07:00(台北)只記 log,不推 LINE
  - 已超過冷卻 → 高信心一樣建,但不推通知(避免半夜叮咚)
    )annotationsN)datedatetimetime	timedeltatimezone)UUIDuuid4)and_select)settings)AsyncSessionLocal)AgentActivity)ChannelMessage)Task)Userzcoba.task_extractorEXTRACTOR_LIVEfalsetruePROACTIVE_MAX_PER_HOUR3PROACTIVE_QUIET_START23PROACTIVE_QUIET_END7PROACTIVE_BUILD_THRESHOLDz0.65PROACTIVE_SUGGEST_THRESHOLDz0.5   hoursc                0   | xs t        j                  t              } | j                  | j	                  t              } | j                  t              j                  }t        t        k  rt        |cxk  xr	 t        k  S c S |t        k\  xs	 |t        k  S )u%   台北 23:00-07:00 為靜音時段。)tzinfo)	r   nowTPEr#   replace
astimezonehourr   r   )r$   hs     ?/volume1/homes/robertsu/coba/app/app/services/task_extractor.py_is_quiet_hoursr+   9   sz    

"c"C
zzkkk%s  A22$?,?????%%@-@)@@    c           	     *  K   t        j                  t        j                        t	        d      z
  }t               4 d{   }|j                  t        t              j                  t        j                  j                  ddg      t        j                  |k\               d{   j                         j                         }ddd      d{    t        |       t!        fdD              S 7 7 Q7 '# 1 d{  7  sw Y   7xY ww)uQ   這個頻道過去 60 分鐘有多少次主動行為(建任務 / 主動建議)。   r    Ntask_create_autoproactive_suggestc              3  h   K   | ])  }|j                   xs i j                  d       k(  s&d + yw)
channel_idr.   N)extraget).0rcids     r*   	<genexpr>z-_proactive_count_last_hour.<locals>.<genexpr>Q   s,     K$Q177=b"5"5l"Cs"Jq$s   '22)r   r$   r   utcr   r   executer   r   whereactivity_typein_
created_atscalarsallstrsum)r2   cutoffdbrowsr7   s       @r*   _proactive_count_last_hourrF   E   s     \\(,,')!*<<F ""bjj=!''++//1CEX0YZ((F2
 

 79SSU 	 #" j/CK$KKK #
 #"""sZ   ?DC8DA"C>(C:) C>	DC<$D:C><D>DDDDu	  你是「派工偵測員」。讀一則團隊訊息,判定是否在派工作給某人。

【判斷準則】
✓ 算派工:
  - 「阿綠你做 X」「請阿白處理 Y」「Z 這個你跟一下」
  - 「煥誠記得在 X 之前完成 Y」
  - 「明天前把客戶 A 的提案弄好」(沒指名 = 派給整個團隊,assignee 留 null)
  - 「@阿綠 把週五會議紀錄整理出來」

✗ 不算派工:
  - 「阿綠你覺得呢?」「等等阿綠來」(問問題 / 提及人)
  - 「我來做 X」「我先試試」(自我表態)
  - 「等等開會聊」「下午處理」(無動作對象)
  - 一般陳述、抱怨、感想、閒聊
  - 庫柏自己的訊息
  - **「覆盤」「體悟」「反省」「教訓」「學到」「下次要 X」「以後要 Y」開頭或主軸的訊息**
    → 一律 is_task: false,不論句中有沒有提到人名或動作。這是反思類,有獨立系統處理。

【輸出 JSON 格式】嚴格 JSON,不要 markdown,不要說明:
{
  "is_task": true/false,
  "confidence": 0.0-1.0,
  "title": "簡短任務描述(15 字內,動詞開頭)",
  "assignee_name": "對應到的成員顯示名稱(必須是『群裡成員清單』裡列出的)" 或 null,
  "due": "YYYY-MM-DD" 或 null,
  "priority": "low" | "medium" | "high" | "urgent",
  "reasoning": "20 字內,為什麼這樣判"
}

【範例】

訊息:「阿綠週三前把客戶 A 的提案 v2 寄出去」
群裡成員:[Robert Su, 神狙手阿綠, Jonina🕊️]
今日日期:2026-05-06(三)
→ {"is_task": true, "confidence": 0.95, "title": "寄出客戶 A 的提案 v2", "assignee_name": "神狙手阿綠", "due": "2026-05-06", "priority": "medium", "reasoning": "明確派給阿綠,有截止日"}

訊息:「Jonina 妳怎麼想」
群裡成員:[Robert Su, 神狙手阿綠, Jonina🕊️]
→ {"is_task": false, "confidence": 0.92, "title": "", "assignee_name": null, "due": null, "priority": "medium", "reasoning": "詢問意見,不是派工"}

訊息:「下週要把官網改版上線,很急」
群裡成員:[Robert Su, 神狙手阿綠, Jonina🕊️]
今日日期:2026-05-06
→ {"is_task": true, "confidence": 0.78, "title": "官網改版上線", "assignee_name": null, "due": "2026-05-13", "priority": "urgent", "reasoning": "未指人但動作明確"}

訊息:「等等開會聊」
→ {"is_task": false, "confidence": 0.95, "title": "", "assignee_name": null, "due": null, "priority": "medium", "reasoning": "非具體任務"}

只回 JSON,不要其他字。
c                   | j                         } t        j                  d| t        j                        }|sy	 t	        j
                  |j                  d            }t        |t              sy|S # t        j                  $ r Y yw xY w)u0   容錯地把 Haiku 回的字串解析成 dict。z\{.*\})flagsNr   )
stripresearchDOTALLjsonloadsgroupJSONDecodeError
isinstancedict)rawmresults      r*   _parse_jsonrV      sr    
))+C
		)S		2AAGGAJ' fd#M	  s   $A2 2BBc                R    | sy| j                         j                         } | dv r| S y)Nmedium)lowrX   highurgent)rI   lower)ps    r*   _normalize_priorityr^      s+    		A//r,   c                    | sy 	 t        j                  | d      j                         }|r||k  ry |S # t        t        f$ r Y y w xY w)Nz%Y-%m-%d)r   strptimer   
ValueError	TypeError)due_strtodayds      r*   _normalize_duerf      sP    gz2779QY	" s   +3 3 AAc                F  K   	 t               4 d{   }|j                  t        | ||||||             |j                          d{    ddd      d{    y7 N7 7 	# 1 d{  7  sw Y   yxY w# t        $ r t
        j                  d|        Y yw xY ww)u(   寫一筆 AgentActivity(失敗吞掉)。N)r<   user_idsummarysuccessr3   cost_usdduration_msu    寫 AgentActivity 失敗 type=%s)r   addr   commit	Exceptionlogger	exception)r<   ri   rh   r3   rk   rl   rj   rD   s           r*   _log_activityrr      s     L$&&"FF=+!'  ))+ '&&  '&&&  L;]KLs   B!A< A!A< 5A'A#A'A< A%A<  B!!A< #A'%A< 'A9-A0.A95A< 8B!9A< <BB!BB!c                  'K   t         j                  syt        j                  t        j
                        }t               4 d{   }|j                  t        t              j                  t        j                  | k(               d{   j                         }|	 ddd      d{    y|j                  	 ddd      d{    yt        |j                  j!                               dk  r	 ddd      d{    y|j"                  dk7  r	 ddd      d{    y|j                  j!                         }|j                  '|j$                  }|j                  t        t&              j                  t&        j(                  j+                  d      t&        j,                  j+                  d                   d{   j/                         j1                         }dj3                  d |D              }|D ci c]  }|j4                  |j                   }	}t7        'fd|D        d      }
ddd      d{    t9        j:                         }d d	
r|
j4                  nd
 d d|j=                          d	}	 t?        j@                  t         j                        }|jB                  jE                  t         jF                  dtH        d|dg       d{   }dj3                  d |j                  D              }tK        |      }|tL        jO                  d|dd        ytQ        |jS                  dd            }tU        |jS                  dd      xs d      }|jV                  }|jX                  dz  dz  |jZ                  dz  dz  z   }t]        t        j                  t        j
                        |z
  j_                         dz        }ta        d|r!|tb        k\  rd|jS                  d d      dd!  nd"|d#d$'te        |       te              |dd% |||jS                  d       |jS                  d&      |jS                  d'      |jS                  d(      |jS                  d)      tf        d*||+       d{    |r	|th        k  ry|jS                  d       xs dj!                         }|sytk        |       d{   }|tl        k\  }to               }t               4 d{   }|j                  t        tp              j                  tp        j                  |k(               d{   j                         }ddd      d{    r|jr                  nd}	jS                  |jS                  d&      xs d      }tu        |jS                  d(            }tw        |jS                  d'      |      }|tb        k  r|s|stf        stL        jy                  d,||||tf               y|jS                  d&      rd-|jS                  d&       d.nd}|rd/|j=                          d0nd} d1| d2|  d| d3}!	 |r d4d5l=m>}" |"j                  ||!       d{    ta        d7d8| 'te        |      ||te        |       |jS                  d&      |r|j=                         nd|d9:       d{    ytf        s8tL        jy                  d;|||jS                  d&      |jS                  d'             y|xs |}#t               4 d{   }t        ||||'d<=      }$|j                  |$       |j                          d{    |j                  |$       d{    ddd      d{    ta        d>d?| 'te        $j                        te        |      |jS                  d&      |te        |       |#|rd@n|rdAnddB:       d{    |#s|sy	 d4d5l=m>}" |rdC|jS                  d&       ndD}|rdE|j=                          nd} |dFk(  rdGn|dHk(  rdInd}%dJ| dK| |  |% dL}&|"j                  ||&       d{    y7 7 7 7 7 R7 47 c c}w 7 # 1 d{  7  sw Y   0xY w7 7 7 7 7 M7 2# 1 d{  7  sw Y   CxY w7 A# t        $ r tL        j                  d6       Y `w xY w7 7 7 7 r7 e# 1 d{  7  sw Y   vxY w7  7 # t        $ r tL        j                  dM       Y yw xY w# t        $ r tL        j                  dN       Y yw xY ww)Ou.   背景任務:對一則訊息跑派工偵測。N   textT
c              3  :   K   | ]  }d |j                      yw)z  - N)display_name)r5   us     r*   r8   z'trigger_task_extract.<locals>.<genexpr>   s      J'Q4'7!8's   c              3  B   K   | ]  }|j                   k(  s|  y wN)id)r5   ry   	sender_ids     r*   r8   z'trigger_task_extract.<locals>.<genexpr>   s     ?'QQTTY->q's   u
   訊息:「u   」
傳訊者:u   未知u   
群裡成員:
u   
今日日期:)api_keyi,  user)rolecontent)model
max_tokenssystemmessages c              3  T   K   | ]   }|j                   d k(  s|j                   " yw)ru   N)typeru   )r5   bs     r*   r8   z'trigger_task_extract.<locals>.<genexpr>   s"     K&6!&&F:Jaff&6s   ((u$   Haiku 回了無法解析的內容:%s   is_taskF
confidenceg        i@B g      ?g      @i  task_extractu   偵測派工:title   u   訊息不像派工(信心 z.2f)x   assignee_nameduepriority	reasoning)
message_idr2   content_previewr   r   r   r   r   r   r   	live_mode)r<   ri   rh   r3   rk   rl   zA[soft-suggest skip] %s | conf=%.2f | cooldown=%s quiet=%s live=%s@ (u    前)u"   🤔 這聽起來像個任務:
《   》uA   要我幫你開嗎?回「好」我就建,不理我就當沒事。r   )line_clientu   LINE 主動建議推送失敗r0   u   主動建議:)r2   r   r   from_message_idr   r   r   r<   ri   rh   r3   uA   [dry-run] 偵測到派工:%s | 信心 %.2f | assignee=%s | due=%stodo)r   r   due_dateassignee_id
created_bystatusr/   u   自動建任務:cooldownquiet_hours)task_idr2   r   r   r   notify_suppressedsuppress_reasonu
   指派給:u$   (沒指明對象,先記在公共池)u    · 截止 r[   u    · ⚡ 緊急rZ   u    · 🔥 高優u"   📋 我把這個記成任務:
《u   》
u5   

錯了 @ 我說「取消最後一個任務」就好u   LINE 通知派工建立失敗u   task_extract 失敗)Fr   ANTHROPIC_API_KEYr   r$   r   r9   r   r:   r   r   r;   r|   scalar_one_or_nonerh   lenr   rI   message_typer2   r   	is_activeis_
deleted_atr?   r@   joinrx   nextr   rd   	isoformat	anthropicAsyncAnthropicr   createCLAUDE_MODEL_HAIKUEXTRACTOR_PROMPTrV   rp   warningboolr4   floatusageinput_tokensoutput_tokensinttotal_secondsrr   r   rA   r   r   rF   r   r+   r   line_source_idr^   rf   infoapp.servicesr   	push_textro   rq   r   rm   rn   refresh)(r   startedrD   msgr   r2   membersmember_linesry   members_for_matchsenderrd   user_promptclientresponserS   rU   r   r   r   costrl   r   recent_countcooldown_hitquietchr   r   r   r   assignee_textdue_textsuggestr   suppress_notifytaskpriority_textack_textr}   s(                                          @r*   trigger_task_extractr      s    %%ll8<<(G ""bZZ7O!!'**
":;
 
 	 ; #"" ;; #"" s{{  "#a' #"" v% #""  ++##%KK	^^
 4Lt~~11$79L9LT9RS
 
79SSU 	 yy J' JJ;BC7aQ^^QTT17C?'?F5 #"8 JJLE
WI ,2V((A B& ()*"	. r0))(2L2LM//--#%+>?	 0 
 
 ggKh&6&6KKS!>NNA3t9Mvzz)U346::lC8?C@
 ""Y.#59L9Ly9X\_8__8<<5?NNPSWWX ( z-FF  

7B 7 <=>1*S1AC!*o!*o#*4C="(G,!'O!<zz%("JJz2#ZZ4+ #-
 	
 	
4 *'BBG$*113 8
CC#'==! %&&"

w%%gjjJ&>? !!#  '& /1**d (++FJJ,G,M2N&vzz*'=>!&**U"3U; 11uNW:|UN BH**_B]a

? ;<A>ceM:B8--/06HWCz /!bd 
B!8%//HHH  1'w/!"%j/"",'*:%+ZZ%@3;8--/ (	    KKSz6::o#>

5@Q  '/% %&&"!!'$D FF4L))+**T""" '& ,&ug.tww<!*o!'O!<(#&z?%4".Je]QU
	
 	
 	
" .	>0  VZZ89:; 
 @HX%7%7%9$:;RH$,$8 T\`fTf?Pln WE /(M? ;FG  ''AAAc #
 #"""*
 D3 #"""L
(	
D D
 ' '&&&: I B  !@AB: ' " '&&&	
L B 	><=	>  0./0s  Ad>a	d>
Aa&aa&"d>-a.d>3a& d>ad>'a&8d>ad>	a&d>$a%d>*Ba&a<a&a"a&9d>a#Ad>	Ad %a<&Ad 2d>3Ed 
a?d d>%d  d>d b%d 5b6d 9Ab;b<bd bBd 8d>9Ad b' #b$$b' (Ad 8c9d =d>>=d ;d><d cd 6c
cc#c$c(d 3c4Ad c1d d>A'c6 c4c6 d>a&d>d>d>d>a&a&#d>&a9,a/-a94d><d ?d d d bd b!bb!d $b' 'c	d c		d d ccd c.!c$"c.)	d 4c6 6dd d>dd d;8d>:d;;d>uu   ^(?:@?庫柏|@coba)\s*[,,]?\s*(?:取消|刪掉|不要)(?:最後一個|那個|剛剛(?:那個|建的)?)?\s*任務?\s*$c                `    | syt        t        j                  | j                                     S )NF)r   	CANCEL_REmatchrI   )r   s    r*   is_cancel_commandr     s"    	011r,   c           	     H  K   t               4 d{   }|j                  t        t              j	                  t        j
                  dk(        j                  t        j                  j                               j                  d             d{   j                         }|'|j                  r|j                  j                  d      s	 ddd      d{    y|j                  d   }|j                  t        t              j	                  t        j                  |k(  t        j                  j!                  d                   d{   j                         }|	 ddd      d{    y|j"                  }t%        j&                  t(        j*                        |_        d|_        |j/                          d{    ddd      d{    t1        dd	 | d
d       d{    d| dS 7 7 l7 '7 7 7 D7 6# 1 d{  7  sw Y   FxY w7 0w)u   取消「最後一個自動建的任務」。

    定義:agent_activity 中最近一筆 type='task_create_auto' 對應的 task。
    操作:把 task 軟刪 + 記 'task_cancel' activity。回傳要在 LINE 講的話。
    Nr/   r.   r   u7   找不到最近的自動任務,沒東西可以取消。u;   那個任務已經不在了(可能你之前已經刪過)。	cancelledtask_cancelu   取消自動任務:user_command)r   triggerr   u   ✓ 取消了:《r   )r   r:   r   r   r;   r<   order_byr>   desclimitr   r3   r4   r   r|   r   r   r   r   r$   r   r9   r   rn   rr   )triggered_by_user_idr   rD   lastr   r   r   s          r*   cancel_last_auto_taskr     s     !""bjj=!U=..2DDEXm..3356U1X	
 

  	 <tzz	1JL #"" **Y'jj4Ltww'14??3F3Ft3LM
 
 	 <P% #""( 

",,x||4!iik/ #"2 #%eW-$!n=	   ugS))A #
 #
 #. 	/ #"""2s   H"G:H"A<HG=<HH"H H"A.HHH#H".H/H"4AHHHH"H	H"/H 0H"=H H"HH"H	H"HHHH"r{   )r$   zdatetime | Nonereturnr   )r2   r	   r   r   )rS   rA   r   dict | None)r]   
str | Noner   rA   )rc   r   rd   date | Noner   r   )NNNNT)r<   rA   ri   rA   rh   zUUID | Noner3   r   rk   zfloat | Nonerl   z
int | Nonerj   r   r   None)r   r	   r   r   )r   rA   r   r   )r   r	   r   r   r   rA   )<__doc__
__future__r   rM   loggingosrJ   r   r   r   dtimer   r   uuidr	   r
   r   
sqlalchemyr   r   
app.configr   app.databaser   app.models.agent_activityr   app.models.channelr   r   app.models.taskr   app.models.userr   	getLoggerrp   environr4   r\   r   r   r   r   r   r   r   r   r%   r+   rF   r   rV   r^   rf   rr   r   compile
IGNORECASEr   r   r    r,   r*   <module>r     s  * #   	 	 G G   #  * 3 /     
		0	1  0':@@BfL RZZ^^,DcJK BJJNN+BDIJ "**..)>DE !"**..1Lf"UV #BJJNN3PRW$XY yq!"	AL1 h
   !"LLL L 	L
 L L L 
L>]0J BJJ|MM	2&*r,   