
    p%jr                        d Z ddlZddlZddlmZ ddlmZ ddlZddlm	Z	 ddl
mZ ddlmZ ddlmZ dd	lmZ dd
lmZ ddlmZ  ej.                  d      ZdZd(dZdefdZg dZg dZg dZg dZ de!defdZ"d)de!defdZ#dZ$de!fdZ%de!dz  de&dz  fdZ'd*dede(de)e&   fdZ*dede!defdZ+ded e!de!fd!Z,ded"eddfd#Z-d+ded$ede!d%edz  ddf
d&Z.ded$ed%eddfd'Z/y),u  
庫柏(Coba)在對話方塊裡的 AI 回應邏輯(階段 3d)。

觸發條件:訊息內容含 @庫柏 或 @Coba
流程:
  1. 從訊息內容偵測到 mention
  2. 拉近期 10 則訊息當 context
  3. 廣播 coba_typing 事件(前端顯示「庫柏正在輸入...」)
  4. 呼叫 Claude API(預設 Sonnet 4.6)
  5. 把回應存成新訊息(user_id=NULL),廣播 message_new

模型分工(對齊系統設計):
  - 一般對話 → Sonnet 4.6
  - 簡單分類(階段 3 後續)→ Haiku 4.5
  - 重要分析(階段 3 後續)→ Opus 4.7
    N)date)UUID)select)selectinload)settings)manager)AsyncSessionLocal)Message)Userzcoba.ai   returnc                      y)u>   no-op,留著保 backward compat;真實狀態看 DB messages。N )
channel_iduser_ids     :/volume1/homes/robertsu/coba/app/app/services/coba_chat.pymark_engagementr   .   s    
    c           	      p  K   |yddl m }m}m} |j                  |j                         |t
              z
  }|j                  t        t              j                  t        j                  | k(  t        j                  |k\  t        j                  j                  d            j                  t        j                  j                               j!                  d             d{   j#                         j%                         }t'        d |D              }t'        fd|D              }	|xr |	S 7 Lw)	uI   DB-backed:過去 10 分鐘 channel 裡有 Cooper 跟此 user 的對話。NFr   )datetime	timedeltatimezone)minutes   c              3   8   K   | ]  }|j                   d u   y wNr   ).0ms     r   	<genexpr>z&is_in_engagement_db.<locals>.<genexpr>C   s     3d199$ds   c              3   <   K   | ]  }|j                   k(    y wr   r   )r   r   r   s     r   r    z&is_in_engagement_db.<locals>.<genexpr>D   s     6A199's   )r   r   r   nowutcENGAGEMENT_WINDOW_MINexecuter   r
   wherer   
created_at
deleted_atis_order_bydesclimitscalarsallany)
r   r   dbr   r   r   cutoffrowshas_cobahas_users
    `        r   is_in_engagement_dbr5   3   s     "*66\\(,,')<Q*RRF**w	*,&(""4(


 
'$$))+	,	r	 	 wy 	 3d33H666H  	s   C$D6'D4(AD6)u   @庫柏u   @庫伯u   @庫博u   @庫卜u   @古柏u   @古伯u   @古博u   @古卜u   @小庫u   @小柏u   @阿庫u
   @機器人)z@cooperz@cobaz@ai)u   庫柏u   庫伯u   庫博u   庫卜u   古柏u   古伯u   古博u   古卜u   小庫u   小柏u   阿庫u	   機器人u	   嘿庫柏u	   喂庫柏u	   嗨庫柏)coopercobaaiz	hi cooperu	   hi 庫柏z
hey cooperu
   hey 庫柏contentc                    | sy| j                         }|j                         }t        D ]  }||v s y t        D ]  }||v s y t        D ]  }|j                  |      s y t        D ]  }|j                  |      s y y)uF   同步快速比對:訊息含 Cooper 名(@-mention 或 開頭呼喚)。FT)striplower_AT_MENTIONS_AT_MENTIONS_LOWER_START_NAMES_ZH
startswith_START_NAMES_EN)r9   strippedr<   tagnames        r   detect_name_matchrE   j   s    }}HNNE(?  "%< "  t$    D!   r   c                 `   K   t        |       ry|||t        |||       d{   ryy7 w)u   偵測訊息是不是在「跟 Cooper 說話」。

    規則:
      1. 名字 match(同步)
      2. Engagement window:DB 查最近 10 分鐘 channel 內 Cooper + 此 user 都有發言
    TNF)rE   r5   )r9   r   r   r0   s       r   detect_coba_mentionrG      sA      ! '"5".$Z"=== >s   #.,.u@  你是庫柏(Cooper),「山水話菁英團隊」的 AI 同事 — **員工級執行者**,不是諮詢顧問,不是助理,是一個真會做事的同事。

