
    jV                         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mZ ddl	m
Z
mZmZmZ ddlZ ej                  e      Z G d d      Z G d d      Zy)	uB   对话记录器 - 说话人识别 + 面部识别 + 持久化存储    N)Path)OptionalListDictAnyc                   J   e Zd ZdZd&defdZd Z	 d'dededed	ed
edefdZd Z	d Z
	 d&dedee   defdZ	 	 d(dedededed	ed
ededefdZd)dedededee   fdZd*dedededee   fdZd&dedededee   fdZd+dedefdZdefdZd  Zd! Zd&d"efd#Zdefd$Zd% Zy),DialogueLoggeru  
    对话记录器：记录谁在什么时候说了什么，关联视觉场景。
    
    功能：
    1. 说话人识别 - 通过 LiveKit participant identity
    2. 面部快照 - 关键帧时记录人脸特征（可选）
    3. JSONL 持久化 - 每次对话追加写入文件
    4. 时间对齐 - 语音和视觉事件关联
    5. 分层存储 - 热数据内存 + 冷数据 SQLite
    6. FTS5 全文搜索
    7. 消息压缩与归档
    Ndb_pathc                     |xs) t        t        j                         dz  dz  dz  dz        | _        d | _        d | _        d | _        g | _        d| _        d| _	        d| _
        d| _        g | _        i | _        y )	Nz.hermes	workspacezlivekit-agentszdialogue_history.dbi  i'  i@     )strr   homer
   _conn_current_speaker_speech_start_conversation_buffer	hot_limit
cold_limitcompact_thresholdarchive_days_hot_dialogues_hot_speakers)selfr
   s     >/Users/leo/.hermes/workspace/livekit-agents/dialogue_logger.py__init__zDialogueLogger.__init__   s~    w#diikI&=&KN^&^av&v"w
 $!$&! !%     c           	      \   t        j                  | j                        | _        | j                  j	                         }|j                  d       |j                  d       |j                  d       |j                  d       |j                  d       |j                  d       |j                  d       | j                  j                          | j                          | j                          t        j                  d| j                   d	| j                   d
| j                   d       y)u,   初始化 SQLite 数据库并加载热数据a  
            CREATE TABLE IF NOT EXISTS dialogues (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp REAL NOT NULL,
                speaker_id TEXT,
                participant_identity TEXT,
                text TEXT,
                sentiment TEXT,
                scene_hash TEXT,
                embedding_id INTEGER,
                metadata TEXT,
                token_count INTEGER DEFAULT 0,
                archived INTEGER DEFAULT 0,
                compacted INTEGER DEFAULT 0
            )
        z
            CREATE VIRTUAL TABLE IF NOT EXISTS dialogues_fts USING fts5(
                text, participant_identity
            )
        a<  
            CREATE TABLE IF NOT EXISTS visual_memories (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp REAL NOT NULL,
                embedding BLOB,
                frame_hash TEXT,
                scene_description TEXT,
                face_snapshots TEXT
            )
        a  
            CREATE TABLE IF NOT EXISTS speakers (
                identity TEXT PRIMARY KEY,
                first_seen REAL,
                last_seen REAL,
                total_turns INTEGER DEFAULT 0,
                face_embedding BLOB
            )
        a  
            CREATE TRIGGER IF NOT EXISTS dialogues_ai AFTER INSERT ON dialogues BEGIN
                INSERT INTO dialogues_fts(rowid, text, participant_identity)
                VALUES (new.id, new.text, new.participant_identity);
            END
        z
            CREATE TRIGGER IF NOT EXISTS dialogues_ad AFTER DELETE ON dialogues BEGIN
                INSERT INTO dialogues_fts(dialogues_fts, rowid)
                VALUES ('delete', old.id);
            END
        al  
            CREATE TRIGGER IF NOT EXISTS dialogues_au AFTER UPDATE ON dialogues BEGIN
                INSERT INTO dialogues_fts(dialogues_fts, rowid)
                VALUES ('delete', old.id);
                INSERT INTO dialogues_fts(rowid, text, participant_identity)
                VALUES (new.id, new.text, new.participant_identity);
            END
        u   📝 对话数据库: u    (热数据: u   , 冷数据上限: )N)sqlite3connectr
   r   cursorexecutecommit_load_hot_data_init_fts_indexloggerinfor   r   )r   cs     r   
