@@ -459,3 +459,139 @@ def summarize(self, transcript: str, user_notes: str = "") -> MeetingSummary:
459459 logger .error (f"All { max_retries } attempts failed for OpenRouter API call" )
460460 logger .error (error_msg , exc_info = True )
461461 raise
462+
463+
464+ class CopilotSummarizer (BaseSummarizer ):
465+ """Summarizer using GitHub Copilot API (included with Copilot subscription)."""
466+
467+ MODELS = {
468+ "mini" : {
469+ "id" : "gpt-4o-mini" ,
470+ "name" : "GPT-4o Mini (Copilot)" ,
471+ },
472+ "standard" : {
473+ "id" : "gpt-4o" ,
474+ "name" : "GPT-4o (Copilot)" ,
475+ },
476+ }
477+
478+ def __init__ (self , api_key : Optional [str ] = None , model : str = "mini" ):
479+ """
480+ Initialize Copilot summarizer.
481+
482+ Args:
483+ api_key: GitHub OAuth access token (from device flow authentication).
484+ model: Model tier - "mini" (gpt-4o-mini) or "standard" (gpt-4o).
485+ """
486+ if not api_key :
487+ raise ValueError (
488+ "GitHub Copilot token required. "
489+ "Authenticate via Settings > GitHub Copilot > Authenticate with GitHub."
490+ )
491+
492+ if model not in self .MODELS :
493+ raise ValueError (
494+ f"Invalid model: { model } . Choose from: { list (self .MODELS .keys ())} "
495+ )
496+
497+ self .model_config = self .MODELS [model ]
498+ self .model = self .model_config ["id" ]
499+
500+ try :
501+ from .copilot_auth import COPILOT_API_BASE
502+
503+ self ._oauth_token = api_key
504+ self .copilot_api_base = COPILOT_API_BASE
505+ except Exception as e :
506+ raise RuntimeError (f"Failed to initialize Copilot auth: { e } " )
507+
508+ try :
509+ from openai import OpenAI
510+
511+ self ._openai_class = OpenAI
512+ except ImportError :
513+ raise ImportError ("openai package not installed. Run: pip install openai" )
514+
515+ # Create initial client (token used directly as Bearer)
516+ self ._client = None
517+ self ._current_token = None
518+
519+ def _get_client (self ):
520+ """Get an OpenAI client using the OAuth token directly as Bearer."""
521+ from meeting_notes import __version__
522+
523+ token = self ._oauth_token
524+
525+ # Re-create client if token changed
526+ if token != self ._current_token :
527+ self ._client = self ._openai_class (
528+ api_key = token ,
529+ base_url = self .copilot_api_base ,
530+ default_headers = {
531+ "Openai-Intent" : "conversation-edits" ,
532+ "User-Agent" : f"meeting-notes/{ __version__ } " ,
533+ },
534+ )
535+ self ._current_token = token
536+
537+ return self ._client
538+
539+ def summarize (self , transcript : str , user_notes : str = "" ) -> MeetingSummary :
540+ """Generate summary using GitHub Copilot with retry logic."""
541+ logger .info (f"Generating AI summary with { self .model_config ['name' ]} ..." )
542+ logger .info (f"Transcript: { len (transcript .split ())} words" )
543+ print (f"Generating AI summary with { self .model_config ['name' ]} ..." )
544+ print (f"Transcript: { len (transcript .split ())} words" )
545+
546+ max_retries = 2
547+ retry_delay = 2 # seconds
548+
549+ for attempt in range (max_retries ):
550+ try :
551+ client = self ._get_client ()
552+
553+ response = client .chat .completions .create (
554+ model = self .model ,
555+ messages = [
556+ {
557+ "role" : "user" ,
558+ "content" : self ._build_prompt (
559+ transcript , user_notes = user_notes
560+ ),
561+ }
562+ ],
563+ temperature = 0.3 ,
564+ )
565+
566+ # Log token usage if available (no cost tracking - included in subscription)
567+ if response .usage :
568+ total_tokens = (
569+ response .usage .prompt_tokens + response .usage .completion_tokens
570+ )
571+ logger .info (
572+ f"Summary generated ({ total_tokens } tokens, included in Copilot subscription)"
573+ )
574+ print (
575+ f"Summary generated ({ total_tokens } tokens, included in Copilot subscription)"
576+ )
577+ else :
578+ logger .info ("Summary generated (Copilot)" )
579+ print ("Summary generated (Copilot)" )
580+
581+ return self ._parse_response (response .choices [0 ].message .content )
582+
583+ except Exception as e :
584+ error_msg = f"Attempt { attempt + 1 } /{ max_retries } failed: { type (e ).__name__ } : { e } "
585+
586+ if attempt < max_retries - 1 :
587+ # If it's a token error, force a refresh before retrying
588+ self ._current_token = None
589+ logger .warning (error_msg + f" - Retrying in { retry_delay } s..." )
590+ print (f"Warning: { error_msg } - Retrying in { retry_delay } s..." )
591+ time .sleep (retry_delay )
592+ else :
593+ logger .error (
594+ f"All { max_retries } attempts failed for Copilot API call"
595+ )
596+ logger .error (error_msg , exc_info = True )
597+ raise
0 commit comments