From deeabae789736388522a0dd76e478686db99ba74 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 8 Aug 2025 22:27:57 -0400 Subject: [PATCH 1/8] Toward clean up with Claude --- gonotego/settings/secure_settings_template.py | 1 + gonotego/settings/server.py | 1 + gonotego/uploader/slack/slack_uploader.py | 140 +++++++++++++++++- pyproject.toml | 1 + 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/gonotego/settings/secure_settings_template.py b/gonotego/settings/secure_settings_template.py index 986ea91..bc2bb7f 100644 --- a/gonotego/settings/secure_settings_template.py +++ b/gonotego/settings/secure_settings_template.py @@ -35,6 +35,7 @@ DROPBOX_ACCESS_TOKEN = '' OPENAI_API_KEY = '' +ANTHROPIC_API_KEY = '' WIFI_NETWORKS = [] CUSTOM_COMMAND_PATHS = [] diff --git a/gonotego/settings/server.py b/gonotego/settings/server.py index 3c1affb..6369396 100644 --- a/gonotego/settings/server.py +++ b/gonotego/settings/server.py @@ -34,6 +34,7 @@ 'EMAIL_PASSWORD', 'DROPBOX_ACCESS_TOKEN', 'OPENAI_API_KEY', + 'ANTHROPIC_API_KEY', ] class SettingsCombinedHandler(BaseHTTPRequestHandler): diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index 9d7e14f..7cd804f 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -1,7 +1,9 @@ """Uploader for Slack workspace channels.""" +import anthropic import logging -from typing import List, Optional +from concurrent.futures import ThreadPoolExecutor +from typing import List, Optional, Dict from slack_sdk import WebClient from slack_sdk.errors import SlackApiError @@ -21,6 +23,10 @@ def __init__(self): self._thread_ts: Optional[str] = None self._session_started: bool = False self._indent_level: int = 0 + self._message_timestamps: List[str] = [] + self._session_messages: List[Dict[str, str]] = [] + self._executor = ThreadPoolExecutor(max_workers=5) + self._anthropic_client = None @property def client(self) -> WebClient: @@ -38,6 +44,18 @@ def client(self) -> WebClient: self._client = WebClient(token=token) return self._client + def _get_anthropic_client(self): + """Get or create the Anthropic client.""" + if self._anthropic_client: + return self._anthropic_client + + api_key = settings.get('ANTHROPIC_API_KEY') + if api_key and api_key != '': + self._anthropic_client = anthropic.Anthropic(api_key=api_key) + return self._anthropic_client + + return None + def _get_channel_id(self) -> str: """Get the channel ID for the configured channel name.""" if self._channel_id: @@ -73,13 +91,19 @@ def _start_session(self, first_note: str) -> bool: # Create the initial message with the note content try: - message_text = f"{first_note}\n\n:keyboard: Go Note Go thread." + message_text = f":wip: {first_note}" response = self.client.chat_postMessage( channel=channel_id, text=message_text ) self._thread_ts = response['ts'] self._session_started = True + self._message_timestamps = [response['ts']] + self._session_messages = [{'ts': response['ts'], 'text': first_note}] + + # Schedule cleanup for first message + self._executor.submit(self._cleanup_message_async, first_note, response['ts']) + return True except SlackApiError as e: logger.error(f"Error starting session: {e}") @@ -102,16 +126,108 @@ def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool: formatted_text = f"{indentation}{bullet} {text}" try: - self.client.chat_postMessage( + response = self.client.chat_postMessage( channel=channel_id, text=formatted_text, thread_ts=self._thread_ts ) + + # Track the message + self._message_timestamps.append(response['ts']) + self._session_messages.append({'ts': response['ts'], 'text': text}) + + # Schedule cleanup for this message + self._executor.submit(self._cleanup_message_async, text, response['ts']) + return True except SlackApiError as e: logger.error(f"Error sending note to thread: {e}") return False + def _cleanup_message_async(self, original_text: str, message_ts: str): + """Clean up a message using Claude in the background.""" + try: + client = self._get_anthropic_client() + if not client: + logger.debug("Anthropic client not available, skipping cleanup") + return + + # Call Claude to clean up the message + 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. + +Original text: {original_text}""" + + message = client.messages.create( + model="claude-4-opus", + max_tokens=5000, + temperature=0.7, + messages=[ + {"role": "user", "content": prompt} + ] + ) + + cleaned_text = message.content[0].text.strip() + + # Update the message in Slack + channel_id = self._get_channel_id() + self._update_message(channel_id, message_ts, cleaned_text) + + except Exception as e: + logger.error(f"Error cleaning up message: {e}") + + def _summarize_session_async(self): + """Summarize the entire session using Claude Opus 4.""" + try: + client = self._get_anthropic_client() + if not client: + logger.debug("Anthropic client not available, skipping summarization") + return + + if not self._session_messages or not self._thread_ts: + logger.debug("No messages to summarize") + return + + # Compile all messages into a thread + thread_text = "\n".join([msg['text'] for msg in self._session_messages]) + + 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. + +Session notes: +{thread_text}""" + + message = client.messages.create( + model="claude-3-5-opus-20241022", + max_tokens=1000, + temperature=0.3, + messages=[ + {"role": "user", "content": prompt} + ] + ) + + summary = message.content[0].text.strip() + + # Update the top-level message with the summary + channel_id = self._get_channel_id() + original_text = self._session_messages[0]['text'] if self._session_messages else "" + updated_text = f":memo: **Session Summary**\n\n{summary}\n\n---\n_Original first note: {original_text}_" + + self._update_message(channel_id, self._thread_ts, updated_text) + + except Exception as e: + logger.error(f"Error summarizing session: {e}") + + def _update_message(self, channel_id: str, ts: str, new_text: str): + """Update a Slack message with new text.""" + try: + self.client.chat_update( + channel=channel_id, + ts=ts, + text=new_text + ) + logger.debug(f"Updated message {ts}") + except SlackApiError as e: + logger.error(f"Error updating message: {e}") + def upload(self, note_events: List[events.NoteEvent]) -> bool: """Upload note events to Slack. @@ -160,16 +276,30 @@ def upload(self, note_events: List[events.NoteEvent]) -> bool: def end_session(self) -> None: """End the current session.""" + # Schedule session summarization before clearing + if self._session_started and self._session_messages: + self._executor.submit(self._summarize_session_async) + + # Clear session state self._thread_ts = None self._session_started = False self._indent_level = 0 + self._message_timestamps = [] + self._session_messages = [] def handle_inactivity(self) -> None: """Handle inactivity by ending the session and clearing client.""" - self._client = None self.end_session() + self._client = None + self._anthropic_client = None def handle_disconnect(self) -> None: """Handle disconnection by ending the session and clearing client.""" - self._client = None self.end_session() + self._client = None + self._anthropic_client = None + + def __del__(self): + """Cleanup executor on deletion.""" + if hasattr(self, '_executor'): + self._executor.shutdown(wait=False) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 48f32fb..2658120 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ dependencies = [ 'absl-py<=2.1.0', + 'anthropic<=0.40.0', 'apscheduler<=3.10.4', 'dropbox<=12.0.2', 'fire<=0.7.0', From 095e50e920142132538b2525c01c1fbf109a092e Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 8 Aug 2025 22:35:06 -0400 Subject: [PATCH 2/8] Debug logging and prompt cleanup --- gonotego/uploader/slack/slack_uploader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index 7cd804f..0459313 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -133,6 +133,7 @@ def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool: ) # Track the message + logger.debug(f"Sent note to thread: {response}") self._message_timestamps.append(response['ts']) self._session_messages.append({'ts': response['ts'], 'text': text}) @@ -153,7 +154,7 @@ def _cleanup_message_async(self, original_text: str, message_ts: str): return # Call Claude to clean up the message - 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. + prompt = f"""Clean up this message. Fix any obvious typographic issues, but keep any stylistic choices that convey emotion, tone, or emphasis. Only clean up typos that were unintentional mistakes. Output the full cleaned text without any explanation or metadata. Original text: {original_text}""" From 1fba952d1b5208ba574ef574eae78e3f659a3494 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 8 Aug 2025 22:36:04 -0400 Subject: [PATCH 3/8] Debug output --- gonotego/uploader/slack/slack_uploader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index 0459313..5f9f9d3 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -133,7 +133,7 @@ def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool: ) # Track the message - logger.debug(f"Sent note to thread: {response}") + logger.warning(f"Sent note to thread: {response}") self._message_timestamps.append(response['ts']) self._session_messages.append({'ts': response['ts'], 'text': text}) From 69ae82d98bd25d28487537bf0daef86790bf1a90 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 8 Aug 2025 22:37:37 -0400 Subject: [PATCH 4/8] Debug output --- gonotego/uploader/slack/slack_uploader.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index 5f9f9d3..b331c59 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -168,6 +168,7 @@ def _cleanup_message_async(self, original_text: str, message_ts: str): ) cleaned_text = message.content[0].text.strip() + logger.warning(f"Cleaned text: {cleaned_text}") # Update the message in Slack channel_id = self._get_channel_id() From e53c2a56a38e93d649d15850adf2b1ec7736af0b Mon Sep 17 00:00:00 2001 From: David Bieber Date: Fri, 8 Aug 2025 22:40:45 -0400 Subject: [PATCH 5/8] Set model name --- gonotego/uploader/slack/slack_uploader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index b331c59..e6a99c9 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -159,7 +159,7 @@ def _cleanup_message_async(self, original_text: str, message_ts: str): Original text: {original_text}""" message = client.messages.create( - model="claude-4-opus", + model="claude-opus-4-1-20250805", max_tokens=5000, temperature=0.7, messages=[ @@ -198,9 +198,9 @@ def _summarize_session_async(self): {thread_text}""" message = client.messages.create( - model="claude-3-5-opus-20241022", - max_tokens=1000, - temperature=0.3, + model="claude-opus-4-1-20250805", + max_tokens=5000, + temperature=0.7, messages=[ {"role": "user", "content": prompt} ] From f6d244191367a536929b0244fdd2bcbb06b3b7d8 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 20 Aug 2025 22:23:44 -0400 Subject: [PATCH 6/8] Use SessionState --- gonotego/uploader/slack/slack_uploader.py | 285 +++++++++++++++++----- 1 file changed, 226 insertions(+), 59 deletions(-) diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index e6a99c9..4f4ceef 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -2,8 +2,10 @@ import anthropic import logging +import time +import threading from concurrent.futures import ThreadPoolExecutor -from typing import List, Optional, Dict +from typing import List, Optional, Dict, TypedDict from slack_sdk import WebClient from slack_sdk.errors import SlackApiError @@ -14,20 +16,37 @@ logger = logging.getLogger(__name__) +class SessionState(TypedDict): + """State for a single Slack session.""" + thread_ts: str + channel_id: str + cleaned_reply_ts: str + raw_reply_ts: str + session_messages: List[Dict[str, str]] + cleaned_messages: Dict[str, str] + last_summary: str + + class Uploader: """Uploader implementation for Slack.""" def __init__(self): self._client: Optional[WebClient] = None self._channel_id: Optional[str] = None - self._thread_ts: Optional[str] = None self._session_started: bool = False self._indent_level: int = 0 - self._message_timestamps: List[str] = [] - self._session_messages: List[Dict[str, str]] = [] self._executor = ThreadPoolExecutor(max_workers=5) self._anthropic_client = None + # Current session state + self._current_session: Optional[SessionState] = None + + # Summary debouncing and version tracking + self._summary_timer: Optional[threading.Timer] = None + self._summary_request_version: int = 0 + self._latest_summary_version: int = 0 + self._is_waiting_for_summary: bool = False + @property def client(self) -> WebClient: """Get or create the Slack WebClient.""" @@ -89,34 +108,141 @@ def _start_session(self, first_note: str) -> bool: """Start a new session thread in the configured Slack channel.""" channel_id = self._get_channel_id() - # Create the initial message with the note content try: - message_text = f":wip: {first_note}" - response = self.client.chat_postMessage( + # Create the header message + header_text = f":wip: {first_note}" + header_response = self.client.chat_postMessage( + channel=channel_id, + text=header_text + ) + thread_ts = header_response['ts'] + + # Create the first reply (cleaned content - initially raw) + cleaned_reply_response = self.client.chat_postMessage( + channel=channel_id, + text=first_note, + thread_ts=thread_ts + ) + cleaned_reply_ts = cleaned_reply_response['ts'] + + # Create the second reply (raw content) + raw_reply_response = self.client.chat_postMessage( channel=channel_id, - text=message_text + text=first_note, + thread_ts=thread_ts ) - self._thread_ts = response['ts'] + raw_reply_ts = raw_reply_response['ts'] + + # Create session state + self._current_session = SessionState( + thread_ts=thread_ts, + channel_id=channel_id, + cleaned_reply_ts=cleaned_reply_ts, + raw_reply_ts=raw_reply_ts, + session_messages=[{'ts': header_response['ts'], 'text': first_note}], + cleaned_messages={}, + last_summary=first_note + ) + + # Initialize tracking self._session_started = True - self._message_timestamps = [response['ts']] - self._session_messages = [{'ts': response['ts'], 'text': first_note}] + self._summary_request_version = 0 + self._latest_summary_version = 0 + self._is_waiting_for_summary = False - # Schedule cleanup for first message - self._executor.submit(self._cleanup_message_async, first_note, response['ts']) + # Schedule cleanup for the first message + self._executor.submit(self._cleanup_message_async, first_note, + header_response['ts'], self._current_session) return True except SlackApiError as e: logger.error(f"Error starting session: {e}") return False + def _request_summary_smart(self): + """Request a summary with smart debouncing.""" + if not self._current_session: + return + + if not self._is_waiting_for_summary: + # Not waiting, send request immediately + self._is_waiting_for_summary = True + self._summary_request_version += 1 + version = self._summary_request_version + self._executor.submit(self._summarize_session_async, version, self._current_session) + + # Set timer to reset waiting flag + self._summary_timer = threading.Timer(0.5, self._reset_summary_waiting) + self._summary_timer.start() + else: + # Already waiting, cancel existing timer and set new one + if self._summary_timer: + self._summary_timer.cancel() + + # Set new timer to send request after 0.5s + self._summary_timer = threading.Timer(0.5, self._send_summary_request) + self._summary_timer.start() + + def _reset_summary_waiting(self): + """Reset the waiting flag after debounce period.""" + self._is_waiting_for_summary = False + + def _send_summary_request(self): + """Send a summary request after debounce period.""" + self._is_waiting_for_summary = False + if not self._current_session: + return + self._summary_request_version += 1 + version = self._summary_request_version + self._executor.submit(self._summarize_session_async, version, self._current_session) + + def _update_replies(self, session: SessionState): + """Update both reply messages with current content.""" + if not session['thread_ts'] or not session['cleaned_reply_ts'] or not session['raw_reply_ts']: + return + + # Compile cleaned content (use cleaned version if available, else raw) + cleaned_content_parts = [] + for msg in session['session_messages']: + ts = msg['ts'] + text = msg['text'] + # Use cleaned version if available + if ts in session['cleaned_messages']: + cleaned_content_parts.append(session['cleaned_messages'][ts]) + else: + cleaned_content_parts.append(text) + + cleaned_content = '\n'.join(cleaned_content_parts) + + # Compile raw content + raw_content = '\n'.join([msg['text'] for msg in session['session_messages']]) + + # Update cleaned reply + try: + self.client.chat_update( + channel=session['channel_id'], + ts=session['cleaned_reply_ts'], + text=cleaned_content + ) + except SlackApiError as e: + logger.error(f"Error updating cleaned reply: {e}") + + # Update raw reply + try: + self.client.chat_update( + channel=session['channel_id'], + ts=session['raw_reply_ts'], + text=raw_content + ) + except SlackApiError as e: + logger.error(f"Error updating raw reply: {e}") + def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool: - """Send a note as a reply in the current thread.""" - if not self._thread_ts: - logger.error("Trying to send to thread but no thread exists") + """Add a note to the session and update reply messages.""" + if not self._current_session: + logger.error("Trying to send to thread but no session exists") return False - channel_id = self._get_channel_id() - # Format the text based on indentation formatted_text = text if indent_level > 0: @@ -125,27 +251,35 @@ def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool: indentation = " " * (indent_level - 1) formatted_text = f"{indentation}{bullet} {text}" - try: - response = self.client.chat_postMessage( - channel=channel_id, - text=formatted_text, - thread_ts=self._thread_ts - ) + # Generate a unique timestamp for this message + msg_ts = str(time.time()) - # Track the message - logger.warning(f"Sent note to thread: {response}") - self._message_timestamps.append(response['ts']) - self._session_messages.append({'ts': response['ts'], 'text': text}) + # Track the message in current session + self._current_session['session_messages'].append({'ts': msg_ts, 'text': formatted_text}) - # Schedule cleanup for this message - self._executor.submit(self._cleanup_message_async, text, response['ts']) + # Update header to add :thread: if this is the second message + if len(self._current_session['session_messages']) == 2: + try: + header_text = f":wip: {self._current_session['last_summary']} :thread:" + self._update_message(self._current_session['channel_id'], + self._current_session['thread_ts'], header_text) + except SlackApiError as e: + logger.error(f"Error updating header with thread indicator: {e}") - return True - except SlackApiError as e: - logger.error(f"Error sending note to thread: {e}") - return False + # Update both reply messages + self._update_replies(self._current_session) + + # Schedule cleanup for this message + self._executor.submit(self._cleanup_message_async, formatted_text, msg_ts, + self._current_session) - def _cleanup_message_async(self, original_text: str, message_ts: str): + # Request summary with smart debouncing + self._request_summary_smart() + + return True + + def _cleanup_message_async(self, original_text: str, message_ts: str, + session: SessionState): """Clean up a message using Claude in the background.""" try: client = self._get_anthropic_client() @@ -154,7 +288,7 @@ def _cleanup_message_async(self, original_text: str, message_ts: str): return # Call Claude to clean up the message - prompt = f"""Clean up this message. Fix any obvious typographic issues, but keep any stylistic choices that convey emotion, tone, or emphasis. Only clean up typos that were unintentional mistakes. Output the full cleaned text without any explanation or metadata. + prompt = f"""Clean up this message. Fix any obvious typographic issues, but keep any stylistic choices that convey emotion, tone, or emphasis. Only clean up typos that were unintentional mistakes. Output the full cleaned text without any explanation or metadata. If you cannot clean the text, state it unchanged. Original text: {original_text}""" @@ -168,16 +302,18 @@ def _cleanup_message_async(self, original_text: str, message_ts: str): ) cleaned_text = message.content[0].text.strip() - logger.warning(f"Cleaned text: {cleaned_text}") + logger.debug(f"Cleaned text: {cleaned_text}") + + # Store the cleaned message in session + session['cleaned_messages'][message_ts] = cleaned_text - # Update the message in Slack - channel_id = self._get_channel_id() - self._update_message(channel_id, message_ts, cleaned_text) + # Update the cleaned reply message + self._update_replies(session) except Exception as e: logger.error(f"Error cleaning up message: {e}") - def _summarize_session_async(self): + def _summarize_session_async(self, version: int, session: SessionState): """Summarize the entire session using Claude Opus 4.""" try: client = self._get_anthropic_client() @@ -185,21 +321,21 @@ def _summarize_session_async(self): logger.debug("Anthropic client not available, skipping summarization") return - if not self._session_messages or not self._thread_ts: + if not session['session_messages']: logger.debug("No messages to summarize") return # Compile all messages into a thread - thread_text = "\n".join([msg['text'] for msg in self._session_messages]) + thread_text = "\n".join([msg['text'] for msg in session['session_messages']]) - 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. + prompt = f"""Please provide a concise one-line summary of this note-taking session. Be very brief and focus on the main topic or purpose. Session notes: {thread_text}""" message = client.messages.create( model="claude-opus-4-1-20250805", - max_tokens=5000, + max_tokens=200, temperature=0.7, messages=[ {"role": "user", "content": prompt} @@ -208,12 +344,18 @@ def _summarize_session_async(self): summary = message.content[0].text.strip() - # Update the top-level message with the summary - channel_id = self._get_channel_id() - original_text = self._session_messages[0]['text'] if self._session_messages else "" - updated_text = f":memo: **Session Summary**\n\n{summary}\n\n---\n_Original first note: {original_text}_" + # Only update if this version is the latest + if version >= self._latest_summary_version: + self._latest_summary_version = version + session['last_summary'] = summary + + # Determine status indicators + has_thread = len(session['session_messages']) > 1 + thread_indicator = " :thread:" if has_thread else "" - self._update_message(channel_id, self._thread_ts, updated_text) + # Update header (keeping :wip: for now, will be removed in end_session) + header_text = f":wip: {summary}{thread_indicator}" + self._update_message(session['channel_id'], session['thread_ts'], header_text) except Exception as e: logger.error(f"Error summarizing session: {e}") @@ -278,16 +420,41 @@ def upload(self, note_events: List[events.NoteEvent]) -> bool: def end_session(self) -> None: """End the current session.""" - # Schedule session summarization before clearing - if self._session_started and self._session_messages: - self._executor.submit(self._summarize_session_async) + # Remove :wip: from header and trigger final summary + if self._session_started and self._current_session: + try: + # Cancel any pending summary timer + if self._summary_timer: + self._summary_timer.cancel() + + # Request final summary + if self._current_session['session_messages']: + self._summary_request_version += 1 + self._executor.submit(self._summarize_session_async, + self._summary_request_version, + self._current_session) + + # Update header to remove :wip: + has_thread = len(self._current_session['session_messages']) > 1 + thread_indicator = " :thread:" if has_thread else "" + header_text = f"{self._current_session['last_summary']}{thread_indicator}" + self._update_message(self._current_session['channel_id'], + self._current_session['thread_ts'], + header_text) + + except Exception as e: + logger.error(f"Error finalizing session: {e}") # Clear session state - self._thread_ts = None + self._current_session = None self._session_started = False self._indent_level = 0 - self._message_timestamps = [] - self._session_messages = [] + self._summary_request_version = 0 + self._latest_summary_version = 0 + self._is_waiting_for_summary = False + if self._summary_timer: + self._summary_timer.cancel() + self._summary_timer = None def handle_inactivity(self) -> None: """Handle inactivity by ending the session and clearing client.""" @@ -302,6 +469,6 @@ def handle_disconnect(self) -> None: self._anthropic_client = None def __del__(self): - """Cleanup executor on deletion.""" - if hasattr(self, '_executor'): - self._executor.shutdown(wait=False) \ No newline at end of file + """Cleanup executor and timer on deletion.""" + self._executor.shutdown(wait=False) + self._summary_timer.cancel() \ No newline at end of file From a9a7e51447de2da65e0aa2396aa4ab86641f206d Mon Sep 17 00:00:00 2001 From: David Bieber Date: Wed, 20 Aug 2025 22:47:33 -0400 Subject: [PATCH 7/8] Check is_active per session --- gonotego/uploader/slack/slack_uploader.py | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index 4f4ceef..b4e6c3d 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -25,6 +25,7 @@ class SessionState(TypedDict): session_messages: List[Dict[str, str]] cleaned_messages: Dict[str, str] last_summary: str + is_active: bool class Uploader: @@ -33,7 +34,6 @@ class Uploader: def __init__(self): self._client: Optional[WebClient] = None self._channel_id: Optional[str] = None - self._session_started: bool = False self._indent_level: int = 0 self._executor = ThreadPoolExecutor(max_workers=5) self._anthropic_client = None @@ -141,11 +141,11 @@ def _start_session(self, first_note: str) -> bool: raw_reply_ts=raw_reply_ts, session_messages=[{'ts': header_response['ts'], 'text': first_note}], cleaned_messages={}, - last_summary=first_note + last_summary=first_note, + is_active=True ) # Initialize tracking - self._session_started = True self._summary_request_version = 0 self._latest_summary_version = 0 self._is_waiting_for_summary = False @@ -353,8 +353,9 @@ def _summarize_session_async(self, version: int, session: SessionState): has_thread = len(session['session_messages']) > 1 thread_indicator = " :thread:" if has_thread else "" - # Update header (keeping :wip: for now, will be removed in end_session) - header_text = f":wip: {summary}{thread_indicator}" + # Only add :wip: if this session is still active + wip_indicator = ":wip: " if session['is_active'] else "" + header_text = f"{wip_indicator}{summary}{thread_indicator}" self._update_message(session['channel_id'], session['thread_ts'], header_text) except Exception as e: @@ -403,7 +404,7 @@ def upload(self, note_events: List[events.NoteEvent]) -> bool: continue # Start a new session for the first note - if not self._session_started: + if not self._current_session: success = self._start_session(text) else: # Send as a reply to the thread with proper indentation @@ -420,21 +421,23 @@ def upload(self, note_events: List[events.NoteEvent]) -> bool: def end_session(self) -> None: """End the current session.""" - # Remove :wip: from header and trigger final summary - if self._session_started and self._current_session: + if self._current_session: try: + # Mark session as inactive + self._current_session['is_active'] = False + # Cancel any pending summary timer if self._summary_timer: self._summary_timer.cancel() - # Request final summary + # Request final summary (will see is_active=False and not add :wip:) if self._current_session['session_messages']: self._summary_request_version += 1 self._executor.submit(self._summarize_session_async, self._summary_request_version, self._current_session) - # Update header to remove :wip: + # Update header to remove :wip: immediately has_thread = len(self._current_session['session_messages']) > 1 thread_indicator = " :thread:" if has_thread else "" header_text = f"{self._current_session['last_summary']}{thread_indicator}" @@ -447,7 +450,6 @@ def end_session(self) -> None: # Clear session state self._current_session = None - self._session_started = False self._indent_level = 0 self._summary_request_version = 0 self._latest_summary_version = 0 From 35719fb62b8dfcbfb6f32243d72b187b496528b4 Mon Sep 17 00:00:00 2001 From: David Bieber Date: Thu, 21 Aug 2025 12:01:59 -0400 Subject: [PATCH 8/8] Code style --- gonotego/uploader/slack/slack_uploader.py | 69 ++++++++++++++--------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/gonotego/uploader/slack/slack_uploader.py b/gonotego/uploader/slack/slack_uploader.py index b4e6c3d..8c55c66 100644 --- a/gonotego/uploader/slack/slack_uploader.py +++ b/gonotego/uploader/slack/slack_uploader.py @@ -151,8 +151,12 @@ def _start_session(self, first_note: str) -> bool: self._is_waiting_for_summary = False # Schedule cleanup for the first message - self._executor.submit(self._cleanup_message_async, first_note, - header_response['ts'], self._current_session) + self._executor.submit( + self._cleanup_message_async, + first_note, + header_response['ts'], + self._current_session + ) return True except SlackApiError as e: @@ -220,9 +224,9 @@ def _update_replies(self, session: SessionState): # Update cleaned reply try: self.client.chat_update( - channel=session['channel_id'], - ts=session['cleaned_reply_ts'], - text=cleaned_content + channel=session['channel_id'], + ts=session['cleaned_reply_ts'], + text=cleaned_content ) except SlackApiError as e: logger.error(f"Error updating cleaned reply: {e}") @@ -230,9 +234,9 @@ def _update_replies(self, session: SessionState): # Update raw reply try: self.client.chat_update( - channel=session['channel_id'], - ts=session['raw_reply_ts'], - text=raw_content + channel=session['channel_id'], + ts=session['raw_reply_ts'], + text=raw_content ) except SlackApiError as e: logger.error(f"Error updating raw reply: {e}") @@ -261,8 +265,11 @@ def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool: if len(self._current_session['session_messages']) == 2: try: header_text = f":wip: {self._current_session['last_summary']} :thread:" - self._update_message(self._current_session['channel_id'], - self._current_session['thread_ts'], header_text) + self._update_message( + self._current_session['channel_id'], + self._current_session['thread_ts'], + header_text, + ) except SlackApiError as e: logger.error(f"Error updating header with thread indicator: {e}") @@ -270,16 +277,24 @@ def _send_note_to_thread(self, text: str, indent_level: int = 0) -> bool: self._update_replies(self._current_session) # Schedule cleanup for this message - self._executor.submit(self._cleanup_message_async, formatted_text, msg_ts, - self._current_session) + self._executor.submit( + self._cleanup_message_async, + formatted_text, + msg_ts, + self._current_session, + ) # Request summary with smart debouncing self._request_summary_smart() return True - def _cleanup_message_async(self, original_text: str, message_ts: str, - session: SessionState): + def _cleanup_message_async( + self, + original_text: str, + message_ts: str, + session: SessionState, + ): """Clean up a message using Claude in the background.""" try: client = self._get_anthropic_client() @@ -293,12 +308,12 @@ def _cleanup_message_async(self, original_text: str, message_ts: str, Original text: {original_text}""" message = client.messages.create( - model="claude-opus-4-1-20250805", - max_tokens=5000, - temperature=0.7, - messages=[ - {"role": "user", "content": prompt} - ] + model="claude-opus-4-1-20250805", + max_tokens=5000, + temperature=0.7, + messages=[ + {"role": "user", "content": prompt} + ] ) cleaned_text = message.content[0].text.strip() @@ -334,12 +349,12 @@ def _summarize_session_async(self, version: int, session: SessionState): {thread_text}""" message = client.messages.create( - model="claude-opus-4-1-20250805", - max_tokens=200, - temperature=0.7, - messages=[ - {"role": "user", "content": prompt} - ] + model="claude-opus-4-1-20250805", + max_tokens=200, + temperature=0.7, + messages=[ + {"role": "user", "content": prompt} + ] ) summary = message.content[0].text.strip() @@ -473,4 +488,4 @@ def handle_disconnect(self) -> None: def __del__(self): """Cleanup executor and timer on deletion.""" self._executor.shutdown(wait=False) - self._summary_timer.cancel() \ No newline at end of file + self._summary_timer.cancel()