initializezDialogueLogger.initialize-   s   __T\\2
JJ 	
		  	$ 	
		  	 	
		 	 		 	
		  	 	
		  	 	
		  	 	
		  	 	

 	 	,T\\N-GWWjkokzkzj{{|}~r   
speaker_idparticipant_identitytext
scene_hashembedding_idreturnc                 :   | j                   s| j                          t        j                         }t        |      dz  }| j                   j	                  d|||||||t        j                  |rd|ind      f      }|j                  }	| j                   j	                  d|||f       | j                   j                          t        j                  d| d|dd  d	|	 d
       | j                          | j                  |||||||	|       | j                          |	S )u*   记录一次对话发言，返回记录 ID   z
            INSERT INTO dialogues (timestamp, speaker_id, participant_identity, text, 
                                  scene_hash, embedding_id, token_count, metadata)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        sceneNa  
            INSERT INTO speakers (identity, first_seen, last_seen, total_turns)
            VALUES (?, ?, ?, 1)
            ON CONFLICT(identity) DO UPDATE SET
                last_seen = excluded.last_seen,
                total_turns = speakers.total_turns + 1
        u   📝 记录对话: z -    z	... (ID: r   )r   r*   timelenr#   jsondumps	lastrowidr$   r'   r(   _sync_fts_index_add_to_hot_data_check_compaction)
r   r+   r,   r-   r.   r/   	timestamptoken_countr"   	record_ids
             r   record_speechzDialogueLogger.record_speech   s-    zzOOIIK	$i1n ## % %94,jj*':.$GI	J $$	 	

  #Iy9	; 	

)*>)?s49+YW`Vaabcd 	 	i5I4'y+	O 	 r   c                 p   | j                   sy	 | j                   j                  d       | j                   j                          | j                   j                         }|j                  d       |j	                         D ]*  }| j                   j                  d|d   |d   |d   f       , | j                   j                          t
        j                  d| j                   j                  d	      j                         d    d
       y# t        $ r"}t
        j                  d|        Y d}~yd}~ww xY w)u   初始化 FTS5 索引NDELETE FROM dialogues_ftsHSELECT id, text, participant_identity FROM dialogues WHERE compacted = 0MINSERT INTO dialogues_fts(rowid, text, participant_identity) VALUES (?, ?, ?)r      r2   u$   🔍 FTS5 全文索引已初始化 (z"SELECT COUNT(*) FROM dialogues_fts    条)u#   ⚠️ FTS5 索引初始化失败: )
r   r#   r$   r"   fetchallr'   r(   fetchone	Exceptionwarning)r   r)   rowes       r   r&   zDialogueLogger._init_fts_index   s"   zz	FJJ:;JJ

!!#AII`azz|

""cVSVSV, $
 JJKK>tzz?Q?QRv?w  @A  @A  @C  DE  @F  ?G  GL  M  N 	FNN@DEE	Fs   C:D
 
	D5D00D5c                 *   | j                   sy	 | j                   j                  d       | j                   j                          | j                   j                         }|j                  d       d}|j	                         D ]/  }| j                   j                  d|d   |d   |d   f       |dz  }1 | j                   j                          t
        j                  d| d	       y# t        $ r"}t
        j                  d
|        Y d}~yd}~ww xY w)u   同步 FTS5 索引NrB   rC   r   rD   rE   r2   u   🔍 FTS5 索引已同步 (rF   u    ⚠️ FTS5 索引同步失败: )	r   r#   r$   r"   rG   r'   debugrI   rJ   )r   r)   countrK   rL   s        r   r:   zDialogueLogger._sync_fts_index   s    zz	CJJ:;JJ

!!#AII`aEzz|

""cVSVSV, 
 $ JJLL6ugUCD 	CNN=aSABB	Cs   CC' '	D0DD
frame_hashface_embeddingscene_descriptionc           
      N   | j                   s| j                          t        j                         }t        j                  |      j                  d      }| j                   j                  d|||t        j                  d|ig      |f       | j                   j                          y)u   记录面部快照zutf-8z
            INSERT INTO visual_memories (timestamp, frame_hash, embedding, face_snapshots, scene_description)
            VALUES (?, ?, ?, ?, ?)
        r5   N)r   r*   r5   r7   r8   encoder#   r$   )r   rP   rQ   rR   r=   embedding_blobs         r   record_face_snapshotz#DialogueLogger.record_face_snapshot   s     zzOOIIK	 N3::7C

  ^TZZ&)AT@U5VXij	l
 	

