Spaces:
Sleeping
Sleeping
Update demo with latest codebase changes
Browse filesSync all files from main sherlock repository including:
- Enhanced app.py with response caching, Google Gemini support, and LangSmith observability
- Updated RAG system with improved vector store handling
- Enhanced agent with better error handling
- Added comprehensive test suite
- Updated documentation and requirements
- Added CITATION.cff and assets structure
Note: Binary assets excluded (will add via Git LFS)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- .gitignore +26 -1
- CITATION.cff +17 -0
- README.md +298 -14
- app.py +573 -59
- assets/favicon/browserconfig.xml +2 -0
- assets/favicon/manifest.json +41 -0
- data/covid_prediction/meetings/2025-01-10-project-initiation.md +33 -0
- data/covid_prediction/meetings/2025-01-17-model-architecture-review.md +41 -0
- data/covid_prediction/meetings/2025-01-24-training-results.md +50 -0
- data/quantum_computing/meetings/2025-01-08-project-kickoff.md +31 -0
- data/quantum_computing/meetings/2025-01-15-week2-technical-review.md +33 -0
- data/quantum_computing/meetings/2025-01-22-decoder-benchmark.md +38 -0
- data/quantum_computing/meetings/2025-12-01-test.md +18 -0
- requirements.txt +14 -3
- src/agent.py +129 -15
- src/rag.py +53 -4
- tests/__init__.py +1 -0
- tests/conftest.py +70 -0
- tests/test_app.py +408 -0
- tests/test_evaluation.py +197 -0
- tests/test_integration.py +151 -0
- tests/test_parsers.py +97 -0
- tests/test_rag.py +96 -0
.gitignore
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
|
|
| 1 |
.env
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
*.pyc
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
.venv/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
chroma_db/
|
| 6 |
*.db
|
| 7 |
-
.DS_Store
|
| 8 |
*.log
|
| 9 |
.pytest_cache/
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment
|
| 2 |
.env
|
| 3 |
+
.env.local
|
| 4 |
+
.env.*.local
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
__pycache__/
|
| 8 |
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
.Python
|
| 12 |
+
*.so
|
| 13 |
.venv/
|
| 14 |
+
venv/
|
| 15 |
+
ENV/
|
| 16 |
+
|
| 17 |
+
# IDE
|
| 18 |
+
.idea/
|
| 19 |
+
.vscode/
|
| 20 |
+
*.swp
|
| 21 |
+
*.swo
|
| 22 |
+
|
| 23 |
+
# OS
|
| 24 |
+
.DS_Store
|
| 25 |
+
Thumbs.db
|
| 26 |
+
|
| 27 |
+
# Project specific
|
| 28 |
chroma_db/
|
| 29 |
*.db
|
|
|
|
| 30 |
*.log
|
| 31 |
.pytest_cache/
|
| 32 |
+
dist/
|
| 33 |
+
build/
|
| 34 |
+
*.egg-info/
|
CITATION.cff
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
cff-version: 1.2.0
|
| 2 |
+
message: "If you use this software, please cite it as below."
|
| 3 |
+
title: "Sherlock Project Assistant"
|
| 4 |
+
type: software
|
| 5 |
+
authors:
|
| 6 |
+
- family-names: "Cajas Ordóñez"
|
| 7 |
+
given-names: "Sebastián Andrés"
|
| 8 |
+
repository-code: "https://github.com/sebasmos/sherlock"
|
| 9 |
+
url: "https://huggingface.co/spaces/sebasmos/sherlock-project-assistant"
|
| 10 |
+
license: CC-BY-NC-SA-4.0
|
| 11 |
+
keywords:
|
| 12 |
+
- rag
|
| 13 |
+
- langchain
|
| 14 |
+
- langgraph
|
| 15 |
+
- gradio
|
| 16 |
+
- chromadb
|
| 17 |
+
- meeting-assistant
|
README.md
CHANGED
|
@@ -1,20 +1,304 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
colorTo: purple
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: "4.20.0"
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
An AI-powered project assistant that helps you manage and query your project meetings and documentation.
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
## Features
|
| 17 |
|
| 18 |
-
- RAG-
|
| 19 |
-
-
|
| 20 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<img src="assets/logo-transparent-bg.png" alt="Sherlock Logo" width="120">
|
| 3 |
+
<h1>Sherlock Project Assistant</h1>
|
| 4 |
+
<p><em>Your intelligent assistant for managing multiple projects through meeting summaries</em></p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
+
[](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
| 7 |
+
[](https://github.com/sebasmos/sherlock)
|
| 8 |
+
[](https://huggingface.co/spaces/sebasmos/sherlock-project-assistant)
|
| 9 |
+
</div>
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
|
| 13 |
An AI-powered project assistant that helps you manage and query your project meetings and documentation.
|
| 14 |
|
| 15 |
+
## Table of Contents
|
| 16 |
+
|
| 17 |
+
- [Demo](#demo)
|
| 18 |
+
- [Features](#features)
|
| 19 |
+
- [System Architecture](#system-architecture)
|
| 20 |
+
- [Tech Stack](#tech-stack)
|
| 21 |
+
- [Agentic Capabilities](#agentic-capabilities)
|
| 22 |
+
- [Quick Start](#quick-start)
|
| 23 |
+
- [Usage](#usage)
|
| 24 |
+
- [LLM Providers](#llm-providers)
|
| 25 |
+
- [Observability](#observability-langsmith)
|
| 26 |
+
- [Performance Features](#performance-features)
|
| 27 |
+
- [Agent Evaluation](#agent-evaluation)
|
| 28 |
+
- [Testing](#testing)
|
| 29 |
+
- [Project Structure](#project-structure)
|
| 30 |
+
- [Unsorted To-Dos](#unsorted-to-dos)
|
| 31 |
+
|
| 32 |
+
## Demo
|
| 33 |
+
|
| 34 |
+
🚀 [Try the live demo here](https://huggingface.co/spaces/sebasmos/sherlock-project-assistant)
|
| 35 |
+
|
| 36 |
## Features
|
| 37 |
|
| 38 |
+
- **RAG-powered Q&A** - Ask questions about your projects using ChromaDB vector search
|
| 39 |
+
- **LangGraph AI Agent** - Intelligent query routing for action items, blockers, and status
|
| 40 |
+
- **Multi-project support** - Manage and filter across multiple projects
|
| 41 |
+
- **Meeting structuring** - Upload raw notes and get AI-structured markdown
|
| 42 |
+
- **Action item tracking** - Track open/completed tasks with assignees and deadlines
|
| 43 |
+
- **Blocker & decision tracking** - Surface blockers and key decisions from meetings
|
| 44 |
+
- **Multiple LLM Providers** - Choose between HuggingFace (free) or Google Gemini (paid)
|
| 45 |
+
- **Meeting summary generation** - Get comprehensive summaries with key takeaways
|
| 46 |
+
- **Trend analysis** - Analyze patterns across meetings: recurring topics, blocker trends, progress
|
| 47 |
+
- **LangSmith Observability** - Trace LLM calls, monitor latency, token usage, and errors
|
| 48 |
+
- **Agent Evaluation** - Automated quality testing with keyword matching and latency metrics
|
| 49 |
+
- **Streaming Responses** - See LLM output in real-time as it generates (better UX)
|
| 50 |
+
- **Response Caching** - Faster repeated queries with 5-minute TTL cache (lower API costs)
|
| 51 |
+
- **Export Chat** - Download your conversation history as PDF
|
| 52 |
+
|
| 53 |
+
## System Architecture
|
| 54 |
+
|
| 55 |
+

|
| 56 |
+
|
| 57 |
+
## Tech Stack
|
| 58 |
+
|
| 59 |
+
| Category | Technology | Purpose |
|
| 60 |
+
|----------|------------|---------|
|
| 61 |
+
| **Frontend** | Gradio 4.44 | Web UI framework |
|
| 62 |
+
| **LLM Framework** | LangChain | LLM orchestration |
|
| 63 |
+
| **Agent Framework** | LangGraph | State machine for agent routing |
|
| 64 |
+
| **Vector Store** | ChromaDB | Persistent vector storage |
|
| 65 |
+
| **Embeddings** | Sentence Transformers | Text embeddings (all-MiniLM-L6-v2) |
|
| 66 |
+
| **LLM (Free)** | HuggingFace Inference | Llama 3.2 3B Instruct |
|
| 67 |
+
| **LLM (Paid)** | Google Generative AI | Gemini 2.5 Flash Lite |
|
| 68 |
+
| **Data Models** | Pydantic | Data validation |
|
| 69 |
+
| **Testing** | Pytest | Unit and integration tests |
|
| 70 |
+
| **Observability** | LangSmith | LLM tracing and monitoring |
|
| 71 |
+
|
| 72 |
+
## Agentic Capabilities
|
| 73 |
+
|
| 74 |
+
| Capability | Description | Trigger Keywords |
|
| 75 |
+
|------------|-------------|------------------|
|
| 76 |
+
| **Query Analysis** | Understands user intent and extracts project context | All queries |
|
| 77 |
+
| **Context Retrieval** | Semantic search across meeting notes | All queries |
|
| 78 |
+
| **Action Item Extraction** | Surfaces open tasks with assignees and deadlines | "action item", "todo", "task", "what's next", "what should" |
|
| 79 |
+
| **Blocker Detection** | Identifies and lists current blockers | "blocker", "issue", "problem", "stuck" |
|
| 80 |
+
| **Decision Tracking** | Retrieves decisions made in meetings | "decision", "decided", "agreed" |
|
| 81 |
+
| **Project Filtering** | Scopes queries to specific projects | Mention project name in query |
|
| 82 |
+
| **Meeting Structuring** | Converts raw notes to formatted markdown | Upload tab |
|
| 83 |
+
|
| 84 |
+
## Quick Start
|
| 85 |
+
|
| 86 |
+
### Using uv (recommended)
|
| 87 |
+
|
| 88 |
+
```bash
|
| 89 |
+
# Clone the repository
|
| 90 |
+
git clone https://github.com/sebasmos/sherlock.git
|
| 91 |
+
cd sherlock
|
| 92 |
+
|
| 93 |
+
# Create venv and install dependencies
|
| 94 |
+
uv venv --python 3.10
|
| 95 |
+
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
| 96 |
+
uv pip install -r requirements.txt
|
| 97 |
+
|
| 98 |
+
# Run the app
|
| 99 |
+
python app.py
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### Using pip
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
# Clone the repository
|
| 106 |
+
git clone https://github.com/sebasmos/sherlock.git
|
| 107 |
+
cd sherlock
|
| 108 |
+
|
| 109 |
+
# Create virtual environment (Python 3.10)
|
| 110 |
+
python3.10 -m venv venv
|
| 111 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 112 |
+
|
| 113 |
+
# Install dependencies
|
| 114 |
+
pip install -r requirements.txt
|
| 115 |
+
|
| 116 |
+
# Run the app
|
| 117 |
+
python app.py
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
The app will be available at http://localhost:7860
|
| 121 |
+
|
| 122 |
+
## Usage
|
| 123 |
+
|
| 124 |
+
1. Choose your LLM provider and enter your API token
|
| 125 |
+
2. Add your meeting notes to `data/your_project/meetings/*.md`
|
| 126 |
+
3. Start asking questions about your projects
|
| 127 |
+
|
| 128 |
+
### Meeting Notes Format
|
| 129 |
+
|
| 130 |
+
```markdown
|
| 131 |
+
# Meeting: Sprint Planning
|
| 132 |
+
Date: 2025-01-15
|
| 133 |
+
Participants: Alice, Bob
|
| 134 |
+
|
| 135 |
+
## Discussion
|
| 136 |
+
Key points discussed...
|
| 137 |
+
|
| 138 |
+
## Decisions
|
| 139 |
+
- Decision 1
|
| 140 |
+
- Decision 2
|
| 141 |
+
|
| 142 |
+
## Action Items
|
| 143 |
+
- [ ] Alice: Implement login by Jan 20
|
| 144 |
+
- [x] Bob: Review PR (completed)
|
| 145 |
+
|
| 146 |
+
## Blockers
|
| 147 |
+
- Waiting for API credentials
|
| 148 |
+
```
|
| 149 |
+
|
| 150 |
+
## LLM Providers
|
| 151 |
+
|
| 152 |
+
### HuggingFace (Free)
|
| 153 |
+
|
| 154 |
+
| Property | Value |
|
| 155 |
+
|----------|-------|
|
| 156 |
+
| **Model** | Llama 3.2 3B Instruct |
|
| 157 |
+
| **Cost** | Free (rate limited) |
|
| 158 |
+
| **Token URL** | [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) |
|
| 159 |
+
| **Setup** | 1. Create account → 2. New token → 3. Select "Read" permission |
|
| 160 |
+
|
| 161 |
+
### Google AI (Paid)
|
| 162 |
+
|
| 163 |
+
| Property | Value |
|
| 164 |
+
|----------|-------|
|
| 165 |
+
| **Model** | Gemini 2.5 Flash Lite |
|
| 166 |
+
| **Cost** | Pay-per-use |
|
| 167 |
+
| **API Key URL** | [aistudio.google.com/apikey](https://aistudio.google.com/apikey) |
|
| 168 |
+
| **Setup** | 1. Create project → 2. Enable API → 3. Create API key |
|
| 169 |
+
|
| 170 |
+
## Observability (LangSmith)
|
| 171 |
+
|
| 172 |
+
Enable LLM tracing and monitoring with [LangSmith](https://smith.langchain.com):
|
| 173 |
+
|
| 174 |
+
| Property | Value |
|
| 175 |
+
|----------|-------|
|
| 176 |
+
| **Dashboard** | [smith.langchain.com](https://smith.langchain.com) |
|
| 177 |
+
| **Cost** | Free tier available |
|
| 178 |
+
| **Features** | Trace LLM calls, latency, token usage, errors |
|
| 179 |
+
|
| 180 |
+
### Setup
|
| 181 |
+
|
| 182 |
+
1. Create account at [smith.langchain.com](https://smith.langchain.com)
|
| 183 |
+
2. Get your API key from Settings
|
| 184 |
+
3. Set environment variables:
|
| 185 |
+
|
| 186 |
+
```bash
|
| 187 |
+
export LANGCHAIN_API_KEY=your_langsmith_api_key
|
| 188 |
+
export LANGCHAIN_PROJECT=sherlock # optional, defaults to "sherlock"
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
Or add to `.env` file:
|
| 192 |
+
|
| 193 |
+
```
|
| 194 |
+
LANGCHAIN_API_KEY=your_langsmith_api_key
|
| 195 |
+
LANGCHAIN_PROJECT=sherlock
|
| 196 |
+
```
|
| 197 |
+
|
| 198 |
+
Once configured, all LLM calls are automatically traced and visible in the LangSmith dashboard.
|
| 199 |
+
|
| 200 |
+
## Performance Features
|
| 201 |
+
|
| 202 |
+
### Streaming Responses
|
| 203 |
+
|
| 204 |
+
LLM responses are streamed token-by-token for a better user experience. You see the answer as it's being generated, reducing perceived latency.
|
| 205 |
+
|
| 206 |
+
### Response Caching
|
| 207 |
+
|
| 208 |
+
Repeated queries are cached for 5 minutes to reduce API costs and improve response times:
|
| 209 |
+
|
| 210 |
+
| Property | Value |
|
| 211 |
+
|----------|-------|
|
| 212 |
+
| **TTL** | 5 minutes |
|
| 213 |
+
| **Cache Key** | Query + Project + Provider |
|
| 214 |
+
| **Indicator** | "_⚡ Cached response_" shown for cached answers |
|
| 215 |
+
|
| 216 |
+
### Chat Export
|
| 217 |
+
|
| 218 |
+
Export your conversation history as a PDF file:
|
| 219 |
+
|
| 220 |
+
1. Click the **📥 Export** button in the chat interface
|
| 221 |
+
2. Download the `.pdf` file with all Q&A pairs
|
| 222 |
+
3. Includes project name, timestamp, and nicely formatted conversation
|
| 223 |
+
|
| 224 |
+
## Agent Evaluation
|
| 225 |
+
|
| 226 |
+
Automated evaluation measures agent quality across different query types.
|
| 227 |
+
|
| 228 |
+
| Metric | Value |
|
| 229 |
+
|--------|-------|
|
| 230 |
+
| **Test Cases** | 5 |
|
| 231 |
+
| **Pass Rate** | 100% |
|
| 232 |
+
| **Keyword Match** | 68% |
|
| 233 |
+
| **Avg Latency** | 5.2s |
|
| 234 |
+
| **Avg Response** | 1404 chars |
|
| 235 |
+
|
| 236 |
+
Run evaluation:
|
| 237 |
+
```bash
|
| 238 |
+
GOOGLE_API_KEY=your_key pytest tests/test_evaluation.py -v -s
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
## Testing
|
| 242 |
+
|
| 243 |
+
Run all tests:
|
| 244 |
+
```bash
|
| 245 |
+
HF_TOKEN=your_token GOOGLE_API_KEY=your_key pytest tests/ -v
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
| File | Description |
|
| 249 |
+
|------|-------------|
|
| 250 |
+
| `test_parsers.py` | Date & action item parsing |
|
| 251 |
+
| `test_rag.py` | RAG indexing & search |
|
| 252 |
+
| `test_app.py` | Upload & project management |
|
| 253 |
+
| `test_integration.py` | LLM provider tests |
|
| 254 |
+
| `test_evaluation.py` | Agent quality metrics |
|
| 255 |
+
|
| 256 |
+
## Project Structure
|
| 257 |
+
|
| 258 |
+
```
|
| 259 |
+
sherlock/
|
| 260 |
+
├── app.py # Main Gradio application
|
| 261 |
+
├── requirements.txt # Python dependencies
|
| 262 |
+
├── README.md # This file
|
| 263 |
+
├── assets/
|
| 264 |
+
│ ├── logo.png # Project logo
|
| 265 |
+
│ ├── logo-transparent-bg.png
|
| 266 |
+
│ └── favicon/ # Favicon assets
|
| 267 |
+
├── src/
|
| 268 |
+
│ ├── __init__.py
|
| 269 |
+
│ ├── agent.py # LangGraph AI agent
|
| 270 |
+
│ ├── rag.py # ChromaDB RAG system
|
| 271 |
+
│ └── parsers.py # Meeting note parsers
|
| 272 |
+
├── tests/
|
| 273 |
+
│ ├── conftest.py # Pytest fixtures
|
| 274 |
+
│ ├── test_parsers.py # Parser tests
|
| 275 |
+
│ ├── test_rag.py # RAG tests
|
| 276 |
+
│ ├── test_app.py # Upload meeting & project tests
|
| 277 |
+
│ ├── test_integration.py # LLM provider tests
|
| 278 |
+
│ └── test_evaluation.py # Agent quality evaluation
|
| 279 |
+
└── data/ # Sample projects included
|
| 280 |
+
├── quantum_computing/
|
| 281 |
+
│ └── meetings/ # Quantum Error Correction research
|
| 282 |
+
└── covid_prediction/
|
| 283 |
+
└── meetings/ # COVID-19 Variant Prediction ML project
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
## Sample Projects
|
| 287 |
+
|
| 288 |
+
The repository includes two realistic research project examples:
|
| 289 |
+
|
| 290 |
+
### Quantum Computing (Quantum Error Correction)
|
| 291 |
+
- **Team**: 5 researchers (physics, CS, mathematics)
|
| 292 |
+
- **Topics**: Surface codes, IBM Quantum hardware, decoder algorithms
|
| 293 |
+
- **Example queries**: "What's the decoder latency issue?", "What hardware access do we have?"
|
| 294 |
+
|
| 295 |
+
### COVID-19 Prediction (Variant Forecasting)
|
| 296 |
+
- **Team**: 5 researchers (epidemiology, ML, bioinformatics)
|
| 297 |
+
- **Topics**: ESM-2 model, GISAID data, CDC collaboration
|
| 298 |
+
- **Example queries**: "What's our model accuracy?", "What are the data quality issues?"
|
| 299 |
+
|
| 300 |
+
## Unsorted To-Dos
|
| 301 |
+
|
| 302 |
+
- [ ] Add support for more LLM providers (OpenAI, Anthropic, Ollama)
|
| 303 |
+
- [ ] Implement meeting calendar integration (Google Calendar, Outlook)
|
| 304 |
+
- [ ] Add user authentication for multi-user support
|
app.py
CHANGED
|
@@ -4,16 +4,67 @@ Gradio app for AI Project Assistant.
|
|
| 4 |
import gradio as gr
|
| 5 |
from pathlib import Path
|
| 6 |
import os
|
|
|
|
|
|
|
| 7 |
from datetime import datetime
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
from src.rag import ProjectRAG
|
| 10 |
from src.agent import ProjectAgent
|
| 11 |
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
|
|
|
| 12 |
from langchain_core.messages import SystemMessage, HumanMessage
|
| 13 |
|
| 14 |
# Load environment variables
|
| 15 |
load_dotenv()
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
# Global state - Initialize RAG only (not agent)
|
| 18 |
rag = None
|
| 19 |
|
|
@@ -34,10 +85,13 @@ def initialize_rag():
|
|
| 34 |
# Initialize RAG on module load
|
| 35 |
initialize_rag()
|
| 36 |
|
| 37 |
-
def chat(message, history, project_filter,
|
| 38 |
-
"""Process chat message."""
|
| 39 |
-
if not
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
| 41 |
return
|
| 42 |
|
| 43 |
if not rag:
|
|
@@ -45,11 +99,22 @@ def chat(message, history, project_filter, hf_token):
|
|
| 45 |
return
|
| 46 |
|
| 47 |
try:
|
| 48 |
-
#
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
# Add project context if specified
|
| 55 |
if project_filter and project_filter != "All Projects":
|
|
@@ -57,10 +122,31 @@ def chat(message, history, project_filter, hf_token):
|
|
| 57 |
else:
|
| 58 |
enhanced_prompt = message
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
except Exception as e:
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
def get_projects():
|
| 66 |
"""Get list of projects."""
|
|
@@ -70,23 +156,82 @@ def get_projects():
|
|
| 70 |
projects = rag.get_all_projects()
|
| 71 |
return ["All Projects"] + projects
|
| 72 |
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
"""Structure meeting notes using AI."""
|
| 75 |
-
if not
|
| 76 |
-
return "❌ Please enter your
|
| 77 |
|
| 78 |
if not project_name or not meeting_text:
|
| 79 |
return "❌ Please provide both project name and meeting notes"
|
| 80 |
|
| 81 |
try:
|
| 82 |
-
#
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
system_prompt = """You are a meeting notes structuring assistant.
|
| 92 |
Convert unstructured meeting notes into a well-formatted markdown document with these sections:
|
|
@@ -137,7 +282,15 @@ Meeting Details:
|
|
| 137 |
return f"✅ Meeting structured and saved to `{file_path}`\n\n---\n\n{structured_md}"
|
| 138 |
|
| 139 |
except Exception as e:
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
|
| 142 |
# Create Gradio interface with custom CSS
|
| 143 |
custom_css = """
|
|
@@ -183,45 +336,121 @@ custom_css = """
|
|
| 183 |
}
|
| 184 |
"""
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
""")
|
| 192 |
|
| 193 |
-
# Global
|
| 194 |
gr.Markdown("### 🔑 Authentication")
|
| 195 |
with gr.Row():
|
| 196 |
-
with gr.Column(scale=
|
| 197 |
-
|
| 198 |
-
label="
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
type="password"
|
| 201 |
)
|
| 202 |
with gr.Column(scale=2):
|
| 203 |
-
gr.Markdown("""
|
| 204 |
-
**
|
| 205 |
1. Visit [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
| 206 |
-
2. Click "New token"
|
| 207 |
-
3. Select "Read" permission
|
| 208 |
-
4. Copy and paste it here
|
| 209 |
""")
|
| 210 |
|
| 211 |
with gr.Row():
|
| 212 |
submit_token_btn = gr.Button("Submit Token", variant="primary")
|
| 213 |
token_status = gr.Markdown("", elem_classes="token-status")
|
| 214 |
|
| 215 |
-
def
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
else:
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
submit_token_btn.click(
|
| 222 |
fn=validate_token,
|
| 223 |
-
inputs=[
|
| 224 |
-
outputs=[token_status]
|
| 225 |
)
|
| 226 |
|
| 227 |
# Main tabs
|
|
@@ -230,6 +459,10 @@ with gr.Blocks(title="Sherlock: AI Project Assistant", theme=gr.themes.Soft(), c
|
|
| 230 |
with gr.Tab("💬 Chat"):
|
| 231 |
gr.Markdown("### Ask questions about your projects")
|
| 232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
# Project selection dropdown
|
| 234 |
project_dropdown = gr.Dropdown(
|
| 235 |
label="Select Project",
|
|
@@ -267,41 +500,106 @@ with gr.Blocks(title="Sherlock: AI Project Assistant", theme=gr.themes.Soft(), c
|
|
| 267 |
msg = gr.Textbox(
|
| 268 |
label="Your Message",
|
| 269 |
placeholder="What are the open action items?",
|
| 270 |
-
lines=
|
| 271 |
show_label=False
|
| 272 |
)
|
| 273 |
|
| 274 |
with gr.Row():
|
| 275 |
submit_btn = gr.Button("Send", variant="primary", scale=1)
|
| 276 |
clear_btn = gr.Button("Clear", scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
if not message:
|
| 280 |
-
return chat_history, ""
|
| 281 |
|
| 282 |
# Get bot response
|
| 283 |
bot_message = ""
|
| 284 |
-
for response_chunk in chat(message, chat_history, project, token):
|
| 285 |
bot_message = response_chunk
|
| 286 |
|
| 287 |
# Add to history as tuple
|
| 288 |
chat_history.append((message, bot_message))
|
| 289 |
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
submit_btn.click(
|
| 293 |
fn=respond,
|
| 294 |
-
inputs=[msg, chatbot, project_dropdown,
|
| 295 |
-
outputs=[chatbot, msg]
|
| 296 |
)
|
| 297 |
|
| 298 |
msg.submit(
|
| 299 |
fn=respond,
|
| 300 |
-
inputs=[msg, chatbot, project_dropdown,
|
| 301 |
-
outputs=[chatbot, msg]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
)
|
| 303 |
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
# Upload Meeting tab
|
| 307 |
with gr.Tab("📤 Upload Meeting"):
|
|
@@ -370,19 +668,235 @@ Charlie is blocked waiting for API credentials.""",
|
|
| 370 |
structure_btn = gr.Button("🤖 Structure Meeting with AI", variant="primary")
|
| 371 |
structure_output = gr.Markdown(label="Structured Output")
|
| 372 |
|
| 373 |
-
def structure_meeting_wrapper(mode, existing_proj, new_proj, title, date, participants, text, token):
|
| 374 |
"""Wrapper to handle both project modes."""
|
|
|
|
| 375 |
# Determine which project name to use
|
| 376 |
project_name = existing_proj if mode == "Use Existing Project" else new_proj
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
structure_btn.click(
|
| 380 |
fn=structure_meeting_wrapper,
|
| 381 |
-
inputs=[project_mode, existing_project, new_project, upload_title, upload_date, upload_participants, upload_text,
|
| 382 |
-
outputs=structure_output
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 383 |
)
|
| 384 |
|
| 385 |
|
| 386 |
# Launch
|
| 387 |
if __name__ == "__main__":
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import gradio as gr
|
| 5 |
from pathlib import Path
|
| 6 |
import os
|
| 7 |
+
import hashlib
|
| 8 |
+
import time
|
| 9 |
from datetime import datetime
|
| 10 |
from dotenv import load_dotenv
|
| 11 |
from src.rag import ProjectRAG
|
| 12 |
from src.agent import ProjectAgent
|
| 13 |
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 14 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 15 |
from langchain_core.messages import SystemMessage, HumanMessage
|
| 16 |
|
| 17 |
# Load environment variables
|
| 18 |
load_dotenv()
|
| 19 |
|
| 20 |
+
|
| 21 |
+
# Response Cache with TTL
|
| 22 |
+
class ResponseCache:
|
| 23 |
+
"""Simple in-memory cache with time-to-live for LLM responses."""
|
| 24 |
+
|
| 25 |
+
def __init__(self, ttl_seconds: int = 300):
|
| 26 |
+
"""Initialize cache with TTL in seconds (default 5 minutes)."""
|
| 27 |
+
self.cache = {}
|
| 28 |
+
self.ttl = ttl_seconds
|
| 29 |
+
|
| 30 |
+
def _make_key(self, query: str, project: str, provider: str) -> str:
|
| 31 |
+
"""Create a unique cache key."""
|
| 32 |
+
key_str = f"{query}|{project}|{provider}"
|
| 33 |
+
return hashlib.md5(key_str.encode()).hexdigest()
|
| 34 |
+
|
| 35 |
+
def get(self, query: str, project: str, provider: str) -> str | None:
|
| 36 |
+
"""Get cached response if exists and not expired."""
|
| 37 |
+
key = self._make_key(query, project, provider)
|
| 38 |
+
if key in self.cache:
|
| 39 |
+
entry = self.cache[key]
|
| 40 |
+
if time.time() - entry["timestamp"] < self.ttl:
|
| 41 |
+
return entry["response"]
|
| 42 |
+
else:
|
| 43 |
+
del self.cache[key]
|
| 44 |
+
return None
|
| 45 |
+
|
| 46 |
+
def set(self, query: str, project: str, provider: str, response: str):
|
| 47 |
+
"""Cache a response."""
|
| 48 |
+
key = self._make_key(query, project, provider)
|
| 49 |
+
self.cache[key] = {
|
| 50 |
+
"response": response,
|
| 51 |
+
"timestamp": time.time()
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
def clear(self):
|
| 55 |
+
"""Clear all cached responses."""
|
| 56 |
+
self.cache = {}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# Initialize response cache (5 minute TTL)
|
| 60 |
+
response_cache = ResponseCache(ttl_seconds=300)
|
| 61 |
+
|
| 62 |
+
# LangSmith Observability - Enable tracing if API key is set
|
| 63 |
+
if os.getenv("LANGCHAIN_API_KEY"):
|
| 64 |
+
os.environ["LANGCHAIN_TRACING_V2"] = "true"
|
| 65 |
+
os.environ["LANGCHAIN_PROJECT"] = os.getenv("LANGCHAIN_PROJECT", "sherlock")
|
| 66 |
+
print("LangSmith tracing enabled")
|
| 67 |
+
|
| 68 |
# Global state - Initialize RAG only (not agent)
|
| 69 |
rag = None
|
| 70 |
|
|
|
|
| 85 |
# Initialize RAG on module load
|
| 86 |
initialize_rag()
|
| 87 |
|
| 88 |
+
def chat(message, history, project_filter, provider, api_token, use_streaming=True):
|
| 89 |
+
"""Process chat message with streaming and caching support."""
|
| 90 |
+
if not api_token or api_token.strip() == "":
|
| 91 |
+
if provider == "HuggingFace (Free)":
|
| 92 |
+
yield "⚠️ Please enter your HuggingFace token first (get one at https://huggingface.co/settings/tokens)"
|
| 93 |
+
else:
|
| 94 |
+
yield "⚠️ Please enter your Google API key first (get one at https://aistudio.google.com/apikey)"
|
| 95 |
return
|
| 96 |
|
| 97 |
if not rag:
|
|
|
|
| 99 |
return
|
| 100 |
|
| 101 |
try:
|
| 102 |
+
# Check cache first
|
| 103 |
+
project_key = project_filter if project_filter and project_filter != "All Projects" else "all"
|
| 104 |
+
provider_key = "hf" if provider == "HuggingFace (Free)" else "google"
|
| 105 |
+
cached_response = response_cache.get(message, project_key, provider_key)
|
| 106 |
|
| 107 |
+
if cached_response:
|
| 108 |
+
yield f"{cached_response}\n\n_⚡ Cached response_"
|
| 109 |
+
return
|
| 110 |
+
|
| 111 |
+
# Set token in environment for this request
|
| 112 |
+
if provider == "HuggingFace (Free)":
|
| 113 |
+
os.environ["HF_TOKEN"] = api_token.strip()
|
| 114 |
+
agent = ProjectAgent(rag, provider="huggingface")
|
| 115 |
+
else:
|
| 116 |
+
os.environ["GOOGLE_API_KEY"] = api_token.strip()
|
| 117 |
+
agent = ProjectAgent(rag, provider="google")
|
| 118 |
|
| 119 |
# Add project context if specified
|
| 120 |
if project_filter and project_filter != "All Projects":
|
|
|
|
| 122 |
else:
|
| 123 |
enhanced_prompt = message
|
| 124 |
|
| 125 |
+
# Use streaming if enabled
|
| 126 |
+
if use_streaming:
|
| 127 |
+
final_response = ""
|
| 128 |
+
for response_chunk in agent.stream_query(enhanced_prompt):
|
| 129 |
+
final_response = response_chunk
|
| 130 |
+
yield response_chunk
|
| 131 |
+
# Cache the final response
|
| 132 |
+
response_cache.set(message, project_key, provider_key, final_response)
|
| 133 |
+
else:
|
| 134 |
+
response = agent.query(enhanced_prompt)
|
| 135 |
+
response_cache.set(message, project_key, provider_key, response)
|
| 136 |
+
yield response
|
| 137 |
+
|
| 138 |
except Exception as e:
|
| 139 |
+
error_msg = str(e).lower()
|
| 140 |
+
if "401" in error_msg or "unauthorized" in error_msg or "invalid" in error_msg:
|
| 141 |
+
yield "❌ **Invalid API Token**\n\nYour token appears to be invalid or expired. Please check:\n- Token is correctly copied (no extra spaces)\n- Token has proper permissions\n- Token is not expired"
|
| 142 |
+
elif "403" in error_msg or "forbidden" in error_msg:
|
| 143 |
+
yield "❌ **Access Denied**\n\nYour token doesn't have permission to access this model. Please ensure:\n- HuggingFace: Token has 'Read' permission\n- Google: API is enabled in your project"
|
| 144 |
+
elif "rate" in error_msg or "quota" in error_msg or "limit" in error_msg:
|
| 145 |
+
yield "❌ **Rate Limit Exceeded**\n\nYou've hit the API rate limit. Please:\n- Wait a few minutes and try again\n- Consider upgrading to a paid plan"
|
| 146 |
+
elif "timeout" in error_msg or "timed out" in error_msg:
|
| 147 |
+
yield "❌ **Request Timeout**\n\nThe request took too long. Please try again."
|
| 148 |
+
else:
|
| 149 |
+
yield f"❌ **Error**: {str(e)}\n\nPlease verify your API token is valid and try again."
|
| 150 |
|
| 151 |
def get_projects():
|
| 152 |
"""Get list of projects."""
|
|
|
|
| 156 |
projects = rag.get_all_projects()
|
| 157 |
return ["All Projects"] + projects
|
| 158 |
|
| 159 |
+
|
| 160 |
+
def export_chat_to_pdf(chat_history, project):
|
| 161 |
+
"""Export chat history to PDF format and return as downloadable file."""
|
| 162 |
+
if not chat_history:
|
| 163 |
+
return None
|
| 164 |
+
|
| 165 |
+
from fpdf import FPDF
|
| 166 |
+
|
| 167 |
+
# Create PDF
|
| 168 |
+
pdf = FPDF()
|
| 169 |
+
pdf.set_auto_page_break(auto=True, margin=15)
|
| 170 |
+
pdf.add_page()
|
| 171 |
+
|
| 172 |
+
# Title
|
| 173 |
+
pdf.set_font("Helvetica", "B", 16)
|
| 174 |
+
pdf.cell(0, 10, "Sherlock Chat Export", ln=True, align="C")
|
| 175 |
+
pdf.ln(5)
|
| 176 |
+
|
| 177 |
+
# Metadata
|
| 178 |
+
pdf.set_font("Helvetica", "", 10)
|
| 179 |
+
pdf.cell(0, 6, f"Project: {project}", ln=True)
|
| 180 |
+
pdf.cell(0, 6, f"Exported: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", ln=True)
|
| 181 |
+
pdf.ln(10)
|
| 182 |
+
|
| 183 |
+
# Chat content
|
| 184 |
+
for i, (user_msg, bot_msg) in enumerate(chat_history, 1):
|
| 185 |
+
# Question header
|
| 186 |
+
pdf.set_font("Helvetica", "B", 11)
|
| 187 |
+
pdf.set_fill_color(230, 230, 250)
|
| 188 |
+
pdf.multi_cell(0, 8, f"Q{i}: {user_msg}", fill=True)
|
| 189 |
+
pdf.ln(2)
|
| 190 |
+
|
| 191 |
+
# Answer
|
| 192 |
+
pdf.set_font("Helvetica", "", 10)
|
| 193 |
+
# Clean up markdown formatting for PDF
|
| 194 |
+
clean_response = bot_msg.replace("**", "").replace("##", "").replace("- ", " * ")
|
| 195 |
+
pdf.multi_cell(0, 6, clean_response)
|
| 196 |
+
pdf.ln(5)
|
| 197 |
+
|
| 198 |
+
# Separator line
|
| 199 |
+
pdf.set_draw_color(200, 200, 200)
|
| 200 |
+
pdf.line(10, pdf.get_y(), 200, pdf.get_y())
|
| 201 |
+
pdf.ln(5)
|
| 202 |
+
|
| 203 |
+
# Save to temp file
|
| 204 |
+
filename = f"sherlock_chat_{project.replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pdf"
|
| 205 |
+
filepath = Path("/tmp") / filename
|
| 206 |
+
pdf.output(str(filepath))
|
| 207 |
+
|
| 208 |
+
return str(filepath)
|
| 209 |
+
|
| 210 |
+
def structure_meeting(project_name, meeting_title, meeting_date, participants, meeting_text, provider, api_token):
|
| 211 |
"""Structure meeting notes using AI."""
|
| 212 |
+
if not api_token or api_token.strip() == "":
|
| 213 |
+
return "❌ Please enter your API token first"
|
| 214 |
|
| 215 |
if not project_name or not meeting_text:
|
| 216 |
return "❌ Please provide both project name and meeting notes"
|
| 217 |
|
| 218 |
try:
|
| 219 |
+
# Create LLM based on provider
|
| 220 |
+
if provider == "HuggingFace (Free)":
|
| 221 |
+
endpoint = HuggingFaceEndpoint(
|
| 222 |
+
repo_id="meta-llama/Llama-3.2-3B-Instruct",
|
| 223 |
+
temperature=0.3,
|
| 224 |
+
max_new_tokens=1024,
|
| 225 |
+
huggingfacehub_api_token=api_token.strip()
|
| 226 |
+
)
|
| 227 |
+
llm = ChatHuggingFace(llm=endpoint)
|
| 228 |
+
else:
|
| 229 |
+
llm = ChatGoogleGenerativeAI(
|
| 230 |
+
model="gemini-2.5-flash-lite",
|
| 231 |
+
temperature=0.3,
|
| 232 |
+
google_api_key=api_token.strip(),
|
| 233 |
+
convert_system_message_to_human=True
|
| 234 |
+
)
|
| 235 |
|
| 236 |
system_prompt = """You are a meeting notes structuring assistant.
|
| 237 |
Convert unstructured meeting notes into a well-formatted markdown document with these sections:
|
|
|
|
| 282 |
return f"✅ Meeting structured and saved to `{file_path}`\n\n---\n\n{structured_md}"
|
| 283 |
|
| 284 |
except Exception as e:
|
| 285 |
+
error_msg = str(e).lower()
|
| 286 |
+
if "401" in error_msg or "unauthorized" in error_msg or "invalid" in error_msg:
|
| 287 |
+
return "❌ **Invalid API Token**\n\nYour token appears to be invalid or expired."
|
| 288 |
+
elif "403" in error_msg or "forbidden" in error_msg:
|
| 289 |
+
return "❌ **Access Denied**\n\nYour token doesn't have permission."
|
| 290 |
+
elif "rate" in error_msg or "quota" in error_msg or "limit" in error_msg:
|
| 291 |
+
return "❌ **Rate Limit Exceeded**\n\nPlease wait a few minutes and try again."
|
| 292 |
+
else:
|
| 293 |
+
return f"❌ **Error**: {str(e)}\n\nPlease verify your API token."
|
| 294 |
|
| 295 |
# Create Gradio interface with custom CSS
|
| 296 |
custom_css = """
|
|
|
|
| 336 |
}
|
| 337 |
"""
|
| 338 |
|
| 339 |
+
favicon_head = '''
|
| 340 |
+
<link rel="apple-touch-icon" sizes="57x57" href="/file=assets/favicon/apple-icon-57x57.png">
|
| 341 |
+
<link rel="apple-touch-icon" sizes="60x60" href="/file=assets/favicon/apple-icon-60x60.png">
|
| 342 |
+
<link rel="apple-touch-icon" sizes="72x72" href="/file=assets/favicon/apple-icon-72x72.png">
|
| 343 |
+
<link rel="apple-touch-icon" sizes="76x76" href="/file=assets/favicon/apple-icon-76x76.png">
|
| 344 |
+
<link rel="apple-touch-icon" sizes="114x114" href="/file=assets/favicon/apple-icon-114x114.png">
|
| 345 |
+
<link rel="apple-touch-icon" sizes="120x120" href="/file=assets/favicon/apple-icon-120x120.png">
|
| 346 |
+
<link rel="apple-touch-icon" sizes="144x144" href="/file=assets/favicon/apple-icon-144x144.png">
|
| 347 |
+
<link rel="apple-touch-icon" sizes="152x152" href="/file=assets/favicon/apple-icon-152x152.png">
|
| 348 |
+
<link rel="apple-touch-icon" sizes="180x180" href="/file=assets/favicon/apple-icon-180x180.png">
|
| 349 |
+
<link rel="icon" type="image/png" sizes="192x192" href="/file=assets/favicon/android-icon-192x192.png">
|
| 350 |
+
<link rel="icon" type="image/png" sizes="32x32" href="/file=assets/favicon/favicon-32x32.png">
|
| 351 |
+
<link rel="icon" type="image/png" sizes="96x96" href="/file=assets/favicon/favicon-96x96.png">
|
| 352 |
+
<link rel="icon" type="image/png" sizes="16x16" href="/file=assets/favicon/favicon-16x16.png">
|
| 353 |
+
<meta name="msapplication-TileColor" content="#ffffff">
|
| 354 |
+
<meta name="msapplication-TileImage" content="/file=assets/favicon/ms-icon-144x144.png">
|
| 355 |
+
<meta name="theme-color" content="#ffffff">
|
| 356 |
+
'''
|
| 357 |
+
|
| 358 |
+
with gr.Blocks(
|
| 359 |
+
title="Sherlock: AI Project Assistant",
|
| 360 |
+
theme=gr.themes.Soft(),
|
| 361 |
+
css=custom_css,
|
| 362 |
+
head=favicon_head
|
| 363 |
+
) as demo:
|
| 364 |
+
# Header with logo
|
| 365 |
+
gr.HTML("""
|
| 366 |
+
<div style="display: flex; align-items: center; gap: 20px; padding: 10px 0;">
|
| 367 |
+
<img src="/file=assets/logo-transparent-bg.png" alt="Sherlock Logo" style="width: 80px; height: 80px; object-fit: contain;">
|
| 368 |
+
<div>
|
| 369 |
+
<h1 style="margin: 0; font-size: 28px;">Sherlock: AI Project Assistant</h1>
|
| 370 |
+
<p style="margin: 5px 0 0 0; color: #666;">Your intelligent assistant for managing multiple projects through meeting summaries.</p>
|
| 371 |
+
</div>
|
| 372 |
+
</div>
|
| 373 |
""")
|
| 374 |
|
| 375 |
+
# Global Authentication
|
| 376 |
gr.Markdown("### 🔑 Authentication")
|
| 377 |
with gr.Row():
|
| 378 |
+
with gr.Column(scale=1):
|
| 379 |
+
provider_dropdown = gr.Dropdown(
|
| 380 |
+
label="Select Provider",
|
| 381 |
+
choices=["HuggingFace (Free)", "Google AI (Paid)"],
|
| 382 |
+
value="HuggingFace (Free)",
|
| 383 |
+
interactive=True
|
| 384 |
+
)
|
| 385 |
+
with gr.Column(scale=2):
|
| 386 |
+
api_token_global = gr.Textbox(
|
| 387 |
+
label="API Token (Required)",
|
| 388 |
+
placeholder="Enter your HuggingFace token",
|
| 389 |
type="password"
|
| 390 |
)
|
| 391 |
with gr.Column(scale=2):
|
| 392 |
+
provider_info = gr.Markdown("""
|
| 393 |
+
**HuggingFace (Free):**
|
| 394 |
1. Visit [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
| 395 |
+
2. Click "New token" → Select "Read"
|
|
|
|
|
|
|
| 396 |
""")
|
| 397 |
|
| 398 |
with gr.Row():
|
| 399 |
submit_token_btn = gr.Button("Submit Token", variant="primary")
|
| 400 |
token_status = gr.Markdown("", elem_classes="token-status")
|
| 401 |
|
| 402 |
+
def update_provider_ui(provider):
|
| 403 |
+
"""Update UI based on selected provider. Also clears token and status."""
|
| 404 |
+
if provider == "HuggingFace (Free)":
|
| 405 |
+
return (
|
| 406 |
+
gr.update(placeholder="Enter your HuggingFace token", value=""),
|
| 407 |
+
"""**HuggingFace (Free):**
|
| 408 |
+
1. Visit [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
|
| 409 |
+
2. Click "New token" → Select "Read"
|
| 410 |
+
""",
|
| 411 |
+
"" # Clear status
|
| 412 |
+
)
|
| 413 |
+
else:
|
| 414 |
+
return (
|
| 415 |
+
gr.update(placeholder="Enter your Google API key", value=""),
|
| 416 |
+
"""**Google AI (Paid):**
|
| 417 |
+
1. Visit [aistudio.google.com/apikey](https://aistudio.google.com/apikey)
|
| 418 |
+
2. Create an API key
|
| 419 |
+
""",
|
| 420 |
+
"" # Clear status
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
provider_dropdown.change(
|
| 424 |
+
fn=update_provider_ui,
|
| 425 |
+
inputs=[provider_dropdown],
|
| 426 |
+
outputs=[api_token_global, provider_info, token_status]
|
| 427 |
+
)
|
| 428 |
+
|
| 429 |
+
def validate_token(token, provider):
|
| 430 |
+
"""Validate token - simplified without heavy API call."""
|
| 431 |
+
if not token or not token.strip():
|
| 432 |
+
return '<div style="background-color: #fee2e2; color: #991b1b; padding: 10px; border-radius: 5px;">❌ Please enter a token</div>', ""
|
| 433 |
+
|
| 434 |
+
token_value = token.strip()
|
| 435 |
+
|
| 436 |
+
# Simple format validation
|
| 437 |
+
if provider == "HuggingFace (Free)":
|
| 438 |
+
# HF tokens start with "hf_"
|
| 439 |
+
if token_value.startswith("hf_") and len(token_value) > 10:
|
| 440 |
+
return '<div class="token-accepted">✅ Token format valid - will verify on first query</div>', token_value
|
| 441 |
+
else:
|
| 442 |
+
return '<div style="background-color: #fee2e2; color: #991b1b; padding: 10px; border-radius: 5px;">❌ Invalid HuggingFace token format (should start with hf_)</div>', ""
|
| 443 |
else:
|
| 444 |
+
# Google API keys are typically 39 chars
|
| 445 |
+
if len(token_value) >= 30:
|
| 446 |
+
return '<div class="token-accepted">✅ API key format valid - will verify on first query</div>', token_value
|
| 447 |
+
else:
|
| 448 |
+
return '<div style="background-color: #fee2e2; color: #991b1b; padding: 10px; border-radius: 5px;">❌ Invalid Google API key format</div>', ""
|
| 449 |
|
| 450 |
submit_token_btn.click(
|
| 451 |
fn=validate_token,
|
| 452 |
+
inputs=[api_token_global, provider_dropdown],
|
| 453 |
+
outputs=[token_status, api_token_global]
|
| 454 |
)
|
| 455 |
|
| 456 |
# Main tabs
|
|
|
|
| 459 |
with gr.Tab("💬 Chat"):
|
| 460 |
gr.Markdown("### Ask questions about your projects")
|
| 461 |
|
| 462 |
+
# State for per-project chat histories
|
| 463 |
+
chat_histories = gr.State({}) # {project_name: [(user_msg, bot_msg), ...]}
|
| 464 |
+
current_project = gr.State("All Projects")
|
| 465 |
+
|
| 466 |
# Project selection dropdown
|
| 467 |
project_dropdown = gr.Dropdown(
|
| 468 |
label="Select Project",
|
|
|
|
| 500 |
msg = gr.Textbox(
|
| 501 |
label="Your Message",
|
| 502 |
placeholder="What are the open action items?",
|
| 503 |
+
lines=1,
|
| 504 |
show_label=False
|
| 505 |
)
|
| 506 |
|
| 507 |
with gr.Row():
|
| 508 |
submit_btn = gr.Button("Send", variant="primary", scale=1)
|
| 509 |
clear_btn = gr.Button("Clear", scale=1)
|
| 510 |
+
export_btn = gr.Button("📥 Export", scale=1)
|
| 511 |
+
|
| 512 |
+
export_file = gr.File(label="Download", visible=False)
|
| 513 |
+
|
| 514 |
+
def respond(message, chat_history, histories, project, provider, token):
|
| 515 |
+
if not message:
|
| 516 |
+
yield chat_history, "", histories
|
| 517 |
+
return
|
| 518 |
|
| 519 |
+
# Add user message with empty bot response placeholder
|
| 520 |
+
chat_history = chat_history + [(message, "")]
|
| 521 |
+
|
| 522 |
+
# Stream bot response
|
| 523 |
+
for response_chunk in chat(message, chat_history, project, provider, token):
|
| 524 |
+
# Update the last message with streaming response
|
| 525 |
+
chat_history[-1] = (message, response_chunk)
|
| 526 |
+
yield chat_history, "", histories
|
| 527 |
+
|
| 528 |
+
# Save final history to per-project histories
|
| 529 |
+
histories[project] = chat_history.copy()
|
| 530 |
+
yield chat_history, "", histories
|
| 531 |
+
return
|
| 532 |
+
|
| 533 |
+
def respond_non_streaming(message, chat_history, histories, project, provider, token):
|
| 534 |
+
"""Non-streaming version for fallback."""
|
| 535 |
if not message:
|
| 536 |
+
return chat_history, "", histories
|
| 537 |
|
| 538 |
# Get bot response
|
| 539 |
bot_message = ""
|
| 540 |
+
for response_chunk in chat(message, chat_history, project, provider, token, use_streaming=False):
|
| 541 |
bot_message = response_chunk
|
| 542 |
|
| 543 |
# Add to history as tuple
|
| 544 |
chat_history.append((message, bot_message))
|
| 545 |
|
| 546 |
+
# Save to per-project histories
|
| 547 |
+
histories[project] = chat_history.copy()
|
| 548 |
+
|
| 549 |
+
return chat_history, "", histories
|
| 550 |
+
|
| 551 |
+
def switch_project(new_project, current_chat, histories, old_project):
|
| 552 |
+
# Save current chat to old project
|
| 553 |
+
if current_chat:
|
| 554 |
+
histories[old_project] = current_chat.copy()
|
| 555 |
+
|
| 556 |
+
# Load chat history for new project (or empty if none)
|
| 557 |
+
new_chat = histories.get(new_project, [])
|
| 558 |
+
|
| 559 |
+
return new_chat, histories, new_project
|
| 560 |
+
|
| 561 |
+
def clear_chat(project, histories):
|
| 562 |
+
# Clear current project's history
|
| 563 |
+
histories[project] = []
|
| 564 |
+
return [], histories
|
| 565 |
|
| 566 |
submit_btn.click(
|
| 567 |
fn=respond,
|
| 568 |
+
inputs=[msg, chatbot, chat_histories, project_dropdown, provider_dropdown, api_token_global],
|
| 569 |
+
outputs=[chatbot, msg, chat_histories]
|
| 570 |
)
|
| 571 |
|
| 572 |
msg.submit(
|
| 573 |
fn=respond,
|
| 574 |
+
inputs=[msg, chatbot, chat_histories, project_dropdown, provider_dropdown, api_token_global],
|
| 575 |
+
outputs=[chatbot, msg, chat_histories]
|
| 576 |
+
)
|
| 577 |
+
|
| 578 |
+
clear_btn.click(
|
| 579 |
+
fn=clear_chat,
|
| 580 |
+
inputs=[project_dropdown, chat_histories],
|
| 581 |
+
outputs=[chatbot, chat_histories]
|
| 582 |
+
)
|
| 583 |
+
|
| 584 |
+
def handle_export(chat_history, project):
|
| 585 |
+
"""Handle export button click."""
|
| 586 |
+
if not chat_history:
|
| 587 |
+
return gr.update(visible=False, value=None)
|
| 588 |
+
filepath = export_chat_to_pdf(chat_history, project)
|
| 589 |
+
return gr.update(visible=True, value=filepath)
|
| 590 |
+
|
| 591 |
+
export_btn.click(
|
| 592 |
+
fn=handle_export,
|
| 593 |
+
inputs=[chatbot, project_dropdown],
|
| 594 |
+
outputs=[export_file]
|
| 595 |
)
|
| 596 |
|
| 597 |
+
# Switch project: save current, load new
|
| 598 |
+
project_dropdown.change(
|
| 599 |
+
fn=switch_project,
|
| 600 |
+
inputs=[project_dropdown, chatbot, chat_histories, current_project],
|
| 601 |
+
outputs=[chatbot, chat_histories, current_project]
|
| 602 |
+
)
|
| 603 |
|
| 604 |
# Upload Meeting tab
|
| 605 |
with gr.Tab("📤 Upload Meeting"):
|
|
|
|
| 668 |
structure_btn = gr.Button("🤖 Structure Meeting with AI", variant="primary")
|
| 669 |
structure_output = gr.Markdown(label="Structured Output")
|
| 670 |
|
| 671 |
+
def structure_meeting_wrapper(mode, existing_proj, new_proj, title, date, participants, text, provider, token):
|
| 672 |
"""Wrapper to handle both project modes."""
|
| 673 |
+
global rag
|
| 674 |
# Determine which project name to use
|
| 675 |
project_name = existing_proj if mode == "Use Existing Project" else new_proj
|
| 676 |
+
result = structure_meeting(project_name, title, date, participants, text, provider, token)
|
| 677 |
+
|
| 678 |
+
# If successful, re-index RAG and update project lists
|
| 679 |
+
if result.startswith("✅"):
|
| 680 |
+
# Re-initialize RAG to pick up new project/meeting
|
| 681 |
+
initialize_rag()
|
| 682 |
+
|
| 683 |
+
# Get updated project list
|
| 684 |
+
updated_projects = get_projects()
|
| 685 |
+
updated_existing = updated_projects[1:] # Exclude "All Projects"
|
| 686 |
+
|
| 687 |
+
return (
|
| 688 |
+
result,
|
| 689 |
+
gr.update(choices=updated_projects, value="All Projects"),
|
| 690 |
+
gr.update(choices=updated_existing)
|
| 691 |
+
)
|
| 692 |
+
|
| 693 |
+
return result, gr.update(), gr.update()
|
| 694 |
|
| 695 |
structure_btn.click(
|
| 696 |
fn=structure_meeting_wrapper,
|
| 697 |
+
inputs=[project_mode, existing_project, new_project, upload_title, upload_date, upload_participants, upload_text, provider_dropdown, api_token_global],
|
| 698 |
+
outputs=[structure_output, project_dropdown, existing_project]
|
| 699 |
+
)
|
| 700 |
+
|
| 701 |
+
# Insights tab
|
| 702 |
+
with gr.Tab("📊 Insights"):
|
| 703 |
+
gr.Markdown("### Project Insights & Analytics")
|
| 704 |
+
|
| 705 |
+
insights_project = gr.Dropdown(
|
| 706 |
+
label="Select Project",
|
| 707 |
+
choices=get_projects()[1:], # Exclude "All Projects"
|
| 708 |
+
interactive=True
|
| 709 |
+
)
|
| 710 |
+
|
| 711 |
+
with gr.Row():
|
| 712 |
+
with gr.Column():
|
| 713 |
+
gr.Markdown("#### 📝 Meeting Summary")
|
| 714 |
+
gr.Markdown("Generate a comprehensive summary with key takeaways from all meetings.")
|
| 715 |
+
summary_btn = gr.Button("Generate Summary", variant="primary")
|
| 716 |
+
summary_output = gr.Markdown(label="Summary")
|
| 717 |
+
|
| 718 |
+
with gr.Column():
|
| 719 |
+
gr.Markdown("#### 📈 Trend Analysis")
|
| 720 |
+
gr.Markdown("Analyze patterns across meetings: recurring topics, blocker trends, action item progress.")
|
| 721 |
+
trends_btn = gr.Button("Analyze Trends", variant="primary")
|
| 722 |
+
trends_output = gr.Markdown(label="Trends")
|
| 723 |
+
|
| 724 |
+
def generate_summary(project, provider, token):
|
| 725 |
+
"""Generate a summary with key takeaways for a project."""
|
| 726 |
+
if not token or token.strip() == "":
|
| 727 |
+
return "❌ Please enter your API token first"
|
| 728 |
+
|
| 729 |
+
if not project:
|
| 730 |
+
return "❌ Please select a project"
|
| 731 |
+
|
| 732 |
+
if not rag:
|
| 733 |
+
return "❌ System not initialized"
|
| 734 |
+
|
| 735 |
+
try:
|
| 736 |
+
# Get all meeting content for the project
|
| 737 |
+
meetings = rag.get_project_documents(project)
|
| 738 |
+
if not meetings:
|
| 739 |
+
return f"❌ No meetings found for project: {project}"
|
| 740 |
+
|
| 741 |
+
meeting_content = "\n\n---\n\n".join([doc.page_content for doc in meetings])
|
| 742 |
+
|
| 743 |
+
# Create LLM
|
| 744 |
+
if provider == "HuggingFace (Free)":
|
| 745 |
+
endpoint = HuggingFaceEndpoint(
|
| 746 |
+
repo_id="meta-llama/Llama-3.2-3B-Instruct",
|
| 747 |
+
temperature=0.3,
|
| 748 |
+
max_new_tokens=1500,
|
| 749 |
+
huggingfacehub_api_token=token.strip()
|
| 750 |
+
)
|
| 751 |
+
llm = ChatHuggingFace(llm=endpoint)
|
| 752 |
+
else:
|
| 753 |
+
llm = ChatGoogleGenerativeAI(
|
| 754 |
+
model="gemini-2.5-flash-lite",
|
| 755 |
+
temperature=0.3,
|
| 756 |
+
google_api_key=token.strip()
|
| 757 |
+
)
|
| 758 |
+
|
| 759 |
+
prompt = f"""Analyze these meeting notes and provide a comprehensive project summary with key takeaways.
|
| 760 |
+
|
| 761 |
+
Meeting Notes:
|
| 762 |
+
{meeting_content}
|
| 763 |
+
|
| 764 |
+
Provide:
|
| 765 |
+
## Project Summary
|
| 766 |
+
A brief overview of the project status and progress.
|
| 767 |
+
|
| 768 |
+
## Key Takeaways
|
| 769 |
+
- List the most important points and insights
|
| 770 |
+
- Highlight critical decisions made
|
| 771 |
+
- Note significant achievements
|
| 772 |
+
|
| 773 |
+
## Open Items
|
| 774 |
+
- List pending action items
|
| 775 |
+
- Note unresolved blockers
|
| 776 |
+
|
| 777 |
+
## Recommendations
|
| 778 |
+
- Suggest next steps based on the meeting content
|
| 779 |
+
"""
|
| 780 |
+
|
| 781 |
+
messages = [HumanMessage(content=prompt)]
|
| 782 |
+
response = llm.invoke(messages)
|
| 783 |
+
return response.content
|
| 784 |
+
|
| 785 |
+
except Exception as e:
|
| 786 |
+
return f"❌ Error: {str(e)}"
|
| 787 |
+
|
| 788 |
+
def analyze_trends(project, provider, token):
|
| 789 |
+
"""Analyze trends across meetings for a project."""
|
| 790 |
+
if not token or token.strip() == "":
|
| 791 |
+
return "❌ Please enter your API token first"
|
| 792 |
+
|
| 793 |
+
if not project:
|
| 794 |
+
return "❌ Please select a project"
|
| 795 |
+
|
| 796 |
+
if not rag:
|
| 797 |
+
return "❌ System not initialized"
|
| 798 |
+
|
| 799 |
+
try:
|
| 800 |
+
# Get all meeting content for the project
|
| 801 |
+
meetings = rag.get_project_documents(project)
|
| 802 |
+
if not meetings:
|
| 803 |
+
return f"❌ No meetings found for project: {project}"
|
| 804 |
+
|
| 805 |
+
if len(meetings) < 2:
|
| 806 |
+
return "⚠️ Need at least 2 meetings to analyze trends"
|
| 807 |
+
|
| 808 |
+
meeting_content = "\n\n---\n\n".join([doc.page_content for doc in meetings])
|
| 809 |
+
|
| 810 |
+
# Create LLM
|
| 811 |
+
if provider == "HuggingFace (Free)":
|
| 812 |
+
endpoint = HuggingFaceEndpoint(
|
| 813 |
+
repo_id="meta-llama/Llama-3.2-3B-Instruct",
|
| 814 |
+
temperature=0.3,
|
| 815 |
+
max_new_tokens=1500,
|
| 816 |
+
huggingfacehub_api_token=token.strip()
|
| 817 |
+
)
|
| 818 |
+
llm = ChatHuggingFace(llm=endpoint)
|
| 819 |
+
else:
|
| 820 |
+
llm = ChatGoogleGenerativeAI(
|
| 821 |
+
model="gemini-2.5-flash-lite",
|
| 822 |
+
temperature=0.3,
|
| 823 |
+
google_api_key=token.strip()
|
| 824 |
+
)
|
| 825 |
+
|
| 826 |
+
prompt = f"""Analyze these meeting notes chronologically and identify trends and patterns.
|
| 827 |
+
|
| 828 |
+
Meeting Notes:
|
| 829 |
+
{meeting_content}
|
| 830 |
+
|
| 831 |
+
Provide a trend analysis with:
|
| 832 |
+
|
| 833 |
+
## Topic Evolution
|
| 834 |
+
How have discussion topics evolved across meetings?
|
| 835 |
+
|
| 836 |
+
## Recurring Themes
|
| 837 |
+
What topics or issues keep coming up repeatedly?
|
| 838 |
+
|
| 839 |
+
## Blocker Patterns
|
| 840 |
+
- Are there recurring blockers?
|
| 841 |
+
- How quickly are blockers typically resolved?
|
| 842 |
+
- Are there systemic issues causing repeated blockers?
|
| 843 |
+
|
| 844 |
+
## Action Item Trends
|
| 845 |
+
- Are action items being completed on time?
|
| 846 |
+
- Who are the most frequently assigned team members?
|
| 847 |
+
- Are there patterns in delayed items?
|
| 848 |
+
|
| 849 |
+
## Team Dynamics
|
| 850 |
+
- Who are the key contributors?
|
| 851 |
+
- Are there communication patterns worth noting?
|
| 852 |
+
|
| 853 |
+
## Progress Trajectory
|
| 854 |
+
Is the project on track? Accelerating or slowing down?
|
| 855 |
+
"""
|
| 856 |
+
|
| 857 |
+
messages = [HumanMessage(content=prompt)]
|
| 858 |
+
response = llm.invoke(messages)
|
| 859 |
+
return response.content
|
| 860 |
+
|
| 861 |
+
except Exception as e:
|
| 862 |
+
return f"❌ Error: {str(e)}"
|
| 863 |
+
|
| 864 |
+
summary_btn.click(
|
| 865 |
+
fn=generate_summary,
|
| 866 |
+
inputs=[insights_project, provider_dropdown, api_token_global],
|
| 867 |
+
outputs=summary_output
|
| 868 |
+
)
|
| 869 |
+
|
| 870 |
+
trends_btn.click(
|
| 871 |
+
fn=analyze_trends,
|
| 872 |
+
inputs=[insights_project, provider_dropdown, api_token_global],
|
| 873 |
+
outputs=trends_output
|
| 874 |
)
|
| 875 |
|
| 876 |
|
| 877 |
# Launch
|
| 878 |
if __name__ == "__main__":
|
| 879 |
+
import socket
|
| 880 |
+
|
| 881 |
+
def find_free_port(start_port=7860, max_attempts=10):
|
| 882 |
+
"""Find an available port starting from start_port."""
|
| 883 |
+
for port in range(start_port, start_port + max_attempts):
|
| 884 |
+
try:
|
| 885 |
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
| 886 |
+
s.bind(('', port))
|
| 887 |
+
return port
|
| 888 |
+
except OSError:
|
| 889 |
+
continue
|
| 890 |
+
return None
|
| 891 |
+
|
| 892 |
+
port = find_free_port()
|
| 893 |
+
if port:
|
| 894 |
+
print(f"Starting on port {port}")
|
| 895 |
+
demo.launch(
|
| 896 |
+
server_name="0.0.0.0",
|
| 897 |
+
server_port=port,
|
| 898 |
+
favicon_path="assets/favicon/favicon.ico",
|
| 899 |
+
allowed_paths=["assets/"]
|
| 900 |
+
)
|
| 901 |
+
else:
|
| 902 |
+
print("Error: Could not find an available port in range 7860-7869")
|
assets/favicon/browserconfig.xml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
assets/favicon/manifest.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "App",
|
| 3 |
+
"icons": [
|
| 4 |
+
{
|
| 5 |
+
"src": "\/android-icon-36x36.png",
|
| 6 |
+
"sizes": "36x36",
|
| 7 |
+
"type": "image\/png",
|
| 8 |
+
"density": "0.75"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"src": "\/android-icon-48x48.png",
|
| 12 |
+
"sizes": "48x48",
|
| 13 |
+
"type": "image\/png",
|
| 14 |
+
"density": "1.0"
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"src": "\/android-icon-72x72.png",
|
| 18 |
+
"sizes": "72x72",
|
| 19 |
+
"type": "image\/png",
|
| 20 |
+
"density": "1.5"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"src": "\/android-icon-96x96.png",
|
| 24 |
+
"sizes": "96x96",
|
| 25 |
+
"type": "image\/png",
|
| 26 |
+
"density": "2.0"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"src": "\/android-icon-144x144.png",
|
| 30 |
+
"sizes": "144x144",
|
| 31 |
+
"type": "image\/png",
|
| 32 |
+
"density": "3.0"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"src": "\/android-icon-192x192.png",
|
| 36 |
+
"sizes": "192x192",
|
| 37 |
+
"type": "image\/png",
|
| 38 |
+
"density": "4.0"
|
| 39 |
+
}
|
| 40 |
+
]
|
| 41 |
+
}
|
data/covid_prediction/meetings/2025-01-10-project-initiation.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: COVID-19 Variant Prediction Model - Project Initiation
|
| 2 |
+
Date: 2025-01-10
|
| 3 |
+
Participants: Dr. Amanda Foster, Kevin Liu, Dr. Rachel Okonkwo, Michael Santos, Dr. Yuki Tanaka
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
The team convened to launch the COVID-19 variant prediction project in collaboration with the CDC and WHO. Dr. Foster outlined the project scope: develop a machine learning system to predict emerging SARS-CoV-2 variants with pandemic potential based on genomic surveillance data.
|
| 7 |
+
|
| 8 |
+
We will analyze sequences from GISAID (Global Initiative on Sharing All Influenza Data) which currently contains over 15 million SARS-CoV-2 sequences. The model will focus on spike protein mutations and their predicted impact on transmissibility and immune escape.
|
| 9 |
+
|
| 10 |
+
Kevin presented the proposed architecture: a transformer-based model fine-tuned on protein sequences (building on ESM-2 from Meta) combined with an epidemiological compartmental model (SEIR variant) for transmission dynamics.
|
| 11 |
+
|
| 12 |
+
Dr. Okonkwo discussed data preprocessing requirements. We need to handle significant class imbalance - variants of concern (VOC) represent less than 0.1% of all sequences. She proposed using focal loss and synthetic minority oversampling.
|
| 13 |
+
|
| 14 |
+
Dr. Tanaka presented the clinical validation strategy using retrospective analysis of Delta and Omicron emergence patterns.
|
| 15 |
+
|
| 16 |
+
## Decisions
|
| 17 |
+
- Use ESM-2 (650M parameter version) as the foundation model
|
| 18 |
+
- Implement SEIR compartmental model for transmission prediction
|
| 19 |
+
- Focus on spike protein receptor binding domain (RBD) mutations
|
| 20 |
+
- Deploy on Google Cloud Platform using Vertex AI
|
| 21 |
+
- Weekly data refresh from GISAID every Monday
|
| 22 |
+
|
| 23 |
+
## Action Items
|
| 24 |
+
- [ ] Dr. Foster: Establish data sharing agreement with GISAID by 2025-01-17
|
| 25 |
+
- [ ] Kevin Liu: Set up GCP project and Vertex AI pipelines by 2025-01-14
|
| 26 |
+
- [ ] Dr. Okonkwo: Implement data preprocessing pipeline for FASTA files by 2025-01-20
|
| 27 |
+
- [ ] Michael Santos: Create feature engineering module for mutation analysis by 2025-01-22
|
| 28 |
+
- [ ] Dr. Tanaka: Prepare validation dataset with labeled VOC sequences by 2025-01-25
|
| 29 |
+
|
| 30 |
+
## Blockers
|
| 31 |
+
- GISAID data access requires institutional agreement (legal review in progress)
|
| 32 |
+
- GPU quota on GCP needs to be increased for A100 instances
|
| 33 |
+
- Need IRB approval for any human mobility data integration
|
data/covid_prediction/meetings/2025-01-17-model-architecture-review.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Model Architecture Review and Data Pipeline Update
|
| 2 |
+
Date: 2025-01-17
|
| 3 |
+
Participants: Dr. Amanda Foster, Kevin Liu, Dr. Rachel Okonkwo, Michael Santos
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
Kevin demonstrated the GCP infrastructure setup. We have a Vertex AI training pipeline configured with 4x A100 GPUs for distributed training. The estimated training time for the full ESM-2 fine-tuning is approximately 72 hours.
|
| 7 |
+
|
| 8 |
+
Dr. Okonkwo presented the data preprocessing pipeline:
|
| 9 |
+
- Successfully downloaded 2.3M sequences from GISAID (last 6 months)
|
| 10 |
+
- Implemented quality filtering: removed sequences with >5% ambiguous bases
|
| 11 |
+
- Created spike protein extraction module achieving 99.7% extraction rate
|
| 12 |
+
- Identified 847 unique RBD mutations across the dataset
|
| 13 |
+
|
| 14 |
+
Michael showed the mutation feature engineering module. Key features include:
|
| 15 |
+
- Mutation frequency trajectory over time
|
| 16 |
+
- Predicted binding affinity changes (using PyRosetta)
|
| 17 |
+
- Phylogenetic distance from reference strain (Wuhan-Hu-1)
|
| 18 |
+
- Geographic spread velocity
|
| 19 |
+
|
| 20 |
+
The team discussed the model evaluation strategy. We agreed to use a temporal split: train on pre-October 2024 data and validate on October 2024 - January 2025 to test prediction of recent variants.
|
| 21 |
+
|
| 22 |
+
Dr. Foster shared that the GISAID data agreement was approved. However, we discovered that mutation annotations need to be recomputed due to reference sequence updates.
|
| 23 |
+
|
| 24 |
+
## Decisions
|
| 25 |
+
- Use temporal split for evaluation (train: pre-Oct 2024, test: Oct 2024-Jan 2025)
|
| 26 |
+
- Implement early stopping based on validation AUC-ROC
|
| 27 |
+
- Add uncertainty quantification using Monte Carlo dropout
|
| 28 |
+
- Create dashboard using Streamlit for weekly variant monitoring
|
| 29 |
+
|
| 30 |
+
## Action Items
|
| 31 |
+
- [ ] Kevin Liu: Implement distributed training script with PyTorch Lightning by 2025-01-24
|
| 32 |
+
- [ ] Dr. Okonkwo: Recompute mutation annotations with updated reference by 2025-01-21
|
| 33 |
+
- [ ] Michael Santos: Integrate PyRosetta binding affinity predictions by 2025-01-28
|
| 34 |
+
- [ ] Dr. Foster: Draft manuscript outline for Nature Communications by 2025-01-30
|
| 35 |
+
- [x] Dr. Foster: Establish data sharing agreement with GISAID (completed)
|
| 36 |
+
- [x] Kevin Liu: Set up GCP project and Vertex AI pipelines (completed)
|
| 37 |
+
|
| 38 |
+
## Blockers
|
| 39 |
+
- PyRosetta license renewal pending - temporary workaround using AlphaFold2 structures
|
| 40 |
+
- GISAID API rate limiting affecting data refresh frequency
|
| 41 |
+
- Need additional compute budget for hyperparameter optimization (~$5K)
|
data/covid_prediction/meetings/2025-01-24-training-results.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Initial Training Results and Variant Detection Analysis
|
| 2 |
+
Date: 2025-01-24
|
| 3 |
+
Participants: Dr. Amanda Foster, Kevin Liu, Dr. Rachel Okonkwo, Michael Santos, Dr. Yuki Tanaka
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
Kevin presented the first complete training run results. The ESM-2 fine-tuned model achieved:
|
| 7 |
+
- AUC-ROC: 0.89 on validation set (Oct 2024 - Jan 2025 sequences)
|
| 8 |
+
- Precision@100: 0.72 (72 of top 100 predicted high-risk mutations appeared in VOCs)
|
| 9 |
+
- Recall for Omicron BA.2.86 (Pirola): Model correctly flagged 85% of key mutations
|
| 10 |
+
|
| 11 |
+
Dr. Tanaka performed the retrospective validation:
|
| 12 |
+
- Model would have detected Delta variant mutations 6 weeks before WHO designation
|
| 13 |
+
- Omicron BA.1 detection: 4 weeks before designation
|
| 14 |
+
- JN.1 variant: 3 weeks early warning
|
| 15 |
+
|
| 16 |
+
Dr. Okonkwo showed concerning data quality issues:
|
| 17 |
+
- 12% of sequences have incomplete metadata (location, collection date)
|
| 18 |
+
- Significant reporting delays from some countries (up to 8 weeks)
|
| 19 |
+
- Batch effects visible between sequencing centers
|
| 20 |
+
|
| 21 |
+
Michael demonstrated the feature importance analysis:
|
| 22 |
+
- ACE2 binding affinity change: most predictive feature (SHAP value: 0.34)
|
| 23 |
+
- Geographic spread velocity: second most important (0.28)
|
| 24 |
+
- Phylogenetic distance: moderate importance (0.15)
|
| 25 |
+
|
| 26 |
+
The team discussed deployment strategy. We agreed to implement a tiered alert system:
|
| 27 |
+
- Red: >90% confidence, immediate notification
|
| 28 |
+
- Yellow: 70-90% confidence, weekly digest
|
| 29 |
+
- Green: <70% confidence, monitoring only
|
| 30 |
+
|
| 31 |
+
## Decisions
|
| 32 |
+
- Deploy tiered alert system to CDC dashboard next week
|
| 33 |
+
- Implement data quality filters (reject sequences with >3 missing metadata fields)
|
| 34 |
+
- Add model calibration using temperature scaling
|
| 35 |
+
- Create public API for academic researchers (rate-limited)
|
| 36 |
+
- Submit preliminary results to WHO surveillance network
|
| 37 |
+
|
| 38 |
+
## Action Items
|
| 39 |
+
- [ ] Kevin Liu: Deploy model to production on Vertex AI by 2025-01-28
|
| 40 |
+
- [ ] Dr. Okonkwo: Implement data quality scoring system by 2025-01-31
|
| 41 |
+
- [ ] Michael Santos: Build Streamlit dashboard for variant monitoring by 2025-02-03
|
| 42 |
+
- [ ] Dr. Tanaka: Write validation methodology section for paper by 2025-02-05
|
| 43 |
+
- [ ] Dr. Foster: Present preliminary results at CDC meeting on 2025-02-10
|
| 44 |
+
- [x] Kevin Liu: Implement distributed training script with PyTorch Lightning (completed)
|
| 45 |
+
- [x] Dr. Okonkwo: Recompute mutation annotations with updated reference (completed)
|
| 46 |
+
|
| 47 |
+
## Blockers
|
| 48 |
+
- Model inference cost higher than budgeted (~$2K/month) - exploring model distillation
|
| 49 |
+
- CDC requires FISMA compliance for dashboard - security review scheduled
|
| 50 |
+
- Some team members need clearance for WHO data access
|
data/quantum_computing/meetings/2025-01-08-project-kickoff.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Quantum Error Correction Project Kickoff
|
| 2 |
+
Date: 2025-01-08
|
| 3 |
+
Participants: Dr. Sarah Chen, Marcus Webb, Dr. Priya Patel, James Rodriguez, Dr. Elena Volkov
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
The team assembled to initiate the Quantum Error Correction (QEC) research project funded by the National Science Foundation. Dr. Chen outlined the 18-month timeline and key milestones.
|
| 7 |
+
|
| 8 |
+
The primary objective is to develop a novel surface code implementation that reduces logical error rates by at least 10x compared to current approaches. We discussed the theoretical framework based on topological codes and the experimental validation strategy using IBM's 127-qubit Eagle processor.
|
| 9 |
+
|
| 10 |
+
Marcus presented the current state of our quantum simulation infrastructure. We have access to AWS Braket for cloud simulations and a local 20-qubit simulator. Dr. Patel raised concerns about the coherence time limitations in current hardware.
|
| 11 |
+
|
| 12 |
+
Dr. Volkov discussed the mathematical foundations and proposed using stabilizer formalism for the initial prototype. James will lead the software engineering effort using Qiskit and Cirq frameworks.
|
| 13 |
+
|
| 14 |
+
## Decisions
|
| 15 |
+
- Adopt surface code architecture as the primary approach
|
| 16 |
+
- Use IBM Quantum for experimental validation (127-qubit Eagle processor)
|
| 17 |
+
- Implement simulation pipeline using Qiskit with Cirq as backup
|
| 18 |
+
- Weekly sync meetings on Wednesdays at 2 PM EST
|
| 19 |
+
- Use Slack channel #quantum-qec for daily communication
|
| 20 |
+
|
| 21 |
+
## Action Items
|
| 22 |
+
- [ ] Dr. Chen: Submit IRB protocol for computational resource allocation by 2025-01-15
|
| 23 |
+
- [ ] Marcus Webb: Set up AWS Braket development environment by 2025-01-12
|
| 24 |
+
- [ ] Dr. Patel: Literature review on recent surface code implementations by 2025-01-20
|
| 25 |
+
- [ ] James Rodriguez: Create GitHub repository and CI/CD pipeline by 2025-01-10
|
| 26 |
+
- [ ] Dr. Volkov: Draft mathematical specification document by 2025-01-18
|
| 27 |
+
|
| 28 |
+
## Blockers
|
| 29 |
+
- Waiting for IBM Quantum Network access approval (submitted 2024-12-20)
|
| 30 |
+
- Need additional GPU compute allocation for tensor network simulations
|
| 31 |
+
- Hardware calibration data from IBM not yet available for Eagle processor
|
data/quantum_computing/meetings/2025-01-15-week2-technical-review.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Week 2 Technical Review - Surface Code Implementation
|
| 2 |
+
Date: 2025-01-15
|
| 3 |
+
Participants: Dr. Sarah Chen, Marcus Webb, Dr. Priya Patel, James Rodriguez
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
Marcus demonstrated the initial AWS Braket setup. We successfully ran benchmark circuits on the SV1 simulator with up to 34 qubits. Performance metrics show approximately 10^6 circuit evaluations per hour, which meets our simulation requirements.
|
| 7 |
+
|
| 8 |
+
James presented the repository structure and the initial Qiskit implementation of the distance-3 surface code. The current implementation includes syndrome extraction circuits and basic decoding using minimum weight perfect matching (MWPM).
|
| 9 |
+
|
| 10 |
+
Dr. Patel shared findings from the literature review:
|
| 11 |
+
- Google's 2023 paper achieved logical error rate of 2.9% with distance-5 code
|
| 12 |
+
- IBM's recent work on heavy-hex lattice shows promise for our approach
|
| 13 |
+
- New decoder architectures using neural networks show 15% improvement over MWPM
|
| 14 |
+
|
| 15 |
+
The team discussed the trade-off between code distance and physical qubit requirements. For distance-5, we need 49 physical qubits, leaving headroom on the 127-qubit processor.
|
| 16 |
+
|
| 17 |
+
## Decisions
|
| 18 |
+
- Target distance-5 surface code for initial experiments
|
| 19 |
+
- Implement both MWPM and neural network decoder for comparison
|
| 20 |
+
- Use heavy-hex lattice connectivity constraint in simulations
|
| 21 |
+
- Schedule bi-weekly demos for stakeholders
|
| 22 |
+
|
| 23 |
+
## Action Items
|
| 24 |
+
- [ ] Marcus Webb: Implement distance-5 surface code in Qiskit by 2025-01-25
|
| 25 |
+
- [ ] James Rodriguez: Integrate MWPM decoder from PyMatching library by 2025-01-22
|
| 26 |
+
- [ ] Dr. Patel: Design neural network decoder architecture by 2025-01-28
|
| 27 |
+
- [ ] Dr. Chen: Prepare Q1 progress report for NSF by 2025-01-30
|
| 28 |
+
- [x] James Rodriguez: Create GitHub repository and CI/CD pipeline (completed)
|
| 29 |
+
|
| 30 |
+
## Blockers
|
| 31 |
+
- IBM Quantum Network access still pending - escalated to program manager
|
| 32 |
+
- PyMatching library has compatibility issues with latest Qiskit version
|
| 33 |
+
- Need clarification on data sharing agreement for publishing results
|
data/quantum_computing/meetings/2025-01-22-decoder-benchmark.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Decoder Benchmarking Results and Hardware Access Update
|
| 2 |
+
Date: 2025-01-22
|
| 3 |
+
Participants: Dr. Sarah Chen, Marcus Webb, Dr. Priya Patel, James Rodriguez, Dr. Elena Volkov
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
Great news - IBM Quantum Network access was approved! We now have priority access to the 127-qubit Eagle processor. Dr. Chen demonstrated the initial calibration runs showing T1 times averaging 120μs and T2 times of 80μs across the device.
|
| 7 |
+
|
| 8 |
+
James presented the decoder benchmarking results:
|
| 9 |
+
- MWPM decoder (PyMatching): 94.2% accuracy at p=0.1% physical error rate
|
| 10 |
+
- Union-Find decoder: 93.8% accuracy, but 3x faster execution
|
| 11 |
+
- Neural network decoder (preliminary): 95.1% accuracy, training ongoing
|
| 12 |
+
|
| 13 |
+
Marcus showed the distance-5 surface code simulation results. With 49 physical qubits, we achieved a logical error rate of 0.8% at p=0.1% physical error rate. This is competitive with published results.
|
| 14 |
+
|
| 15 |
+
Dr. Volkov proposed an optimization to the syndrome extraction circuit that reduces depth by 15%. This could significantly improve performance on noisy hardware.
|
| 16 |
+
|
| 17 |
+
Dr. Patel raised concerns about the decoder latency requirements for real-time feedback. The neural network decoder currently takes 50ms per decode cycle, which exceeds the 10ms requirement.
|
| 18 |
+
|
| 19 |
+
## Decisions
|
| 20 |
+
- Use Union-Find decoder for real-time experiments (meets latency requirements)
|
| 21 |
+
- Continue neural network decoder development for offline analysis
|
| 22 |
+
- Implement Dr. Volkov's circuit optimization immediately
|
| 23 |
+
- Schedule first hardware experiment for February 5th
|
| 24 |
+
- Target submission to Physical Review Letters by March
|
| 25 |
+
|
| 26 |
+
## Action Items
|
| 27 |
+
- [ ] Marcus Webb: Implement Volkov's circuit optimization by 2025-01-26
|
| 28 |
+
- [ ] James Rodriguez: Optimize neural network decoder inference time by 2025-01-29
|
| 29 |
+
- [ ] Dr. Patel: Prepare hardware experiment protocol by 2025-02-01
|
| 30 |
+
- [ ] Dr. Volkov: Theoretical analysis of optimized circuit fidelity by 2025-01-30
|
| 31 |
+
- [ ] Dr. Chen: Book IBM Quantum time slots for February experiments by 2025-01-25
|
| 32 |
+
- [x] Marcus Webb: Implement distance-5 surface code in Qiskit (completed)
|
| 33 |
+
- [x] James Rodriguez: Integrate MWPM decoder from PyMatching library (completed)
|
| 34 |
+
|
| 35 |
+
## Blockers
|
| 36 |
+
- Neural network decoder latency too high for real-time use - exploring FPGA implementation
|
| 37 |
+
- IBM device calibration drifts daily - need to implement recalibration protocol
|
| 38 |
+
- Publication embargo from IBM on hardware results until March 1st
|
data/quantum_computing/meetings/2025-12-01-test.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Here are the structured meeting notes in markdown format:
|
| 2 |
+
|
| 3 |
+
# Meeting: test
|
| 4 |
+
Date: 2025-12-01
|
| 5 |
+
Participants: alice, bob
|
| 6 |
+
|
| 7 |
+
## Discussion (key points discussed)
|
| 8 |
+
- test
|
| 9 |
+
|
| 10 |
+
## Decisions (decisions made)
|
| 11 |
+
- None mentioned
|
| 12 |
+
|
| 13 |
+
## Action Items (as checkboxes with assignee and deadline if mentioned)
|
| 14 |
+
- [ ] alice: test (no deadline mentioned)
|
| 15 |
+
- [ ] bob: test (no deadline mentioned)
|
| 16 |
+
|
| 17 |
+
## Blockers (any blockers or issues raised)
|
| 18 |
+
- None mentioned
|
requirements.txt
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
# Core dependencies
|
| 2 |
-
gradio==4.
|
| 3 |
langchain>=0.3.0
|
| 4 |
langchain-community>=0.0.10
|
| 5 |
langchain-huggingface>=0.1.0
|
|
|
|
| 6 |
langchain-text-splitters>=0.3.0
|
| 7 |
|
| 8 |
# Vector store and embeddings
|
|
@@ -13,13 +14,23 @@ huggingface-hub>=0.20.0
|
|
| 13 |
# Agent framework
|
| 14 |
langgraph>=0.0.20
|
| 15 |
|
|
|
|
|
|
|
|
|
|
| 16 |
# Document processing
|
| 17 |
python-dotenv>=1.0.0
|
| 18 |
-
pydantic>=2.5.3
|
| 19 |
|
| 20 |
# Utilities
|
| 21 |
pyyaml>=6.0.0
|
| 22 |
python-dateutil>=2.8.2
|
| 23 |
|
| 24 |
# For HF Inference
|
| 25 |
-
transformers>=4.30.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# Core dependencies
|
| 2 |
+
gradio==4.44.1
|
| 3 |
langchain>=0.3.0
|
| 4 |
langchain-community>=0.0.10
|
| 5 |
langchain-huggingface>=0.1.0
|
| 6 |
+
langchain-google-genai>=2.0.0
|
| 7 |
langchain-text-splitters>=0.3.0
|
| 8 |
|
| 9 |
# Vector store and embeddings
|
|
|
|
| 14 |
# Agent framework
|
| 15 |
langgraph>=0.0.20
|
| 16 |
|
| 17 |
+
# Observability
|
| 18 |
+
langsmith>=0.1.0
|
| 19 |
+
|
| 20 |
# Document processing
|
| 21 |
python-dotenv>=1.0.0
|
| 22 |
+
pydantic>=2.5.3,<2.11.0
|
| 23 |
|
| 24 |
# Utilities
|
| 25 |
pyyaml>=6.0.0
|
| 26 |
python-dateutil>=2.8.2
|
| 27 |
|
| 28 |
# For HF Inference
|
| 29 |
+
transformers>=4.30.0
|
| 30 |
+
|
| 31 |
+
# Testing
|
| 32 |
+
pytest>=7.0.0
|
| 33 |
+
pytest-cov>=4.0.0
|
| 34 |
+
|
| 35 |
+
# PDF Export
|
| 36 |
+
fpdf2>=2.7.0
|
src/agent.py
CHANGED
|
@@ -6,6 +6,7 @@ import operator
|
|
| 6 |
import os
|
| 7 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
|
| 8 |
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
|
|
|
| 9 |
from langgraph.graph import StateGraph, END
|
| 10 |
from src.rag import ProjectRAG
|
| 11 |
|
|
@@ -23,20 +24,44 @@ class AgentState(TypedDict):
|
|
| 23 |
|
| 24 |
class ProjectAgent:
|
| 25 |
"""AI Agent for project management queries."""
|
| 26 |
-
|
| 27 |
-
def __init__(self, rag: ProjectRAG,
|
| 28 |
-
"""Initialize the agent.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
self.rag = rag
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
self.graph = self._build_graph()
|
| 41 |
|
| 42 |
def _build_graph(self) -> StateGraph:
|
|
@@ -234,6 +259,95 @@ Example format:
|
|
| 234 |
"next_step": "",
|
| 235 |
"final_answer": ""
|
| 236 |
}
|
| 237 |
-
|
| 238 |
result = self.graph.invoke(initial_state)
|
| 239 |
-
return result["final_answer"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import os
|
| 7 |
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, SystemMessage
|
| 8 |
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 9 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 10 |
from langgraph.graph import StateGraph, END
|
| 11 |
from src.rag import ProjectRAG
|
| 12 |
|
|
|
|
| 24 |
|
| 25 |
class ProjectAgent:
|
| 26 |
"""AI Agent for project management queries."""
|
| 27 |
+
|
| 28 |
+
def __init__(self, rag: ProjectRAG, provider: str = "huggingface", model_name: str = None):
|
| 29 |
+
"""Initialize the agent.
|
| 30 |
+
|
| 31 |
+
Args:
|
| 32 |
+
rag: ProjectRAG instance for retrieval
|
| 33 |
+
provider: "huggingface" (free) or "google" (paid)
|
| 34 |
+
model_name: Optional model name override
|
| 35 |
+
"""
|
| 36 |
self.rag = rag
|
| 37 |
+
self.provider = provider
|
| 38 |
+
|
| 39 |
+
if provider == "google":
|
| 40 |
+
# Use Google Gemini API (paid)
|
| 41 |
+
google_api_key = os.getenv("GOOGLE_API_KEY")
|
| 42 |
+
if not google_api_key:
|
| 43 |
+
raise ValueError("GOOGLE_API_KEY environment variable not set")
|
| 44 |
+
self.llm = ChatGoogleGenerativeAI(
|
| 45 |
+
model=model_name or "gemini-2.5-flash-lite",
|
| 46 |
+
temperature=0.1,
|
| 47 |
+
google_api_key=google_api_key,
|
| 48 |
+
timeout=60, # 60 second timeout
|
| 49 |
+
convert_system_message_to_human=True # Better compatibility
|
| 50 |
+
)
|
| 51 |
+
else:
|
| 52 |
+
# Use HF Inference API (free tier)
|
| 53 |
+
hf_token = os.getenv("HF_TOKEN") or os.getenv("HUGGING_FACE_HUB_TOKEN")
|
| 54 |
+
if not hf_token:
|
| 55 |
+
raise ValueError("HF_TOKEN environment variable not set")
|
| 56 |
+
llm = HuggingFaceEndpoint(
|
| 57 |
+
repo_id=model_name or "meta-llama/Llama-3.2-3B-Instruct",
|
| 58 |
+
temperature=0.1,
|
| 59 |
+
max_new_tokens=512,
|
| 60 |
+
huggingfacehub_api_token=hf_token,
|
| 61 |
+
timeout=60 # 60 second timeout to prevent hanging
|
| 62 |
+
)
|
| 63 |
+
self.llm = ChatHuggingFace(llm=llm)
|
| 64 |
+
|
| 65 |
self.graph = self._build_graph()
|
| 66 |
|
| 67 |
def _build_graph(self) -> StateGraph:
|
|
|
|
| 259 |
"next_step": "",
|
| 260 |
"final_answer": ""
|
| 261 |
}
|
| 262 |
+
|
| 263 |
result = self.graph.invoke(initial_state)
|
| 264 |
+
return result["final_answer"]
|
| 265 |
+
|
| 266 |
+
def stream_query(self, user_query: str):
|
| 267 |
+
"""Process a user query and stream the answer token by token."""
|
| 268 |
+
# First run analysis and retrieval (non-streaming)
|
| 269 |
+
initial_state = {
|
| 270 |
+
"messages": [],
|
| 271 |
+
"query": user_query,
|
| 272 |
+
"retrieved_context": [],
|
| 273 |
+
"action_items": [],
|
| 274 |
+
"blockers": [],
|
| 275 |
+
"next_step": "",
|
| 276 |
+
"final_answer": ""
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
# Run through analysis and retrieval nodes
|
| 280 |
+
state = self.analyze_query(initial_state)
|
| 281 |
+
state = self.retrieve_context(state)
|
| 282 |
+
|
| 283 |
+
# Determine route and get additional data
|
| 284 |
+
route = self.route_after_retrieval(state)
|
| 285 |
+
if route == "action_items":
|
| 286 |
+
state = self.get_action_items(state)
|
| 287 |
+
elif route == "blockers":
|
| 288 |
+
state = self.get_blockers(state)
|
| 289 |
+
|
| 290 |
+
# Now stream the final answer generation
|
| 291 |
+
query = state["query"]
|
| 292 |
+
context = state.get("retrieved_context", [])
|
| 293 |
+
action_items = state.get("action_items", [])
|
| 294 |
+
blockers = state.get("blockers", [])
|
| 295 |
+
|
| 296 |
+
# Build context string
|
| 297 |
+
context_parts = []
|
| 298 |
+
|
| 299 |
+
if context:
|
| 300 |
+
context_parts.append("Relevant meeting context:")
|
| 301 |
+
for i, result in enumerate(context[:3], 1):
|
| 302 |
+
context_parts.append(f"\n[Context {i}]")
|
| 303 |
+
context_parts.append(result['content'])
|
| 304 |
+
if 'metadata' in result:
|
| 305 |
+
meta = result['metadata']
|
| 306 |
+
context_parts.append(f"(From: {meta.get('project', 'Unknown')} - {meta.get('title', 'Unknown')})")
|
| 307 |
+
|
| 308 |
+
if action_items:
|
| 309 |
+
context_parts.append("\nOpen Action Items:")
|
| 310 |
+
for item in action_items:
|
| 311 |
+
assignee = f" ({item['assignee']})" if item.get('assignee') else ""
|
| 312 |
+
deadline = f" by {item['deadline']}" if item.get('deadline') else ""
|
| 313 |
+
context_parts.append(f"- {item['task']}{assignee}{deadline}")
|
| 314 |
+
|
| 315 |
+
if blockers:
|
| 316 |
+
context_parts.append("\nCurrent Blockers:")
|
| 317 |
+
for blocker in blockers:
|
| 318 |
+
context_parts.append(f"- {blocker['blocker']}")
|
| 319 |
+
|
| 320 |
+
context_str = "\n".join(context_parts)
|
| 321 |
+
|
| 322 |
+
# Generate streaming answer
|
| 323 |
+
system_prompt = """You are a helpful AI assistant that helps users manage their projects.
|
| 324 |
+
Use the provided context to answer the user's question accurately and concisely.
|
| 325 |
+
Format your response using bullet points for clarity.
|
| 326 |
+
For action items, list the task with the assignee in parentheses at the end.
|
| 327 |
+
For blockers and risks, list them directly without project names.
|
| 328 |
+
Keep responses brief and to the point. Avoid lengthy explanations.
|
| 329 |
+
Example format:
|
| 330 |
+
## Next Actions
|
| 331 |
+
- Task description (Assignee) by deadline
|
| 332 |
+
- Another task (Assignee)
|
| 333 |
+
|
| 334 |
+
## Blockers/Risks
|
| 335 |
+
- Blocker description
|
| 336 |
+
- Another blocker"""
|
| 337 |
+
|
| 338 |
+
messages = [
|
| 339 |
+
SystemMessage(content=system_prompt),
|
| 340 |
+
HumanMessage(content=f"Context:\n{context_str}\n\nQuestion: {query}\n\nAnswer:")
|
| 341 |
+
]
|
| 342 |
+
|
| 343 |
+
# Stream tokens
|
| 344 |
+
full_response = ""
|
| 345 |
+
try:
|
| 346 |
+
for chunk in self.llm.stream(messages):
|
| 347 |
+
if hasattr(chunk, 'content') and chunk.content:
|
| 348 |
+
full_response += chunk.content
|
| 349 |
+
yield full_response
|
| 350 |
+
except Exception:
|
| 351 |
+
# Fallback to non-streaming if streaming not supported
|
| 352 |
+
response = self.llm.invoke(messages)
|
| 353 |
+
yield response.content
|
src/rag.py
CHANGED
|
@@ -197,11 +197,11 @@ class ProjectRAG:
|
|
| 197 |
def get_recent_decisions(self, project: str = None, limit: int = 10) -> List[Dict[str, Any]]:
|
| 198 |
"""Get recent decisions, optionally filtered by project."""
|
| 199 |
decisions = []
|
| 200 |
-
|
| 201 |
for meeting in sorted(self.meetings, key=lambda m: m.date or datetime.min, reverse=True):
|
| 202 |
if project and meeting.project_name != project:
|
| 203 |
continue
|
| 204 |
-
|
| 205 |
for decision in meeting.decisions:
|
| 206 |
decisions.append({
|
| 207 |
'project': meeting.project_name,
|
|
@@ -209,8 +209,57 @@ class ProjectRAG:
|
|
| 209 |
'date': meeting.date,
|
| 210 |
'decision': decision
|
| 211 |
})
|
| 212 |
-
|
| 213 |
if len(decisions) >= limit:
|
| 214 |
return decisions
|
| 215 |
-
|
| 216 |
return decisions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
def get_recent_decisions(self, project: str = None, limit: int = 10) -> List[Dict[str, Any]]:
|
| 198 |
"""Get recent decisions, optionally filtered by project."""
|
| 199 |
decisions = []
|
| 200 |
+
|
| 201 |
for meeting in sorted(self.meetings, key=lambda m: m.date or datetime.min, reverse=True):
|
| 202 |
if project and meeting.project_name != project:
|
| 203 |
continue
|
| 204 |
+
|
| 205 |
for decision in meeting.decisions:
|
| 206 |
decisions.append({
|
| 207 |
'project': meeting.project_name,
|
|
|
|
| 209 |
'date': meeting.date,
|
| 210 |
'decision': decision
|
| 211 |
})
|
| 212 |
+
|
| 213 |
if len(decisions) >= limit:
|
| 214 |
return decisions
|
| 215 |
+
|
| 216 |
return decisions
|
| 217 |
+
|
| 218 |
+
def get_project_documents(self, project: str) -> List:
|
| 219 |
+
"""Get all meeting documents for a specific project."""
|
| 220 |
+
from langchain_core.documents import Document
|
| 221 |
+
|
| 222 |
+
documents = []
|
| 223 |
+
for meeting in sorted(self.meetings, key=lambda m: m.date or datetime.min):
|
| 224 |
+
if meeting.project_name != project:
|
| 225 |
+
continue
|
| 226 |
+
|
| 227 |
+
# Build full meeting content
|
| 228 |
+
doc_parts = [
|
| 229 |
+
f"# Meeting: {meeting.title}",
|
| 230 |
+
f"**Date:** {meeting.date.strftime('%Y-%m-%d') if meeting.date else 'Unknown'}",
|
| 231 |
+
]
|
| 232 |
+
|
| 233 |
+
if meeting.participants:
|
| 234 |
+
doc_parts.append(f"**Participants:** {', '.join(meeting.participants)}")
|
| 235 |
+
|
| 236 |
+
if meeting.discussion:
|
| 237 |
+
doc_parts.append(f"\n## Discussion\n{meeting.discussion}")
|
| 238 |
+
|
| 239 |
+
if meeting.decisions:
|
| 240 |
+
doc_parts.append("\n## Decisions")
|
| 241 |
+
doc_parts.extend([f"- {d}" for d in meeting.decisions])
|
| 242 |
+
|
| 243 |
+
if meeting.action_items:
|
| 244 |
+
doc_parts.append("\n## Action Items")
|
| 245 |
+
for item in meeting.action_items:
|
| 246 |
+
status = "[x]" if item.completed else "[ ]"
|
| 247 |
+
assignee = f"{item.assignee}: " if item.assignee else ""
|
| 248 |
+
deadline = f" (by {item.deadline})" if item.deadline else ""
|
| 249 |
+
doc_parts.append(f"- {status} {assignee}{item.task}{deadline}")
|
| 250 |
+
|
| 251 |
+
if meeting.blockers:
|
| 252 |
+
doc_parts.append("\n## Blockers")
|
| 253 |
+
doc_parts.extend([f"- {b}" for b in meeting.blockers])
|
| 254 |
+
|
| 255 |
+
full_content = "\n".join(doc_parts)
|
| 256 |
+
documents.append(Document(
|
| 257 |
+
page_content=full_content,
|
| 258 |
+
metadata={
|
| 259 |
+
"project": meeting.project_name,
|
| 260 |
+
"title": meeting.title,
|
| 261 |
+
"date": meeting.date.isoformat() if meeting.date else ""
|
| 262 |
+
}
|
| 263 |
+
))
|
| 264 |
+
|
| 265 |
+
return documents
|
tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Tests for Sherlock Project Assistant."""
|
tests/conftest.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pytest configuration and fixtures."""
|
| 2 |
+
import pytest
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import tempfile
|
| 6 |
+
import shutil
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@pytest.fixture
|
| 10 |
+
def sample_meeting_md():
|
| 11 |
+
"""Sample meeting markdown content."""
|
| 12 |
+
return """# Meeting: Sprint Planning
|
| 13 |
+
Date: 2025-01-15
|
| 14 |
+
Participants: Alice, Bob, Charlie
|
| 15 |
+
|
| 16 |
+
## Discussion
|
| 17 |
+
We discussed the new feature requirements and timeline.
|
| 18 |
+
The team agreed on the architecture approach.
|
| 19 |
+
|
| 20 |
+
## Decisions
|
| 21 |
+
- Use PostgreSQL for the database
|
| 22 |
+
- Deploy on AWS ECS
|
| 23 |
+
|
| 24 |
+
## Action Items
|
| 25 |
+
- [ ] Alice: Implement login page by 2025-01-20
|
| 26 |
+
- [ ] Bob: Set up CI/CD pipeline by 2025-01-18
|
| 27 |
+
- [x] Charlie: Review requirements (completed)
|
| 28 |
+
|
| 29 |
+
## Blockers
|
| 30 |
+
- Waiting for API credentials from vendor
|
| 31 |
+
- Need design approval from stakeholders
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@pytest.fixture
|
| 36 |
+
def temp_data_dir(sample_meeting_md):
|
| 37 |
+
"""Create a temporary data directory with sample meetings."""
|
| 38 |
+
temp_dir = tempfile.mkdtemp()
|
| 39 |
+
data_dir = Path(temp_dir) / "data"
|
| 40 |
+
|
| 41 |
+
# Create project structure
|
| 42 |
+
project_dir = data_dir / "test_project" / "meetings"
|
| 43 |
+
project_dir.mkdir(parents=True)
|
| 44 |
+
|
| 45 |
+
# Write sample meeting
|
| 46 |
+
meeting_file = project_dir / "2025-01-15-sprint-planning.md"
|
| 47 |
+
meeting_file.write_text(sample_meeting_md)
|
| 48 |
+
|
| 49 |
+
yield data_dir
|
| 50 |
+
|
| 51 |
+
# Cleanup
|
| 52 |
+
shutil.rmtree(temp_dir)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@pytest.fixture
|
| 56 |
+
def hf_token():
|
| 57 |
+
"""Get HuggingFace token from environment."""
|
| 58 |
+
token = os.getenv("HF_TOKEN")
|
| 59 |
+
if not token:
|
| 60 |
+
pytest.skip("HF_TOKEN not set - skipping HuggingFace tests")
|
| 61 |
+
return token
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@pytest.fixture
|
| 65 |
+
def google_api_key():
|
| 66 |
+
"""Get Google API key from environment."""
|
| 67 |
+
key = os.getenv("GOOGLE_API_KEY")
|
| 68 |
+
if not key:
|
| 69 |
+
pytest.skip("GOOGLE_API_KEY not set - skipping Google API tests")
|
| 70 |
+
return key
|
tests/test_app.py
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for app.py functionality - Upload Meeting and Create Project."""
|
| 2 |
+
import pytest
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import tempfile
|
| 6 |
+
import shutil
|
| 7 |
+
|
| 8 |
+
# Import app functions
|
| 9 |
+
import sys
|
| 10 |
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
| 11 |
+
|
| 12 |
+
from src.rag import ProjectRAG
|
| 13 |
+
from src.parsers import MeetingParser
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# Sample raw meeting notes for testing
|
| 17 |
+
SAMPLE_RAW_NOTES_QUANTUM = """
|
| 18 |
+
We had a sync meeting about the quantum error correction project.
|
| 19 |
+
Dr. Chen presented the new surface code implementation results.
|
| 20 |
+
The decoder is showing 15ms latency which is too slow for real-time correction.
|
| 21 |
+
|
| 22 |
+
Sarah mentioned we got access to IBM's 127-qubit Eagle processor next month.
|
| 23 |
+
Marcus is still blocked on the FPGA development - waiting for the new boards.
|
| 24 |
+
|
| 25 |
+
We decided to switch from Union-Find to MWPM decoder for better accuracy.
|
| 26 |
+
Also agreed to target 1ms latency for the final system.
|
| 27 |
+
|
| 28 |
+
Tasks:
|
| 29 |
+
- Dr. Chen will optimize the decoder by end of week
|
| 30 |
+
- Sarah needs to prepare the calibration scripts for IBM hardware
|
| 31 |
+
- Marcus to follow up with vendor about FPGA delivery
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
SAMPLE_RAW_NOTES_NEW_PROJECT = """
|
| 35 |
+
Kickoff meeting for the new recommendation engine project.
|
| 36 |
+
Team: Alice, Bob, Carol, David
|
| 37 |
+
|
| 38 |
+
Discussed architecture options - decided on collaborative filtering approach.
|
| 39 |
+
Bob raised concern about cold start problem for new users.
|
| 40 |
+
|
| 41 |
+
Alice will research embedding models this week.
|
| 42 |
+
Carol to set up the data pipeline by Friday.
|
| 43 |
+
David blocked on getting production database access.
|
| 44 |
+
|
| 45 |
+
Next meeting scheduled for Monday.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class TestMeetingParser:
|
| 50 |
+
"""Test meeting parsing functionality."""
|
| 51 |
+
|
| 52 |
+
def test_parse_action_items_from_raw_notes(self):
|
| 53 |
+
"""Test that action items can be extracted from structured notes."""
|
| 54 |
+
# Create a structured meeting file
|
| 55 |
+
structured_content = """# Meeting: Test Meeting
|
| 56 |
+
Date: 2025-01-30
|
| 57 |
+
Participants: Alice, Bob
|
| 58 |
+
|
| 59 |
+
## Discussion
|
| 60 |
+
Test discussion.
|
| 61 |
+
|
| 62 |
+
## Action Items
|
| 63 |
+
- [ ] Alice: Complete task by 2025-02-05
|
| 64 |
+
- [ ] Bob: Review code
|
| 65 |
+
- [x] Carol: Setup done (completed)
|
| 66 |
+
|
| 67 |
+
## Blockers
|
| 68 |
+
- Waiting for API access
|
| 69 |
+
"""
|
| 70 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
| 71 |
+
f.write(structured_content)
|
| 72 |
+
temp_path = Path(f.name)
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
meeting = MeetingParser.parse(temp_path, "test_project")
|
| 76 |
+
assert meeting is not None
|
| 77 |
+
assert len(meeting.action_items) == 3
|
| 78 |
+
|
| 79 |
+
# Check open items
|
| 80 |
+
open_items = [a for a in meeting.action_items if not a.completed]
|
| 81 |
+
assert len(open_items) == 2
|
| 82 |
+
|
| 83 |
+
# Check assignees
|
| 84 |
+
assignees = [a.assignee for a in meeting.action_items if a.assignee]
|
| 85 |
+
assert "Alice" in assignees
|
| 86 |
+
assert "Bob" in assignees
|
| 87 |
+
finally:
|
| 88 |
+
os.unlink(temp_path)
|
| 89 |
+
|
| 90 |
+
def test_parse_blockers(self):
|
| 91 |
+
"""Test blocker extraction."""
|
| 92 |
+
structured_content = """# Meeting: Blocker Test
|
| 93 |
+
Date: 2025-01-30
|
| 94 |
+
Participants: Team
|
| 95 |
+
|
| 96 |
+
## Discussion
|
| 97 |
+
Discussed blockers.
|
| 98 |
+
|
| 99 |
+
## Blockers
|
| 100 |
+
- Waiting for hardware delivery
|
| 101 |
+
- Need security clearance for data access
|
| 102 |
+
- Vendor has not responded to queries
|
| 103 |
+
"""
|
| 104 |
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as f:
|
| 105 |
+
f.write(structured_content)
|
| 106 |
+
temp_path = Path(f.name)
|
| 107 |
+
|
| 108 |
+
try:
|
| 109 |
+
meeting = MeetingParser.parse(temp_path, "test_project")
|
| 110 |
+
assert meeting is not None
|
| 111 |
+
assert len(meeting.blockers) == 3
|
| 112 |
+
# blockers is a List[str], not objects
|
| 113 |
+
assert any("hardware" in b.lower() for b in meeting.blockers)
|
| 114 |
+
finally:
|
| 115 |
+
os.unlink(temp_path)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class TestProjectCreation:
|
| 119 |
+
"""Test creating new projects."""
|
| 120 |
+
|
| 121 |
+
def test_create_new_project_directory(self):
|
| 122 |
+
"""Test that new project directories are created correctly."""
|
| 123 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 124 |
+
project_name = "test_new_project"
|
| 125 |
+
project_dir = Path(temp_dir) / project_name / "meetings"
|
| 126 |
+
|
| 127 |
+
# Simulate what app.py does
|
| 128 |
+
project_dir.mkdir(parents=True, exist_ok=True)
|
| 129 |
+
|
| 130 |
+
assert project_dir.exists()
|
| 131 |
+
assert project_dir.is_dir()
|
| 132 |
+
assert (Path(temp_dir) / project_name).exists()
|
| 133 |
+
|
| 134 |
+
def test_save_meeting_to_new_project(self):
|
| 135 |
+
"""Test saving a meeting file to a new project."""
|
| 136 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 137 |
+
project_name = "recommendation_engine"
|
| 138 |
+
meeting_date = "2025-01-30"
|
| 139 |
+
meeting_title = "kickoff"
|
| 140 |
+
|
| 141 |
+
project_dir = Path(temp_dir) / project_name / "meetings"
|
| 142 |
+
project_dir.mkdir(parents=True, exist_ok=True)
|
| 143 |
+
|
| 144 |
+
filename = f"{meeting_date}-{meeting_title}.md"
|
| 145 |
+
file_path = project_dir / filename
|
| 146 |
+
|
| 147 |
+
content = """# Meeting: Kickoff
|
| 148 |
+
Date: 2025-01-30
|
| 149 |
+
Participants: Alice, Bob
|
| 150 |
+
|
| 151 |
+
## Discussion
|
| 152 |
+
Initial project discussion.
|
| 153 |
+
|
| 154 |
+
## Action Items
|
| 155 |
+
- [ ] Alice: Research models by 2025-02-05
|
| 156 |
+
|
| 157 |
+
## Blockers
|
| 158 |
+
- Need database access
|
| 159 |
+
"""
|
| 160 |
+
with open(file_path, 'w') as f:
|
| 161 |
+
f.write(content)
|
| 162 |
+
|
| 163 |
+
assert file_path.exists()
|
| 164 |
+
|
| 165 |
+
# Verify RAG can load it
|
| 166 |
+
rag = ProjectRAG(Path(temp_dir))
|
| 167 |
+
rag.load_and_index()
|
| 168 |
+
|
| 169 |
+
projects = rag.get_all_projects()
|
| 170 |
+
assert project_name in projects
|
| 171 |
+
|
| 172 |
+
# Verify action items are indexed
|
| 173 |
+
action_items = rag.get_open_action_items(project=project_name)
|
| 174 |
+
assert len(action_items) >= 1
|
| 175 |
+
|
| 176 |
+
def test_create_project_with_special_characters(self):
|
| 177 |
+
"""Test project creation handles names properly."""
|
| 178 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 179 |
+
# Test with underscores and numbers
|
| 180 |
+
project_name = "project_v2_2025"
|
| 181 |
+
project_dir = Path(temp_dir) / project_name / "meetings"
|
| 182 |
+
project_dir.mkdir(parents=True, exist_ok=True)
|
| 183 |
+
|
| 184 |
+
assert project_dir.exists()
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class TestUploadMeeting:
|
| 188 |
+
"""Test the upload meeting functionality."""
|
| 189 |
+
|
| 190 |
+
def test_meeting_file_naming(self):
|
| 191 |
+
"""Test that meeting files are named correctly."""
|
| 192 |
+
meeting_date = "2025-01-30"
|
| 193 |
+
meeting_title = "Sprint Planning"
|
| 194 |
+
|
| 195 |
+
# Simulate the naming logic from app.py
|
| 196 |
+
filename = f"{meeting_date}-{meeting_title.lower().replace(' ', '-')}.md"
|
| 197 |
+
|
| 198 |
+
assert filename == "2025-01-30-sprint-planning.md"
|
| 199 |
+
|
| 200 |
+
def test_meeting_file_naming_no_title(self):
|
| 201 |
+
"""Test meeting file naming when no title provided."""
|
| 202 |
+
meeting_date = "2025-01-30"
|
| 203 |
+
meeting_title = None
|
| 204 |
+
|
| 205 |
+
filename = f"{meeting_date}-{meeting_title.lower().replace(' ', '-') if meeting_title else 'meeting'}.md"
|
| 206 |
+
|
| 207 |
+
assert filename == "2025-01-30-meeting.md"
|
| 208 |
+
|
| 209 |
+
def test_quantum_project_upload(self):
|
| 210 |
+
"""Test uploading a meeting to quantum_computing project."""
|
| 211 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 212 |
+
# Create existing quantum_computing project
|
| 213 |
+
quantum_dir = Path(temp_dir) / "quantum_computing" / "meetings"
|
| 214 |
+
quantum_dir.mkdir(parents=True)
|
| 215 |
+
|
| 216 |
+
# Add an existing meeting
|
| 217 |
+
existing_meeting = """# Meeting: Previous Sync
|
| 218 |
+
Date: 2025-01-25
|
| 219 |
+
Participants: Dr. Chen, Sarah
|
| 220 |
+
|
| 221 |
+
## Discussion
|
| 222 |
+
Previous work discussion.
|
| 223 |
+
"""
|
| 224 |
+
(quantum_dir / "2025-01-25-previous-sync.md").write_text(existing_meeting)
|
| 225 |
+
|
| 226 |
+
# Now simulate uploading a new meeting
|
| 227 |
+
new_meeting_content = """# Meeting: Decoder Optimization Review
|
| 228 |
+
Date: 2025-01-30
|
| 229 |
+
Participants: Dr. Chen, Sarah, Marcus
|
| 230 |
+
|
| 231 |
+
## Discussion
|
| 232 |
+
Reviewed decoder performance. Current latency is 15ms, target is 1ms.
|
| 233 |
+
Discussed MWPM vs Union-Find approaches.
|
| 234 |
+
|
| 235 |
+
## Decisions
|
| 236 |
+
- Switch to MWPM decoder for better accuracy
|
| 237 |
+
- Target 1ms latency for production
|
| 238 |
+
|
| 239 |
+
## Action Items
|
| 240 |
+
- [ ] Dr. Chen: Implement MWPM decoder by 2025-02-07
|
| 241 |
+
- [ ] Sarah: Benchmark on IBM simulator by 2025-02-10
|
| 242 |
+
- [ ] Marcus: Order additional FPGA boards
|
| 243 |
+
|
| 244 |
+
## Blockers
|
| 245 |
+
- FPGA delivery delayed by 2 weeks
|
| 246 |
+
- Need IBM quantum credits approval
|
| 247 |
+
"""
|
| 248 |
+
new_file = quantum_dir / "2025-01-30-decoder-optimization-review.md"
|
| 249 |
+
new_file.write_text(new_meeting_content)
|
| 250 |
+
|
| 251 |
+
# Verify both meetings exist
|
| 252 |
+
meetings = list(quantum_dir.glob("*.md"))
|
| 253 |
+
assert len(meetings) == 2
|
| 254 |
+
|
| 255 |
+
# Verify RAG indexes both
|
| 256 |
+
rag = ProjectRAG(Path(temp_dir))
|
| 257 |
+
rag.load_and_index()
|
| 258 |
+
|
| 259 |
+
assert "quantum_computing" in rag.get_all_projects()
|
| 260 |
+
assert len(rag.meetings) == 2
|
| 261 |
+
|
| 262 |
+
# Verify action items
|
| 263 |
+
action_items = rag.get_open_action_items(project="quantum_computing")
|
| 264 |
+
assert len(action_items) >= 3
|
| 265 |
+
|
| 266 |
+
# Verify blockers
|
| 267 |
+
blockers = rag.get_blockers(project="quantum_computing")
|
| 268 |
+
assert len(blockers) >= 2
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
class TestRAGWithMultipleProjects:
|
| 272 |
+
"""Test RAG functionality with multiple projects."""
|
| 273 |
+
|
| 274 |
+
def test_multiple_projects_isolation(self):
|
| 275 |
+
"""Test that queries can be isolated to specific projects."""
|
| 276 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 277 |
+
# Create two projects
|
| 278 |
+
project1_dir = Path(temp_dir) / "quantum_computing" / "meetings"
|
| 279 |
+
project2_dir = Path(temp_dir) / "covid_prediction" / "meetings"
|
| 280 |
+
project1_dir.mkdir(parents=True)
|
| 281 |
+
project2_dir.mkdir(parents=True)
|
| 282 |
+
|
| 283 |
+
# Add meeting to project 1
|
| 284 |
+
(project1_dir / "2025-01-30-quantum.md").write_text("""# Meeting: Quantum Sync
|
| 285 |
+
Date: 2025-01-30
|
| 286 |
+
Participants: Dr. Chen
|
| 287 |
+
|
| 288 |
+
## Action Items
|
| 289 |
+
- [ ] Dr. Chen: Optimize decoder
|
| 290 |
+
|
| 291 |
+
## Blockers
|
| 292 |
+
- FPGA delivery delayed
|
| 293 |
+
""")
|
| 294 |
+
|
| 295 |
+
# Add meeting to project 2
|
| 296 |
+
(project2_dir / "2025-01-30-covid.md").write_text("""# Meeting: COVID Analysis
|
| 297 |
+
Date: 2025-01-30
|
| 298 |
+
Participants: Dr. Foster
|
| 299 |
+
|
| 300 |
+
## Action Items
|
| 301 |
+
- [ ] Dr. Foster: Review model accuracy
|
| 302 |
+
|
| 303 |
+
## Blockers
|
| 304 |
+
- Data quality issues with GISAID
|
| 305 |
+
""")
|
| 306 |
+
|
| 307 |
+
rag = ProjectRAG(Path(temp_dir))
|
| 308 |
+
rag.load_and_index()
|
| 309 |
+
|
| 310 |
+
# Test project filtering
|
| 311 |
+
quantum_items = rag.get_open_action_items(project="quantum_computing")
|
| 312 |
+
covid_items = rag.get_open_action_items(project="covid_prediction")
|
| 313 |
+
|
| 314 |
+
assert any("decoder" in item['task'].lower() for item in quantum_items)
|
| 315 |
+
assert any("model" in item['task'].lower() for item in covid_items)
|
| 316 |
+
|
| 317 |
+
# Test blocker filtering
|
| 318 |
+
quantum_blockers = rag.get_blockers(project="quantum_computing")
|
| 319 |
+
covid_blockers = rag.get_blockers(project="covid_prediction")
|
| 320 |
+
|
| 321 |
+
assert any("fpga" in b['blocker'].lower() for b in quantum_blockers)
|
| 322 |
+
assert any("gisaid" in b['blocker'].lower() for b in covid_blockers)
|
| 323 |
+
|
| 324 |
+
def test_all_projects_query(self):
|
| 325 |
+
"""Test querying across all projects."""
|
| 326 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 327 |
+
# Create two projects with meetings
|
| 328 |
+
for project in ["project_a", "project_b"]:
|
| 329 |
+
project_dir = Path(temp_dir) / project / "meetings"
|
| 330 |
+
project_dir.mkdir(parents=True)
|
| 331 |
+
(project_dir / "2025-01-30-meeting.md").write_text(f"""# Meeting: {project} Sync
|
| 332 |
+
Date: 2025-01-30
|
| 333 |
+
Participants: Team
|
| 334 |
+
|
| 335 |
+
## Action Items
|
| 336 |
+
- [ ] Someone: Task for {project}
|
| 337 |
+
""")
|
| 338 |
+
|
| 339 |
+
rag = ProjectRAG(Path(temp_dir))
|
| 340 |
+
rag.load_and_index()
|
| 341 |
+
|
| 342 |
+
# Get all action items without filter
|
| 343 |
+
all_items = rag.get_open_action_items()
|
| 344 |
+
assert len(all_items) >= 2
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
class TestIntegrationUploadMeeting:
|
| 348 |
+
"""Integration tests for upload meeting with LLM (requires tokens)."""
|
| 349 |
+
|
| 350 |
+
def test_structure_meeting_hf(self):
|
| 351 |
+
"""Test meeting structuring with HuggingFace."""
|
| 352 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 353 |
+
if not hf_token:
|
| 354 |
+
pytest.skip("HF_TOKEN not set")
|
| 355 |
+
|
| 356 |
+
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 357 |
+
from langchain_core.messages import SystemMessage, HumanMessage
|
| 358 |
+
|
| 359 |
+
endpoint = HuggingFaceEndpoint(
|
| 360 |
+
repo_id="meta-llama/Llama-3.2-3B-Instruct",
|
| 361 |
+
temperature=0.3,
|
| 362 |
+
max_new_tokens=1024,
|
| 363 |
+
huggingfacehub_api_token=hf_token,
|
| 364 |
+
timeout=60
|
| 365 |
+
)
|
| 366 |
+
llm = ChatHuggingFace(llm=endpoint)
|
| 367 |
+
|
| 368 |
+
system_prompt = """Structure these meeting notes into markdown with sections:
|
| 369 |
+
# Meeting: [title], Date:, Participants:, ## Discussion, ## Action Items, ## Blockers"""
|
| 370 |
+
|
| 371 |
+
messages = [
|
| 372 |
+
SystemMessage(content=system_prompt),
|
| 373 |
+
HumanMessage(content=f"Raw notes: {SAMPLE_RAW_NOTES_QUANTUM}")
|
| 374 |
+
]
|
| 375 |
+
|
| 376 |
+
response = llm.invoke(messages)
|
| 377 |
+
assert response.content is not None
|
| 378 |
+
assert len(response.content) > 100
|
| 379 |
+
# Should have some structure
|
| 380 |
+
assert "action" in response.content.lower() or "task" in response.content.lower()
|
| 381 |
+
|
| 382 |
+
def test_structure_meeting_google(self):
|
| 383 |
+
"""Test meeting structuring with Google."""
|
| 384 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 385 |
+
if not api_key:
|
| 386 |
+
pytest.skip("GOOGLE_API_KEY not set")
|
| 387 |
+
|
| 388 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 389 |
+
from langchain_core.messages import SystemMessage, HumanMessage
|
| 390 |
+
|
| 391 |
+
llm = ChatGoogleGenerativeAI(
|
| 392 |
+
model="gemini-2.0-flash",
|
| 393 |
+
temperature=0.3,
|
| 394 |
+
google_api_key=api_key,
|
| 395 |
+
timeout=60
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
system_prompt = """Structure these meeting notes into markdown with sections:
|
| 399 |
+
# Meeting: [title], Date:, Participants:, ## Discussion, ## Action Items, ## Blockers"""
|
| 400 |
+
|
| 401 |
+
messages = [
|
| 402 |
+
SystemMessage(content=system_prompt),
|
| 403 |
+
HumanMessage(content=f"Raw notes: {SAMPLE_RAW_NOTES_NEW_PROJECT}")
|
| 404 |
+
]
|
| 405 |
+
|
| 406 |
+
response = llm.invoke(messages)
|
| 407 |
+
assert response.content is not None
|
| 408 |
+
assert len(response.content) > 100
|
tests/test_evaluation.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent Evaluation Tests - Measures agent quality and performance.
|
| 3 |
+
|
| 4 |
+
Run with: pytest tests/test_evaluation.py -v -s
|
| 5 |
+
"""
|
| 6 |
+
import pytest
|
| 7 |
+
import time
|
| 8 |
+
import os
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from dataclasses import dataclass
|
| 11 |
+
from typing import List
|
| 12 |
+
|
| 13 |
+
# Skip if no API tokens available
|
| 14 |
+
pytestmark = pytest.mark.skipif(
|
| 15 |
+
not (os.getenv("HF_TOKEN") or os.getenv("GOOGLE_API_KEY")),
|
| 16 |
+
reason="Requires HF_TOKEN or GOOGLE_API_KEY"
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass
|
| 21 |
+
class EvalCase:
|
| 22 |
+
"""Test case for evaluation."""
|
| 23 |
+
name: str
|
| 24 |
+
query: str
|
| 25 |
+
expected_keywords: List[str]
|
| 26 |
+
category: str
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
# Evaluation test cases - keywords aligned with actual agent responses
|
| 30 |
+
EVAL_CASES = [
|
| 31 |
+
EvalCase(
|
| 32 |
+
name="action_items_query",
|
| 33 |
+
query="What are the open action items?",
|
| 34 |
+
expected_keywords=["action", "item", "implement", "complete", "next"],
|
| 35 |
+
category="action_items"
|
| 36 |
+
),
|
| 37 |
+
EvalCase(
|
| 38 |
+
name="blockers_query",
|
| 39 |
+
query="What blockers do we have?",
|
| 40 |
+
expected_keywords=["blocker", "block", "risk", "waiting", "issue"],
|
| 41 |
+
category="blockers"
|
| 42 |
+
),
|
| 43 |
+
EvalCase(
|
| 44 |
+
name="project_summary",
|
| 45 |
+
query="Give me a summary of the project",
|
| 46 |
+
expected_keywords=["project", "meeting", "discuss", "team", "work"],
|
| 47 |
+
category="general"
|
| 48 |
+
),
|
| 49 |
+
EvalCase(
|
| 50 |
+
name="next_steps_query",
|
| 51 |
+
query="What should we do next?",
|
| 52 |
+
expected_keywords=["next", "action", "should", "need", "implement"],
|
| 53 |
+
category="action_items"
|
| 54 |
+
),
|
| 55 |
+
EvalCase(
|
| 56 |
+
name="issues_query",
|
| 57 |
+
query="What issues or problems were discussed?",
|
| 58 |
+
expected_keywords=["issue", "problem", "blocker", "challenge", "risk"],
|
| 59 |
+
category="blockers"
|
| 60 |
+
),
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class EvaluationMetrics:
|
| 65 |
+
"""Collect and compute evaluation metrics."""
|
| 66 |
+
|
| 67 |
+
def __init__(self):
|
| 68 |
+
self.results = []
|
| 69 |
+
|
| 70 |
+
def add_result(self, case: EvalCase, response: str, latency: float):
|
| 71 |
+
"""Add a single evaluation result."""
|
| 72 |
+
# Keyword match score
|
| 73 |
+
keywords_found = sum(
|
| 74 |
+
1 for kw in case.expected_keywords
|
| 75 |
+
if kw.lower() in response.lower()
|
| 76 |
+
)
|
| 77 |
+
keyword_score = keywords_found / len(case.expected_keywords) if case.expected_keywords else 1.0
|
| 78 |
+
|
| 79 |
+
# Response validity
|
| 80 |
+
is_valid = (
|
| 81 |
+
len(response) > 50 and
|
| 82 |
+
not response.startswith("❌") and
|
| 83 |
+
not response.startswith("⚠️")
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
# Response length (penalize too short or too long)
|
| 87 |
+
length_score = 1.0
|
| 88 |
+
if len(response) < 100:
|
| 89 |
+
length_score = 0.5
|
| 90 |
+
elif len(response) > 2000:
|
| 91 |
+
length_score = 0.8
|
| 92 |
+
|
| 93 |
+
self.results.append({
|
| 94 |
+
"name": case.name,
|
| 95 |
+
"category": case.category,
|
| 96 |
+
"keyword_score": keyword_score,
|
| 97 |
+
"is_valid": is_valid,
|
| 98 |
+
"length_score": length_score,
|
| 99 |
+
"latency_ms": latency,
|
| 100 |
+
"response_length": len(response)
|
| 101 |
+
})
|
| 102 |
+
|
| 103 |
+
def compute_summary(self) -> dict:
|
| 104 |
+
"""Compute summary metrics."""
|
| 105 |
+
if not self.results:
|
| 106 |
+
return {}
|
| 107 |
+
|
| 108 |
+
total = len(self.results)
|
| 109 |
+
passed = sum(1 for r in self.results if r["keyword_score"] >= 0.4 and r["is_valid"] and r["response_length"] >= 100)
|
| 110 |
+
|
| 111 |
+
avg_keyword_score = sum(r["keyword_score"] for r in self.results) / total
|
| 112 |
+
avg_latency = sum(r["latency_ms"] for r in self.results) / total
|
| 113 |
+
avg_length = sum(r["response_length"] for r in self.results) / total
|
| 114 |
+
|
| 115 |
+
return {
|
| 116 |
+
"total_cases": total,
|
| 117 |
+
"passed": passed,
|
| 118 |
+
"failed": total - passed,
|
| 119 |
+
"pass_rate": round(passed / total * 100, 1),
|
| 120 |
+
"avg_keyword_score": round(avg_keyword_score * 100, 1),
|
| 121 |
+
"avg_latency_ms": round(avg_latency, 0),
|
| 122 |
+
"avg_response_length": round(avg_length, 0)
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
@pytest.fixture(scope="module")
|
| 127 |
+
def agent():
|
| 128 |
+
"""Initialize agent for evaluation."""
|
| 129 |
+
from src.rag import ProjectRAG
|
| 130 |
+
from src.agent import ProjectAgent
|
| 131 |
+
|
| 132 |
+
data_dir = Path("./data")
|
| 133 |
+
rag = ProjectRAG(data_dir)
|
| 134 |
+
rag.load_and_index()
|
| 135 |
+
|
| 136 |
+
# Use Google if available (faster), otherwise HuggingFace
|
| 137 |
+
if os.getenv("GOOGLE_API_KEY"):
|
| 138 |
+
agent = ProjectAgent(rag, provider="google")
|
| 139 |
+
else:
|
| 140 |
+
agent = ProjectAgent(rag, provider="huggingface")
|
| 141 |
+
|
| 142 |
+
return agent
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
@pytest.fixture(scope="module")
|
| 146 |
+
def metrics():
|
| 147 |
+
"""Shared metrics collector."""
|
| 148 |
+
return EvaluationMetrics()
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
class TestAgentEvaluation:
|
| 152 |
+
"""Evaluation test suite."""
|
| 153 |
+
|
| 154 |
+
@pytest.mark.parametrize("case", EVAL_CASES, ids=lambda c: c.name)
|
| 155 |
+
def test_query(self, agent, metrics, case):
|
| 156 |
+
"""Test individual query case."""
|
| 157 |
+
start = time.time()
|
| 158 |
+
response = agent.query(case.query)
|
| 159 |
+
latency = (time.time() - start) * 1000
|
| 160 |
+
|
| 161 |
+
metrics.add_result(case, response, latency)
|
| 162 |
+
|
| 163 |
+
# Basic assertions
|
| 164 |
+
assert response is not None
|
| 165 |
+
assert len(response) > 0
|
| 166 |
+
|
| 167 |
+
# Check at least one keyword found
|
| 168 |
+
keywords_found = sum(
|
| 169 |
+
1 for kw in case.expected_keywords
|
| 170 |
+
if kw.lower() in response.lower()
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
print(f"\n Query: {case.query}")
|
| 174 |
+
print(f" Keywords found: {keywords_found}/{len(case.expected_keywords)}")
|
| 175 |
+
print(f" Latency: {latency:.0f}ms")
|
| 176 |
+
print(f" Response length: {len(response)} chars")
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def test_evaluation_summary(metrics):
|
| 180 |
+
"""Print evaluation summary after all tests."""
|
| 181 |
+
summary = metrics.compute_summary()
|
| 182 |
+
|
| 183 |
+
if summary:
|
| 184 |
+
print("\n" + "="*60)
|
| 185 |
+
print("EVALUATION SUMMARY")
|
| 186 |
+
print("="*60)
|
| 187 |
+
print(f"Total Cases: {summary['total_cases']}")
|
| 188 |
+
print(f"Passed: {summary['passed']}")
|
| 189 |
+
print(f"Failed: {summary['failed']}")
|
| 190 |
+
print(f"Pass Rate: {summary['pass_rate']}%")
|
| 191 |
+
print(f"Avg Keyword Score: {summary['avg_keyword_score']}%")
|
| 192 |
+
print(f"Avg Latency: {summary['avg_latency_ms']}ms")
|
| 193 |
+
print(f"Avg Response Len: {summary['avg_response_length']} chars")
|
| 194 |
+
print("="*60)
|
| 195 |
+
|
| 196 |
+
# Assert minimum quality (80% pass rate required)
|
| 197 |
+
assert summary["pass_rate"] >= 80, f"Pass rate too low: {summary['pass_rate']}%"
|
tests/test_integration.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Integration tests for LLM providers."""
|
| 2 |
+
import pytest
|
| 3 |
+
import os
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import tempfile
|
| 6 |
+
from src.rag import ProjectRAG
|
| 7 |
+
from src.agent import ProjectAgent
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
# Sample meeting for testing
|
| 11 |
+
SAMPLE_MEETING = """# Meeting: Test Sprint Planning
|
| 12 |
+
Date: 2025-01-15
|
| 13 |
+
Participants: Alice, Bob
|
| 14 |
+
|
| 15 |
+
## Discussion
|
| 16 |
+
Discussed the test implementation.
|
| 17 |
+
|
| 18 |
+
## Decisions
|
| 19 |
+
- Use pytest for testing
|
| 20 |
+
|
| 21 |
+
## Action Items
|
| 22 |
+
- [ ] Alice: Write unit tests by 2025-01-20
|
| 23 |
+
- [ ] Bob: Review code by 2025-01-18
|
| 24 |
+
|
| 25 |
+
## Blockers
|
| 26 |
+
- Waiting for API access
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@pytest.fixture
|
| 31 |
+
def test_rag():
|
| 32 |
+
"""Create a RAG system with test data."""
|
| 33 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 34 |
+
# Create data structure
|
| 35 |
+
data_dir = Path(temp_dir) / "data"
|
| 36 |
+
project_dir = data_dir / "test_project" / "meetings"
|
| 37 |
+
project_dir.mkdir(parents=True)
|
| 38 |
+
|
| 39 |
+
# Write sample meeting
|
| 40 |
+
(project_dir / "2025-01-15-sprint.md").write_text(SAMPLE_MEETING)
|
| 41 |
+
|
| 42 |
+
# Create persistent dir for ChromaDB
|
| 43 |
+
persist_dir = Path(temp_dir) / "chroma"
|
| 44 |
+
|
| 45 |
+
# Initialize RAG
|
| 46 |
+
rag = ProjectRAG(data_dir, persist_dir=persist_dir)
|
| 47 |
+
rag.load_and_index()
|
| 48 |
+
|
| 49 |
+
yield rag
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class TestHuggingFaceProvider:
|
| 53 |
+
"""Integration tests for HuggingFace provider."""
|
| 54 |
+
|
| 55 |
+
def test_hf_agent_creation(self, test_rag):
|
| 56 |
+
"""Test that HuggingFace agent can be created with valid token."""
|
| 57 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 58 |
+
if not hf_token:
|
| 59 |
+
pytest.skip("HF_TOKEN not set")
|
| 60 |
+
|
| 61 |
+
agent = ProjectAgent(test_rag, provider="huggingface")
|
| 62 |
+
assert agent is not None
|
| 63 |
+
assert agent.provider == "huggingface"
|
| 64 |
+
assert agent.llm is not None
|
| 65 |
+
|
| 66 |
+
def test_hf_simple_query(self, test_rag):
|
| 67 |
+
"""Test a simple query with HuggingFace."""
|
| 68 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 69 |
+
if not hf_token:
|
| 70 |
+
pytest.skip("HF_TOKEN not set")
|
| 71 |
+
|
| 72 |
+
agent = ProjectAgent(test_rag, provider="huggingface")
|
| 73 |
+
response = agent.query("What are the action items?")
|
| 74 |
+
|
| 75 |
+
assert response is not None
|
| 76 |
+
assert len(response) > 0
|
| 77 |
+
# Should mention Alice or Bob from the test data
|
| 78 |
+
assert "alice" in response.lower() or "bob" in response.lower() or "test" in response.lower()
|
| 79 |
+
|
| 80 |
+
def test_hf_blockers_query(self, test_rag):
|
| 81 |
+
"""Test blockers query with HuggingFace."""
|
| 82 |
+
hf_token = os.getenv("HF_TOKEN")
|
| 83 |
+
if not hf_token:
|
| 84 |
+
pytest.skip("HF_TOKEN not set")
|
| 85 |
+
|
| 86 |
+
agent = ProjectAgent(test_rag, provider="huggingface")
|
| 87 |
+
response = agent.query("What blockers do we have?")
|
| 88 |
+
|
| 89 |
+
assert response is not None
|
| 90 |
+
assert len(response) > 0
|
| 91 |
+
|
| 92 |
+
def test_hf_invalid_token(self, test_rag):
|
| 93 |
+
"""Test that invalid token raises appropriate error."""
|
| 94 |
+
os.environ["HF_TOKEN"] = "invalid_token_12345"
|
| 95 |
+
|
| 96 |
+
agent = ProjectAgent(test_rag, provider="huggingface")
|
| 97 |
+
|
| 98 |
+
with pytest.raises(Exception) as exc_info:
|
| 99 |
+
agent.query("What are the action items?")
|
| 100 |
+
|
| 101 |
+
# Should get an authentication error
|
| 102 |
+
error_msg = str(exc_info.value).lower()
|
| 103 |
+
assert "401" in error_msg or "unauthorized" in error_msg or "invalid" in error_msg or "error" in error_msg
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class TestGoogleProvider:
|
| 107 |
+
"""Integration tests for Google provider."""
|
| 108 |
+
|
| 109 |
+
def test_google_agent_creation(self, test_rag):
|
| 110 |
+
"""Test that Google agent can be created with valid key."""
|
| 111 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 112 |
+
if not api_key:
|
| 113 |
+
pytest.skip("GOOGLE_API_KEY not set")
|
| 114 |
+
|
| 115 |
+
agent = ProjectAgent(test_rag, provider="google")
|
| 116 |
+
assert agent is not None
|
| 117 |
+
assert agent.provider == "google"
|
| 118 |
+
assert agent.llm is not None
|
| 119 |
+
|
| 120 |
+
def test_google_simple_query(self, test_rag):
|
| 121 |
+
"""Test a simple query with Google."""
|
| 122 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 123 |
+
if not api_key:
|
| 124 |
+
pytest.skip("GOOGLE_API_KEY not set")
|
| 125 |
+
|
| 126 |
+
agent = ProjectAgent(test_rag, provider="google")
|
| 127 |
+
response = agent.query("What are the action items?")
|
| 128 |
+
|
| 129 |
+
assert response is not None
|
| 130 |
+
assert len(response) > 0
|
| 131 |
+
|
| 132 |
+
def test_google_blockers_query(self, test_rag):
|
| 133 |
+
"""Test blockers query with Google."""
|
| 134 |
+
api_key = os.getenv("GOOGLE_API_KEY")
|
| 135 |
+
if not api_key:
|
| 136 |
+
pytest.skip("GOOGLE_API_KEY not set")
|
| 137 |
+
|
| 138 |
+
agent = ProjectAgent(test_rag, provider="google")
|
| 139 |
+
response = agent.query("What blockers do we have?")
|
| 140 |
+
|
| 141 |
+
assert response is not None
|
| 142 |
+
assert len(response) > 0
|
| 143 |
+
|
| 144 |
+
def test_google_invalid_key(self, test_rag):
|
| 145 |
+
"""Test that invalid key raises appropriate error."""
|
| 146 |
+
os.environ["GOOGLE_API_KEY"] = "invalid_key_12345"
|
| 147 |
+
|
| 148 |
+
agent = ProjectAgent(test_rag, provider="google")
|
| 149 |
+
|
| 150 |
+
with pytest.raises(Exception):
|
| 151 |
+
agent.query("What are the action items?")
|
tests/test_parsers.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for meeting parsers."""
|
| 2 |
+
import pytest
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from src.parsers import MeetingParser, ActionItem, MeetingNote, load_meetings_from_directory
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class TestMeetingParser:
|
| 9 |
+
"""Tests for MeetingParser class."""
|
| 10 |
+
|
| 11 |
+
def test_parse_date_iso_format(self):
|
| 12 |
+
"""Test parsing ISO format date."""
|
| 13 |
+
result = MeetingParser.parse_date("2025-01-15")
|
| 14 |
+
assert result == datetime(2025, 1, 15)
|
| 15 |
+
|
| 16 |
+
def test_parse_date_us_format(self):
|
| 17 |
+
"""Test parsing US format date."""
|
| 18 |
+
result = MeetingParser.parse_date("01/15/2025")
|
| 19 |
+
assert result == datetime(2025, 1, 15)
|
| 20 |
+
|
| 21 |
+
def test_parse_date_verbose_format(self):
|
| 22 |
+
"""Test parsing verbose format date."""
|
| 23 |
+
result = MeetingParser.parse_date("January 15, 2025")
|
| 24 |
+
assert result == datetime(2025, 1, 15)
|
| 25 |
+
|
| 26 |
+
def test_parse_date_invalid(self):
|
| 27 |
+
"""Test parsing invalid date returns None."""
|
| 28 |
+
result = MeetingParser.parse_date("not a date")
|
| 29 |
+
assert result is None
|
| 30 |
+
|
| 31 |
+
def test_parse_action_item_simple(self):
|
| 32 |
+
"""Test parsing simple action item."""
|
| 33 |
+
result = MeetingParser.parse_action_item("- [ ] Complete the report")
|
| 34 |
+
assert result is not None
|
| 35 |
+
assert result.task == "Complete the report"
|
| 36 |
+
assert result.completed is False
|
| 37 |
+
assert result.assignee is None
|
| 38 |
+
|
| 39 |
+
def test_parse_action_item_completed(self):
|
| 40 |
+
"""Test parsing completed action item."""
|
| 41 |
+
result = MeetingParser.parse_action_item("- [x] Review PR")
|
| 42 |
+
assert result is not None
|
| 43 |
+
assert result.completed is True
|
| 44 |
+
|
| 45 |
+
def test_parse_action_item_with_assignee(self):
|
| 46 |
+
"""Test parsing action item with assignee."""
|
| 47 |
+
result = MeetingParser.parse_action_item("- [ ] Alice: Implement feature")
|
| 48 |
+
assert result is not None
|
| 49 |
+
assert result.assignee == "Alice"
|
| 50 |
+
assert result.task == "Implement feature"
|
| 51 |
+
|
| 52 |
+
def test_parse_action_item_with_deadline(self):
|
| 53 |
+
"""Test parsing action item with deadline."""
|
| 54 |
+
result = MeetingParser.parse_action_item("- [ ] Bob: Fix bug by 2025-01-20")
|
| 55 |
+
assert result is not None
|
| 56 |
+
assert result.assignee == "Bob"
|
| 57 |
+
assert result.deadline == "2025-01-20"
|
| 58 |
+
|
| 59 |
+
def test_parse_meeting_file(self, temp_data_dir):
|
| 60 |
+
"""Test parsing a complete meeting file."""
|
| 61 |
+
meeting_file = temp_data_dir / "test_project" / "meetings" / "2025-01-15-sprint-planning.md"
|
| 62 |
+
result = MeetingParser.parse(meeting_file, "test_project")
|
| 63 |
+
|
| 64 |
+
assert result is not None
|
| 65 |
+
assert result.project_name == "test_project"
|
| 66 |
+
assert result.title == "Sprint Planning"
|
| 67 |
+
assert result.date == datetime(2025, 1, 15)
|
| 68 |
+
assert "Alice" in result.participants
|
| 69 |
+
assert "Bob" in result.participants
|
| 70 |
+
assert len(result.decisions) == 2
|
| 71 |
+
assert len(result.action_items) == 3
|
| 72 |
+
assert len(result.blockers) == 2
|
| 73 |
+
|
| 74 |
+
def test_parse_nonexistent_file(self):
|
| 75 |
+
"""Test parsing nonexistent file returns None."""
|
| 76 |
+
result = MeetingParser.parse(Path("/nonexistent/file.md"), "test")
|
| 77 |
+
assert result is None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
class TestLoadMeetings:
|
| 81 |
+
"""Tests for load_meetings_from_directory function."""
|
| 82 |
+
|
| 83 |
+
def test_load_meetings_from_directory(self, temp_data_dir):
|
| 84 |
+
"""Test loading meetings from directory."""
|
| 85 |
+
meetings = load_meetings_from_directory(temp_data_dir)
|
| 86 |
+
assert len(meetings) == 1
|
| 87 |
+
assert meetings[0].project_name == "test_project"
|
| 88 |
+
|
| 89 |
+
def test_load_meetings_empty_directory(self, tmp_path):
|
| 90 |
+
"""Test loading from empty directory."""
|
| 91 |
+
meetings = load_meetings_from_directory(tmp_path)
|
| 92 |
+
assert len(meetings) == 0
|
| 93 |
+
|
| 94 |
+
def test_load_meetings_nonexistent_directory(self):
|
| 95 |
+
"""Test loading from nonexistent directory."""
|
| 96 |
+
meetings = load_meetings_from_directory(Path("/nonexistent"))
|
| 97 |
+
assert len(meetings) == 0
|
tests/test_rag.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for RAG system."""
|
| 2 |
+
import pytest
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import tempfile
|
| 5 |
+
import shutil
|
| 6 |
+
from src.rag import ProjectRAG
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class TestProjectRAG:
|
| 10 |
+
"""Tests for ProjectRAG class."""
|
| 11 |
+
|
| 12 |
+
def test_rag_initialization(self, temp_data_dir):
|
| 13 |
+
"""Test RAG system initializes correctly."""
|
| 14 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 15 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 16 |
+
assert rag is not None
|
| 17 |
+
assert rag.data_dir == temp_data_dir
|
| 18 |
+
|
| 19 |
+
def test_load_and_index(self, temp_data_dir):
|
| 20 |
+
"""Test loading and indexing meetings."""
|
| 21 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 22 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 23 |
+
rag.load_and_index()
|
| 24 |
+
|
| 25 |
+
assert len(rag.meetings) == 1
|
| 26 |
+
assert rag.meetings[0].project_name == "test_project"
|
| 27 |
+
|
| 28 |
+
def test_get_all_projects(self, temp_data_dir):
|
| 29 |
+
"""Test getting all project names."""
|
| 30 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 31 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 32 |
+
rag.load_and_index()
|
| 33 |
+
|
| 34 |
+
projects = rag.get_all_projects()
|
| 35 |
+
assert "test_project" in projects
|
| 36 |
+
|
| 37 |
+
def test_get_open_action_items(self, temp_data_dir):
|
| 38 |
+
"""Test getting open action items."""
|
| 39 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 40 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 41 |
+
rag.load_and_index()
|
| 42 |
+
|
| 43 |
+
items = rag.get_open_action_items()
|
| 44 |
+
# Should have 2 open items (Alice and Bob's tasks)
|
| 45 |
+
assert len(items) == 2
|
| 46 |
+
|
| 47 |
+
def test_get_open_action_items_filtered(self, temp_data_dir):
|
| 48 |
+
"""Test getting open action items filtered by project."""
|
| 49 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 50 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 51 |
+
rag.load_and_index()
|
| 52 |
+
|
| 53 |
+
items = rag.get_open_action_items(project="test_project")
|
| 54 |
+
assert len(items) == 2
|
| 55 |
+
|
| 56 |
+
items = rag.get_open_action_items(project="nonexistent")
|
| 57 |
+
assert len(items) == 0
|
| 58 |
+
|
| 59 |
+
def test_get_blockers(self, temp_data_dir):
|
| 60 |
+
"""Test getting blockers."""
|
| 61 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 62 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 63 |
+
rag.load_and_index()
|
| 64 |
+
|
| 65 |
+
blockers = rag.get_blockers()
|
| 66 |
+
assert len(blockers) == 2
|
| 67 |
+
|
| 68 |
+
def test_search(self, temp_data_dir):
|
| 69 |
+
"""Test semantic search."""
|
| 70 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 71 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 72 |
+
rag.load_and_index()
|
| 73 |
+
|
| 74 |
+
results = rag.search("login page implementation")
|
| 75 |
+
assert len(results) > 0
|
| 76 |
+
|
| 77 |
+
def test_search_with_project_filter(self, temp_data_dir):
|
| 78 |
+
"""Test semantic search with project filter."""
|
| 79 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 80 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 81 |
+
rag.load_and_index()
|
| 82 |
+
|
| 83 |
+
results = rag.search("login", project_filter="test_project")
|
| 84 |
+
assert len(results) > 0
|
| 85 |
+
|
| 86 |
+
results = rag.search("login", project_filter="nonexistent")
|
| 87 |
+
assert len(results) == 0
|
| 88 |
+
|
| 89 |
+
def test_get_recent_decisions(self, temp_data_dir):
|
| 90 |
+
"""Test getting recent decisions."""
|
| 91 |
+
with tempfile.TemporaryDirectory() as persist_dir:
|
| 92 |
+
rag = ProjectRAG(temp_data_dir, persist_dir=Path(persist_dir))
|
| 93 |
+
rag.load_and_index()
|
| 94 |
+
|
| 95 |
+
decisions = rag.get_recent_decisions()
|
| 96 |
+
assert len(decisions) == 2
|