@@ -459,3 +459,138 @@ 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 , CopilotTokenManager
502+
503+ self ._token_manager = CopilotTokenManager (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+ self ._client = None
516+ self ._current_token = None
517+
518+ def _get_client (self ):
519+ """Get an OpenAI client using a short-lived Copilot session token."""
520+ from meeting_notes import __version__
521+
522+ token = self ._token_manager .get_token ()
523+
524+ if token != self ._current_token :
525+ self ._client = self ._openai_class (
526+ api_key = token ,
527+ base_url = self .copilot_api_base ,
528+ default_headers = {
529+ "Openai-Intent" : "conversation-edits" ,
530+ "User-Agent" : f"meeting-notes/{ __version__ } " ,
531+ },
532+ )
533+ self ._current_token = token
534+
535+ return self ._client
536+
537+ def summarize (self , transcript : str , user_notes : str = "" ) -> MeetingSummary :
538+ """Generate summary using GitHub Copilot with retry logic."""
539+ logger .info (f"Generating AI summary with { self .model_config ['name' ]} ..." )
540+ logger .info (f"Transcript: { len (transcript .split ())} words" )
541+ print (f"Generating AI summary with { self .model_config ['name' ]} ..." )
542+ print (f"Transcript: { len (transcript .split ())} words" )
543+
544+ max_retries = 2
545+ retry_delay = 2 # seconds
546+
547+ for attempt in range (max_retries ):
548+ try :
549+ client = self ._get_client ()
550+
551+ response = client .chat .completions .create (
552+ model = self .model ,
553+ messages = [
554+ {
555+ "role" : "user" ,
556+ "content" : self ._build_prompt (
557+ transcript , user_notes = user_notes
558+ ),
559+ }
560+ ],
561+ temperature = 0.3 ,
562+ )
563+
564+ # Log token usage if available (no cost tracking - included in subscription)
565+ if response .usage :
566+ total_tokens = (
567+ response .usage .prompt_tokens + response .usage .completion_tokens
568+ )
569+ logger .info (
570+ f"Summary generated ({ total_tokens } tokens, included in Copilot subscription)"
571+ )
572+ print (
573+ f"Summary generated ({ total_tokens } tokens, included in Copilot subscription)"
574+ )
575+ else :
576+ logger .info ("Summary generated (Copilot)" )
577+ print ("Summary generated (Copilot)" )
578+
579+ return self ._parse_response (response .choices [0 ].message .content )
580+
581+ except Exception as e :
582+ error_msg = f"Attempt { attempt + 1 } /{ max_retries } failed: { type (e ).__name__ } : { e } "
583+
584+ if attempt < max_retries - 1 :
585+ # If it's a token error, force a refresh before retrying
586+ self ._current_token = None
587+ self ._token_manager .invalidate ()
588+ logger .warning (error_msg + f" - Retrying in { retry_delay } s..." )
589+ print (f"Warning: { error_msg } - Retrying in { retry_delay } s..." )
590+ time .sleep (retry_delay )
591+ else :
592+ logger .error (
593+ f"All { max_retries } attempts failed for Copilot API call"
594+ )
595+ logger .error (error_msg , exc_info = True )
596+ raise
0 commit comments