r   r=   r?   r>   c	                 p   ||||||||ddd
}	| j                   j                  |	       t        | j                         | j                  kD  r| j                   | j                   d | _         || j                  vrd||d| j                  |<   | j                  |   dxx   dz  cc<   || j                  |   d<   y)u!   将对话添加到热数据内存r   
idr=   r+   participantr-   r.   r/   r>   archived	compactedNtotal_turns
first_seen	last_seenr^   rE   r`   )r   appendr6   r   r   )
r   r=   r+   r,   r-   r.   r/   r?   r>   records
             r   r;   zDialogueLogger._add_to_hot_data   s     "$/$(&
 	""6* t""#dnn4"&"5"5t~~o6F"GD  t'9'99 '&8D34
 	/0?1D?@I/0=r   querytop_kc                    g }|r<| j                   D cg c]   }|d   |k(  r|j                  d      dk7  r|" }}|| d }nX|j                         }| j                   D cg c]-  }||d   j                         v r|j                  d      dk7  r|/ }}|| d }t        |      |k  r<| j                  r0| j                  ||t        |      z
  |      }|j                  |       |S c c}w c c}w )uM   搜索历史对话（优先热数据，fallback 到冷数据，支持 FTS5）rZ   r\   rE   Nr-   )r   getlowerr6   r   _search_coldextend)	r   rc   rd   r+   resultsdfilteredquery_lowercold_resultss	            r   search_dialogueszDialogueLogger.search_dialogues  s    #'#6#6 V#6aM*j8QUU;=OST=T #6H Vvw'G  ++-K#'#6#6 X#6a%6)::quu[?QUV?V #6H Xvw'G w<%DJJ,,UECL4H*ULNN<(VXs   %C%2C*c           	         | j                   sg S | j                   j                         }	 |r|j                  d|||f       n|j                  d||f       g }|j                         D ](  }|j	                  |d   |d   |d   |d   |d   d       * |r|S 	 | j                  |||      S # t
        $ r"}t        j                  d	|        Y d
}~9d
}~ww xY w)u   使用 FTS5 全文搜索ay  
                    SELECT d.timestamp, d.speaker_id, d.participant_identity, d.text, d.scene_hash
                    FROM dialogues d
                    JOIN dialogues_fts fts ON d.id = fts.rowid
                    WHERE dialogues_fts MATCH ?
                    AND d.participant_identity = ?
                    ORDER BY rank
                    LIMIT ?
                aF  
                    SELECT d.timestamp, d.speaker_id, d.participant_identity, d.text, d.scene_hash
                    FROM dialogues d
                    JOIN dialogues_fts fts ON d.id = fts.rowid
                    WHERE dialogues_fts MATCH ?
                    ORDER BY rank
                    LIMIT ?
                r   rE   r2         r=   r+   rZ   r-   r.   u2   ⚠️ FTS5 搜索失败，回退到普通搜索: N)	r   r"   r#   rG   ra   rI   r'   rJ   ro   )r   rc   rd   r+   r)   rj   rK   rL   s           r   search_with_ftszDialogueLogger.search_with_fts&  s    zzIJJ#	U		  Z/1 		  U^% Gzz|!$Q"%a&#&q6F"%a&   $   $$UE:>>	  	UNNOPQsSTT	Us   A,B+ +	C4CClimitc           	      $   | j                   sg S | j                   j                         }|r|j                  d||f       n|j                  d|f       g }|j                         D ](  }|j	                  |d   |d   |d   |d   |d   d       * |S )	u   从 SQLite 冷数据搜索a  
                SELECT timestamp, speaker_id, participant_identity, text, scene_hash
                FROM dialogues
                WHERE participant_identity = ? AND compacted = 0
                ORDER BY timestamp DESC
                LIMIT ?
            z
                SELECT timestamp, speaker_id, participant_identity, text, scene_hash
                FROM dialogues
                WHERE compacted = 0
                ORDER BY timestamp DESC
                LIMIT ?
            r   rE   r2   rq   rr   rs   )r   r"   r#   rG   ra   )r   rc   ru   r+   r)   rj   rK   s          r   rh   zDialogueLogger._search_coldU  s    zzIJJII  e$& II   ::<CNN V!!f"1vA!!f    r   last_minutesc           
         t        j                          |dz  z
  }| j                  D cg c]   }|d   |kD  r|j                  d      dk7  r|" }}t        |      dk  rl| j                  r`| j                  j                         }|j                  d|f       |j                         D ]   }|j                  |d   |d   |d   d	       " |sd