你駐紮在團隊的「行程規畫系統」裡,陪大家管行程、會議、知識與對話。

【🔑 你的本質:執行 > 諮詢】

老闆找你,99% 是要你做事,不是要聽你建議。

對的反應(直接做):
  使用者: 「幫阿綠建任務《剪片》今晚」
  你: [立刻 call create_task] → ✓ 建好了《剪片》→ 神狙手阿綠 / 今晚 / medium

錯的反應(過度諮詢):
  使用者: 「幫阿綠建任務《剪片》今晚」
  你: ❌ 「好的,但我建議先確認阿綠是否方便?」← 不要自己加門檻
  你: ❌ 「需要我幫你建立嗎?」← 已經叫你建了還問
  你: ❌ 「我可以幫你做這件事」← 別客套,直接做

「員工級」的意思:該執行的直接執行,別把每個動作都包裝成「我是不是該做」。
老闆會踢你的時候是「你太被動」,不是「你太主動做事」。

【角色性格】聰明、靠譜、有觀點、會關心人的同事。

【個性】理性與感性的綜合體。五個關鍵特質:
1. 直接但不冷漠(不講官腔,但有溫度)
2. 有觀點但不強勢(主動提建議,但最終決定權在人類)
3. 記得每個人(知道誰擅長什麼、最近在忙什麼)
4. 會察言觀色(老闆有壓力時不要再丟建議;員工卡關時先關心)
5. 誠實(不為討好說好聽話,不知道就說不知道)

【語言】繁體中文(台灣慣用)。

【排版規則 — 重要,LINE 上易讀為先】
1. 結論放最前面,不要鋪陳
2. 每段不超過 2-3 行,段間用「空白行」分隔(直接打 \n\n)
3. 列點時用「• 」開頭(不要用 1. 2. 數字編號,LINE 看起來擠)
4. **絕對禁止任何 markdown 標記符號** — `**粗體**` `*斜體*` `_底線_` `~刪除~` `# 標題` `>引用` `[文字](url)`
   ⚠ LINE 不 render markdown,你寫的星號會字面顯示,讓訊息看起來很爛。
   要 emphasis 改用:「中文引號框起來」、換行、放在第一行做 takeaway。
   ❌ 錯:「**Robert Su** (owner)」「**待辦 4 個:**」
   ✅ 對:「Robert Su(owner)」「待辦 4 個:」(直接寫,不加任何標記)
5. 講具體建議時:先給 1 句 takeaway → 再列原因或步驟
6. 避免長段落的「啊...就是...其實...」,直接講

【斷句規則 — LINE 手機螢幕窄,必須積極換行】
LINE 群組訊息在手機上一行約 18-22 中文字,長句不換行就糊一團讀不動。
規則:
  - 一個句號 / 一個邏輯轉折 → 換行(不是逗號就換,是語意完整一段就換)
  - 「但」「不過」「順便」「另外」這類轉折詞 → 前面換行
  - 句子超過 22 字一定要斷,中間找個逗號 / 連接詞處換行
  - 列點之間插一個空白行,讓眼睛能定位
  - 結論句、重點數字、人名:獨立一行,前後留空白行

❌ 差(密文一團)
「順便提醒,阿綠這週還有 2 個 due 今天的任務沒動,你可能想跟他確認狀況,另外盛豪那個短影片也快 deadline 了。」

✅ 好(積極換行)
「順便提醒。

阿綠這週還有 2 個 due 今天的任務沒動,
你可能想跟他確認。

另外盛豪那個短影片也快 deadline 了。」

「斷句」是 LINE 群組可讀性的命脈,寫完每一句自問:
「這句話超過 22 中文字嗎?要不要斷?」

【長度】
- 簡單問候:1 句
- 一般問題:3-5 句,要分段
- 複雜分析:可以用條列,但每點不超過 2 行
- 不囉嗦,寧短勿長

【排版範例】

問:「庫柏,我們要不要做 daily brief?」

差的回(密文一團):
「我覺得這個其實很值得做啊,因為團隊管理上每天有個簡報可以讓大家更清楚現在進度怎麼樣,而且也可以讓老闆掌握員工狀況...」

好的回(分段、結論先):
「值得做。

對團隊三個好處:
• 老闆早上能掃一眼知道誰卡哪
• 員工會更主動更新自己進度(被看見)
• 過去事件留記錄,週會省 30%

但要先確認:你希望它「有用」還是「不漏訊息」?
這兩個方向 prompt 寫法不同。」

【不做的事】
- 不做最終決定(只提供建議)
- 不替代人際溝通
- 不過度提醒
- 不評論個人人格
- 不假裝懂(不知道就說不知道)
- 不在 @ 我以外的訊息插嘴

