diff --git a/__init__.py b/__init__.py index d50439d..db4c289 100644 --- a/__init__.py +++ b/__init__.py @@ -1 +1 @@ -"""Meeting Assistant Multi-Agent System""" \ No newline at end of file +"""Meeting Assistant Multi-Agent System""" diff --git a/action_item_extraction_agent.py b/action_item_extraction_agent.py index ec172d4..36ea31f 100644 --- a/action_item_extraction_agent.py +++ b/action_item_extraction_agent.py @@ -1,5 +1,6 @@ import json import re + import openai @@ -9,61 +10,55 @@ def __init__(self, api_key=None, model="gpt-3.5-turbo"): self.model = model # Regular expressions for simple action item extraction self.action_keywords = [ - r'(?:need to|must|should|will|going to|have to|shall) ([^.!?]*)', - r'(?:action item|task|todo|to-do|to do|follow-up|followup)[:\s]* ([^.!?]*)', - r'(\w+)(?:\s*will|\s*is going to|\s*needs to|\s*must) ([^.!?]*)', - r'(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))', + r"(?:need to|must|should|will|going to|have to|shall) ([^.!?]*)", + r"(?:action item|task|todo|to-do|to do|follow-up|followup)[:\s]* ([^.!?]*)", + r"(\w+)(?:\s*will|\s*is going to|\s*needs to|\s*must) ([^.!?]*)", + r"(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))", ] - + def extract_action_items(self, transcription, summary=None): """ Extract action items from meeting transcription and/or summary. - + Args: transcription (dict): Transcription data from the TranscriptionAgent summary (dict, optional): Summary data from the SummarizationAgent - + Returns: dict: Extraction result with list of action items and metadata """ print("ActionItemExtractionAgent: Extracting action items") - + try: # Use both transcription and summary if available text = transcription.get("transcription", "") - + if summary and "summary" in summary: text += "\n\n" + summary["summary"] - + if not text: raise ValueError("No text provided for action item extraction") - + # Extract action items using provided method action_items = self._extract_action_items(text) - + return { "action_items": action_items, - "metadata": { - "items_found": len(action_items), - "status": "completed" - } + "metadata": {"items_found": len(action_items), "status": "completed"}, } - + except Exception as e: print(f"Error during action item extraction: {str(e)}") return { "action_items": [], - "metadata": { - "status": "error", - "error": str(e) - } + "metadata": {"status": "error", "error": str(e)}, } - + def _extract_action_items(self, text): """ Extract action items from the provided text. - - This is a placeholder method. In a production environment, + + This is a placeholder method. In a production environment, this would use more sophisticated NLP techniques or an LLM. """ # If API key is provided, use LLM for extraction @@ -76,21 +71,27 @@ def _extract_action_items(self, text): else: # Fallback to regex-based extraction return self._extract_with_regex(text) - + def _extract_with_llm(self, text): """Extract action items using an LLM""" client = openai.OpenAI(api_key=self.api_key) - + response = client.chat.completions.create( model=self.model, messages=[ - {"role": "system", "content": "You are a meeting assistant that extracts action items from meeting transcripts. Extract all tasks, responsibilities, and deadlines in a structured format."}, - {"role": "user", "content": f"Extract all action items from this meeting transcript as a JSON array. Each action item should have 'task', 'assignee', and 'deadline' fields. Use null for missing information:\n\n{text}"} + { + "role": "system", + "content": "You are a meeting assistant that extracts action items from meeting transcripts. Extract all tasks, responsibilities, and deadlines in a structured format.", + }, + { + "role": "user", + "content": f"Extract all action items from this meeting transcript as a JSON array. Each action item should have 'task', 'assignee', and 'deadline' fields. Use null for missing information:\n\n{text}", + }, ], response_format={"type": "json_object"}, - max_tokens=1000 + max_tokens=1000, ) - + # Parse the response result_content = response.choices[0].message.content try: @@ -99,53 +100,52 @@ def _extract_with_llm(self, text): except: # If parsing fails, return an empty list return [] - + def _extract_with_regex(self, text): """Extract action items using regular expressions""" action_items = [] - sentences = re.split(r'[.!?]\s+', text) - + sentences = re.split(r"[.!?]\s+", text) + for sentence in sentences: item = self._extract_from_sentence(sentence) if item and all(item != existing for existing in action_items): action_items.append(item) - + return action_items - + def _extract_from_sentence(self, sentence): """Extract an action item from a single sentence using pattern matching""" sentence = sentence.strip() if not sentence: return None - + # Look for action patterns task = None assignee = None deadline = None - + # Find potential task for pattern in self.action_keywords: match = re.search(pattern, sentence, re.IGNORECASE) if match: task = match.group(1).strip() break - + if not task: return None - + # Try to find assignee - look for names followed by verbs - assignee_match = re.search(r'(\b[A-Z][a-z]+\b)(?:\s+will|\s+should|\s+is going to|\s+needs to)', sentence) + assignee_match = re.search( + r"(\b[A-Z][a-z]+\b)(?:\s+will|\s+should|\s+is going to|\s+needs to)", + sentence, + ) if assignee_match: assignee = assignee_match.group(1) - + # Try to find deadline - deadline_pattern = r'(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))' + deadline_pattern = r"(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))" deadline_match = re.search(deadline_pattern, sentence, re.IGNORECASE) if deadline_match: deadline = deadline_match.group(1) - - return { - "task": task, - "assignee": assignee, - "deadline": deadline - } \ No newline at end of file + + return {"task": task, "assignee": assignee, "deadline": deadline} diff --git a/app.py b/app.py index de5619a..001f1d2 100644 --- a/app.py +++ b/app.py @@ -1,19 +1,26 @@ -import os import json -from typing import Optional -from fastapi import FastAPI, File, UploadFile, Form, Request -from fastapi.responses import HTMLResponse, JSONResponse -from fastapi.staticfiles import StaticFiles -from fastapi.templating import Jinja2Templates +import os from pathlib import Path +from typing import Any, Dict, Optional + import uvicorn from dotenv import load_dotenv +from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates from orchestrator import MeetingAssistantOrchestrator +from meeting_assistant.config import load_config + # Load environment variables load_dotenv() -app = FastAPI(title="Meeting Assistant") +app = FastAPI( + title="Meeting Assistant API", + description="API for processing meeting recordings using multi-agent system", + version="1.0.0", +) # Create directories for static files and templates BASE_DIR = Path(__file__).resolve().parent @@ -32,71 +39,67 @@ # Setup templates templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) +# Initialize the orchestrator +config = load_config() +orchestrator = MeetingAssistantOrchestrator(config) + + @app.get("/", response_class=HTMLResponse) async def home(request: Request): """Render the home page""" - return templates.TemplateResponse( - "index.html", - {"request": request} - ) - -@app.post("/process") -async def process_meeting( - file: UploadFile = File(...), - openai_api_key: Optional[str] = Form(None), - azure_speech_key: Optional[str] = Form(None) -): - """Process a meeting recording""" + return templates.TemplateResponse("index.html", {"request": request}) + + +@app.post("/process-meeting") +async def process_meeting(audio_file: UploadFile = File(...)) -> Dict[str, Any]: + """Process a meeting recording. + + Args: + audio_file: The uploaded meeting audio file. + + Returns: + Dict containing the processing results. + + Raises: + HTTPException: If file upload or processing fails. + """ try: - # Save the uploaded file - file_path = UPLOAD_DIR / file.filename - with open(file_path, "wb") as buffer: - content = await file.read() - buffer.write(content) - - # Configure the orchestrator - config = { - "openai_api_key": openai_api_key or os.getenv("OPENAI_API_KEY"), - "azure_speech_key": azure_speech_key or os.getenv("AZURE_SPEECH_KEY") - } - + # Save uploaded file + temp_path = f"temp/{audio_file.filename}" + with open(temp_path, "wb") as f: + content = await audio_file.read() + f.write(content) + # Process the meeting - orchestrator = MeetingAssistantOrchestrator(config) - results = orchestrator.process_meeting(str(file_path)) - - # Generate report - report = orchestrator.generate_report(results) - - # Save results and report with unique names based on timestamp - results_file = UPLOAD_DIR / f"results_{file.filename}.json" - report_file = UPLOAD_DIR / f"report_{file.filename}.md" - - with open(results_file, "w") as f: - json.dump(results, f, indent=2) - - with open(report_file, "w") as f: - f.write(report) - - # Clean up the uploaded audio file - os.remove(file_path) - - return JSONResponse({ - "status": "success", - "message": "Meeting processed successfully", - "results": results, - "report": report - }) - + results = orchestrator.process_meeting(temp_path) + + # Clean up + os.remove(temp_path) + + return results + except Exception as e: - return JSONResponse({ - "status": "error", - "message": str(e) - }, status_code=500) + raise HTTPException( + status_code=500, detail=f"Failed to process meeting: {str(e)}" + ) + @app.get("/health") -async def health_check(): - """Health check endpoint""" +async def health_check() -> Dict[str, str]: + """Check the health of the API. + + Returns: + Dict containing status information. + """ return {"status": "healthy"} + if __name__ == "__main__": - uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file + # Use environment variable for host in production, default to localhost + host = os.getenv("API_HOST", "127.0.0.1") + port = int(os.getenv("API_PORT", "8000")) + + # Enable reload only in development + reload = os.getenv("API_ENV", "development").lower() == "development" + + uvicorn.run("app:app", host=host, port=port, reload=reload) diff --git a/main.py b/main.py index 194e407..152b3b1 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ -import os import argparse +import os + from orchestrator import MeetingAssistantOrchestrator @@ -8,70 +9,92 @@ def main(): Main function to demonstrate the Meeting Assistant workflow. """ # Parse command line arguments - parser = argparse.ArgumentParser(description='Intelligent Meeting Assistant') - parser.add_argument('--audio', type=str, required=True, help='Path to the meeting audio file') - parser.add_argument('--openai_api_key', type=str, help='OpenAI API key for summarization and extraction') - parser.add_argument('--azure_speech_key', type=str, help='Azure Speech API key for transcription') - parser.add_argument('--output', type=str, default='meeting_results.json', help='Output JSON file path') - parser.add_argument('--report', type=str, default='meeting_report.md', help='Output report file path') - + parser = argparse.ArgumentParser(description="Intelligent Meeting Assistant") + parser.add_argument( + "--audio", type=str, required=True, help="Path to the meeting audio file" + ) + parser.add_argument( + "--openai_api_key", + type=str, + help="OpenAI API key for summarization and extraction", + ) + parser.add_argument( + "--azure_speech_key", type=str, help="Azure Speech API key for transcription" + ) + parser.add_argument( + "--output", + type=str, + default="meeting_results.json", + help="Output JSON file path", + ) + parser.add_argument( + "--report", + type=str, + default="meeting_report.md", + help="Output report file path", + ) + args = parser.parse_args() - + # Check if the audio file exists if not os.path.isfile(args.audio): print(f"Error: Audio file '{args.audio}' not found.") return - + # Get API keys from environment variables if not provided - openai_api_key = args.openai_api_key or os.environ.get('OPENAI_API_KEY') - azure_speech_key = args.azure_speech_key or os.environ.get('AZURE_SPEECH_KEY') - + openai_api_key = args.openai_api_key or os.environ.get("OPENAI_API_KEY") + azure_speech_key = args.azure_speech_key or os.environ.get("AZURE_SPEECH_KEY") + # Configure the orchestrator config = { "openai_api_key": openai_api_key, "azure_speech_key": azure_speech_key, "summarization_model": "gpt-3.5-turbo", - "extraction_model": "gpt-3.5-turbo" + "extraction_model": "gpt-3.5-turbo", } - + # Create and run the orchestrator orchestrator = MeetingAssistantOrchestrator(config) - + print(f"Processing meeting audio: {args.audio}") - + # Process the meeting results = orchestrator.process_meeting(args.audio) - + # Save results to JSON orchestrator.save_results(results, args.output) - + # Generate and save a human-readable report report = orchestrator.generate_report(results) - with open(args.report, 'w') as f: + with open(args.report, "w") as f: f.write(report) - + print(f"Report saved to {args.report}") - + # Print a summary to the console print("\nMeeting Processing Summary:") print(f"- Audio file: {args.audio}") - print(f"- Transcription: {len(results['transcription'].get('transcription', ''))} characters") + print( + f"- Transcription: {len(results['transcription'].get('transcription', ''))} characters" + ) print(f"- Summary: {len(results['summary'].get('summary', ''))} characters") - print(f"- Action items: {len(results['action_items'].get('action_items', []))} items") - + print( + f"- Action items: {len(results['action_items'].get('action_items', []))} items" + ) + # Print the action items - action_items = results['action_items'].get('action_items', []) + action_items = results["action_items"].get("action_items", []) if action_items: print("\nAction Items:") for i, item in enumerate(action_items, 1): - task = item.get('task', 'No task specified') - assignee = item.get('assignee', 'Unassigned') - deadline = item.get('deadline', 'No deadline') - + task = item.get("task", "No task specified") + assignee = item.get("assignee", "Unassigned") + deadline = item.get("deadline", "No deadline") + print(f" {i}. Task: {task}") print(f" Assignee: {assignee}") print(f" Deadline: {deadline}") - + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/meeting_assistant/__init__.py b/meeting_assistant/__init__.py index 73ab5c9..666ea82 100644 --- a/meeting_assistant/__init__.py +++ b/meeting_assistant/__init__.py @@ -3,4 +3,4 @@ from .orchestrator import MeetingAssistantOrchestrator __version__ = "0.1.0" -__all__ = ["MeetingAssistantOrchestrator"] \ No newline at end of file +__all__ = ["MeetingAssistantOrchestrator"] diff --git a/meeting_assistant/action_item_extraction_agent.py b/meeting_assistant/action_item_extraction_agent.py index c9ae316..92445fe 100644 --- a/meeting_assistant/action_item_extraction_agent.py +++ b/meeting_assistant/action_item_extraction_agent.py @@ -1,18 +1,41 @@ -"""Action item extraction agent for identifying tasks and assignments""" +"""Action item extraction agent for identifying tasks and assignments.""" + +from typing import Any, Dict + class ActionItemExtractionAgent: - def __init__(self, config): + """Agent responsible for extracting action items from meetings.""" + + def __init__(self, config: Dict[str, Any]) -> None: + """Initialize the action item extraction agent. + + Args: + config: Configuration dictionary for the agent. + """ self.config = config - def extract_action_items(self, transcription_result, summary_result): - """Mock action item extraction for testing""" + def extract_action_items(self, transcript: str, summary: str) -> Dict[str, Any]: + """Extract action items from meeting content. + + Args: + transcript: The meeting transcript text. + summary: The meeting summary text. + + Returns: + A dictionary containing extracted action items. + """ + # Mock implementation - replace with actual extraction logic return { "action_items": [ { - "task": "Test task", + "task": "Create project timeline", "assignee": "John", - "deadline": "tomorrow" - } - ], - "metadata": {"status": "completed"} - } \ No newline at end of file + "due_date": "2024-03-15", + }, + { + "task": "Review requirements document", + "assignee": "Sarah", + "due_date": "2024-03-10", + }, + ] + } diff --git a/meeting_assistant/config.py b/meeting_assistant/config.py index dd1b95f..4393adb 100644 --- a/meeting_assistant/config.py +++ b/meeting_assistant/config.py @@ -1,71 +1,69 @@ -from typing import Optional -from pydantic import BaseModel, Field, ConfigDict -from pathlib import Path import os +from pathlib import Path + +from pydantic import BaseModel, ConfigDict, Field + class AgentConfig(BaseModel): - """Configuration for individual agents""" - model_config = ConfigDict(extra='forbid') - - openai_api_key: str = Field(..., description="OpenAI API key for GPT models") - azure_speech_key: str = Field(..., description="Azure Speech Services API key") - model_name: str = Field("gpt-4", description="GPT model to use") - temperature: float = Field(0.7, description="Temperature for GPT responses") - max_tokens: int = Field(1000, description="Maximum tokens for GPT responses") + """Configuration for individual agents.""" -class WorkspaceConfig(BaseModel): - """Configuration for workspace and file management""" - model_config = ConfigDict(extra='forbid') - - upload_dir: Path = Field( - default=Path("uploads"), - description="Directory for uploaded files" + model_config = ConfigDict(extra="allow") + + api_key: str = Field( + default_factory=lambda: os.getenv("OPENAI_API_KEY", ""), + description="API key for the agent", ) - results_dir: Path = Field( - default=Path("results"), - description="Directory for saving results" + model: str = Field( + default="gpt-4-turbo-preview", description="Model to use for the agent" + ) + + +class WorkspaceConfig(BaseModel): + """Configuration for workspace settings.""" + + model_config = ConfigDict(extra="allow") + + output_dir: Path = Field( + default=Path("output"), description="Directory for output files" ) temp_dir: Path = Field( - default=Path("temp"), - description="Directory for temporary files" + default=Path("temp"), description="Directory for temporary files" ) + class AutoGenConfig(BaseModel): - """Configuration for AutoGen settings""" - model_config = ConfigDict(extra='forbid') - - use_docker: bool = Field(False, description="Whether to use Docker for code execution") - max_consecutive_auto_reply: int = Field( - 10, - description="Maximum number of consecutive auto-replies" + """Configuration for AutoGen settings.""" + + model_config = ConfigDict(extra="allow") + + use_docker: bool = Field( + default=False, description="Whether to use Docker for code execution" ) - human_input_mode: str = Field( - "NEVER", - description="Mode for human input in conversations" + timeout: int = Field( + default=600, description="Timeout for agent operations in seconds" ) + class AppConfig(BaseModel): - """Main application configuration""" - model_config = ConfigDict(extra='forbid') - - agent: AgentConfig - workspace: WorkspaceConfig = WorkspaceConfig() - autogen: AutoGenConfig = AutoGenConfig() - debug: bool = Field(False, description="Enable debug mode") - log_level: str = Field("INFO", description="Logging level") + """Main application configuration.""" -def load_config() -> AppConfig: - """Load configuration from environment variables and defaults""" - agent_config = AgentConfig( - openai_api_key=os.getenv("OPENAI_API_KEY", ""), - azure_speech_key=os.getenv("AZURE_SPEECH_KEY", ""), - model_name=os.getenv("MODEL_NAME", "gpt-4"), - temperature=float(os.getenv("TEMPERATURE", "0.7")), - max_tokens=int(os.getenv("MAX_TOKENS", "1000")) + model_config = ConfigDict(extra="allow") + + agents: AgentConfig = Field( + default_factory=AgentConfig, description="Agent-specific configuration" ) - - return AppConfig( - agent=agent_config, - debug=os.getenv("DEBUG", "").lower() == "true", - log_level=os.getenv("LOG_LEVEL", "INFO") - ) \ No newline at end of file + workspace: WorkspaceConfig = Field( + default_factory=WorkspaceConfig, description="Workspace settings" + ) + autogen: AutoGenConfig = Field( + default_factory=AutoGenConfig, description="AutoGen configuration" + ) + + +def load_config() -> AppConfig: + """Load configuration from environment variables and defaults. + + Returns: + AppConfig: The loaded configuration object. + """ + return AppConfig() diff --git a/meeting_assistant/logger.py b/meeting_assistant/logger.py index 59e2737..f5375a2 100644 --- a/meeting_assistant/logger.py +++ b/meeting_assistant/logger.py @@ -1,50 +1,47 @@ import logging import sys -from pathlib import Path from logging.handlers import RotatingFileHandler +from pathlib import Path from typing import Optional + def setup_logger( name: str, log_file: Optional[Path] = None, level: str = "INFO", max_bytes: int = 10 * 1024 * 1024, # 10MB - backup_count: int = 5 + backup_count: int = 5, ) -> logging.Logger: """Set up a logger with both file and console handlers""" - + # Create logger logger = logging.getLogger(name) logger.setLevel(getattr(logging, level.upper())) - + # Create formatters - console_formatter = logging.Formatter( - '%(levelname)s - %(message)s' - ) + console_formatter = logging.Formatter("%(levelname)s - %(message)s") file_formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) - + # Console handler console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) - + # File handler (if log_file is provided) if log_file: log_file.parent.mkdir(parents=True, exist_ok=True) file_handler = RotatingFileHandler( - str(log_file), - maxBytes=max_bytes, - backupCount=backup_count + str(log_file), maxBytes=max_bytes, backupCount=backup_count ) file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) - + return logger + # Create default logger default_logger = setup_logger( - "meeting_assistant", - log_file=Path("logs/meeting_assistant.log") -) \ No newline at end of file + "meeting_assistant", log_file=Path("logs/meeting_assistant.log") +) diff --git a/meeting_assistant/orchestrator.py b/meeting_assistant/orchestrator.py index 9539fa4..d24f848 100644 --- a/meeting_assistant/orchestrator.py +++ b/meeting_assistant/orchestrator.py @@ -1,221 +1,152 @@ import json -import autogen -from typing import Dict, Any, List, Optional +from datetime import datetime from pathlib import Path +from typing import Any, Dict -from .config import AppConfig, load_config -from .logger import setup_logger -from .transcription_agent import TranscriptionAgent -from .summarization_agent import SummarizationAgent from .action_item_extraction_agent import ActionItemExtractionAgent +from .config import AppConfig +from .summarization_agent import SummarizationAgent +from .transcription_agent import TranscriptionAgent + class MeetingAssistantOrchestrator: - """ - Orchestrator that manages the workflow between the specialized agents - in the meeting assistant system using Microsoft AutoGen. - """ - - def __init__(self, config: Optional[AppConfig] = None): - """Initialize the orchestrator with configuration""" - self.config = config or load_config() - self.logger = setup_logger( - "orchestrator", - log_file=Path("logs/orchestrator.log"), - level=self.config.log_level - ) - - # Initialize agents as None - self.transcription_agent = None - self.summarization_agent = None - self.action_item_extraction_agent = None - - # Set up AutoGen agents - self._setup_autogen_agents() - - def _setup_autogen_agents(self): - """Set up the AutoGen agents""" - self.logger.info("Setting up AutoGen agents") - - # User proxy agent that acts as the initiator - self.user_proxy = autogen.UserProxyAgent( - name="user_proxy", - human_input_mode=self.config.autogen.human_input_mode, - max_consecutive_auto_reply=self.config.autogen.max_consecutive_auto_reply, - is_termination_msg=lambda x: x.get("content", "").rstrip().endswith("TERMINATE"), - code_execution_config={ - "work_dir": "workspace", - "use_docker": self.config.autogen.use_docker - }, - ) - - # Transcription AutoGen agent - self.transcription_autogen = autogen.AssistantAgent( - name="transcription_agent", - llm_config=None, # No LLM needed as we're using our own transcription logic - system_message="I am a transcription agent that converts audio to text.", - ) - - # Summarization AutoGen agent - self.summarization_autogen = autogen.AssistantAgent( - name="summarization_agent", - llm_config=None, # No LLM needed as we're using our own summarization logic - system_message="I am a summarization agent that creates concise meeting summaries.", - ) - - # Action Item Extraction AutoGen agent - self.action_item_autogen = autogen.AssistantAgent( - name="action_item_agent", - llm_config=None, # No LLM needed as we're using our own extraction logic - system_message="I am an action item extraction agent that identifies tasks and responsibilities.", - ) - - # Group chat for the agents to collaborate - self.groupchat = autogen.GroupChat( - agents=[self.user_proxy, self.transcription_autogen, - self.summarization_autogen, self.action_item_autogen], - messages=[], - max_round=10 - ) - - # Manager to coordinate the group chat - self.manager = autogen.GroupChatManager( - groupchat=self.groupchat, - llm_config=None, # No LLM needed for our orchestration - ) - - self.logger.info("AutoGen agents setup completed") - - def _setup_specialized_agents(self): - """Set up the specialized agents when needed""" - self.logger.info("Setting up specialized agents") - - if self.transcription_agent is None: - self.transcription_agent = TranscriptionAgent(self.config) - - if self.summarization_agent is None: - self.summarization_agent = SummarizationAgent(self.config) - - if self.action_item_extraction_agent is None: - self.action_item_extraction_agent = ActionItemExtractionAgent(self.config) - - self.logger.info("Specialized agents setup completed") - - def process_meeting(self, audio_file_path: str) -> Dict[str, Any]: - """Process a meeting recording""" - self.logger.info(f"Processing meeting from {audio_file_path}") - - # Check if audio file exists - if not Path(audio_file_path).exists(): - error_msg = f"Audio file not found: {audio_file_path}" - self.logger.error(error_msg) - raise FileNotFoundError(error_msg) - - # Initialize specialized agents when needed - self._setup_specialized_agents() - + """Orchestrates the multi-agent system for processing meetings.""" + + def __init__(self, config: AppConfig) -> None: + """Initialize the orchestrator with configuration. + + Args: + config: Configuration object for the orchestrator and agents. + """ + self.config = config + self._setup_agents() + self._setup_workspace() + + def _setup_agents(self) -> None: + """Set up the specialized agents for meeting processing.""" + self.transcription_agent = TranscriptionAgent(self.config) + self.summarization_agent = SummarizationAgent(self.config) + self.action_item_agent = ActionItemExtractionAgent(self.config) + + def _setup_workspace(self) -> None: + """Set up the workspace directories.""" + self.output_dir = self.config.workspace.results_dir + self.temp_dir = self.config.workspace.temp_dir + + # Create directories if they don't exist + self.output_dir.mkdir(parents=True, exist_ok=True) + self.temp_dir.mkdir(parents=True, exist_ok=True) + + def process_meeting(self, audio_file: str) -> Dict[str, Any]: + """Process a meeting recording through the agent pipeline. + + Args: + audio_file: Path to the meeting audio file. + + Returns: + Dict containing the processing results from all agents. + + Raises: + FileNotFoundError: If the audio file doesn't exist. + ValueError: If the audio file is invalid or processing fails. + """ + if not Path(audio_file).exists(): + raise FileNotFoundError(f"Audio file not found: {audio_file}") + try: - # For testing purposes, return mock data - results = { - "transcription": { - "transcription": "Test transcription", - "metadata": {"status": "completed"} - }, - "summary": { - "summary": "Test summary", - "metadata": {"status": "completed"} + # Step 1: Transcribe the meeting audio + transcription_result = self.transcription_agent.transcribe(audio_file) + + # Step 2: Generate meeting summary + summary_result = self.summarization_agent.summarize(transcription_result) + + # Step 3: Extract action items + action_items = self.action_item_agent.extract_action_items( + transcription_result, summary_result + ) + + # Combine all results + return { + "transcription": transcription_result, + "summary": summary_result, + "action_items": action_items, + "metadata": { + "audio_file": audio_file, + "timestamp": str(datetime.now()), }, - "action_items": { - "action_items": [ - { - "task": "Test task", - "assignee": "John", - "deadline": "tomorrow" - } - ], - "metadata": {"status": "completed"} - } } - - self.logger.info("Meeting processing completed successfully") - return results - + except Exception as e: - self.logger.error(f"Error processing meeting: {str(e)}", exc_info=True) - raise + raise ValueError(f"Failed to process meeting: {str(e)}") from e def generate_report(self, results: Dict[str, Any]) -> str: - """Generate a markdown report from the results""" - self.logger.info("Generating meeting report") - - try: - # Validate required fields - required_fields = ["transcription", "summary", "action_items"] - for field in required_fields: - if field not in results: - error_msg = f"Missing required field: {field}" - self.logger.error(error_msg) - raise KeyError(error_msg) - - report = "# Meeting Assistant Report\n\n" - - # Add summary section - report += "## Meeting Summary\n\n" - report += results.get("summary", {}).get("summary", "No summary available") + "\n\n" - - # Add action items section - report += "## Action Items\n\n" - action_items = results.get("action_items", {}).get("action_items", []) - if action_items: - for i, item in enumerate(action_items, 1): - task = item.get("task", "No task specified") - assignee = item.get("assignee", "Unassigned") - deadline = item.get("deadline", "No deadline") - - report += f"{i}. **Task**: {task}\n" - report += f" **Assignee**: {assignee}\n" - report += f" **Deadline**: {deadline}\n\n" - else: - report += "No action items identified.\n\n" - - # Add transcription section - report += "## Full Transcription\n\n" - report += results.get("transcription", {}).get("transcription", "No transcription available") - - self.logger.info("Report generation completed") - return report - - except Exception as e: - self.logger.error(f"Error generating report: {str(e)}", exc_info=True) - raise + """Generate a formatted report from the meeting results. + + Args: + results: Dictionary containing meeting processing results. + + Returns: + Formatted report as a string. + + Raises: + ValueError: If the results data is invalid or incomplete. + """ + required_fields = {"transcription", "summary", "action_items"} + if not all(field in results for field in required_fields): + raise ValueError("Missing required fields in results data") + + report = [] + report.append("# Meeting Summary Report\n") + report.append(f"Generated: {results['metadata']['timestamp']}\n") + report.append("\n## Meeting Summary\n") + report.append(results["summary"]) + report.append("\n## Action Items\n") + + for item in results["action_items"]["action_items"]: + report.append( + f"- {item['task']} (Assignee: {item['assignee']}, " + f"Due: {item['due_date']})" + ) + + return "\n".join(report) + + def save_results(self, results: Dict[str, Any], output_path: str = None) -> str: + """Save the meeting results to files. + + Args: + results: Dictionary containing meeting processing results. + output_path: Optional custom output path. + + Returns: + Path to the saved report file. + + Raises: + ValueError: If the results data is invalid or saving fails. + """ + if not isinstance(results, dict): + raise ValueError("Results must be a dictionary") + + # Use custom output path or default to output directory + output_dir = Path(output_path) if output_path else self.output_dir + output_dir.mkdir(parents=True, exist_ok=True) + + # Generate timestamp for filenames + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Save the full results as JSON + results_file = output_dir / f"meeting_results_{timestamp}.json" + with open(results_file, "w") as f: + json.dump(results, f, indent=2) + + # Generate and save the report + report = self.generate_report(results) + report_file = output_dir / f"meeting_report_{timestamp}.md" + with open(report_file, "w") as f: + f.write(report) + + return str(report_file) - def save_results(self, results: Dict[str, Any], output_file: str = "meeting_results.json"): - """Save results to a JSON file""" - self.logger.info(f"Saving results to {output_file}") - - try: - # Create directory if it doesn't exist - output_path = Path(output_file) - - # Check if path is absolute and not in a valid location - if output_path.is_absolute(): - error_msg = f"Invalid output path: {output_file}. Must be a relative path." - self.logger.error(error_msg) - raise ValueError(error_msg) - - # Create directory if it doesn't exist - output_path.parent.mkdir(parents=True, exist_ok=True) - - # Save results - with open(output_path, 'w') as f: - json.dump(results, f, indent=2) - - self.logger.info(f"Results saved successfully to {output_file}") - - except Exception as e: - self.logger.error(f"Error saving results: {str(e)}", exc_info=True) - raise - def _notify_autogen_agent(self, agent, message: str): """Send a message to an AutoGen agent""" self.logger.debug(f"Notifying agent {agent.name}: {message}") - agent.receive({"content": message, "role": "user"}) \ No newline at end of file + agent.receive({"content": message, "role": "user"}) diff --git a/meeting_assistant/summarization_agent.py b/meeting_assistant/summarization_agent.py index 5ab5e8d..4233116 100644 --- a/meeting_assistant/summarization_agent.py +++ b/meeting_assistant/summarization_agent.py @@ -1,12 +1,10 @@ """Summarization agent for generating meeting summaries""" + class SummarizationAgent: def __init__(self, config): self.config = config def summarize(self, transcription_result): """Mock summarization for testing""" - return { - "summary": "Test summary", - "metadata": {"status": "completed"} - } \ No newline at end of file + return {"summary": "Test summary", "metadata": {"status": "completed"}} diff --git a/meeting_assistant/transcription_agent.py b/meeting_assistant/transcription_agent.py index 2675aa0..29a4549 100644 --- a/meeting_assistant/transcription_agent.py +++ b/meeting_assistant/transcription_agent.py @@ -1,5 +1,6 @@ """Transcription agent for converting audio to text""" + class TranscriptionAgent: def __init__(self, config): self.config = config @@ -8,5 +9,5 @@ def transcribe(self, audio_file_path): """Mock transcription for testing""" return { "transcription": "Test transcription", - "metadata": {"status": "completed"} - } \ No newline at end of file + "metadata": {"status": "completed"}, + } diff --git a/requirements.txt b/requirements.txt index 6c3038c..fc99cdc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ SpeechRecognition openai==1.12.0 fastapi==0.110.0 uvicorn==0.27.1 -python-multipart==0.0.9 +python-multipart==0.0.18 jinja2 pytest==8.0.2 pytest-cov==4.1.0 @@ -18,7 +18,7 @@ python-dotenv==1.0.1 aiofiles pydantic==2.6.3 azure-cognitiveservices-speech==1.35.0 -black==24.2.0 +black==24.3.0 flake8==7.0.0 isort==5.13.2 httpx==0.27.0 diff --git a/sample.py b/sample.py index 36654ea..b7a50f8 100644 --- a/sample.py +++ b/sample.py @@ -1,38 +1,41 @@ +import json import os import tempfile -import json + from orchestrator import MeetingAssistantOrchestrator + def create_mock_audio_file(): """ Create a temporary mock audio file for demonstration purposes. In a real scenario, you would use an actual audio file. - + Returns: str: Path to the temporary file """ # Create a temporary file - temp_file = tempfile.NamedTemporaryFile(suffix='.wav', delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) temp_file.close() - + print(f"Created mock audio file at: {temp_file.name}") - + return temp_file.name + def mock_transcription_process(orchestrator, audio_file_path): """ Mock the transcription process with predefined text. - + Args: orchestrator: MeetingAssistantOrchestrator instance audio_file_path: Path to the audio file (not actually used) - + Returns: dict: Results with mock transcription, summary, and action items """ # Store the original transcribe method original_transcribe = orchestrator.transcription_agent.transcribe - + # Mock transcription data mock_transcript = """ John: Good morning everyone. Let's start our weekly project meeting. First, let's review the progress from last week. @@ -73,7 +76,7 @@ def mock_transcription_process(orchestrator, audio_file_path): John: Perfect. If there's nothing else, we can wrap up. Thanks everyone for your updates. """ - + # Replace the transcribe method with a mock function def mock_transcribe(audio_file_path): return { @@ -81,13 +84,13 @@ def mock_transcribe(audio_file_path): "metadata": { "file": audio_file_path, "duration_seconds": 720, # 12 minutes - "status": "completed" - } + "status": "completed", + }, } - + # Set the mock function orchestrator.transcription_agent.transcribe = mock_transcribe - + try: # Process the meeting with mock data results = orchestrator.process_meeting(audio_file_path) @@ -101,47 +104,47 @@ def run_sample(): """Run a sample demonstration of the Meeting Assistant""" # Create a mock audio file audio_file_path = create_mock_audio_file() - + try: # Configure the orchestrator config = { - "openai_api_key": os.environ.get('OPENAI_API_KEY'), - "azure_speech_key": os.environ.get('AZURE_SPEECH_KEY') + "openai_api_key": os.environ.get("OPENAI_API_KEY"), + "azure_speech_key": os.environ.get("AZURE_SPEECH_KEY"), } - + # Create the orchestrator orchestrator = MeetingAssistantOrchestrator(config) - + print("Running Meeting Assistant with mock data...") - + # Process the meeting with mock transcription results = mock_transcription_process(orchestrator, audio_file_path) - + # Save results orchestrator.save_results(results, "sample_results.json") - + # Generate and save a report report = orchestrator.generate_report(results) - with open("sample_report.md", 'w') as f: + with open("sample_report.md", "w") as f: f.write(report) - + print("\nSample completed successfully!") print("- Results saved to: sample_results.json") print("- Report saved to: sample_report.md") - + # Print action items - action_items = results['action_items'].get('action_items', []) + action_items = results["action_items"].get("action_items", []) if action_items: print("\nExtracted Action Items:") for i, item in enumerate(action_items, 1): - task = item.get('task', 'No task specified') - assignee = item.get('assignee', 'Unassigned') - deadline = item.get('deadline', 'No deadline') - + task = item.get("task", "No task specified") + assignee = item.get("assignee", "Unassigned") + deadline = item.get("deadline", "No deadline") + print(f" {i}. Task: {task}") print(f" Assignee: {assignee}") print(f" Deadline: {deadline}") - + finally: # Clean up the temporary file if os.path.exists(audio_file_path): @@ -150,4 +153,4 @@ def run_sample(): if __name__ == "__main__": - run_sample() \ No newline at end of file + run_sample() diff --git a/setup.py b/setup.py index dbf6d8e..d2b8e29 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup( name="meeting-assistant", @@ -27,4 +27,4 @@ "Programming Language :: Python :: 3.12", ], python_requires=">=3.9", -) \ No newline at end of file +) diff --git a/summarization_agent.py b/summarization_agent.py index 984b60d..f870f6e 100644 --- a/summarization_agent.py +++ b/summarization_agent.py @@ -1,4 +1,5 @@ import json + import openai @@ -6,54 +7,48 @@ class SummarizationAgent: def __init__(self, api_key=None, model="gpt-3.5-turbo"): self.api_key = api_key self.model = model - + def summarize(self, transcription): """ Generate a concise summary of the meeting transcription. - + Args: transcription (dict): Transcription data from the TranscriptionAgent - + Returns: dict: Summary result with summarized text and metadata """ print("SummarizationAgent: Generating meeting summary") - + try: # Extract the transcription text text = transcription.get("transcription", "") - + if not text: raise ValueError("No transcription text provided") - + # For demonstration purposes, we'll use a placeholder summary generator # In production, this would use the OpenAI API or similar service summary = self._generate_summary(text) - + return { "summary": summary, "metadata": { "original_length": len(text), "summary_length": len(summary), - "status": "completed" - } + "status": "completed", + }, } - + except Exception as e: print(f"Error during summarization: {str(e)}") - return { - "summary": "", - "metadata": { - "status": "error", - "error": str(e) - } - } - + return {"summary": "", "metadata": {"status": "error", "error": str(e)}} + def _generate_summary(self, text): """ Generate a summary of the provided text. - - This is a placeholder method. In a production environment, + + This is a placeholder method. In a production environment, this would call an LLM API like OpenAI's GPT models. """ # Placeholder implementation - in production, use OpenAI API or similar @@ -61,35 +56,41 @@ def _generate_summary(self, text): try: # Initialize OpenAI client with the API key client = openai.OpenAI(api_key=self.api_key) - + # Call the OpenAI API to generate a summary response = client.chat.completions.create( model=self.model, messages=[ - {"role": "system", "content": "You are a meeting assistant that creates concise summaries."}, - {"role": "user", "content": f"Please summarize this meeting transcript:\n\n{text}"} + { + "role": "system", + "content": "You are a meeting assistant that creates concise summaries.", + }, + { + "role": "user", + "content": f"Please summarize this meeting transcript:\n\n{text}", + }, ], - max_tokens=500 + max_tokens=500, ) - + # Extract the summary from the response summary = response.choices[0].message.content return summary - + except Exception as e: print(f"Error with OpenAI API: {str(e)}") return self._fallback_summary(text) else: return self._fallback_summary(text) - + def _fallback_summary(self, text): """Fallback method to generate a simple summary when API is not available""" # Simple extractive summary - take the first few sentences as a summary - sentences = text.split('. ') + sentences = text.split(". ") num_summary_sentences = min(5, len(sentences)) - summary = '. '.join(sentences[:num_summary_sentences]) - - if summary and not summary.endswith('.'): - summary += '.' - - return summary \ No newline at end of file + summary = ". ".join(sentences[:num_summary_sentences]) + + if summary and not summary.endswith("."): + summary += "." + + return summary diff --git a/tall pytest pytest-cov b/tall pytest pytest-cov new file mode 100644 index 0000000..5d130a3 --- /dev/null +++ b/tall pytest pytest-cov @@ -0,0 +1,365 @@ +warning: in the working copy of '__init__.py', LF will be replaced by CRLF the next time Git touches it +diff --git a/__init__.py b/__init__.py +index d50439d..db4c289 100644 +--- a/__init__.py ++++ b/__init__.py +@@ -1 +1 @@ +-"""Meeting Assistant Multi-Agent System"""  +\ No newline at end of file ++"""Meeting Assistant Multi-Agent System""" +diff --git a/action_item_extraction_agent.py b/action_item_extraction_agent.py +index ec172d4..36ea31f 100644 +--- a/action_item_extraction_agent.py ++++ b/action_item_extraction_agent.py +@@ -1,5 +1,6 @@ + import json + import re ++ + import openai +  +  +@@ -9,61 +10,55 @@ class ActionItemExtractionAgent: + self.model = model + # Regular expressions for simple action item extraction + self.action_keywords = [ +- r'(?:need to|must|should|will|going to|have to|shall) ([^.!?]*)', +- r'(?:action item|task|todo|to-do|to do|follow-up|followup)[:\s]* ([^.!?]*)', +- r'(\w+)(?:\s*will|\s*is going to|\s*needs to|\s*must) ([^.!?]*)', +- r'(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))', ++ r"(?:need to|must|should|will|going to|have to|shall) ([^.!?]*)", ++ r"(?:action item|task|todo|to-do|to do|follow-up|followup)[:\s]* ([^.!?]*)", ++ r"(\w+)(?:\s*will|\s*is going to|\s*needs to|\s*must) ([^.!?]*)", ++ r"(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))", + ] +-  ++ + def extract_action_items(self, transcription, summary=None): + """ + Extract action items from meeting transcription and/or summary. +-  ++ + Args: + transcription (dict): Transcription data from the TranscriptionAgent + summary (dict, optional): Summary data from the SummarizationAgent +-  ++ + Returns: + dict: Extraction result with list of action items and metadata + """ + print("ActionItemExtractionAgent: Extracting action items") +-  ++ + try: + # Use both transcription and summary if available + text = transcription.get("transcription", "") +-  ++ + if summary and "summary" in summary: + text += "\n\n" + summary["summary"] +-  ++ + if not text: + raise ValueError("No text provided for action item extraction") +-  ++ + # Extract action items using provided method + action_items = self._extract_action_items(text) +-  ++ + return { + "action_items": action_items, +- "metadata": { +- "items_found": len(action_items), +- "status": "completed" +- } ++ "metadata": {"items_found": len(action_items), "status": "completed"}, + } +-  ++ + except Exception as e: + print(f"Error during action item extraction: {str(e)}") + return { + "action_items": [], +- "metadata": { +- "status": "error", +- "error": str(e) +- } ++ "metadata": {"status": "error", "error": str(e)}, + } +-  ++ + def _extract_action_items(self, text): + """ + Extract action items from the provided text. +-  +- This is a placeholder method. In a production environment,  ++ ++ This is a placeholder method. In a production environment, + this would use more sophisticated NLP techniques or an LLM. + """ + # If API key is provided, use LLM for extraction +@@ -76,21 +71,27 @@ class ActionItemExtractionAgent: + else: + # Fallback to regex-based extraction + return self._extract_with_regex(text) +-  ++ + def _extract_with_llm(self, text): + """Extract action items using an LLM""" + client = openai.OpenAI(api_key=self.api_key) +-  ++ + response = client.chat.completions.create( + model=self.model, + messages=[ +- {"role": "system", "content": "You are a meeting assistant that extracts action items from meeting transcripts. Extract all tasks, responsibilities, and deadlines in a structured format."}, +- {"role": "user", "content": f"Extract all action items from this meeting transcript as a JSON array. Each action item should have 'task', 'assignee', and 'deadline' fields. Use null for missing information:\n\n{text}"} ++ { ++ "role": "system", ++ "content": "You are a meeting assistant that extracts action items from meeting transcripts. Extract all tasks, responsibilities, and deadlines in a structured format.", ++ }, ++ { ++ "role": "user", ++ "content": f"Extract all action items from this meeting transcript as a JSON array. Each action item should have 'task', 'assignee', and 'deadline' fields. Use null for missing information:\n\n{text}", ++ }, + ], + response_format={"type": "json_object"}, +- max_tokens=1000 ++ max_tokens=1000, + ) +-  ++ + # Parse the response + result_content = response.choices[0].message.content + try: +@@ -99,53 +100,52 @@ class ActionItemExtractionAgent: + except: + # If parsing fails, return an empty list + return [] +-  ++ + def _extract_with_regex(self, text): + """Extract action items using regular expressions""" + action_items = [] +- sentences = re.split(r'[.!?]\s+', text) +-  ++ sentences = re.split(r"[.!?]\s+", text) ++ + for sentence in sentences: + item = self._extract_from_sentence(sentence) + if item and all(item != existing for existing in action_items): + action_items.append(item) +-  ++ + return action_items +-  ++ + def _extract_from_sentence(self, sentence): + """Extract an action item from a single sentence using pattern matching""" + sentence = sentence.strip() + if not sentence: + return None +-  ++ + # Look for action patterns + task = None + assignee = None + deadline = None +-  ++ + # Find potential task + for pattern in self.action_keywords: + match = re.search(pattern, sentence, re.IGNORECASE) + if match: + task = match.group(1).strip() + break +-  ++ + if not task: + return None +-  ++ + # Try to find assignee - look for names followed by verbs +- assignee_match = re.search(r'(\b[A-Z][a-z]+\b)(?:\s+will|\s+should|\s+is going to|\s+needs to)', sentence) ++ assignee_match = re.search( ++ r"(\b[A-Z][a-z]+\b)(?:\s+will|\s+should|\s+is going to|\s+needs to)", ++ sentence, ++ ) + if assignee_match: + assignee = assignee_match.group(1) +-  ++ + # Try to find deadline +- deadline_pattern = r'(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))' ++ deadline_pattern = r"(?:by|before|due)(?:\s*the)?\s*(\d{1,2}(?:st|nd|rd|th)?\s+(?:of\s+)?(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)|tomorrow|next week|(?:this|next) month|(?:Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday))" + deadline_match = re.search(deadline_pattern, sentence, re.IGNORECASE) + if deadline_match: + deadline = deadline_match.group(1) +-  +- return { +- "task": task, +- "assignee": assignee, +- "deadline": deadline +- }  +\ No newline at end of file ++ ++ return {"task": task, "assignee": assignee, "deadline": deadline} +diff --git a/app.py b/app.py +index de5619a..a6809a5 100644 +--- a/app.py ++++ b/app.py +@@ -1,13 +1,14 @@ +-import os + import json ++import os ++from pathlib import Path + from typing import Optional +-from fastapi import FastAPI, File, UploadFile, Form, Request ++ ++import uvicorn ++from dotenv import load_dotenv ++from fastapi import FastAPI, File, Form, Request, UploadFile + from fastapi.responses import HTMLResponse, JSONResponse + from fastapi.staticfiles import StaticFiles + from fastapi.templating import Jinja2Templates +-from pathlib import Path +-import uvicorn +-from dotenv import load_dotenv + from orchestrator import MeetingAssistantOrchestrator +  + # Load environment variables +@@ -32,19 +33,18 @@ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") + # Setup templates + templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) +  ++ + @app.get("/", response_class=HTMLResponse) + async def home(request: Request): + """Render the home page""" +- return templates.TemplateResponse( +- "index.html", +- {"request": request} +- ) ++ return templates.TemplateResponse("index.html", {"request": request}) ++ +  + @app.post("/process") + async def process_meeting( + file: UploadFile = File(...), + openai_api_key: Optional[str] = Form(None), +- azure_speech_key: Optional[str] = Form(None) ++ azure_speech_key: Optional[str] = Form(None), + ): + """Process a meeting recording""" + try: +@@ -53,50 +53,51 @@ async def process_meeting( + with open(file_path, "wb") as buffer: + content = await file.read() + buffer.write(content) +-  ++ + # Configure the orchestrator + config = { + "openai_api_key": openai_api_key or os.getenv("OPENAI_API_KEY"), +- "azure_speech_key": azure_speech_key or os.getenv("AZURE_SPEECH_KEY") ++ "azure_speech_key": azure_speech_key or os.getenv("AZURE_SPEECH_KEY"), + } +-  ++ + # Process the meeting + orchestrator = MeetingAssistantOrchestrator(config) + results = orchestrator.process_meeting(str(file_path)) +-  ++ + # Generate report + report = orchestrator.generate_report(results) +-  ++ + # Save results and report with unique names based on timestamp + results_file = UPLOAD_DIR / f"results_{file.filename}.json" + report_file = UPLOAD_DIR / f"report_{file.filename}.md" +-  ++ + with open(results_file, "w") as f: + json.dump(results, f, indent=2) +-  ++ + with open(report_file, "w") as f: + f.write(report) +-  ++ + # Clean up the uploaded audio file + os.remove(file_path) +-  +- return JSONResponse({ +- "status": "success", +- "message": "Meeting processed successfully", +- "results": results, +- "report": report +- }) +-  ++ ++ return JSONResponse( ++ { ++ "status": "success", ++ "message": "Meeting processed successfully", ++ "results": results, ++ "report": report, ++ } ++ ) ++ + except Exception as e: +- return JSONResponse({ +- "status": "error", +- "message": str(e) +- }, status_code=500) ++ return JSONResponse({"status": "error", "message": str(e)}, status_code=500) ++ +  + @app.get("/health") + async def health_check(): + """Health check endpoint""" + return {"status": "healthy"} +  ++ + if __name__ == "__main__": +- uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)  +\ No newline at end of file ++ uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) +diff --git a/main.py b/main.py +index 194e407..152b3b1 100644 +--- a/main.py ++++ b/main.py +@@ -1,5 +1,6 @@ +-import os + import argparse ++import os ++ + from orchestrator import MeetingAssistantOrchestrator +  +  +@@ -8,70 +9,92 @@ def main(): + Main function to demonstrate the Meeting Assistant workflow. + """ + # Parse command line arguments +- parser = argparse.ArgumentParser(description='Intelligent Meeting Assistant') +- parser.add_argument('--audio', type=str, required=True, help='Path to the meeting audio file') +- parser.add_argument('--openai_api_key', type=str, help='OpenAI API key for summarization and extraction') +- parser.add_argument('--azure_speech_key', type=str, help='Azure Speech API key for transcription') +- parser.add_argument('--output', type=str, default='meeting_results.json', help='Output JSON file path') +- parser.add_argument('--report', type=str, default='meeting_report.md', help='Output report file path') +-  ++ parser = argparse.ArgumentParser(description="Intelligent Meeting Assistant") ++ parser.add_argument( ++ "--audio", type=str, required=True, help="Path to the meeting audio file" ++ ) ++ parser.add_argument( ++ "--openai_api_key", ++ type=str, ++ help="OpenAI API key for summarization and extraction", ++ ) ++ parser.add_argument( ++ "--azure_speech_key", type=str, help="Azure Speech API key for transcription" ++ ) ++ parser.add_argument( ++ "-- \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 16f6ef4..b2d3b9c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,29 @@ import os +import shutil import tempfile -import pytest from pathlib import Path -from meeting_assistant.config import AppConfig, AgentConfig, WorkspaceConfig, AutoGenConfig + +import pytest + +from meeting_assistant.config import ( + AgentConfig, + AppConfig, + AutoGenConfig, + WorkspaceConfig, +) +from meeting_assistant.orchestrator import MeetingAssistantOrchestrator + @pytest.fixture def mock_audio_file(): """Create a temporary mock audio file for testing""" - temp_file = tempfile.NamedTemporaryFile(suffix='.wav', delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) temp_file.close() yield temp_file.name if os.path.exists(temp_file.name): os.remove(temp_file.name) + @pytest.fixture def test_config(): """Create a test configuration""" @@ -21,29 +32,28 @@ def test_config(): azure_speech_key="test_azure_key", model_name="gpt-4", temperature=0.7, - max_tokens=1000 + max_tokens=1000, ) - + workspace_config = WorkspaceConfig( upload_dir=Path("test_uploads"), results_dir=Path("test_results"), - temp_dir=Path("test_temp") + temp_dir=Path("test_temp"), ) - + autogen_config = AutoGenConfig( - use_docker=False, - max_consecutive_auto_reply=5, - human_input_mode="NEVER" + use_docker=False, max_consecutive_auto_reply=5, human_input_mode="NEVER" ) - + return AppConfig( agent=agent_config, workspace=workspace_config, autogen=autogen_config, debug=True, - log_level="DEBUG" + log_level="DEBUG", ) + @pytest.fixture def test_dirs(test_config): """Create and clean up test directories""" @@ -51,15 +61,51 @@ def test_dirs(test_config): test_config.workspace.upload_dir.mkdir(parents=True, exist_ok=True) test_config.workspace.results_dir.mkdir(parents=True, exist_ok=True) test_config.workspace.temp_dir.mkdir(parents=True, exist_ok=True) - + yield test_config.workspace - + # Clean up test directories - import shutil for dir_path in [ test_config.workspace.upload_dir, test_config.workspace.results_dir, - test_config.workspace.temp_dir + test_config.workspace.temp_dir, ]: if dir_path.exists(): - shutil.rmtree(dir_path) \ No newline at end of file + shutil.rmtree(dir_path) + + +@pytest.fixture +def mock_config(): + """Create a mock configuration for testing.""" + return { + "api_key": "test_key", + "model": "gpt-4-turbo-preview", + "output_dir": "test_output", + "temp_dir": "test_temp", + "use_docker": False, + "timeout": 300, + } + + +@pytest.fixture +def orchestrator(mock_config): + """Create an orchestrator instance for testing.""" + return MeetingAssistantOrchestrator(mock_config) + + +@pytest.fixture +def sample_results(): + """Create sample results for testing.""" + return { + "transcription": ( + "Sample meeting transcription with multiple " + "lines of text for testing purposes" + ), + "summary": "Sample meeting summary", + "action_items": { + "action_items": [ + {"task": "Test task", "assignee": "John", "due_date": "2024-03-15"} + ] + }, + "metadata": {"audio_file": "test.wav", "timestamp": "2024-03-01 12:00:00"}, + } diff --git a/tests/test_orchestrator.py b/tests/test_orchestrator.py index b3590ce..d68763b 100644 --- a/tests/test_orchestrator.py +++ b/tests/test_orchestrator.py @@ -1,115 +1,68 @@ import os import tempfile -import pytest from pathlib import Path -from meeting_assistant import MeetingAssistantOrchestrator + +import pytest + +from meeting_assistant.orchestrator import MeetingAssistantOrchestrator + @pytest.fixture def mock_audio_file(): """Create a temporary mock audio file for testing""" - temp_file = tempfile.NamedTemporaryFile(suffix='.wav', delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) temp_file.close() yield temp_file.name if os.path.exists(temp_file.name): os.remove(temp_file.name) + @pytest.fixture def orchestrator(test_config): """Create an orchestrator instance for testing""" return MeetingAssistantOrchestrator(test_config) -def test_orchestrator_initialization(orchestrator, test_config): + +def test_orchestrator_initialization(orchestrator): """Test that the orchestrator initializes correctly""" - assert orchestrator.config.agent.openai_api_key == "test_openai_key" - assert orchestrator.config.agent.azure_speech_key == "test_azure_key" - assert orchestrator.transcription_agent is None - assert orchestrator.summarization_agent is None - assert orchestrator.action_item_extraction_agent is None - -def test_process_meeting(orchestrator, mock_audio_file, test_dirs): - """Test the meeting processing workflow""" + assert isinstance(orchestrator, MeetingAssistantOrchestrator) + assert orchestrator.output_dir.exists() + assert orchestrator.temp_dir.exists() + + +def test_process_meeting(orchestrator, mock_audio_file): + """Test processing a meeting recording""" results = orchestrator.process_meeting(mock_audio_file) - - # Check that all components are present in results assert "transcription" in results assert "summary" in results assert "action_items" in results - - # Check metadata - assert "metadata" in results["transcription"] - assert "metadata" in results["summary"] - assert "metadata" in results["action_items"] - - # Check status - assert results["transcription"]["metadata"]["status"] == "completed" - assert results["summary"]["metadata"]["status"] == "completed" - assert results["action_items"]["metadata"]["status"] == "completed" - -def test_generate_report(orchestrator): - """Test report generation""" - # Mock results - results = { - "transcription": { - "transcription": "Test transcription", - "metadata": {"status": "completed"} - }, - "summary": { - "summary": "Test summary", - "metadata": {"status": "completed"} - }, - "action_items": { - "action_items": [ - { - "task": "Test task", - "assignee": "John", - "deadline": "tomorrow" - } - ], - "metadata": {"status": "completed"} - } - } - - report = orchestrator.generate_report(results) - - # Check that report contains all sections - assert "# Meeting Assistant Report" in report - assert "## Meeting Summary" in report - assert "## Action Items" in report - assert "## Full Transcription" in report - - # Check content - assert "Test summary" in report + assert "metadata" in results + assert results["metadata"]["audio_file"] == mock_audio_file + + +def test_generate_report(orchestrator, sample_results): + """Test generating a report from meeting results""" + report = orchestrator.generate_report(sample_results) + assert "Meeting Summary Report" in report + assert "Sample meeting summary" in report assert "Test task" in report assert "John" in report - assert "Test transcription" in report - -def test_save_results(orchestrator, test_dirs): - """Test saving results to a file""" - # Mock results - results = { - "test": "data" - } - - # Save to test results directory - output_file = test_dirs.results_dir / "test_results.json" - orchestrator.save_results(results, str(output_file)) - - # Check that file exists and contains correct data - assert output_file.exists() - with open(output_file) as f: - saved_data = f.read() - assert '"test": "data"' in saved_data - -def test_error_handling(orchestrator, test_dirs): - """Test error handling in various methods""" - # Test invalid audio file + + +def test_save_results(orchestrator, sample_results, tmp_path): + """Test saving meeting results to files""" + report_path = orchestrator.save_results(sample_results, str(tmp_path)) + assert Path(report_path).exists() + assert (tmp_path / "meeting_results_").parent.exists() + + +def test_error_handling(orchestrator): + """Test error handling for invalid inputs""" with pytest.raises(FileNotFoundError): - orchestrator.process_meeting("nonexistent_file.wav") - - # Test invalid results data - with pytest.raises(KeyError): + orchestrator.process_meeting("nonexistent.wav") + + with pytest.raises(ValueError): orchestrator.generate_report({"invalid": "data"}) - - # Test invalid output directory + with pytest.raises(ValueError): - orchestrator.save_results({}, str(Path.cwd() / "invalid" / "path" / "results.json")) \ No newline at end of file + orchestrator.save_results("not a dict") diff --git a/transcription_agent.py b/transcription_agent.py index 732d1b4..a3f872f 100644 --- a/transcription_agent.py +++ b/transcription_agent.py @@ -1,59 +1,62 @@ import json -import speech_recognition as sr -from pydub import AudioSegment import os import tempfile +import speech_recognition as sr +from pydub import AudioSegment + class TranscriptionAgent: def __init__(self, api_key=None): self.api_key = api_key self.recognizer = sr.Recognizer() - + def transcribe(self, audio_file_path): """ Transcribe audio file to text using speech recognition. - + Args: audio_file_path (str): Path to the audio file - + Returns: dict: Transcription result with text and metadata """ print(f"TranscriptionAgent: Transcribing file {audio_file_path}") - + # For prototype, use a simple transcription method # In production, this would use Azure Speech-to-Text or similar service try: # Handle different audio formats - convert to wav if needed - audio_format = audio_file_path.split('.')[-1].lower() - if audio_format != 'wav': + audio_format = audio_file_path.split(".")[-1].lower() + if audio_format != "wav": temp_wav = self._convert_to_wav(audio_file_path) audio_path = temp_wav else: audio_path = audio_file_path - + # Perform the transcription with sr.AudioFile(audio_path) as source: audio_data = self.recognizer.record(source) - text = self.recognizer.recognize_google(audio_data) # Placeholder for Azure service - + text = self.recognizer.recognize_google( + audio_data + ) # Placeholder for Azure service + # Clean up temp file if one was created - if audio_format != 'wav': + if audio_format != "wav": os.remove(temp_wav) - + # Return the transcription result result = { "transcription": text, "metadata": { "file": audio_file_path, "duration_seconds": self._get_audio_duration(audio_file_path), - "status": "completed" - } + "status": "completed", + }, } - + return result - + except Exception as e: print(f"Error during transcription: {str(e)}") return { @@ -61,18 +64,18 @@ def transcribe(self, audio_file_path): "metadata": { "file": audio_file_path, "status": "error", - "error": str(e) - } + "error": str(e), + }, } - + def _convert_to_wav(self, audio_file_path): """Convert audio file to WAV format for processing""" audio = AudioSegment.from_file(audio_file_path) - temp_file = tempfile.NamedTemporaryFile(suffix='.wav', delete=False) + temp_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) audio.export(temp_file.name, format="wav") return temp_file.name - + def _get_audio_duration(self, audio_file_path): """Get duration of audio file in seconds""" audio = AudioSegment.from_file(audio_file_path) - return len(audio) / 1000 # Convert milliseconds to seconds \ No newline at end of file + return len(audio) / 1000 # Convert milliseconds to seconds