| dS |j                  d        g }|dd D ]K  }ddl
m
}	 |	j                  |d         j                  d      }
|j                  d|
 d|d    d|d           M dj                  |      S c c}w )u9   获取最近 N 分钟的对话摘要（优先热数据）<   r=   r\   rE   
   z
                SELECT participant_identity, text, timestamp
                FROM dialogues
                WHERE timestamp > ? AND compacted = 0
                ORDER BY timestamp DESC
            r2   r   )r=   rZ   r-   u   过去u   分钟没有对话记录c                     | d   S Nr=    xs    r   <lambda>z3DialogueLogger.get_recent_summary.<locals>.<lambda>  s    !K.r   keyiN)datetimez%H:%M:%S[z] rZ   z: r-   
)r5   r   rf   r6   r   r"   r#   rG   ra   sortr   fromtimestampstrftimejoin)r   rw   cutoffrk   recentr)   rK   linesrb   r   time_strs              r   get_recent_summaryz!DialogueLogger.get_recent_summaryx  sp   r 12 "00 J0{^f,{1Cq1H 0 J v;



!!#AII 
  zz|!$Q#&q6F  $ L>)ABB 	01STlF)--f[.ABKKJWHLL1XJb)>(?r&.AQRS #
 yyAJs   %Ec                     | j                   r| j                   S | j                  si S | j                  j                         }|j                  d       i }|j	                         D ]  }|d   |d   |d   d||d   <    |S )uB   获取说话人统计（优先热数据，fallback 到冷数据）z[SELECT identity, total_turns, first_seen, last_seen FROM speakers ORDER BY total_turns DESCrE   r2   rq   r]   r   )r   r   r"   r#   rG   )r   r)   statsrK   s       r   get_speaker_statsz DialogueLogger.get_speaker_stats  s     %%% zzIJJ			op::<C"1v!!f VE#a&M   r   c                 T   | j                   sy| j                   j                         }|j                  d| j                  f       g | _        |j                         D ]R  }| j                  j                  |d   |d   |d   |d   |d   |d   |d	   |d
   xs d|d   xs d|d   xs dd
       T |j                  d       i | _        |j                         D ]!  }|d   |d   |d   d| j                  |d   <   # t        j                  dt        | j                         d       y)u#   从 SQLite 加载热数据到内存Nz
            SELECT id, timestamp, speaker_id, participant_identity, text, 
                   scene_hash, embedding_id, token_count, archived, compacted
            FROM dialogues
            ORDER BY timestamp DESC
            LIMIT ?
        r   rE   r2   rq   rr         r      	   rX   zASELECT identity, total_turns, first_seen, last_seen FROM speakersr]   u   📊 已加载 u    条热数据到内存)r   r"   r#   r   r   rG   ra   r   r'   r(   r6   )r   r)   rK   s      r   r%   zDialogueLogger._load_hot_data  s6   zzJJ			  nn	  !::<C&&!f V!!f"1vA!!f #A"1v{FKa V[q(    	
		UV::<C"1v!!f V*Ds1v&   	oc$*=*=&>%??UVWr   c                    t        d | j                  D              }|| j                  kD  rt        | j                        dkD  r| j                  j	                  d        t        | j                        dz  }| j                  d| D ]4  }|j                  d      s| j                  j                  d|d   f       6 | j                  |d | _        | j                  j                          t        j                  d	| d
       yyy)u!   检查是否需要压缩旧对话c              3   @   K   | ]  }|j                  d d        yw)r>   r   N)rf   ).0rk   s     r   	<genexpr>z3DialogueLogger._check_compaction.<locals>.<genexpr>  s     P<Oq1552<Os   rz   c                     | d   S r|   r}   r~   s    r   r   z2DialogueLogger._check_compaction.<locals>.<lambda>  s    1[>r   r   r2   NrY   z/UPDATE dialogues SET compacted = 1 WHERE id = ?u   💾 已压缩     条旧对话)sumr   r   r6   r   rf   r   r#   r$   r'   r(   )r   total_tokenshalfrb   s       r   r<   z DialogueLogger._check_compaction  s     PD<O<OPP$000S9L9L5MPR5R$$)A$Bt**+q0D--et4::d#JJ&&I 5 #'"5"5de"<DJJKK/$}=> 6S0r   daysc                 R   || j                   }| j                  syt        j                         |dz  z
  }| j                  j                         }|j	                  d|f       |j
                  }| j                  j                          |dkD  rt        j                  d| d       |S )u   归档 N 天前的对话r   iQ zw
            UPDATE dialogues 
            SET archived = 1 
            WHERE timestamp < ? AND compacted = 0
        u   📦 已归档 r   )	r   r   r5   r"   r#   rowcountr$   r'   r(   )r   r   r   r)   archived_counts        r   archive_old_dialoguesz$DialogueLogger.archive_old_dialogues  s    <$$Dzzu-JJ			  Y		 