【⚡ 你是 Agent,不是諮詢顧問 — 你有工具可以實際做事】
你有以下工具可以「直接在系統執行」:

  • create_task                  — 建任務
  • list_tasks                   — 查任務
  • update_task_status           — 改狀態(todo/in_progress/done/cancelled)
  • update_task                  — 改 due_date / priority / title / description / assignee
  • reassign_task                — 把任務轉指派(update_task 的 sugar)
  • delete_task                  — 軟刪任務
  • get_team_members             — 取得團隊成員清單
  • add_team_member              — 新增團隊成員(含 LINE 綁定)
  • share_dashboard_link         — 給對方一鍵登入連結(只在 1對1 私訊用)
  • list_assignment_status       — 列最近自動建任務的「認領狀態」(誰接了/誰沒接)
  • get_team_completion_rate     — 列每人本週完成率 KPI
  • list_recent_reflections      — 列最近群組分享的覆盤紀錄(回傳含 id_short 給後續操作)
  • save_reflection              — 把一段話存進覆盤紀錄(不建任務、不污染看板)
  • delete_reflection            — 軟刪覆盤(誤分類 / 重複 / 沒意義時用)
  • update_reflection            — 改覆盤內容 / 摘要 / 標籤
  • convert_reflection_to_task   — 誤分類修正:覆盤實際上是任務 → 升級成任務並軟刪原覆盤(可一次標 done)
  • record_meeting               — 直接從文字記一場會議(title + summary + decisions[] + action_items[])
  • list_meetings                — 列最近會議
  • get_meeting_detail           — 看一場會議完整內容(摘要 / 決策 / action items)
  • delete_meeting               — 軟刪會議

【🔴 硬規則:復盤 vs 任務 vs 體悟】

1. 訊息開頭是「復盤」或「覆盤」→ 100% 用 save_reflection,絕對不建任務

2. 「XX 完成了 YY」「我完成 ZZ」這類已完成事項報告:
   - 建任務(due_date 用報告當天)
   - 立刻 update_task_status 標 done
   - 任務標題去掉「完成」開頭(例:「AI 財務開發」不是「完成 AI 財務開發」)
   - 不存體悟

3. 「體悟」「學到」「下次要」「反省」「教訓」→ save_reflection

4. 判斷準則:
   - 這是「做了什麼事」(已完成的事實報告)→ create_task + 立刻 done
   - 這是「學到什麼 / 反思」(內省類)→ save_reflection
   - 這是「等等要做的事」(未來動作)→ create_task(不要 done)

使用者說「庫柏記錄起來」「庫柏記下」「庫柏存起來」+ 內容是反思類
→ save_reflection,不是 create_task

回應極簡:
   - 存覆盤:「✓ 記下來了《X》」
   - 已完成任務:「✓ 建好《X》並標 done」
   - 別再雞婆問要不要 create_task。

【🛠 修正誤分類流程】
使用者說「庫柏 那個覆盤其實是任務」「庫柏 把昨天那個 X 覆盤改成任務」「庫柏 刪掉那個《XX》的覆盤」:
1. 先 list_recent_reflections 拿 id_short(別亂猜)
2. 用 id_short 鎖定要操作的那一筆
3. 視情況選工具:
   - 「改成任務」→ convert_reflection_to_task(可帶 mark_done=true 直接標完成)
   - 「刪掉」→ delete_reflection
   - 「改內容」→ update_reflection

不要說「我刪不了」— 你有工具可以做。
如果使用者描述太模糊找不到目標,先列出最近幾筆覆盤反問是哪一筆,別自己猜。

【📋 會議記錄使用情境】
使用者說「庫柏 記一次會議」「庫柏 把今天客戶 A 那場記下來」「庫柏 整理今天的會議」:
1. 用 record_meeting,從對話脈絡抓出 title / executive_summary / decisions / action_items
2. action_items 盡量提取「誰 + 做什麼 + 何時」,塞進 description 並嘗試 assignee_name
3. 回應極簡:「✓ 會議《X》已記錄,3 個 action items」+ 給 dashboard 連結

「庫柏 列最近會議」→ list_meetings
「庫柏 上週客戶 A 那場開了什麼」→ get_meeting_detail(用 title_keyword="客戶A")
「庫柏 把那個 X 會議刪掉」→ delete_meeting

【⛔ 絕對禁止捏造的話題 — 你不知道的事不准順著話講】

你完全沒有以下資訊的訪問權限:
  • LINE Bot 是免費還是付費方案、月推播 quota 剩多少、訂閱狀態
  • Anthropic API 用量 / 月費 / 帳單 / 是否還在 trial
  • 系統「測試階段」「正式版」「內部討論的定價」「即將推出 X」
  • Robert 是否付過款、付了多少、用哪個方案
  • 任何 commercial / pricing / billing / quota 細節

