|
|
"""Tests for app.py functionality - Upload Meeting and Create Project.""" |
|
|
import pytest |
|
|
import os |
|
|
from pathlib import Path |
|
|
import tempfile |
|
|
import shutil |
|
|
|
|
|
|
|
|
import sys |
|
|
sys.path.insert(0, str(Path(__file__).parent.parent)) |
|
|
|
|
|
from src.rag import ProjectRAG |
|
|
from src.parsers import MeetingParser |
|
|
|
|
|
|
|
|
|
|
|
SAMPLE_RAW_NOTES_QUANTUM = """ |
|
|
We had a sync meeting about the quantum error correction project. |
|
|
Dr. Chen presented the new surface code implementation results. |
|
|
The decoder is showing 15ms latency which is too slow for real-time correction. |
|
|
|
|
|
Sarah mentioned we got access to IBM's 127-qubit Eagle processor next month. |
|
|
Marcus is still blocked on the FPGA development - waiting for the new boards. |
|
|
|
|
|
We decided to switch from Union-Find to MWPM decoder for better accuracy. |
|
|
Also agreed to target 1ms latency for the final system. |
|
|
|
|
|
Tasks: |
|
|
- Dr. Chen will optimize the decoder by end of week |
|
|
- Sarah needs to prepare the calibration scripts for IBM hardware |
|
|
- Marcus to follow up with vendor about FPGA delivery |
|
|
""" |
|
|
|
|
|
SAMPLE_RAW_NOTES_NEW_PROJECT = """ |
|
|
Kickoff meeting for the new recommendation engine project. |
|
|
Team: Alice, Bob, Carol, David |
|
|
|
|
|
Discussed architecture options - decided on collaborative filtering approach. |
|
|
Bob raised concern about cold start problem for new users. |
|
|
|
|
|
Alice will research embedding models this week. |
|
|
Carol to set up the data pipeline by Friday. |
|
|
David blocked on getting production database access. |
|
|
|
|
|
Next meeting scheduled for Monday. |
|
|
""" |
|
|
|
|
|
|
|
|
class TestMeetingParser: |
|
|
"""Test meeting parsing functionality.""" |
|
|
|
|
|
def test_parse_action_items_from_raw_notes(self): |
|
|
"""Test that action items can be extracted from structured notes.""" |
|
|
|
|
|
structured_content = """# Meeting: Test Meeting |
|
|
Date: 2025-01-30 |
|
|
Participants: Alice, Bob |
|
|
|
|
|
## Discussion |
|
|
Test discussion. |
|
|
|
|
|
## Action Items |
|
|
- [ ] Alice: Complete task by 2025-02-05 |
|
|
- [ ] Bob: Review code |
|
|
- [x] Carol: Setup done (completed) |
|
|
|
|
|
## Blockers |
|
|
- Waiting for API access |
|
|
""" |
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: |
|
|
f.write(structured_content) |
|
|
temp_path = Path(f.name) |
|
|
|
|
|
try: |
|
|
meeting = MeetingParser.parse(temp_path, "test_project") |
|
|
assert meeting is not None |
|
|
assert len(meeting.action_items) == 3 |
|
|
|
|
|
|
|
|
open_items = [a for a in meeting.action_items if not a.completed] |
|
|
assert len(open_items) == 2 |
|
|
|
|
|
|
|
|
assignees = [a.assignee for a in meeting.action_items if a.assignee] |
|
|
assert "Alice" in assignees |
|
|
assert "Bob" in assignees |
|
|
finally: |
|
|
os.unlink(temp_path) |
|
|
|
|
|
def test_parse_blockers(self): |
|
|
"""Test blocker extraction.""" |
|
|
structured_content = """# Meeting: Blocker Test |
|
|
Date: 2025-01-30 |
|
|
Participants: Team |
|
|
|
|
|
## Discussion |
|
|
Discussed blockers. |
|
|
|
|
|
## Blockers |
|
|
- Waiting for hardware delivery |
|
|
- Need security clearance for data access |
|
|
- Vendor has not responded to queries |
|
|
""" |
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f: |
|
|
f.write(structured_content) |
|
|
temp_path = Path(f.name) |
|
|
|
|
|
try: |
|
|
meeting = MeetingParser.parse(temp_path, "test_project") |
|
|
assert meeting is not None |
|
|
assert len(meeting.blockers) == 3 |
|
|
|
|
|
assert any("hardware" in b.lower() for b in meeting.blockers) |
|
|
finally: |
|
|
os.unlink(temp_path) |
|
|
|
|
|
|
|
|
class TestProjectCreation: |
|
|
"""Test creating new projects.""" |
|
|
|
|
|
def test_create_new_project_directory(self): |
|
|
"""Test that new project directories are created correctly.""" |
|
|
with tempfile.TemporaryDirectory() as temp_dir: |
|
|
project_name = "test_new_project" |
|
|
project_dir = Path(temp_dir) / project_name / "meetings" |
|
|
|
|
|
|
|
|
project_dir.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
assert project_dir.exists() |
|
|
assert project_dir.is_dir() |
|
|
assert (Path(temp_dir) / project_name).exists() |
|
|
|
|
|
def test_save_meeting_to_new_project(self): |
|
|
"""Test saving a meeting file to a new project.""" |
|
|
with tempfile.TemporaryDirectory() as temp_dir: |
|
|
project_name = "recommendation_engine" |
|
|
meeting_date = "2025-01-30" |
|
|
meeting_title = "kickoff" |
|
|
|
|
|
project_dir = Path(temp_dir) / project_name / "meetings" |
|
|
project_dir.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
filename = f"{meeting_date}-{meeting_title}.md" |
|
|
file_path = project_dir / filename |
|
|
|
|
|
content = """# Meeting: Kickoff |
|
|
Date: 2025-01-30 |
|
|
Participants: Alice, Bob |
|
|
|
|
|
## Discussion |
|
|
Initial project discussion. |
|
|
|
|
|
## Action Items |
|
|
- [ ] Alice: Research models by 2025-02-05 |
|
|
|
|
|
## Blockers |
|
|
- Need database access |
|
|
""" |
|
|
with open(file_path, 'w') as f: |
|
|
f.write(content) |
|
|
|
|
|
assert file_path.exists() |
|
|
|
|
|
|
|
|
rag = ProjectRAG(Path(temp_dir)) |
|
|
rag.load_and_index() |
|
|
|
|
|
projects = rag.get_all_projects() |
|
|
assert project_name in projects |
|
|
|
|
|
|
|
|
action_items = rag.get_open_action_items(project=project_name) |
|
|
assert len(action_items) >= 1 |
|
|
|
|
|
def test_create_project_with_special_characters(self): |
|
|
"""Test project creation handles names properly.""" |
|
|
with tempfile.TemporaryDirectory() as temp_dir: |
|
|
|
|
|
project_name = "project_v2_2025" |
|
|
project_dir = Path(temp_dir) / project_name / "meetings" |
|
|
project_dir.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
assert project_dir.exists() |
|
|
|
|
|
|
|
|
class TestUploadMeeting: |
|
|
"""Test the upload meeting functionality.""" |
|
|
|
|
|
def test_meeting_file_naming(self): |
|
|
"""Test that meeting files are named correctly.""" |
|
|
meeting_date = "2025-01-30" |
|
|
meeting_title = "Sprint Planning" |
|
|
|
|
|
|
|
|
filename = f"{meeting_date}-{meeting_title.lower().replace(' ', '-')}.md" |
|
|
|
|
|
assert filename == "2025-01-30-sprint-planning.md" |
|
|
|
|
|
def test_meeting_file_naming_no_title(self): |
|
|
"""Test meeting file naming when no title provided.""" |
|
|
meeting_date = "2025-01-30" |
|
|
meeting_title = None |
|
|
|
|
|
filename = f"{meeting_date}-{meeting_title.lower().replace(' ', '-') if meeting_title else 'meeting'}.md" |
|
|
|
|
|
assert filename == "2025-01-30-meeting.md" |
|
|
|
|
|
def test_quantum_project_upload(self): |
|
|
"""Test uploading a meeting to quantum_computing project.""" |
|
|
with tempfile.TemporaryDirectory() as temp_dir: |
|
|
|
|
|
quantum_dir = Path(temp_dir) / "quantum_computing" / "meetings" |
|
|
quantum_dir.mkdir(parents=True) |
|
|
|
|
|
|
|
|
existing_meeting = """# Meeting: Previous Sync |
|
|
Date: 2025-01-25 |
|
|
Participants: Dr. Chen, Sarah |
|
|
|
|
|
## Discussion |
|
|
Previous work discussion. |
|
|
""" |
|
|
(quantum_dir / "2025-01-25-previous-sync.md").write_text(existing_meeting) |
|
|
|
|
|
|
|
|
new_meeting_content = """# Meeting: Decoder Optimization Review |
|
|
Date: 2025-01-30 |
|
|
Participants: Dr. Chen, Sarah, Marcus |
|
|
|
|
|
## Discussion |
|
|
Reviewed decoder performance. Current latency is 15ms, target is 1ms. |
|
|
Discussed MWPM vs Union-Find approaches. |
|
|
|
|
|
## Decisions |
|
|
- Switch to MWPM decoder for better accuracy |
|
|
- Target 1ms latency for production |
|
|
|
|
|
## Action Items |
|
|
- [ ] Dr. Chen: Implement MWPM decoder by 2025-02-07 |
|
|
- [ ] Sarah: Benchmark on IBM simulator by 2025-02-10 |
|
|
- [ ] Marcus: Order additional FPGA boards |
|
|
|
|
|
## Blockers |
|
|
- FPGA delivery delayed by 2 weeks |
|
|
- Need IBM quantum credits approval |
|
|
""" |
|
|
new_file = quantum_dir / "2025-01-30-decoder-optimization-review.md" |
|
|
new_file.write_text(new_meeting_content) |
|
|
|
|
|
|
|
|
meetings = list(quantum_dir.glob("*.md")) |
|
|
assert len(meetings) == 2 |
|
|
|
|
|
|
|
|
rag = ProjectRAG(Path(temp_dir)) |
|
|
rag.load_and_index() |
|
|
|
|
|
assert "quantum_computing" in rag.get_all_projects() |
|
|
assert len(rag.meetings) == 2 |
|
|
|
|
|
|
|
|
action_items = rag.get_open_action_items(project="quantum_computing") |
|
|
assert len(action_items) >= 3 |
|
|
|
|
|
|
|
|
blockers = rag.get_blockers(project="quantum_computing") |
|
|
assert len(blockers) >= 2 |
|
|
|
|
|
|
|
|
class TestRAGWithMultipleProjects: |
|
|
"""Test RAG functionality with multiple projects.""" |
|
|
|
|
|
def test_multiple_projects_isolation(self): |
|
|
"""Test that queries can be isolated to specific projects.""" |
|
|
with tempfile.TemporaryDirectory() as temp_dir: |
|
|
|
|
|
project1_dir = Path(temp_dir) / "quantum_computing" / "meetings" |
|
|
project2_dir = Path(temp_dir) / "covid_prediction" / "meetings" |
|
|
project1_dir.mkdir(parents=True) |
|
|
project2_dir.mkdir(parents=True) |
|
|
|
|
|
|
|
|
(project1_dir / "2025-01-30-quantum.md").write_text("""# Meeting: Quantum Sync |
|
|
Date: 2025-01-30 |
|
|
Participants: Dr. Chen |
|
|
|
|
|
## Action Items |
|
|
- [ ] Dr. Chen: Optimize decoder |
|
|
|
|
|
## Blockers |
|
|
- FPGA delivery delayed |
|
|
""") |
|
|
|
|
|
|
|
|
(project2_dir / "2025-01-30-covid.md").write_text("""# Meeting: COVID Analysis |
|
|
Date: 2025-01-30 |
|
|
Participants: Dr. Foster |
|
|
|
|
|
## Action Items |
|
|
- [ ] Dr. Foster: Review model accuracy |
|
|
|
|
|
## Blockers |
|
|
- Data quality issues with GISAID |
|
|
""") |
|
|
|
|
|
rag = ProjectRAG(Path(temp_dir)) |
|
|
rag.load_and_index() |
|
|
|
|
|
|
|
|
quantum_items = rag.get_open_action_items(project="quantum_computing") |
|
|
covid_items = rag.get_open_action_items(project="covid_prediction") |
|
|
|
|
|
assert any("decoder" in item['task'].lower() for item in quantum_items) |
|
|
assert any("model" in item['task'].lower() for item in covid_items) |
|
|
|
|
|
|
|
|
quantum_blockers = rag.get_blockers(project="quantum_computing") |
|
|
covid_blockers = rag.get_blockers(project="covid_prediction") |
|
|
|
|
|
assert any("fpga" in b['blocker'].lower() for b in quantum_blockers) |
|
|
assert any("gisaid" in b['blocker'].lower() for b in covid_blockers) |
|
|
|
|
|
def test_all_projects_query(self): |
|
|
"""Test querying across all projects.""" |
|
|
with tempfile.TemporaryDirectory() as temp_dir: |
|
|
|
|
|
for project in ["project_a", "project_b"]: |
|
|
project_dir = Path(temp_dir) / project / "meetings" |
|
|
project_dir.mkdir(parents=True) |
|
|
(project_dir / "2025-01-30-meeting.md").write_text(f"""# Meeting: {project} Sync |
|
|
Date: 2025-01-30 |
|
|
Participants: Team |
|
|
|
|
|
## Action Items |
|
|
- [ ] Someone: Task for {project} |
|
|
""") |
|
|
|
|
|
rag = ProjectRAG(Path(temp_dir)) |
|
|
rag.load_and_index() |
|
|
|
|
|
|
|
|
all_items = rag.get_open_action_items() |
|
|
assert len(all_items) >= 2 |
|
|
|
|
|
|
|
|
class TestIntegrationUploadMeeting: |
|
|
"""Integration tests for upload meeting with LLM (requires tokens).""" |
|
|
|
|
|
def test_structure_meeting_hf(self): |
|
|
"""Test meeting structuring with HuggingFace.""" |
|
|
hf_token = os.getenv("HF_TOKEN") |
|
|
if not hf_token: |
|
|
pytest.skip("HF_TOKEN not set") |
|
|
|
|
|
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint |
|
|
from langchain_core.messages import SystemMessage, HumanMessage |
|
|
|
|
|
endpoint = HuggingFaceEndpoint( |
|
|
repo_id="meta-llama/Llama-3.2-3B-Instruct", |
|
|
temperature=0.3, |
|
|
max_new_tokens=1024, |
|
|
huggingfacehub_api_token=hf_token, |
|
|
timeout=60 |
|
|
) |
|
|
llm = ChatHuggingFace(llm=endpoint) |
|
|
|
|
|
system_prompt = """Structure these meeting notes into markdown with sections: |
|
|
# Meeting: [title], Date:, Participants:, ## Discussion, ## Action Items, ## Blockers""" |
|
|
|
|
|
messages = [ |
|
|
SystemMessage(content=system_prompt), |
|
|
HumanMessage(content=f"Raw notes: {SAMPLE_RAW_NOTES_QUANTUM}") |
|
|
] |
|
|
|
|
|
response = llm.invoke(messages) |
|
|
assert response.content is not None |
|
|
assert len(response.content) > 100 |
|
|
|
|
|
assert "action" in response.content.lower() or "task" in response.content.lower() |
|
|
|
|
|
def test_structure_meeting_google(self): |
|
|
"""Test meeting structuring with Google.""" |
|
|
api_key = os.getenv("GOOGLE_API_KEY") |
|
|
if not api_key: |
|
|
pytest.skip("GOOGLE_API_KEY not set") |
|
|
|
|
|
from langchain_google_genai import ChatGoogleGenerativeAI |
|
|
from langchain_core.messages import SystemMessage, HumanMessage |
|
|
|
|
|
llm = ChatGoogleGenerativeAI( |
|
|
model="gemini-2.0-flash", |
|
|
temperature=0.3, |
|
|
google_api_key=api_key, |
|
|
timeout=60 |
|
|
) |
|
|
|
|
|
system_prompt = """Structure these meeting notes into markdown with sections: |
|
|
# Meeting: [title], Date:, Participants:, ## Discussion, ## Action Items, ## Blockers""" |
|
|
|
|
|
messages = [ |
|
|
SystemMessage(content=system_prompt), |
|
|
HumanMessage(content=f"Raw notes: {SAMPLE_RAW_NOTES_NEW_PROJECT}") |
|
|
] |
|
|
|
|
|
response = llm.invoke(messages) |
|
|
assert response.content is not None |
|
|
assert len(response.content) > 100 |
|
|
|