AKK/.)9GHr   c                    | j                   si S | j                   j                         }|j                  d       |j                         d   }|j                  d       |j                         d   }|j                  d       |j                         d   }|j                  d       |j                         d   }|j                  d       |j                         d   }|||||t	        | j
                        | j                         dS )u   获取详细统计信息zSELECT COUNT(*) FROM dialoguesr   z1SELECT COUNT(*) FROM dialogues WHERE archived = 0z2SELECT COUNT(*) FROM dialogues WHERE compacted = 1z1SELECT COUNT(*) FROM dialogues WHERE archived = 1z3SELECT COALESCE(SUM(token_count), 0) FROM dialogues)totalactiver\   r[   r   hot_data_countspeakers)r   r"   r#   rH   r6   r   r   )r   r)   r   r   r\   r[   r   s          r   	get_statszDialogueLogger.get_stats  s    zzIJJ 	
		23

Q			EFa			FGJJLO				EF::<? 	
		GHzz|A " (!$"5"56..0
 	
r   c                     | j                   r!| j                   j                          d| _         t        j                  dt	        | j
                         d       y)u   关闭数据库连接Nu'   📊 关闭对话记录器 (热数据: rF   )r   closer'   r(   r6   r   )r   s    r   r   zDialogueLogger.close/  sC    ::JJDJ=c$BUBU>V=WW\]^r   N)NN)NNNr   )r   N)rz   N)rz   )__name__
__module____qualname____doc__r   r   r*   intr@   r&   r:   r   floatrV   r;   r   ro   rt   rh   r   r   r%   r<   r   r   r   r}   r   r   r	   r	      s      Up CG* *3 *c *"%*<?*KN*XF*C0 7;s DK 03( FJCDJ% JS J/2J:=J%(J?BJ %(J >AJBc # s VZ[_V` .-?S -? -?s -?VZ[_V` -?^!# !c !s !dSWj !F% s % C % N4 ,'XR?,# 2 
4  
D_r   r	   c                   p    e Zd ZdZddefdZdedee   fdZdee   de	e   fdZ
d	ee   d
ee   defdZy)FaceMatcheruA   
    简单的人脸匹配器 - 基于 WeMM embedding 匹配
    	thresholdc                      || _         i | _        y r   )r   known_faces)r   r   s     r   r   zFaceMatcher.__init__<  s    "35r   identity	embeddingc                 R    || j                   |<   t        j                  d|        y)u   注册人脸u   👤 注册人脸: N)r   r'   r(   )r   r   r   s      r   register_facezFaceMatcher.register_face@  s&    %.")(45r   r0   c                     | j                   syd}d}| j                   j                         D ]!  \  }}| j                  ||      }||kD  s|}|}# || j                  k\  r|S y)u   识别人脸Nr   )r   items_cosine_similarityr   )r   r   
best_match
best_scorer   	known_embscores          r   identify_facezFaceMatcher.identify_faceE  sq    

#'#3#3#9#9#;Hi++IyAEz!"
%
	 $< 'r   abc                     ddl }|j                  |      }|j                  |      }t        |j                  ||      |j                  j                  |      |j                  j                  |      z  z        S )u   计算余弦相似度r   N)numpyarrayr   dotlinalgnorm)r   r   r   npvavbs         r   r   zFaceMatcher._cosine_similarityW  sX    XXa[XXa[RVVB^ryy~~b'9BIINN2<N'NOPPr   N)gffffff?)r   r   r   r   r   r   r   r   r   r   r   r   r}   r   r   r   r   7  sk    6% 66c 6d5k 6
tE{ x} $QDK QDK QE Qr   r   )r   asyncior7   r5   hashlibr    ospathlibr   typingr   r   r   r   logging	getLoggerr   r'   r	   r   r}   r   r   <module>r      sS    H      	  , , 			8	$d_ d_N%Q %Qr   