❌ 絕對禁止的回答方式:
  • 「你們現在都還在免費額度內」(你不知道)
  • 「付費版還沒上,還在測試階段」(你不知道)
  • 「圖三那個定價是我們內部還在討論的方案」(瞎掰)
  • 順著使用者話講「對啊老闆用免費的」「我們還在試用」

✅ 正確回答方式:
  • 「這方面我沒掌握確切資訊 — Robert 比較清楚,你問他」
  • 「方案 / 帳單細節我看不到,Robert 可以從 LINE OA Manager 後台查」
  • 直接跳開這個話題:「不講這個 — 回到你剛剛的任務上...」

關鍵原則:**「我不掌握」≠「不存在」**。
別人起鬨「老闆用免費的啦」≠ 你要附和。你不知道就老實說不知道,別腦補。

這條違反 = 你給使用者錯誤事實,比你說「我不知道」嚴重得多。

【❓ 使用說明回答(help / 怎麼用 / 你會什麼)】
使用者問:「庫柏 help」「庫柏 怎麼用你」「庫柏 你會什麼」「庫柏 教我用」「你能幹嘛」「使用說明」
→ 不要 call 任何工具,直接回下面的速查(LINE 友善排版,中文引號框名詞,不要 markdown 星號):

「我是團隊 AI 同事,3 分鐘速查:

📋 任務
• 「庫柏 幫阿綠建《剪片》明天 high」
• 「庫柏 列阿綠這週任務」
• 「庫柏 把《剪片》延到下週五」
• 「庫柏 把《剪片》轉給盛豪」
• 「庫柏 X 完成了」我幫你標 done
• 「庫柏 我今天做完了 Y」我建任務 + 直接標 done

💡 覆盤(學到的事 / 反思)
• 直接打「復盤 ...」或「覆盤 ...」開頭 → 100% 進覆盤,不會誤建任務
• 「庫柏 記下這次學到 X」
• 「庫柏 那個覆盤其實是任務」我幫你升級

📋 會議
• 「庫柏 記下今天會議:標題 / 結論 / action items」
• 「庫柏 列最近會議」

👥 團隊
• 「庫柏 我們團隊有誰」
• 「庫柏 這週大家完成率」

📷 看圖
• 先在群組傳圖 → 再 @ 我問「這張圖在拍什麼」

🤖 自動推播
• 早 8 點:逾期任務 ping
• 晚 9 點:今天還沒完成的人提醒
• 晚 10 點:今日完成公告

完整版說明:打開行程規畫系統右上「❓ 使用」分頁。」

回完就停,別補話。

【📷 你能看圖】
群組裡有人傳圖片,系統會把圖片本身放進你的 messages context(image content block)。
你看得到圖,可以描述、分析、抓重點、對截圖內容做行動。不要說「我看不到」 — 真的有就有。
若 image block 沒附上(舊訊息或檔丟了)content 會明說「(傳了一張圖)」沒附 image,那才回「圖暫時看不到」。

【🚫 LINE 訊息格式硬規則 — 違反會讓使用者連結點不開】
LINE 不 render markdown — 你寫的 `**粗體**` `*斜體*` `_底線_` `[文字](url)` 在 LINE 都會
變成「字面上的星號 / 中括號」,還會被併進 URL。
特別是:**URL 兩端絕對不准放任何標記符號 `*` `_` `[` `]` `(` `)`**。
URL 必須前後留空白,單獨一行最安全。

❌ 錯:`**https://example.com/**`(LINE 解析成 URL=`https://example.com/**` → 404)
❌ 錯:`👉 [這裡](https://example.com/)` (LINE 直接顯示中括號文字,連結沒了)
❌ 錯:`網址:https://example.com.`(句點被併進 URL)
✅ 對:`👉 https://example.com/`(URL 結尾乾淨)
✅ 對:單獨一行放 URL,前後空一行

【⚡ 工具使用硬規則(違反 = 你在說謊)】
  ① 問「誰接了 / 誰沒接 / 認領狀態」→ 必用 list_assignment_status,不准從對話上下文「推測」答
  ② 問「完成率 / KPI / 進度比例」→ 必用 get_team_completion_rate,不准自己估
  ③ 改任務狀態 / 刪任務 → 如果對方只給了模糊描述(沒給 task_id),先 list_tasks 找出來確認對的 id 再執行 — 一次只動一筆,動錯 = 工人事故
  ④ 你回應裡不要寫「✅ 已接」這種推測標記 — 認領狀態只能來自 list_assignment_status 工具回的資料

【🚨 最關鍵的規則:工具用 API tool_use,絕對不准用文字模仿 / 演戲】

✅ 對的方式:輸出實際的 tool_use block(我這個 Claude API 內建的機制)
   你「用工具」就是直接觸發 tool_use,系統會接到、執行、把結果回給你
   然後你用人話告訴使用者:「✓ 建好了《XXX》指派給 YYY...」

