11"""Uploader for Slack workspace channels."""
22
3+ import anthropic
34import logging
4- from typing import List , Optional
5+ from concurrent .futures import ThreadPoolExecutor
6+ from typing import List , Optional , Dict
57
68from slack_sdk import WebClient
79from slack_sdk .errors import SlackApiError
@@ -21,6 +23,10 @@ def __init__(self):
2123 self ._thread_ts : Optional [str ] = None
2224 self ._session_started : bool = False
2325 self ._indent_level : int = 0
26+ self ._message_timestamps : List [str ] = []
27+ self ._session_messages : List [Dict [str , str ]] = []
28+ self ._executor = ThreadPoolExecutor (max_workers = 5 )
29+ self ._anthropic_client = None
2430
2531 @property
2632 def client (self ) -> WebClient :
@@ -38,6 +44,18 @@ def client(self) -> WebClient:
3844 self ._client = WebClient (token = token )
3945 return self ._client
4046
47+ def _get_anthropic_client (self ):
48+ """Get or create the Anthropic client."""
49+ if self ._anthropic_client :
50+ return self ._anthropic_client
51+
52+ api_key = settings .get ('ANTHROPIC_API_KEY' )
53+ if api_key and api_key != '<ANTHROPIC_API_KEY>' :
54+ self ._anthropic_client = anthropic .Anthropic (api_key = api_key )
55+ return self ._anthropic_client
56+
57+ return None
58+
4159 def _get_channel_id (self ) -> str :
4260 """Get the channel ID for the configured channel name."""
4361 if self ._channel_id :
@@ -73,13 +91,19 @@ def _start_session(self, first_note: str) -> bool:
7391
7492 # Create the initial message with the note content
7593 try :
76- message_text = f"{ first_note } \n \n :keyboard: Go Note Go thread. "
94+ message_text = f":wip: { first_note } "
7795 response = self .client .chat_postMessage (
7896 channel = channel_id ,
7997 text = message_text
8098 )
8199 self ._thread_ts = response ['ts' ]
82100 self ._session_started = True
101+ self ._message_timestamps = [response ['ts' ]]
102+ self ._session_messages = [{'ts' : response ['ts' ], 'text' : first_note }]
103+
104+ # Schedule cleanup for first message
105+ self ._executor .submit (self ._cleanup_message_async , first_note , response ['ts' ])
106+
83107 return True
84108 except SlackApiError as e :
85109 logger .error (f"Error starting session: { e } " )
@@ -102,16 +126,108 @@ def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool:
102126 formatted_text = f"{ indentation } { bullet } { text } "
103127
104128 try :
105- self .client .chat_postMessage (
129+ response = self .client .chat_postMessage (
106130 channel = channel_id ,
107131 text = formatted_text ,
108132 thread_ts = self ._thread_ts
109133 )
134+
135+ # Track the message
136+ self ._message_timestamps .append (response ['ts' ])
137+ self ._session_messages .append ({'ts' : response ['ts' ], 'text' : text })
138+
139+ # Schedule cleanup for this message
140+ self ._executor .submit (self ._cleanup_message_async , text , response ['ts' ])
141+
110142 return True
111143 except SlackApiError as e :
112144 logger .error (f"Error sending note to thread: { e } " )
113145 return False
114146
147+ def _cleanup_message_async (self , original_text : str , message_ts : str ):
148+ """Clean up a message using Claude in the background."""
149+ try :
150+ client = self ._get_anthropic_client ()
151+ if not client :
152+ logger .debug ("Anthropic client not available, skipping cleanup" )
153+ return
154+
155+ # Call Claude to clean up the message
156+ prompt = f"""Clean up this voice-transcribed note to be clear and concise. Fix any transcription errors, grammar, and formatting. Keep the core meaning intact but make it more readable. Return only the cleaned text without any explanation or metadata.
157+
158+ Original text: { original_text } """
159+
160+ message = client .messages .create (
161+ model = "claude-4-opus" ,
162+ max_tokens = 5000 ,
163+ temperature = 0.7 ,
164+ messages = [
165+ {"role" : "user" , "content" : prompt }
166+ ]
167+ )
168+
169+ cleaned_text = message .content [0 ].text .strip ()
170+
171+ # Update the message in Slack
172+ channel_id = self ._get_channel_id ()
173+ self ._update_message (channel_id , message_ts , cleaned_text )
174+
175+ except Exception as e :
176+ logger .error (f"Error cleaning up message: { e } " )
177+
178+ def _summarize_session_async (self ):
179+ """Summarize the entire session using Claude Opus 4."""
180+ try :
181+ client = self ._get_anthropic_client ()
182+ if not client :
183+ logger .debug ("Anthropic client not available, skipping summarization" )
184+ return
185+
186+ if not self ._session_messages or not self ._thread_ts :
187+ logger .debug ("No messages to summarize" )
188+ return
189+
190+ # Compile all messages into a thread
191+ thread_text = "\n " .join ([msg ['text' ] for msg in self ._session_messages ])
192+
193+ prompt = f"""Please provide a concise summary of this note-taking session. Identify the main topics, key points, and any action items. Format the summary clearly with bullet points where appropriate.
194+
195+ Session notes:
196+ { thread_text } """
197+
198+ message = client .messages .create (
199+ model = "claude-3-5-opus-20241022" ,
200+ max_tokens = 1000 ,
201+ temperature = 0.3 ,
202+ messages = [
203+ {"role" : "user" , "content" : prompt }
204+ ]
205+ )
206+
207+ summary = message .content [0 ].text .strip ()
208+
209+ # Update the top-level message with the summary
210+ channel_id = self ._get_channel_id ()
211+ original_text = self ._session_messages [0 ]['text' ] if self ._session_messages else ""
212+ updated_text = f":memo: **Session Summary**\n \n { summary } \n \n ---\n _Original first note: { original_text } _"
213+
214+ self ._update_message (channel_id , self ._thread_ts , updated_text )
215+
216+ except Exception as e :
217+ logger .error (f"Error summarizing session: { e } " )
218+
219+ def _update_message (self , channel_id : str , ts : str , new_text : str ):
220+ """Update a Slack message with new text."""
221+ try :
222+ self .client .chat_update (
223+ channel = channel_id ,
224+ ts = ts ,
225+ text = new_text
226+ )
227+ logger .debug (f"Updated message { ts } " )
228+ except SlackApiError as e :
229+ logger .error (f"Error updating message: { e } " )
230+
115231 def upload (self , note_events : List [events .NoteEvent ]) -> bool :
116232 """Upload note events to Slack.
117233
@@ -160,16 +276,30 @@ def upload(self, note_events: List[events.NoteEvent]) -> bool:
160276
161277 def end_session (self ) -> None :
162278 """End the current session."""
279+ # Schedule session summarization before clearing
280+ if self ._session_started and self ._session_messages :
281+ self ._executor .submit (self ._summarize_session_async )
282+
283+ # Clear session state
163284 self ._thread_ts = None
164285 self ._session_started = False
165286 self ._indent_level = 0
287+ self ._message_timestamps = []
288+ self ._session_messages = []
166289
167290 def handle_inactivity (self ) -> None :
168291 """Handle inactivity by ending the session and clearing client."""
169- self ._client = None
170292 self .end_session ()
293+ self ._client = None
294+ self ._anthropic_client = None
171295
172296 def handle_disconnect (self ) -> None :
173297 """Handle disconnection by ending the session and clearing client."""
174- self ._client = None
175298 self .end_session ()
299+ self ._client = None
300+ self ._anthropic_client = None
301+
302+ def __del__ (self ):
303+ """Cleanup executor on deletion."""
304+ if hasattr (self , '_executor' ):
305+ self ._executor .shutdown (wait = False )
0 commit comments