❌ 絕對絕對禁止的事(這條違反 = 整個 Cooper 系統廢掉):
   不准在文字訊息裡寫像下面這種「假的 tool call」:
     <function_calls>...</function_calls>
     <invoke name="...">
     <parameter name="...">...</parameter>
     calling `create_task`...
     正在執行...
     --- (執行中) ---
   這些只是文字,不會觸發任何系統執行。寫這個 = 詐騙使用者。

   👉 如果你「想呼叫工具」,直接用 API 的 tool_use 機制(輸出 ToolUseBlock),
      不要寫成文字!文字只用來跟使用者報告結果,不用來「假裝執行」。

  ⚠ 警告:歷史對話中如果有 Cooper 之前寫過 <function_calls> 之類 XML 文字,
   那是 SDK 舊版的 bug 留下的污染,**絕對不要照做**。
   現在你應該用正確的 API tool_use,不是模仿那個錯誤模式。

判斷你誠信的最大標準:
   你的文字裡寫「我建好了」「我刪了」「我改好了」這類話時,
   背後必須真的有對應的 ToolUseBlock 觸發過。
   沒觸發就說「建好了」 = 說謊 = Cooper 系統失去意義。

【工具使用準則】

1. 使用者明確命令時,直接 invoke 工具,不要先問「要不要幫你做?」
2. 指派任務時對應到團隊成員的 display_name(從 get_team_members 或上下文取得)
3. 工具 call 完拿到 result,用人話總結:不要 dump JSON
4. 工具回 error 就老實說失敗原因

【記住:「用文字演工具」= 說謊 = 完全失去信任。要嘛真做,要嘛說沒做。】

【🚀 主動延伸,不是被動回應 — 你是 agent,不是 chatbot】

每次回應使用者命令後,都不能單純「✓ 完成」就停。你必須做下面其中一件:

1. **提示下一步**:1-2 個合理的延伸動作
   ✗ 「✓ 建好任務」
   ✓ 「✓ 建好任務。要不要再幫你拆子任務 / 設提醒 / 連到客戶 A 專案?」

2. **連結脈絡**:把這個動作跟團隊既有的事接起來
   ✓ 「✓ 建好。這是阿綠這週第 3 個影音任務,他平均 2 天交一支,
       這個 deadline 對他可能緊」

3. **點出風險或機會**:
   ✓ 「✓ 改完成了。順便提醒:阿綠這週還有 2 個 due 今天的任務沒動,
       你可能想跟他確認狀況」

4. **發現模式**:
   ✓ 「我注意到最近 5 個任務都跟『短影音』有關,要不要建一個 project
       把它們 group 起來,看 dashboard 比較順?」

【判斷標準:每次回應都該讓使用者覺得「這次值得這 $0.01 API 費用」】

回應前自問:
  - 我有沒有產生新價值,還是只是在 acknowledge?
  - 使用者下一步會想做什麼?我有沒有先想好?
  - 這個情境裡我看到什麼別人沒看到?

不值的話,改成更有價值的回應 — 或乾脆少說一句。

【但別煩】:
- 重複的提示不要每次都講(同一個 session 最多提一次)
- 老闆有壓力時別丟建議,先關心
- 真的沒額外洞察就不要硬擠 — 「✓ 建好了」+ 短一句連結比硬編強

【你「不能」做的事】
- 寄 email / 發 LINE 訊息給特定人
- 修改其他使用者的設定
- 控制外部系統
- 看其他人的私訊內容

碰到上述要求 → 老實說「我做不到這個」+ 建議替代方案。

簡單講:能做的就做,不能做的就直接講。誠實 + 行動力 兩者並重。
c           
        K   t        | j                  t        t              j	                  t        j
                  j                  d      t        j                  j                  d                   d{   j                         j                               }dj                  d |D              }t         d| dt        j                         j                          dS 7 kw)uA   組合系統 prompt:角色設定 + 團隊成員 + 今日日期。TN
c              3   V   K   | ]!  }d |j                    d|j                   d # yw)z- ()N)display_namerole)r   us     r   r    z'_build_system_prompt.<locals>.<genexpr>  s)     JEqR/q:Es   ')u   

【目前團隊成員】
u   

今日日期:)listr%   r   r   r&   	is_activer)   r(   r-   r.   joinCOBA_BASE_PROMPTr   today	isoformat)r0   users
user_liness      r   _build_system_promptrX     s     

t4>>--d3T__5H5H5NO wyE JEJJJ ! "  jjl$$&' ( 	s   A1C!3C4A,C!line_message_idc                 H   | sy	 ddl }ddlm}  |t        j                        |  dz  }|j                         sy|j                         j                  }|dkD  ry|j                         }|j                  |      j                  d      }ddd	|d
dS # t        $ r Y yw xY w)u   讀 LINE 下載的圖片變成 Anthropic vision content block。

    回 {"type": "image", "source": {"type": "base64", ...}} 或 None。
    Nr   )Pathz.jpgi D asciiimagebase64z
image/jpeg)type
media_typedata)r_   source)r^   pathlibr[   r   LINE_MEDIA_DIRexistsstatst_size
read_bytesstandard_b64encodedecode	Exception)rY   r^   r[   pathsizera   b64s          r   _load_line_image_b64ro     s    
  H++,/1B$/GG{{}yy{"") ''-44W=  *
 	
  s   6B B 8B 	B! B!r   r,   c                   K   t        t              j                  t        t        j                              j                  t        j                  |k(  t        j                  j                  d            j                  t        j                  j                               j                  |      }t        | j                  |       d{   j                         j!                               }|j#                          d }g }|D ]P  }|j$                  du }|r|j'                  d|j(                  d       3|j                  r|j                  j*                  nd}	d}
|j,                  dk(  rt/        |j0                        }
d|	 d|j(                   }|
r3d|	 d	|j(                  r|j(                  d
k7  rd|j(                   ndz   }d|d}g }|
r|j'                  |
       |j'                  |       |r4|d   d   dk(  r) ||d   d         }|j3                  |       ||d   d<   %|
r|j'                  d|d       =|j'                  d|d       S |r|d   d   dk(  r|j'                  ddd       |S 7 w)u{  拉近期 N 則訊息,組成 Claude messages 格式。

    Claude 的 messages 規則:
      - role 必須交錯 user / assistant(同 role 連兩則會出錯)
      - 我們把多人對話「壓成」單一 user,在內容前綴 [姓名]
      - 庫柏自己的訊息 → assistant
      - 階段 9-E:圖片訊息 → 讀本地檔 base64 + image content block(Sonnet vision)
    Nc                 2    t        | t              r| S d| dgS )u<   把 content 統一成 list of blocks,方便 append 圖片。textr_   rr   )
isinstancerP   )r9   s    r   _ensure_listz-_build_messages_context.<locals>._ensure_list'  s    gt$N122r   	assistantrN   r9   u   成員r]   [] u   ] (傳了一張圖)u   [圖片]  rr   rs   rN   userr9   u   (請庫柏接續回應))r   r
   optionsr   r}   r&   r   r(   r)   r*   r'   r+   r,   rP   r%   r-   r.   reverser   appendr9   rM   message_typero   rY   extend)r0   r   r,   stmtrecentru   claude_messagesr   is_cobasender_name	img_blockrr   
text_block
new_blocksexistings                  r   _build_messages_contextr     sB     	w	gll+	,	w!!Z/1C1C1G1G1M	N	'$$))+	,	u 	 D))22488:;F
NN3 #%O))t#""KAII#NO-.VVaff))	>>W$,Q->->?I;-r!))-{m#67aii\]\e\eis\sQqyyk?y{|D$d3

i(*% r26:fD#OB$7	$BCHOOJ'-5OB	* &&:'NO&&4'HI= B ?2.v6+E;TUV] *s   CI?	I<
F3I?c                   K   t        |d|d      }| j                  |       | j                          d{    | j                  |       d{    | j	                  t        t               j                  t        t         j                              j                  t         j                  |j                  k(               d{   j                         }|S 7 7 7 w)u/   把庫柏回應存成新訊息(user_id=NULL)。Nrr   )r   r   r9   r   )r
   addcommitrefreshr%   r   r~   r   	reactionsr&   id
scalar_one)r0   r   r9   msgfulls        r   _save_coba_messager   U  s     
	C FF3K
))+
**S/**w	g//0	1	wzzSVV#	$  z|	 	
 K s4   4C CC CA5C CC C C queryc                    K   ddl m}  ||d|        d{   }|syg }|D ]:  }|d   dk  r|j                  d	d      dd
 }|j                  d| d|d           < |syddj	                  |      z   dz   S 7 bw)uB   階段 3f:用 Qdrant 找跟 query 最相關的過去訊息(RAG)。r   )search_relevant   )r,   r   Nr{   scoreg?r'   
   z- [ry   r9   u<   
【參考:跟使用者問題語意相關的過去討論】
rI   u3   
(回答時若引用,請自然帶過,不要列點)
)app.services.coba_memoryr   getr   rR   )r   r   r   relevantlinesrwhens          r   _build_rag_contextr   h  s     8$U!
KKHEW:uu\2&s+s4&1Y<.12	 
 H
))E
	
A	B Ls   A<A:A#A<triggered_by_message_idc                 t  K   t         j                  r)t         j                  r	 ddlm}  || |       d{    yt         j                  st        j                  d       yddlm}m} t        j                   dt#        |       d	       d{    t               4 d{   }	 t%        |       d{   }t'        ||        d{   }|j)                  t+        t,              j/                  t,        j0                  |k(               d{   j3                         }	d
}
|	rt5        | |	j6                         d{   }
||
z   }t9        j:                  t         j                        }|	r|	j<                  r|	j<                  nd}t?        |      }d}d}g }d
}d}tA        |      D ]  }|jB                  jE                  t         jF                  d|||       d{   }||jH                  jJ                  z  }||jH                  jL                  z  }|j6                  D cg c]  }tO        |dd       }}t        jQ                  d||jR                  ||jH                  jJ                  |jH                  jL                         |jR                  dk(  r||jU                  d|j6                  D cg c]*  }tW        |d      r|jY                         n|jZ                  , c}d       g }|j6                  D ]  }|j\                  dk(  s|!|jU                  d|j0                  ddd       7t        jQ                  d|j^                  |j`                          ||j^                  |j`                  xs i |       d{   }|jU                  |j^                  |j`                  t?        |jc                               d       ddl2}|jU                  d|j0                  |jg                  |d      d|v d        |jU                  d|d       hd
ji                  d  |j6                  D              jk                         xs d!} n d"}|d#z  d$z  |d#z  d%z  z   }t        jQ                  d&| |||tm        |             	 dd'l7m8} t               4 d{   }|js                   |d(||r|dd) nd
dt#        |       t         jF                  |||d*|+             |ju                          d{    ddd      d{    t        || |       d{   } 	r"|	j<                  rt}        | |	j<                         dd2l?m@}!mA}"mB}#mC}$  |"| |       d{   }% |#|  |!| |%            }&t        j                    |$d3t#        |       |&d4             d{    ddd      d{    y7 r# t
        $ r| t        j                  d       t         j                  sSt               4 d{  7  }t        || d       d{  7   ddd      d{  7   Y y# 1 d{  7  sw Y   Y yxY wY w xY w7 7 7 7 s7 07 7 gc c}w c c}w 7 7 7 l7 _# 1 d{  7  sw Y   pxY w# t
        $ r t        j                  d,       Y w xY w# t8        jv                  $ r d-}Y t8        jx                  $ r}d.| d/}Y d}~d}~wt
        $ r9}t        j                  d0       d1t]        |      jz                   d/}Y d}~d}~ww xY w7 7 7 7 ~# 1 d{  7  sw Y   yxY ww)5u2  背景任務:讀近期對話 + RAG 過去討論 → 呼叫 Claude(支援 tool use)→ 在頻道發回應。

    階段 7:加入 Anthropic tool use,Cooper 可以 call create_task / list_tasks / 等等。
    階段 10:預設走 claude_agent_sdk(OAuth + Max 訂閱),沒 token 才退回 anthropic API。
    r   )coba_respond_to_mention_sdkNu   SDK 路徑失敗u)   (庫柏 SDK 出狀況了,請稍後再試)u>   ANTHROPIC_API_KEY 跟 OAuth token 都沒設,跳過庫柏回應)TOOLS_SCHEMAexecute_toolcoba_typing)r_   r   r{   )api_key   i   )model
max_tokenssystemtoolsmessagesr_   ?z-Cooper iter=%d stop=%s blocks=%s tokens=%d/%dtool_userv   
model_dumprw   tool_resultuE   {"error": "no_requester:無法判定誰下令,不執行寫入操作"}T)r_   tool_use_idr9   is_errorzCooper tool_call: %s %s)rD   inputresult_keysF)ensure_asciierrorr}   c              3   T   K   | ]   }|j                   d k(  s|j                   " yw)rr   Nrs   )r   blocks     r   r    z*coba_respond_to_mention.<locals>.<genexpr>  s$      (,<5

f@TEJJ,<s   ((u   (庫柏沒說話)uH   (工具呼叫超過上限,先停下來。請再說一次或拆小一點)i@B g      @g      .@u?   庫柏回應 channel=%s tokens=%d/%d cost=US$%.5f tool_calls=%d)AgentActivityline_responsex   )r   r   input_tokensoutput_tokens
tool_calls)activity_typer   summarysuccessextracost_usdu(   寫 AgentActivity (line_response) 失敗u-   (庫柏的 API key 無效,請聯絡管理員)u   (庫柏暫時無法回應:rL   u   Claude API 失敗u$   (庫柏現在抽風了,稍後再試:)_build_read_by_get_channel_read_states_serialize_message_ws_safemessage_new)r_   r   message)Dr   USE_CLAUDE_SDKCLAUDE_CODE_OAUTH_TOKENapp.services.coba_chat_sdkr   rk   logger	exceptionANTHROPIC_API_KEYr	   r   warningapp.services.coba_toolsr   r   
ws_manager	broadcaststrrX   r   r%   r   r
   r&   r   scalar_one_or_noner   r9   	anthropicAsyncAnthropicr   rP   ranger   createCLAUDE_MODEL_SONNETusager   r   getattrinfostop_reasonr   hasattrr   __dict__r_   rD   r   keysjsondumpsrR   r;   lenapp.models.agent_activityr   r   r   AuthenticationErrorBadRequestError__name__r   app.api.messagesr   r   r   r   )'r   r   r   _dbr   r   r0   system_promptr   triggered_msgrag_textfull_systemclientrequester_idmessages_for_apitotal_input_tokenstotal_output_tokenstool_calls_summaryresponse_textMAX_ITER	iterationresponseb_block_typesr   tool_resultsresult_jsonr   r   _adber   r   r   r   r   member_reads
serializeds'                                          r   coba_respond_to_mentionr  }  s     8#C#C	N-j:QRRR %%WXB 

*o    
 !""bx	W"6r"::M$;B
$KKO#%::w%%gjj4K&KL$ !!#  H!3J@U@U!VV'(2K--h6P6PQF 5BmF[F[=00aeL  $O4!""#!#MH"8_	!'!7!7"66#&&- "8 "  #hnn&A&AA"#x~~'C'CC# BJAQAQRAQA63 7AQRCx33\NN//1M1M '':5$++ + *2)9)9$)9 3:%2NE,,.TYTbTbb)9$-  $&L!)!1!1 ::3+3 , 3 3,93888/v04	5" !# !)"KK(A5::u{{[+7

EKKDUSUWc+d%dF.55uzzTYT_T_ptu{  vA  vA  vC  qD  7E  F0(//(5/4xx+0;;vE;+R,3v,=	1  "2* %++V,UV !# (,4,<,<( !%'!10  u -x !k*Y6#=ATW`A`dh@hhHKKQ.0CXsSeOf
MC,..$HH]&5 ,7Dds 3" $*-j/%-%A%A,>-@*< "*  ++-'' /.8 'r:}EE ]22J(=(=>	
 	
 6j"EE'^C-NO
""8!j/!-
 $  	 	[ #""3 S 		/0--,..#,Z)T   /.. 	 /..  .		$
 #:K
 W"  S$& &e8 / ( /...   M  !KLM ,, 	LKM(( 	>81=M 	W01B47CSCSBTTUVM	W
 F F	[ #"""sh  !\8V+ V(V+ A\8X3\8!X6"\8%\#'Z5X96Z	X<
AZX?/Z?Y BZYAZ#Y7A8Z//Y*Z	A3Z<Y
=DZ?Y4YY4AY,Y-Y1Y4<Y=Y4\#\A \#\?\#\\#\8"\ #\8(V+ +<X0'W*(X0,X=X >XX0XX0\8X)	XX)	$X0'\8)X0,\8/X00\86\89Z<Z?ZZZZY4YY4Y1	$Y'%Y1	,Y44ZZZZ\.\#1\[	\#\.\	\#\\#\#\# \8#\5)\,*\51\8
message_idr   c                     t        |      r t        j                  t        | |             y|yt        j                  t	        | ||             y)u   同步呼叫的入口,內部用 asyncio.create_task 起背景任務。

    階段 7:engagement window 改 DB-backed,async 才能查。
    N)rE   asynciocreate_taskr  _check_engagement_then_respond)r   r  r9   r   s       r   trigger_coba_if_mentionedr  4  sE     !3J
KL6z:wWXr   c                    K   t               4 d{   }t        | ||       d{   rt        | |       d{    ddd      d{    y7 >7 +7 7 # 1 d{  7  sw Y   yxY ww)u<   先查 engagement,過了再呼叫 coba_respond_to_mention。N)r	   r5   r  )r   r  r   r0   s       r   r  r  C  sV      ""b$Z"===)*jAAA #""=A #"""sc   A-AA-AAAAAA-
AA-AAA-A*A!A*&A-)r   N)NNN)r   r   )0__doc__r	  loggingr   r   uuidr   r   
sqlalchemyr   sqlalchemy.ormr   
app.configr   app.core.ws_managerr   r   app.databaser	   app.models.channelr
   app.models.userr   	getLoggerr   r$   r   boolr5   r=   r>   r?   rA   r   rE   rG   rS   rX   dictro   intrP   r   r   r   r  r  r  r   r   r   <module>r     s  "       '  5 * &   
		9	%  
!$ !< s t *s W[ &Q h
c #*  >>$ >s >DQUJ >BT C G & c c *td tT tVZ tnY$ YD Y3 YY]`dYd Ypt YBT Bt BVZ B_c Br   