Spaces:
Sleeping
Sleeping
Commit
·
d6f13c4
0
Parent(s):
Deploy Sherlock
Browse files- .gitignore +9 -0
- .python-version +1 -0
- app.py +333 -0
- app_streamlit.py +388 -0
- data/ai_model_training/meetings/2025-01-10-kickoff.md +27 -0
- data/ai_model_training/meetings/2025-01-17-week2-sync.md +25 -0
- data/mobile_app_redesign/meetings/2025-01-12-design-review.md +25 -0
- data/mobile_app_redesign/meetings/2025-01-19-stakeholder-demo.md +26 -0
- demo.py +96 -0
- requirements.txt +25 -0
- src/__init__.py +12 -0
- src/agent.py +237 -0
- src/parsers.py +215 -0
- src/rag.py +216 -0
- src/utils.py +97 -0
.gitignore
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.venv/
|
| 5 |
+
chroma_db/
|
| 6 |
+
*.db
|
| 7 |
+
.DS_Store
|
| 8 |
+
*.log
|
| 9 |
+
.pytest_cache/
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.10
|
app.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Gradio app for AI Project Assistant.
|
| 3 |
+
"""
|
| 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 on startup
|
| 18 |
+
rag = None
|
| 19 |
+
agent = None
|
| 20 |
+
initialized = False
|
| 21 |
+
|
| 22 |
+
def initialize_on_startup():
|
| 23 |
+
"""Initialize system automatically on startup."""
|
| 24 |
+
global rag, agent, initialized
|
| 25 |
+
|
| 26 |
+
data_dir = Path("./data")
|
| 27 |
+
|
| 28 |
+
if not data_dir.exists():
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
rag = ProjectRAG(data_dir)
|
| 33 |
+
rag.load_and_index()
|
| 34 |
+
|
| 35 |
+
if rag.meetings:
|
| 36 |
+
agent = ProjectAgent(rag)
|
| 37 |
+
initialized = True
|
| 38 |
+
except Exception as e:
|
| 39 |
+
print(f"Initialization error: {e}")
|
| 40 |
+
|
| 41 |
+
# Initialize on module load
|
| 42 |
+
initialize_on_startup()
|
| 43 |
+
|
| 44 |
+
def chat(message, history, project_filter):
|
| 45 |
+
"""Process chat message."""
|
| 46 |
+
if not initialized or not agent:
|
| 47 |
+
yield "⚠️ System initializing... Please wait and try again."
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
# Add project context if specified
|
| 52 |
+
if project_filter and project_filter != "All Projects":
|
| 53 |
+
enhanced_prompt = f"[Project: {project_filter}] {message}"
|
| 54 |
+
else:
|
| 55 |
+
enhanced_prompt = message
|
| 56 |
+
|
| 57 |
+
response = agent.query(enhanced_prompt)
|
| 58 |
+
yield response
|
| 59 |
+
except Exception as e:
|
| 60 |
+
yield f"❌ Error: {str(e)}"
|
| 61 |
+
|
| 62 |
+
def get_projects():
|
| 63 |
+
"""Get list of projects."""
|
| 64 |
+
if not initialized or not rag:
|
| 65 |
+
return ["All Projects"]
|
| 66 |
+
|
| 67 |
+
projects = rag.get_all_projects()
|
| 68 |
+
return ["All Projects"] + projects
|
| 69 |
+
|
| 70 |
+
def structure_meeting(project_name, meeting_title, meeting_date, participants, meeting_text):
|
| 71 |
+
"""Structure meeting notes using AI."""
|
| 72 |
+
if not project_name or not meeting_text:
|
| 73 |
+
return "❌ Please provide both project name and meeting notes"
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
# Use HF Inference API
|
| 77 |
+
endpoint = HuggingFaceEndpoint(
|
| 78 |
+
repo_id="meta-llama/Llama-3.2-3B-Instruct",
|
| 79 |
+
temperature=0.3,
|
| 80 |
+
max_new_tokens=1024,
|
| 81 |
+
huggingfacehub_api_token=os.getenv("HF_TOKEN")
|
| 82 |
+
)
|
| 83 |
+
llm = ChatHuggingFace(llm=endpoint)
|
| 84 |
+
|
| 85 |
+
system_prompt = """You are a meeting notes structuring assistant.
|
| 86 |
+
Convert unstructured meeting notes into a well-formatted markdown document with these sections:
|
| 87 |
+
1. # Meeting: [title]
|
| 88 |
+
2. Date: [date]
|
| 89 |
+
3. Participants: [list]
|
| 90 |
+
4. ## Discussion (key points discussed)
|
| 91 |
+
5. ## Decisions (decisions made)
|
| 92 |
+
6. ## Action Items (as checkboxes with assignee and deadline if mentioned)
|
| 93 |
+
7. ## Blockers (any blockers or issues raised)
|
| 94 |
+
|
| 95 |
+
Format action items as:
|
| 96 |
+
- [ ] Person: Task description by deadline
|
| 97 |
+
or
|
| 98 |
+
- [ ] Task description (if no person/deadline mentioned)
|
| 99 |
+
|
| 100 |
+
Extract all relevant information from the raw notes."""
|
| 101 |
+
|
| 102 |
+
user_prompt = f"""Structure these meeting notes:
|
| 103 |
+
|
| 104 |
+
Raw Notes:
|
| 105 |
+
{meeting_text}
|
| 106 |
+
|
| 107 |
+
Meeting Details:
|
| 108 |
+
- Title: {meeting_title or 'Meeting'}
|
| 109 |
+
- Date: {meeting_date}
|
| 110 |
+
- Participants: {participants or 'Not specified'}
|
| 111 |
+
"""
|
| 112 |
+
|
| 113 |
+
messages = [
|
| 114 |
+
SystemMessage(content=system_prompt),
|
| 115 |
+
HumanMessage(content=user_prompt)
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
response = llm.invoke(messages)
|
| 119 |
+
structured_md = response.content
|
| 120 |
+
|
| 121 |
+
# Save to file
|
| 122 |
+
project_dir = Path("data") / project_name / "meetings"
|
| 123 |
+
project_dir.mkdir(parents=True, exist_ok=True)
|
| 124 |
+
|
| 125 |
+
filename = f"{meeting_date}-{meeting_title.lower().replace(' ', '-') if meeting_title else 'meeting'}.md"
|
| 126 |
+
file_path = project_dir / filename
|
| 127 |
+
|
| 128 |
+
with open(file_path, 'w') as f:
|
| 129 |
+
f.write(structured_md)
|
| 130 |
+
|
| 131 |
+
return f"✅ Meeting structured and saved to `{file_path}`\n\n---\n\n{structured_md}"
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
return f"❌ Error: {str(e)}"
|
| 135 |
+
|
| 136 |
+
# Create Gradio interface with custom CSS
|
| 137 |
+
custom_css = """
|
| 138 |
+
.chatbot-container {
|
| 139 |
+
background-color: #f7f7f8;
|
| 140 |
+
border-radius: 8px;
|
| 141 |
+
padding: 10px;
|
| 142 |
+
}
|
| 143 |
+
.example-panel {
|
| 144 |
+
background-color: #f0f2f6;
|
| 145 |
+
border-radius: 8px;
|
| 146 |
+
padding: 15px;
|
| 147 |
+
height: 100%;
|
| 148 |
+
}
|
| 149 |
+
/* Mobile responsiveness */
|
| 150 |
+
@media (max-width: 768px) {
|
| 151 |
+
.row {
|
| 152 |
+
flex-direction: column !important;
|
| 153 |
+
}
|
| 154 |
+
.chatbot-container {
|
| 155 |
+
margin-top: 10px;
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
"""
|
| 159 |
+
|
| 160 |
+
with gr.Blocks(title="Sherlock: AI Project Assistant", theme=gr.themes.Soft(), css=custom_css) as demo:
|
| 161 |
+
gr.Markdown("""
|
| 162 |
+
# 🤖 Sherlock: AI Project Assistant
|
| 163 |
+
|
| 164 |
+
Your intelligent assistant for managing multiple projects through meeting summaries.
|
| 165 |
+
""")
|
| 166 |
+
|
| 167 |
+
# Main tabs
|
| 168 |
+
with gr.Tabs():
|
| 169 |
+
# Chat tab
|
| 170 |
+
with gr.Tab("💬 Chat"):
|
| 171 |
+
gr.Markdown("### Ask questions about your projects")
|
| 172 |
+
|
| 173 |
+
# Project selection dropdown
|
| 174 |
+
project_dropdown = gr.Dropdown(
|
| 175 |
+
label="Select Project",
|
| 176 |
+
choices=get_projects(),
|
| 177 |
+
value="All Projects",
|
| 178 |
+
interactive=True
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Chat interface with example queries on the side
|
| 182 |
+
with gr.Row(elem_classes="row"):
|
| 183 |
+
# Left panel - Example queries (same width as right panel chat box)
|
| 184 |
+
with gr.Column(scale=1, elem_classes="example-panel"):
|
| 185 |
+
gr.Markdown("""
|
| 186 |
+
### 📖 How to Use
|
| 187 |
+
1. Select the project you want to query from the dropdown above
|
| 188 |
+
2. Type your question in the chat box or use one of the examples below
|
| 189 |
+
3. Press Enter or click Send
|
| 190 |
+
|
| 191 |
+
### 💡 Example Queries
|
| 192 |
+
- What are the open action items?
|
| 193 |
+
- What blockers do we have?
|
| 194 |
+
- What decisions were made?
|
| 195 |
+
- What should I focus on next?
|
| 196 |
+
- Summarize the project status
|
| 197 |
+
""")
|
| 198 |
+
|
| 199 |
+
# Right panel - Chat (same width as left panel)
|
| 200 |
+
with gr.Column(scale=1, elem_classes="chatbot-container"):
|
| 201 |
+
chatbot = gr.Chatbot(
|
| 202 |
+
label="Chat",
|
| 203 |
+
height=350,
|
| 204 |
+
type="messages",
|
| 205 |
+
show_label=False
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
msg = gr.Textbox(
|
| 209 |
+
label="Your Message",
|
| 210 |
+
placeholder="What are the open action items?",
|
| 211 |
+
lines=2,
|
| 212 |
+
show_label=False
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
with gr.Row():
|
| 216 |
+
submit_btn = gr.Button("Send", variant="primary", scale=1)
|
| 217 |
+
clear_btn = gr.Button("Clear", scale=1)
|
| 218 |
+
|
| 219 |
+
def respond(message, chat_history, project):
|
| 220 |
+
if not message:
|
| 221 |
+
return chat_history, ""
|
| 222 |
+
|
| 223 |
+
# Add user message to history
|
| 224 |
+
chat_history.append({"role": "user", "content": message})
|
| 225 |
+
|
| 226 |
+
# Get bot response
|
| 227 |
+
bot_message = ""
|
| 228 |
+
for response_chunk in chat(message, chat_history, project):
|
| 229 |
+
bot_message = response_chunk
|
| 230 |
+
|
| 231 |
+
# Add bot message to history
|
| 232 |
+
chat_history.append({"role": "assistant", "content": bot_message})
|
| 233 |
+
|
| 234 |
+
return chat_history, ""
|
| 235 |
+
|
| 236 |
+
submit_btn.click(
|
| 237 |
+
fn=respond,
|
| 238 |
+
inputs=[msg, chatbot, project_dropdown],
|
| 239 |
+
outputs=[chatbot, msg]
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
msg.submit(
|
| 243 |
+
fn=respond,
|
| 244 |
+
inputs=[msg, chatbot, project_dropdown],
|
| 245 |
+
outputs=[chatbot, msg]
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
clear_btn.click(fn=lambda: [], outputs=chatbot)
|
| 249 |
+
|
| 250 |
+
# Upload Meeting tab
|
| 251 |
+
with gr.Tab("📤 Upload Meeting"):
|
| 252 |
+
gr.Markdown("### Upload plain text meeting notes and let AI structure them")
|
| 253 |
+
|
| 254 |
+
# Project selection with toggle
|
| 255 |
+
with gr.Row():
|
| 256 |
+
with gr.Column():
|
| 257 |
+
project_mode = gr.Radio(
|
| 258 |
+
choices=["Use Existing Project", "Create New Project"],
|
| 259 |
+
value="Use Existing Project",
|
| 260 |
+
label="Project Selection"
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
# Existing project dropdown (shown when "Use Existing" is selected)
|
| 264 |
+
existing_project = gr.Dropdown(
|
| 265 |
+
label="Select Existing Project",
|
| 266 |
+
choices=get_projects()[1:], # Exclude "All Projects"
|
| 267 |
+
visible=True
|
| 268 |
+
)
|
| 269 |
+
# New project textbox (shown when "Create New" is selected)
|
| 270 |
+
new_project = gr.Textbox(
|
| 271 |
+
label="New Project Name",
|
| 272 |
+
placeholder="e.g., mobile_app_redesign",
|
| 273 |
+
visible=False
|
| 274 |
+
)
|
| 275 |
+
upload_title = gr.Textbox(
|
| 276 |
+
label="Meeting Title",
|
| 277 |
+
placeholder="e.g., Sprint Planning"
|
| 278 |
+
)
|
| 279 |
+
with gr.Column():
|
| 280 |
+
upload_date = gr.Textbox(
|
| 281 |
+
label="Meeting Date (YYYY-MM-DD)",
|
| 282 |
+
value=datetime.now().strftime("%Y-%m-%d"),
|
| 283 |
+
placeholder="2025-01-15",
|
| 284 |
+
type="text"
|
| 285 |
+
)
|
| 286 |
+
upload_participants = gr.Textbox(
|
| 287 |
+
label="Participants (comma-separated)",
|
| 288 |
+
placeholder="e.g., Alice, Bob, Charlie"
|
| 289 |
+
)
|
| 290 |
+
|
| 291 |
+
# Toggle visibility based on project mode
|
| 292 |
+
def toggle_project_input(mode):
|
| 293 |
+
if mode == "Use Existing Project":
|
| 294 |
+
return gr.update(visible=True), gr.update(visible=False)
|
| 295 |
+
else:
|
| 296 |
+
return gr.update(visible=False), gr.update(visible=True)
|
| 297 |
+
|
| 298 |
+
project_mode.change(
|
| 299 |
+
fn=toggle_project_input,
|
| 300 |
+
inputs=[project_mode],
|
| 301 |
+
outputs=[existing_project, new_project]
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
upload_text = gr.Textbox(
|
| 305 |
+
label="Meeting Notes (plain text)",
|
| 306 |
+
placeholder="""Example:
|
| 307 |
+
We discussed the new feature requirements.
|
| 308 |
+
Alice will implement the login page by next Friday.
|
| 309 |
+
Bob raised a concern about the database migration.
|
| 310 |
+
We decided to use PostgreSQL instead of MySQL.
|
| 311 |
+
Charlie is blocked waiting for API credentials.""",
|
| 312 |
+
lines=10
|
| 313 |
+
)
|
| 314 |
+
|
| 315 |
+
structure_btn = gr.Button("🤖 Structure Meeting with AI", variant="primary")
|
| 316 |
+
structure_output = gr.Markdown(label="Structured Output")
|
| 317 |
+
|
| 318 |
+
def structure_meeting_wrapper(mode, existing_proj, new_proj, title, date, participants, text):
|
| 319 |
+
"""Wrapper to handle both project modes."""
|
| 320 |
+
# Determine which project name to use
|
| 321 |
+
project_name = existing_proj if mode == "Use Existing Project" else new_proj
|
| 322 |
+
return structure_meeting(project_name, title, date, participants, text)
|
| 323 |
+
|
| 324 |
+
structure_btn.click(
|
| 325 |
+
fn=structure_meeting_wrapper,
|
| 326 |
+
inputs=[project_mode, existing_project, new_project, upload_title, upload_date, upload_participants, upload_text],
|
| 327 |
+
outputs=structure_output
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
# Launch
|
| 332 |
+
if __name__ == "__main__":
|
| 333 |
+
demo.launch()
|
app_streamlit.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Streamlit app for AI Project Assistant.
|
| 3 |
+
"""
|
| 4 |
+
import streamlit as st
|
| 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 src.parsers import load_meetings_from_directory
|
| 12 |
+
from langchain_huggingface import ChatHuggingFace, HuggingFaceEndpoint
|
| 13 |
+
from langchain_core.messages import SystemMessage, HumanMessage
|
| 14 |
+
|
| 15 |
+
# Load environment variables
|
| 16 |
+
load_dotenv()
|
| 17 |
+
|
| 18 |
+
# Page config
|
| 19 |
+
st.set_page_config(
|
| 20 |
+
page_title="AI Project Assistant",
|
| 21 |
+
page_icon="🤖",
|
| 22 |
+
layout="wide"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
# Custom CSS
|
| 26 |
+
st.markdown("""
|
| 27 |
+
<style>
|
| 28 |
+
.main-header {
|
| 29 |
+
font-size: 2.5rem;
|
| 30 |
+
font-weight: bold;
|
| 31 |
+
margin-bottom: 1rem;
|
| 32 |
+
}
|
| 33 |
+
.project-card {
|
| 34 |
+
padding: 1rem;
|
| 35 |
+
border-radius: 0.5rem;
|
| 36 |
+
background-color: #f0f2f6;
|
| 37 |
+
margin: 0.5rem 0;
|
| 38 |
+
}
|
| 39 |
+
.action-item {
|
| 40 |
+
padding: 0.5rem;
|
| 41 |
+
margin: 0.25rem 0;
|
| 42 |
+
border-left: 3px solid #1f77b4;
|
| 43 |
+
background-color: #e8f4f8;
|
| 44 |
+
}
|
| 45 |
+
.blocker {
|
| 46 |
+
padding: 0.5rem;
|
| 47 |
+
margin: 0.25rem 0;
|
| 48 |
+
border-left: 3px solid #d62728;
|
| 49 |
+
background-color: #ffe8e8;
|
| 50 |
+
}
|
| 51 |
+
</style>
|
| 52 |
+
""", unsafe_allow_html=True)
|
| 53 |
+
|
| 54 |
+
# Initialize session state
|
| 55 |
+
if 'rag' not in st.session_state:
|
| 56 |
+
st.session_state.rag = None
|
| 57 |
+
if 'agent' not in st.session_state:
|
| 58 |
+
st.session_state.agent = None
|
| 59 |
+
if 'messages' not in st.session_state:
|
| 60 |
+
st.session_state.messages = []
|
| 61 |
+
if 'initialized' not in st.session_state:
|
| 62 |
+
st.session_state.initialized = False
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def initialize_system():
|
| 66 |
+
"""Initialize RAG and Agent systems."""
|
| 67 |
+
data_dir = Path("./data")
|
| 68 |
+
|
| 69 |
+
if not data_dir.exists():
|
| 70 |
+
data_dir.mkdir(parents=True)
|
| 71 |
+
st.warning("Created data directory. Please add your meeting notes to 'data/project_name/meetings/'")
|
| 72 |
+
return False
|
| 73 |
+
|
| 74 |
+
with st.spinner("Loading and indexing meetings..."):
|
| 75 |
+
st.session_state.rag = ProjectRAG(data_dir)
|
| 76 |
+
st.session_state.rag.load_and_index()
|
| 77 |
+
|
| 78 |
+
if not st.session_state.rag.meetings:
|
| 79 |
+
return False
|
| 80 |
+
|
| 81 |
+
st.session_state.agent = ProjectAgent(st.session_state.rag)
|
| 82 |
+
st.session_state.initialized = True
|
| 83 |
+
|
| 84 |
+
return True
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def main():
|
| 88 |
+
"""Main app function."""
|
| 89 |
+
|
| 90 |
+
# Header
|
| 91 |
+
st.markdown('<div class="main-header">🤖 AI Project Assistant</div>', unsafe_allow_html=True)
|
| 92 |
+
st.markdown("Your intelligent assistant for managing multiple projects through meeting summaries")
|
| 93 |
+
|
| 94 |
+
# Sidebar
|
| 95 |
+
with st.sidebar:
|
| 96 |
+
st.header("⚙️ Settings")
|
| 97 |
+
|
| 98 |
+
# Project filter
|
| 99 |
+
if st.session_state.rag and st.session_state.initialized:
|
| 100 |
+
projects = st.session_state.rag.get_all_projects()
|
| 101 |
+
selected_project = st.selectbox(
|
| 102 |
+
"Select Project",
|
| 103 |
+
options=["All Projects"] + projects,
|
| 104 |
+
key="selected_project"
|
| 105 |
+
)
|
| 106 |
+
st.session_state.project_filter = None if selected_project == "All Projects" else selected_project
|
| 107 |
+
|
| 108 |
+
if st.button("🔄 Reload Meetings", use_container_width=True):
|
| 109 |
+
st.session_state.initialized = False
|
| 110 |
+
st.rerun()
|
| 111 |
+
|
| 112 |
+
st.divider()
|
| 113 |
+
|
| 114 |
+
st.header("📊 Quick Stats")
|
| 115 |
+
if st.session_state.rag and st.session_state.initialized:
|
| 116 |
+
current_filter = st.session_state.get("project_filter")
|
| 117 |
+
|
| 118 |
+
if current_filter:
|
| 119 |
+
st.info(f"Showing: **{current_filter}**")
|
| 120 |
+
action_items = st.session_state.rag.get_open_action_items(project=current_filter)
|
| 121 |
+
blockers = st.session_state.rag.get_blockers(project=current_filter)
|
| 122 |
+
else:
|
| 123 |
+
projects = st.session_state.rag.get_all_projects()
|
| 124 |
+
st.metric("Total Projects", len(projects))
|
| 125 |
+
action_items = st.session_state.rag.get_open_action_items()
|
| 126 |
+
blockers = st.session_state.rag.get_blockers()
|
| 127 |
+
|
| 128 |
+
st.metric("Total Meetings", len(st.session_state.rag.meetings))
|
| 129 |
+
st.metric("Open Action Items", len(action_items))
|
| 130 |
+
st.metric("Current Blockers", len(blockers))
|
| 131 |
+
|
| 132 |
+
st.divider()
|
| 133 |
+
|
| 134 |
+
st.header("💡 Example Queries")
|
| 135 |
+
st.markdown("""
|
| 136 |
+
- What are the open action items?
|
| 137 |
+
- What blockers do we have?
|
| 138 |
+
- What decisions were made?
|
| 139 |
+
- What should I focus on next?
|
| 140 |
+
- Summarize the project status
|
| 141 |
+
""")
|
| 142 |
+
|
| 143 |
+
# Check HF Token (auto-provided on Spaces, optional locally)
|
| 144 |
+
if not os.getenv("HF_TOKEN"):
|
| 145 |
+
st.warning("⚠️ HF_TOKEN not found. Running with limited functionality.")
|
| 146 |
+
st.info("On Spaces: Token is automatically provided")
|
| 147 |
+
st.info("Locally: Set HF_TOKEN in .env file (optional for free API)")
|
| 148 |
+
|
| 149 |
+
# Initialize system
|
| 150 |
+
if not st.session_state.initialized:
|
| 151 |
+
if not initialize_system():
|
| 152 |
+
st.warning("No meetings found. Add your meeting notes to get started!")
|
| 153 |
+
|
| 154 |
+
with st.expander("📝 How to add meetings"):
|
| 155 |
+
st.markdown("""
|
| 156 |
+
1. Create a folder structure: `data/your_project_name/meetings/`
|
| 157 |
+
2. Add markdown files with your meeting notes
|
| 158 |
+
3. Use this format:
|
| 159 |
+
|
| 160 |
+
```markdown
|
| 161 |
+
# Meeting: Project Kickoff
|
| 162 |
+
Date: 2025-01-15
|
| 163 |
+
Participants: Alice, Bob
|
| 164 |
+
|
| 165 |
+
## Discussion
|
| 166 |
+
Your meeting notes here
|
| 167 |
+
|
| 168 |
+
## Decisions
|
| 169 |
+
- Decision 1
|
| 170 |
+
- Decision 2
|
| 171 |
+
|
| 172 |
+
## Action Items
|
| 173 |
+
- [ ] Alice: Task 1 by Jan 20
|
| 174 |
+
- [x] Bob: Task 2 (completed)
|
| 175 |
+
|
| 176 |
+
## Blockers
|
| 177 |
+
- Waiting for approval
|
| 178 |
+
```
|
| 179 |
+
""")
|
| 180 |
+
return
|
| 181 |
+
|
| 182 |
+
st.success(f"Loaded {len(st.session_state.rag.meetings)} meetings!")
|
| 183 |
+
|
| 184 |
+
# Main content tabs - ONLY 2 TABS
|
| 185 |
+
tab1, tab2 = st.tabs(["💬 Chat", "📤 Upload Meeting"])
|
| 186 |
+
|
| 187 |
+
with tab1:
|
| 188 |
+
st.header("💬 Ask Questions About Your Projects")
|
| 189 |
+
|
| 190 |
+
# Project selection BEFORE chat
|
| 191 |
+
if st.session_state.rag and st.session_state.initialized:
|
| 192 |
+
projects = st.session_state.rag.get_all_projects()
|
| 193 |
+
|
| 194 |
+
st.markdown("### Select a Project to Chat About")
|
| 195 |
+
|
| 196 |
+
# Create columns for project buttons
|
| 197 |
+
cols = st.columns(len(projects) + 1)
|
| 198 |
+
|
| 199 |
+
# "All Projects" button
|
| 200 |
+
with cols[0]:
|
| 201 |
+
if st.button("🌐 All Projects", use_container_width=True, type="secondary"):
|
| 202 |
+
st.session_state.selected_chat_project = None
|
| 203 |
+
st.rerun()
|
| 204 |
+
|
| 205 |
+
# Individual project buttons
|
| 206 |
+
for i, project in enumerate(projects, 1):
|
| 207 |
+
with cols[i]:
|
| 208 |
+
if st.button(f"📁 {project}", use_container_width=True, type="primary"):
|
| 209 |
+
st.session_state.selected_chat_project = project
|
| 210 |
+
st.rerun()
|
| 211 |
+
|
| 212 |
+
st.divider()
|
| 213 |
+
|
| 214 |
+
# Show selected project
|
| 215 |
+
selected_project = st.session_state.get("selected_chat_project")
|
| 216 |
+
if selected_project:
|
| 217 |
+
st.success(f"💬 Chatting about: **{selected_project}**")
|
| 218 |
+
else:
|
| 219 |
+
st.info("💬 Chatting about: **All Projects**")
|
| 220 |
+
|
| 221 |
+
# Only show chat if a selection has been made
|
| 222 |
+
if "selected_chat_project" in st.session_state:
|
| 223 |
+
# Display chat messages
|
| 224 |
+
for message in st.session_state.messages:
|
| 225 |
+
with st.chat_message(message["role"]):
|
| 226 |
+
st.markdown(message["content"])
|
| 227 |
+
else:
|
| 228 |
+
st.warning("👆 Please select a project above to start chatting")
|
| 229 |
+
return
|
| 230 |
+
|
| 231 |
+
# Chat input (must be outside tabs/columns/expanders) - only show if project selected
|
| 232 |
+
if tab1 and "selected_chat_project" in st.session_state:
|
| 233 |
+
prompt = st.chat_input("What would you like to know about your projects?")
|
| 234 |
+
|
| 235 |
+
# Process chat input
|
| 236 |
+
if prompt:
|
| 237 |
+
# Add user message
|
| 238 |
+
st.session_state.messages.append({"role": "user", "content": prompt})
|
| 239 |
+
with st.chat_message("user"):
|
| 240 |
+
st.markdown(prompt)
|
| 241 |
+
|
| 242 |
+
# Get agent response with project filter
|
| 243 |
+
with st.chat_message("assistant"):
|
| 244 |
+
with st.spinner("Thinking..."):
|
| 245 |
+
# Add project context to query if specific project selected
|
| 246 |
+
selected_project = st.session_state.get("selected_chat_project")
|
| 247 |
+
if selected_project:
|
| 248 |
+
enhanced_prompt = f"[Project: {selected_project}] {prompt}"
|
| 249 |
+
else:
|
| 250 |
+
enhanced_prompt = prompt
|
| 251 |
+
|
| 252 |
+
response = st.session_state.agent.query(enhanced_prompt)
|
| 253 |
+
st.markdown(response)
|
| 254 |
+
|
| 255 |
+
st.session_state.messages.append({"role": "assistant", "content": response})
|
| 256 |
+
st.rerun()
|
| 257 |
+
|
| 258 |
+
with tab2:
|
| 259 |
+
st.header("📤 Upload Meeting Notes")
|
| 260 |
+
st.markdown("Upload plain text meeting notes and let AI structure them for you!")
|
| 261 |
+
|
| 262 |
+
col1, col2 = st.columns(2)
|
| 263 |
+
|
| 264 |
+
with col1:
|
| 265 |
+
project_name = st.text_input("Project Name", placeholder="e.g., mobile_app_redesign")
|
| 266 |
+
meeting_date = st.date_input("Meeting Date", value=datetime.now())
|
| 267 |
+
meeting_title = st.text_input("Meeting Title", placeholder="e.g., Sprint Planning")
|
| 268 |
+
|
| 269 |
+
with col2:
|
| 270 |
+
participants = st.text_input("Participants (comma-separated)", placeholder="e.g., Alice, Bob, Charlie")
|
| 271 |
+
|
| 272 |
+
st.markdown("### Paste or Upload Meeting Notes")
|
| 273 |
+
|
| 274 |
+
# Option 1: Text area
|
| 275 |
+
meeting_text = st.text_area(
|
| 276 |
+
"Paste your meeting notes here (plain text)",
|
| 277 |
+
height=300,
|
| 278 |
+
placeholder="""Example:
|
| 279 |
+
We discussed the new feature requirements.
|
| 280 |
+
Alice will implement the login page by next Friday.
|
| 281 |
+
Bob raised a concern about the database migration.
|
| 282 |
+
We decided to use PostgreSQL instead of MySQL.
|
| 283 |
+
Charlie is blocked waiting for API credentials.
|
| 284 |
+
"""
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
# Option 2: File upload
|
| 288 |
+
uploaded_file = st.file_uploader("Or upload a text file", type=['txt', 'md'])
|
| 289 |
+
|
| 290 |
+
if uploaded_file is not None:
|
| 291 |
+
meeting_text = uploaded_file.read().decode('utf-8')
|
| 292 |
+
st.info(f"Loaded {len(meeting_text)} characters from file")
|
| 293 |
+
|
| 294 |
+
if st.button("🤖 Structure Meeting with AI", type="primary", disabled=not meeting_text or not project_name):
|
| 295 |
+
with st.spinner("AI is structuring your meeting notes..."):
|
| 296 |
+
try:
|
| 297 |
+
# Use HF Inference API to structure the meeting
|
| 298 |
+
endpoint = HuggingFaceEndpoint(
|
| 299 |
+
repo_id="meta-llama/Llama-3.2-3B-Instruct",
|
| 300 |
+
temperature=0.3,
|
| 301 |
+
max_new_tokens=1024,
|
| 302 |
+
huggingfacehub_api_token=os.getenv("HF_TOKEN")
|
| 303 |
+
)
|
| 304 |
+
llm = ChatHuggingFace(llm=endpoint)
|
| 305 |
+
|
| 306 |
+
system_prompt = """You are a meeting notes structuring assistant.
|
| 307 |
+
Convert unstructured meeting notes into a well-formatted markdown document with these sections:
|
| 308 |
+
1. # Meeting: [title]
|
| 309 |
+
2. Date: [date]
|
| 310 |
+
3. Participants: [list]
|
| 311 |
+
4. ## Discussion (key points discussed)
|
| 312 |
+
5. ## Decisions (decisions made)
|
| 313 |
+
6. ## Action Items (as checkboxes with assignee and deadline if mentioned)
|
| 314 |
+
7. ## Blockers (any blockers or issues raised)
|
| 315 |
+
|
| 316 |
+
Format action items as:
|
| 317 |
+
- [ ] Person: Task description by deadline
|
| 318 |
+
or
|
| 319 |
+
- [ ] Task description (if no person/deadline mentioned)
|
| 320 |
+
|
| 321 |
+
Extract all relevant information from the raw notes."""
|
| 322 |
+
|
| 323 |
+
user_prompt = f"""Structure these meeting notes:
|
| 324 |
+
|
| 325 |
+
Raw Notes:
|
| 326 |
+
{meeting_text}
|
| 327 |
+
|
| 328 |
+
Meeting Details:
|
| 329 |
+
- Title: {meeting_title or 'Meeting'}
|
| 330 |
+
- Date: {meeting_date}
|
| 331 |
+
- Participants: {participants or 'Not specified'}
|
| 332 |
+
"""
|
| 333 |
+
|
| 334 |
+
messages = [
|
| 335 |
+
SystemMessage(content=system_prompt),
|
| 336 |
+
HumanMessage(content=user_prompt)
|
| 337 |
+
]
|
| 338 |
+
|
| 339 |
+
response = llm.invoke(messages)
|
| 340 |
+
structured_md = response.content
|
| 341 |
+
|
| 342 |
+
# Display preview
|
| 343 |
+
st.success("✅ Meeting structured successfully!")
|
| 344 |
+
st.markdown("### Preview")
|
| 345 |
+
st.markdown(structured_md)
|
| 346 |
+
|
| 347 |
+
# Save option
|
| 348 |
+
st.markdown("### Save Meeting")
|
| 349 |
+
|
| 350 |
+
save_col1, save_col2 = st.columns([3, 1])
|
| 351 |
+
|
| 352 |
+
with save_col1:
|
| 353 |
+
filename = st.text_input(
|
| 354 |
+
"Filename",
|
| 355 |
+
value=f"{meeting_date.strftime('%Y-%m-%d')}-{meeting_title.lower().replace(' ', '-') if meeting_title else 'meeting'}.md"
|
| 356 |
+
)
|
| 357 |
+
|
| 358 |
+
with save_col2:
|
| 359 |
+
st.markdown("<br>", unsafe_allow_html=True)
|
| 360 |
+
if st.button("💾 Save to Project"):
|
| 361 |
+
# Create project directory if needed
|
| 362 |
+
project_dir = Path("data") / project_name / "meetings"
|
| 363 |
+
project_dir.mkdir(parents=True, exist_ok=True)
|
| 364 |
+
|
| 365 |
+
# Save file
|
| 366 |
+
file_path = project_dir / filename
|
| 367 |
+
with open(file_path, 'w') as f:
|
| 368 |
+
f.write(structured_md)
|
| 369 |
+
|
| 370 |
+
st.success(f"✅ Saved to `{file_path}`")
|
| 371 |
+
st.info("💡 Refresh the page to reload meetings into the RAG system")
|
| 372 |
+
|
| 373 |
+
# Download option
|
| 374 |
+
st.download_button(
|
| 375 |
+
label="📥 Download Markdown",
|
| 376 |
+
data=structured_md,
|
| 377 |
+
file_name=filename,
|
| 378 |
+
mime="text/markdown"
|
| 379 |
+
)
|
| 380 |
+
|
| 381 |
+
except Exception as e:
|
| 382 |
+
st.error(f"Error: {str(e)}")
|
| 383 |
+
if "quota" in str(e).lower() or "rate" in str(e).lower():
|
| 384 |
+
st.warning("⚠️ API rate limit reached. Please wait a moment and try again.")
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
if __name__ == "__main__":
|
| 388 |
+
main()
|
data/ai_model_training/meetings/2025-01-10-kickoff.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Project Kickoff - AI Model Training
|
| 2 |
+
Date: 2025-01-10
|
| 3 |
+
Participants: Alice, Bob, Charlie
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
We discussed the AI model training project scope and timeline. The goal is to train a new recommendation model using our customer data. We reviewed the current infrastructure and identified gaps in our training pipeline.
|
| 7 |
+
|
| 8 |
+
Key points:
|
| 9 |
+
- Need to set up new GPU cluster
|
| 10 |
+
- Data preprocessing pipeline needs optimization
|
| 11 |
+
- Model architecture still under discussion (transformer vs CNN)
|
| 12 |
+
|
| 13 |
+
## Decisions
|
| 14 |
+
- Use PyTorch as the primary framework
|
| 15 |
+
- Deploy on AWS with spot instances for cost savings
|
| 16 |
+
- Weekly sync meetings every Monday at 10 AM
|
| 17 |
+
- Charlie will lead the model architecture design
|
| 18 |
+
|
| 19 |
+
## Action Items
|
| 20 |
+
- [ ] Alice: Set up GPU cluster by Jan 20
|
| 21 |
+
- [ ] Bob: Optimize data preprocessing pipeline by Jan 25
|
| 22 |
+
- [ ] Charlie: Propose model architecture by Jan 15
|
| 23 |
+
- [ ] Alice: Research spot instance pricing and prepare budget
|
| 24 |
+
|
| 25 |
+
## Blockers
|
| 26 |
+
- Waiting for budget approval from finance team
|
| 27 |
+
- Need access to production database for training data
|
data/ai_model_training/meetings/2025-01-17-week2-sync.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Week 2 Sync - AI Model Training
|
| 2 |
+
Date: 2025-01-17
|
| 3 |
+
Participants: Alice, Bob, Charlie, Diana
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
Progress update on the AI model training project. Charlie presented the proposed transformer-based architecture which was well-received. Alice reported delays in GPU cluster setup due to procurement issues.
|
| 7 |
+
|
| 8 |
+
Diana (from finance) joined to discuss budget concerns and approved the spot instance approach which should save 70% on compute costs.
|
| 9 |
+
|
| 10 |
+
## Decisions
|
| 11 |
+
- Approved: Transformer-based architecture with attention mechanisms
|
| 12 |
+
- Approved: Budget for 6 months of AWS spot instances
|
| 13 |
+
- Use HuggingFace Transformers library for faster development
|
| 14 |
+
- Add Diana to weekly syncs for ongoing budget visibility
|
| 15 |
+
|
| 16 |
+
## Action Items
|
| 17 |
+
- [x] Charlie: Propose model architecture by Jan 15 (completed)
|
| 18 |
+
- [ ] Alice: Set up GPU cluster by Jan 25 (deadline extended)
|
| 19 |
+
- [ ] Bob: Complete data preprocessing optimization by Jan 25
|
| 20 |
+
- [ ] Charlie: Start initial model implementation by Jan 30
|
| 21 |
+
- [ ] Alice: Document infrastructure setup for team
|
| 22 |
+
|
| 23 |
+
## Blockers
|
| 24 |
+
- GPU procurement delayed by 2 weeks due to supplier issues
|
| 25 |
+
- Still waiting for production database access credentials
|
data/mobile_app_redesign/meetings/2025-01-12-design-review.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Design Review - Mobile App Redesign
|
| 2 |
+
Date: 2025-01-12
|
| 3 |
+
Participants: Emma, Frank, Grace
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
First design review for the mobile app redesign project. Emma presented the new UI mockups focusing on improving user navigation and reducing cognitive load. The team agreed the new design is cleaner and more intuitive.
|
| 7 |
+
|
| 8 |
+
We discussed the technical feasibility of implementing the new animations and transitions. Frank raised concerns about performance on older devices.
|
| 9 |
+
|
| 10 |
+
## Decisions
|
| 11 |
+
- Approved overall design direction
|
| 12 |
+
- Use React Native for cross-platform development
|
| 13 |
+
- Implement progressive enhancement for animations
|
| 14 |
+
- Target iOS 14+ and Android 10+ (drop older OS support)
|
| 15 |
+
- Weekly design reviews every Friday at 2 PM
|
| 16 |
+
|
| 17 |
+
## Action Items
|
| 18 |
+
- [ ] Emma: Finalize color palette and design system by Jan 19
|
| 19 |
+
- [ ] Frank: Create technical feasibility assessment by Jan 20
|
| 20 |
+
- [ ] Grace: User testing plan for new navigation by Jan 22
|
| 21 |
+
- [ ] Emma: Prepare clickable prototype for stakeholder demo
|
| 22 |
+
|
| 23 |
+
## Blockers
|
| 24 |
+
- Need stakeholder approval before proceeding to development
|
| 25 |
+
- Waiting for user research data from marketing team
|
data/mobile_app_redesign/meetings/2025-01-19-stakeholder-demo.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Meeting: Stakeholder Demo - Mobile App Redesign
|
| 2 |
+
Date: 2025-01-19
|
| 3 |
+
Participants: Emma, Frank, Grace, Henry (CEO), Isabel (Marketing)
|
| 4 |
+
|
| 5 |
+
## Discussion
|
| 6 |
+
Presented the mobile app redesign to key stakeholders. Henry loved the new design and gave immediate approval to proceed. Isabel shared user research data showing that 80% of users find the current app navigation confusing.
|
| 7 |
+
|
| 8 |
+
The stakeholders emphasized the importance of launching before the competitor's product release in March. This means we need to accelerate the timeline.
|
| 9 |
+
|
| 10 |
+
## Decisions
|
| 11 |
+
- Approved: Proceed with redesign implementation
|
| 12 |
+
- Approved: Additional budget for 2 more developers
|
| 13 |
+
- Target launch: End of February (aggressive but feasible)
|
| 14 |
+
- Focus on core features first, nice-to-haves for v2
|
| 15 |
+
- Isabel's team will handle beta testing coordination
|
| 16 |
+
|
| 17 |
+
## Action Items
|
| 18 |
+
- [x] Emma: Prepare clickable prototype for stakeholder demo (completed)
|
| 19 |
+
- [x] Emma: Finalize color palette and design system (completed)
|
| 20 |
+
- [ ] Frank: Start development sprint planning by Jan 22
|
| 21 |
+
- [ ] Frank: Hire 2 additional React Native developers by Jan 26
|
| 22 |
+
- [ ] Grace: Coordinate with Isabel's team for beta testing by Feb 1
|
| 23 |
+
- [ ] Emma: Create asset library for developers by Jan 24
|
| 24 |
+
|
| 25 |
+
## Blockers
|
| 26 |
+
- None! Full approval received and budget allocated
|
demo.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Demo script to showcase the AI Project Assistant capabilities.
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
from src.rag import ProjectRAG
|
| 8 |
+
from src.agent import ProjectAgent
|
| 9 |
+
|
| 10 |
+
# Load environment variables
|
| 11 |
+
load_dotenv()
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
"""Run the demo."""
|
| 16 |
+
print("=" * 60)
|
| 17 |
+
print("AI Project Assistant - Demo")
|
| 18 |
+
print("=" * 60)
|
| 19 |
+
|
| 20 |
+
# Check for API key
|
| 21 |
+
if not os.getenv("GOOGLE_API_KEY"):
|
| 22 |
+
print("\n⚠️ ERROR: GOOGLE_API_KEY not found!")
|
| 23 |
+
print("Please create a .env file with your Google API key:")
|
| 24 |
+
print(" GOOGLE_API_KEY=your_key_here")
|
| 25 |
+
print("\nGet your API key from: https://aistudio.google.com/apikey")
|
| 26 |
+
return
|
| 27 |
+
|
| 28 |
+
# Initialize RAG system
|
| 29 |
+
print("\n📁 Initializing RAG system...")
|
| 30 |
+
data_dir = Path("./data")
|
| 31 |
+
rag = ProjectRAG(data_dir)
|
| 32 |
+
rag.load_and_index()
|
| 33 |
+
|
| 34 |
+
if not rag.meetings:
|
| 35 |
+
print("\n⚠️ No meetings found!")
|
| 36 |
+
print("Please add meeting notes to the data directory.")
|
| 37 |
+
return
|
| 38 |
+
|
| 39 |
+
print(f"✅ Loaded {len(rag.meetings)} meetings")
|
| 40 |
+
print(f"✅ Projects: {', '.join(rag.get_all_projects())}")
|
| 41 |
+
|
| 42 |
+
# Initialize agent
|
| 43 |
+
print("\n🤖 Initializing AI Agent...")
|
| 44 |
+
agent = ProjectAgent(rag)
|
| 45 |
+
print("✅ Agent ready!")
|
| 46 |
+
|
| 47 |
+
# Demo queries
|
| 48 |
+
demo_queries = [
|
| 49 |
+
"What are all the open action items?",
|
| 50 |
+
"What blockers do we have across all projects?",
|
| 51 |
+
"What is the status of the AI model training project?",
|
| 52 |
+
"What should the team focus on next for the mobile app redesign?",
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
print("\n" + "=" * 60)
|
| 56 |
+
print("Running Demo Queries")
|
| 57 |
+
print("=" * 60)
|
| 58 |
+
|
| 59 |
+
for i, query in enumerate(demo_queries, 1):
|
| 60 |
+
print(f"\n📝 Query {i}: {query}")
|
| 61 |
+
print("-" * 60)
|
| 62 |
+
|
| 63 |
+
answer = agent.query(query)
|
| 64 |
+
print(answer)
|
| 65 |
+
print()
|
| 66 |
+
|
| 67 |
+
# Interactive mode
|
| 68 |
+
print("\n" + "=" * 60)
|
| 69 |
+
print("Interactive Mode - Ask your own questions!")
|
| 70 |
+
print("(Type 'quit' or 'exit' to end)")
|
| 71 |
+
print("=" * 60)
|
| 72 |
+
|
| 73 |
+
while True:
|
| 74 |
+
try:
|
| 75 |
+
user_query = input("\n💬 Your question: ").strip()
|
| 76 |
+
|
| 77 |
+
if user_query.lower() in ['quit', 'exit', 'q']:
|
| 78 |
+
print("\n👋 Thanks for using AI Project Assistant!")
|
| 79 |
+
break
|
| 80 |
+
|
| 81 |
+
if not user_query:
|
| 82 |
+
continue
|
| 83 |
+
|
| 84 |
+
print("\n🤖 Assistant:", end=" ")
|
| 85 |
+
answer = agent.query(user_query)
|
| 86 |
+
print(answer)
|
| 87 |
+
|
| 88 |
+
except KeyboardInterrupt:
|
| 89 |
+
print("\n\n👋 Thanks for using AI Project Assistant!")
|
| 90 |
+
break
|
| 91 |
+
except Exception as e:
|
| 92 |
+
print(f"\n❌ Error: {e}")
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
gradio>=4.0.0
|
| 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
|
| 9 |
+
chromadb>=0.4.22
|
| 10 |
+
sentence-transformers>=2.3.0
|
| 11 |
+
huggingface-hub>=0.20.0
|
| 12 |
+
|
| 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
|
src/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AI Project Assistant - Source package.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
'MeetingNote',
|
| 7 |
+
'ActionItem',
|
| 8 |
+
'MeetingParser',
|
| 9 |
+
'load_meetings_from_directory',
|
| 10 |
+
'ProjectRAG',
|
| 11 |
+
'ProjectAgent',
|
| 12 |
+
]
|
src/agent.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AI Agent for project management using LangGraph.
|
| 3 |
+
"""
|
| 4 |
+
from typing import TypedDict, Annotated, Sequence, List, Dict, Any
|
| 5 |
+
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 |
+
|
| 12 |
+
|
| 13 |
+
class AgentState(TypedDict):
|
| 14 |
+
"""State for the agent."""
|
| 15 |
+
messages: Annotated[Sequence[BaseMessage], operator.add]
|
| 16 |
+
query: str
|
| 17 |
+
retrieved_context: List[Dict[str, Any]]
|
| 18 |
+
action_items: List[Dict[str, Any]]
|
| 19 |
+
blockers: List[Dict[str, Any]]
|
| 20 |
+
next_step: str
|
| 21 |
+
final_answer: str
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class ProjectAgent:
|
| 25 |
+
"""AI Agent for project management queries."""
|
| 26 |
+
|
| 27 |
+
def __init__(self, rag: ProjectRAG, model_name: str = "meta-llama/Llama-3.2-3B-Instruct"):
|
| 28 |
+
"""Initialize the agent."""
|
| 29 |
+
self.rag = rag
|
| 30 |
+
# Use HF Inference API (free tier)
|
| 31 |
+
llm = HuggingFaceEndpoint(
|
| 32 |
+
repo_id=model_name,
|
| 33 |
+
temperature=0.1,
|
| 34 |
+
max_new_tokens=512,
|
| 35 |
+
huggingfacehub_api_token=os.getenv("HF_TOKEN")
|
| 36 |
+
)
|
| 37 |
+
self.llm = ChatHuggingFace(llm=llm)
|
| 38 |
+
self.graph = self._build_graph()
|
| 39 |
+
|
| 40 |
+
def _build_graph(self) -> StateGraph:
|
| 41 |
+
"""Build the agent's state graph."""
|
| 42 |
+
workflow = StateGraph(AgentState)
|
| 43 |
+
|
| 44 |
+
# Add nodes
|
| 45 |
+
workflow.add_node("analyze_query", self.analyze_query)
|
| 46 |
+
workflow.add_node("retrieve_context", self.retrieve_context)
|
| 47 |
+
workflow.add_node("get_action_items", self.get_action_items)
|
| 48 |
+
workflow.add_node("get_blockers", self.get_blockers)
|
| 49 |
+
workflow.add_node("generate_answer", self.generate_answer)
|
| 50 |
+
|
| 51 |
+
# Add edges
|
| 52 |
+
workflow.set_entry_point("analyze_query")
|
| 53 |
+
workflow.add_edge("analyze_query", "retrieve_context")
|
| 54 |
+
workflow.add_conditional_edges(
|
| 55 |
+
"retrieve_context",
|
| 56 |
+
self.route_after_retrieval,
|
| 57 |
+
{
|
| 58 |
+
"action_items": "get_action_items",
|
| 59 |
+
"blockers": "get_blockers",
|
| 60 |
+
"answer": "generate_answer"
|
| 61 |
+
}
|
| 62 |
+
)
|
| 63 |
+
workflow.add_edge("get_action_items", "generate_answer")
|
| 64 |
+
workflow.add_edge("get_blockers", "generate_answer")
|
| 65 |
+
workflow.add_edge("generate_answer", END)
|
| 66 |
+
|
| 67 |
+
return workflow.compile()
|
| 68 |
+
|
| 69 |
+
def analyze_query(self, state: AgentState) -> AgentState:
|
| 70 |
+
"""Analyze the user's query to understand intent."""
|
| 71 |
+
query = state["query"]
|
| 72 |
+
|
| 73 |
+
system_prompt = """You are a query analyzer for a project management assistant.
|
| 74 |
+
Analyze queries and determine what information is being requested."""
|
| 75 |
+
|
| 76 |
+
analysis_prompt = f"""Analyze this query and determine:
|
| 77 |
+
1. What information is being requested?
|
| 78 |
+
2. Which project (if specified)?
|
| 79 |
+
3. What type of query is this (action items, blockers, status, decisions, general)?
|
| 80 |
+
|
| 81 |
+
Query: {query}
|
| 82 |
+
|
| 83 |
+
Respond in this format:
|
| 84 |
+
Type: [action_items|blockers|status|decisions|general]
|
| 85 |
+
Project: [project name or "all"]
|
| 86 |
+
Intent: [brief description]
|
| 87 |
+
"""
|
| 88 |
+
|
| 89 |
+
messages = [
|
| 90 |
+
SystemMessage(content=system_prompt),
|
| 91 |
+
HumanMessage(content=analysis_prompt)
|
| 92 |
+
]
|
| 93 |
+
response = self.llm.invoke(messages)
|
| 94 |
+
|
| 95 |
+
state["messages"] = state.get("messages", []) + [
|
| 96 |
+
HumanMessage(content=query),
|
| 97 |
+
AIMessage(content=f"Analysis: {response.content}")
|
| 98 |
+
]
|
| 99 |
+
|
| 100 |
+
return state
|
| 101 |
+
|
| 102 |
+
def retrieve_context(self, state: AgentState) -> AgentState:
|
| 103 |
+
"""Retrieve relevant context from the RAG system."""
|
| 104 |
+
query = state["query"]
|
| 105 |
+
|
| 106 |
+
# Extract project name if mentioned
|
| 107 |
+
project_filter = None
|
| 108 |
+
projects = self.rag.get_all_projects()
|
| 109 |
+
for project in projects:
|
| 110 |
+
if project.lower() in query.lower():
|
| 111 |
+
project_filter = project
|
| 112 |
+
break
|
| 113 |
+
|
| 114 |
+
# Search for relevant context
|
| 115 |
+
results = self.rag.search(query, n_results=5, project_filter=project_filter)
|
| 116 |
+
state["retrieved_context"] = results
|
| 117 |
+
|
| 118 |
+
return state
|
| 119 |
+
|
| 120 |
+
def route_after_retrieval(self, state: AgentState) -> str:
|
| 121 |
+
"""Route to appropriate node based on query type."""
|
| 122 |
+
query = state["query"].lower()
|
| 123 |
+
|
| 124 |
+
if any(term in query for term in ["action item", "todo", "task", "what's next", "what should"]):
|
| 125 |
+
return "action_items"
|
| 126 |
+
elif any(term in query for term in ["blocker", "issue", "problem", "stuck"]):
|
| 127 |
+
return "blockers"
|
| 128 |
+
else:
|
| 129 |
+
return "answer"
|
| 130 |
+
|
| 131 |
+
def get_action_items(self, state: AgentState) -> AgentState:
|
| 132 |
+
"""Get action items from the RAG system."""
|
| 133 |
+
query = state["query"].lower()
|
| 134 |
+
|
| 135 |
+
# Extract project name if mentioned
|
| 136 |
+
project_filter = None
|
| 137 |
+
projects = self.rag.get_all_projects()
|
| 138 |
+
for project in projects:
|
| 139 |
+
if project.lower() in query:
|
| 140 |
+
project_filter = project
|
| 141 |
+
break
|
| 142 |
+
|
| 143 |
+
action_items = self.rag.get_open_action_items(project=project_filter)
|
| 144 |
+
state["action_items"] = action_items
|
| 145 |
+
|
| 146 |
+
return state
|
| 147 |
+
|
| 148 |
+
def get_blockers(self, state: AgentState) -> AgentState:
|
| 149 |
+
"""Get blockers from the RAG system."""
|
| 150 |
+
query = state["query"].lower()
|
| 151 |
+
|
| 152 |
+
# Extract project name if mentioned
|
| 153 |
+
project_filter = None
|
| 154 |
+
projects = self.rag.get_all_projects()
|
| 155 |
+
for project in projects:
|
| 156 |
+
if project.lower() in query:
|
| 157 |
+
project_filter = project
|
| 158 |
+
break
|
| 159 |
+
|
| 160 |
+
blockers = self.rag.get_blockers(project=project_filter)
|
| 161 |
+
state["blockers"] = blockers
|
| 162 |
+
|
| 163 |
+
return state
|
| 164 |
+
|
| 165 |
+
def generate_answer(self, state: AgentState) -> AgentState:
|
| 166 |
+
"""Generate the final answer using retrieved context."""
|
| 167 |
+
query = state["query"]
|
| 168 |
+
context = state.get("retrieved_context", [])
|
| 169 |
+
action_items = state.get("action_items", [])
|
| 170 |
+
blockers = state.get("blockers", [])
|
| 171 |
+
|
| 172 |
+
# Build context string
|
| 173 |
+
context_parts = []
|
| 174 |
+
|
| 175 |
+
if context:
|
| 176 |
+
context_parts.append("Relevant meeting context:")
|
| 177 |
+
for i, result in enumerate(context[:3], 1):
|
| 178 |
+
context_parts.append(f"\n[Context {i}]")
|
| 179 |
+
context_parts.append(result['content'])
|
| 180 |
+
if 'metadata' in result:
|
| 181 |
+
meta = result['metadata']
|
| 182 |
+
context_parts.append(f"(From: {meta.get('project', 'Unknown')} - {meta.get('title', 'Unknown')})")
|
| 183 |
+
|
| 184 |
+
if action_items:
|
| 185 |
+
context_parts.append("\nOpen Action Items:")
|
| 186 |
+
for item in action_items:
|
| 187 |
+
assignee = f" ({item['assignee']})" if item.get('assignee') else ""
|
| 188 |
+
deadline = f" by {item['deadline']}" if item.get('deadline') else ""
|
| 189 |
+
context_parts.append(f"- {item['task']}{assignee}{deadline}")
|
| 190 |
+
|
| 191 |
+
if blockers:
|
| 192 |
+
context_parts.append("\nCurrent Blockers:")
|
| 193 |
+
for blocker in blockers:
|
| 194 |
+
context_parts.append(f"- {blocker['blocker']}")
|
| 195 |
+
|
| 196 |
+
context_str = "\n".join(context_parts)
|
| 197 |
+
|
| 198 |
+
# Generate answer
|
| 199 |
+
system_prompt = """You are a helpful AI assistant that helps users manage their projects.
|
| 200 |
+
Use the provided context to answer the user's question accurately and concisely.
|
| 201 |
+
Format your response using bullet points for clarity.
|
| 202 |
+
For action items, list the task with the assignee in parentheses at the end.
|
| 203 |
+
For blockers and risks, list them directly without project names.
|
| 204 |
+
Keep responses brief and to the point. Avoid lengthy explanations.
|
| 205 |
+
Example format:
|
| 206 |
+
## Next Actions
|
| 207 |
+
- Task description (Assignee) by deadline
|
| 208 |
+
- Another task (Assignee)
|
| 209 |
+
|
| 210 |
+
## Blockers/Risks
|
| 211 |
+
- Blocker description
|
| 212 |
+
- Another blocker"""
|
| 213 |
+
|
| 214 |
+
messages = [
|
| 215 |
+
SystemMessage(content=system_prompt),
|
| 216 |
+
HumanMessage(content=f"Context:\n{context_str}\n\nQuestion: {query}\n\nAnswer:")
|
| 217 |
+
]
|
| 218 |
+
|
| 219 |
+
response = self.llm.invoke(messages)
|
| 220 |
+
state["final_answer"] = response.content
|
| 221 |
+
|
| 222 |
+
return state
|
| 223 |
+
|
| 224 |
+
def query(self, user_query: str) -> str:
|
| 225 |
+
"""Process a user query and return an answer."""
|
| 226 |
+
initial_state = {
|
| 227 |
+
"messages": [],
|
| 228 |
+
"query": user_query,
|
| 229 |
+
"retrieved_context": [],
|
| 230 |
+
"action_items": [],
|
| 231 |
+
"blockers": [],
|
| 232 |
+
"next_step": "",
|
| 233 |
+
"final_answer": ""
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
result = self.graph.invoke(initial_state)
|
| 237 |
+
return result["final_answer"]
|
src/parsers.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Meeting note parsers for extracting structured data from markdown files.
|
| 3 |
+
"""
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Optional
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
+
import re
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class ActionItem(BaseModel):
|
| 12 |
+
"""Represents an action item from a meeting."""
|
| 13 |
+
task: str
|
| 14 |
+
assignee: Optional[str] = None
|
| 15 |
+
deadline: Optional[str] = None
|
| 16 |
+
completed: bool = False
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class MeetingNote(BaseModel):
|
| 20 |
+
"""Represents a parsed meeting note."""
|
| 21 |
+
project_name: str
|
| 22 |
+
title: str
|
| 23 |
+
date: Optional[datetime] = None
|
| 24 |
+
participants: List[str] = Field(default_factory=list)
|
| 25 |
+
discussion: Optional[str] = None
|
| 26 |
+
decisions: List[str] = Field(default_factory=list)
|
| 27 |
+
action_items: List[ActionItem] = Field(default_factory=list)
|
| 28 |
+
blockers: List[str] = Field(default_factory=list)
|
| 29 |
+
file_path: str
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class MeetingParser:
|
| 33 |
+
"""Parser for markdown meeting notes."""
|
| 34 |
+
|
| 35 |
+
@staticmethod
|
| 36 |
+
def parse_date(date_str: str) -> Optional[datetime]:
|
| 37 |
+
"""Parse date from various formats."""
|
| 38 |
+
date_formats = [
|
| 39 |
+
"%Y-%m-%d",
|
| 40 |
+
"%d/%m/%Y",
|
| 41 |
+
"%m/%d/%Y",
|
| 42 |
+
"%B %d, %Y",
|
| 43 |
+
"%b %d, %Y",
|
| 44 |
+
"%Y/%m/%d"
|
| 45 |
+
]
|
| 46 |
+
|
| 47 |
+
for fmt in date_formats:
|
| 48 |
+
try:
|
| 49 |
+
return datetime.strptime(date_str.strip(), fmt)
|
| 50 |
+
except ValueError:
|
| 51 |
+
continue
|
| 52 |
+
return None
|
| 53 |
+
|
| 54 |
+
@staticmethod
|
| 55 |
+
def parse_action_item(line: str) -> Optional[ActionItem]:
|
| 56 |
+
"""Parse an action item line."""
|
| 57 |
+
# Match patterns like:
|
| 58 |
+
# - [ ] Task
|
| 59 |
+
# - [x] Task
|
| 60 |
+
# - [ ] Alice: Task by Jan 20
|
| 61 |
+
# - [x] Bob: Task (by 2025-01-20)
|
| 62 |
+
|
| 63 |
+
completed = False
|
| 64 |
+
if "[x]" in line.lower() or "[✓]" in line or "[✔]" in line:
|
| 65 |
+
completed = True
|
| 66 |
+
|
| 67 |
+
# Remove checkbox markers
|
| 68 |
+
line = re.sub(r'\[[ xX✓✔]\]', '', line).strip()
|
| 69 |
+
line = line.lstrip('- ').strip()
|
| 70 |
+
|
| 71 |
+
if not line:
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
# Try to extract assignee
|
| 75 |
+
assignee = None
|
| 76 |
+
assignee_match = re.match(r'^([A-Za-z\s]+):\s*(.+)$', line)
|
| 77 |
+
if assignee_match:
|
| 78 |
+
assignee = assignee_match.group(1).strip()
|
| 79 |
+
line = assignee_match.group(2).strip()
|
| 80 |
+
|
| 81 |
+
# Try to extract deadline
|
| 82 |
+
deadline = None
|
| 83 |
+
deadline_patterns = [
|
| 84 |
+
r'by\s+([A-Za-z]+\s+\d{1,2}(?:,\s+\d{4})?)',
|
| 85 |
+
r'by\s+(\d{4}-\d{2}-\d{2})',
|
| 86 |
+
r'\(by\s+([^)]+)\)',
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
for pattern in deadline_patterns:
|
| 90 |
+
deadline_match = re.search(pattern, line, re.IGNORECASE)
|
| 91 |
+
if deadline_match:
|
| 92 |
+
deadline = deadline_match.group(1).strip()
|
| 93 |
+
line = re.sub(pattern, '', line, flags=re.IGNORECASE).strip()
|
| 94 |
+
break
|
| 95 |
+
|
| 96 |
+
return ActionItem(
|
| 97 |
+
task=line,
|
| 98 |
+
assignee=assignee,
|
| 99 |
+
deadline=deadline,
|
| 100 |
+
completed=completed
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
def parse(file_path: Path, project_name: str) -> Optional[MeetingNote]:
|
| 105 |
+
"""Parse a markdown meeting note file."""
|
| 106 |
+
if not file_path.exists():
|
| 107 |
+
return None
|
| 108 |
+
|
| 109 |
+
content = file_path.read_text(encoding='utf-8')
|
| 110 |
+
lines = content.split('\n')
|
| 111 |
+
|
| 112 |
+
# Initialize fields
|
| 113 |
+
title = file_path.stem.replace('-', ' ').replace('_', ' ').title()
|
| 114 |
+
date = None
|
| 115 |
+
participants = []
|
| 116 |
+
discussion = []
|
| 117 |
+
decisions = []
|
| 118 |
+
action_items = []
|
| 119 |
+
blockers = []
|
| 120 |
+
|
| 121 |
+
current_section = None
|
| 122 |
+
|
| 123 |
+
for line in lines:
|
| 124 |
+
line_stripped = line.strip()
|
| 125 |
+
|
| 126 |
+
# Skip empty lines
|
| 127 |
+
if not line_stripped:
|
| 128 |
+
continue
|
| 129 |
+
|
| 130 |
+
# Check for title
|
| 131 |
+
if line_stripped.startswith('# '):
|
| 132 |
+
title = line_stripped[2:].strip()
|
| 133 |
+
# Try to extract from "Meeting: X" format
|
| 134 |
+
if title.lower().startswith('meeting:'):
|
| 135 |
+
title = title[8:].strip()
|
| 136 |
+
continue
|
| 137 |
+
|
| 138 |
+
# Check for metadata
|
| 139 |
+
if line_stripped.lower().startswith('date:'):
|
| 140 |
+
date_str = line_stripped[5:].strip()
|
| 141 |
+
date = MeetingParser.parse_date(date_str)
|
| 142 |
+
continue
|
| 143 |
+
|
| 144 |
+
if line_stripped.lower().startswith('participants:'):
|
| 145 |
+
participants_str = line_stripped[13:].strip()
|
| 146 |
+
participants = [p.strip() for p in participants_str.split(',')]
|
| 147 |
+
continue
|
| 148 |
+
|
| 149 |
+
# Check for sections
|
| 150 |
+
if line_stripped.startswith('## '):
|
| 151 |
+
section_name = line_stripped[3:].strip().lower()
|
| 152 |
+
if 'discussion' in section_name or 'notes' in section_name:
|
| 153 |
+
current_section = 'discussion'
|
| 154 |
+
elif 'decision' in section_name:
|
| 155 |
+
current_section = 'decisions'
|
| 156 |
+
elif 'action' in section_name or 'todo' in section_name or 'task' in section_name:
|
| 157 |
+
current_section = 'action_items'
|
| 158 |
+
elif 'blocker' in section_name or 'issue' in section_name:
|
| 159 |
+
current_section = 'blockers'
|
| 160 |
+
else:
|
| 161 |
+
current_section = 'discussion'
|
| 162 |
+
continue
|
| 163 |
+
|
| 164 |
+
# Add content to current section
|
| 165 |
+
if current_section == 'discussion':
|
| 166 |
+
discussion.append(line_stripped)
|
| 167 |
+
elif current_section == 'decisions':
|
| 168 |
+
if line_stripped.startswith('-') or line_stripped.startswith('*'):
|
| 169 |
+
decisions.append(line_stripped.lstrip('-*').strip())
|
| 170 |
+
elif current_section == 'action_items':
|
| 171 |
+
if '[' in line_stripped:
|
| 172 |
+
action_item = MeetingParser.parse_action_item(line_stripped)
|
| 173 |
+
if action_item:
|
| 174 |
+
action_items.append(action_item)
|
| 175 |
+
elif current_section == 'blockers':
|
| 176 |
+
if line_stripped.startswith('-') or line_stripped.startswith('*'):
|
| 177 |
+
blockers.append(line_stripped.lstrip('-*').strip())
|
| 178 |
+
|
| 179 |
+
return MeetingNote(
|
| 180 |
+
project_name=project_name,
|
| 181 |
+
title=title,
|
| 182 |
+
date=date,
|
| 183 |
+
participants=participants,
|
| 184 |
+
discussion='\n'.join(discussion) if discussion else None,
|
| 185 |
+
decisions=decisions,
|
| 186 |
+
action_items=action_items,
|
| 187 |
+
blockers=blockers,
|
| 188 |
+
file_path=str(file_path)
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def load_meetings_from_directory(data_dir: Path) -> List[MeetingNote]:
|
| 193 |
+
"""Load all meeting notes from a directory structure."""
|
| 194 |
+
meetings = []
|
| 195 |
+
|
| 196 |
+
if not data_dir.exists():
|
| 197 |
+
return meetings
|
| 198 |
+
|
| 199 |
+
# Expected structure: data_dir/project_name/meetings/*.md
|
| 200 |
+
for project_dir in data_dir.iterdir():
|
| 201 |
+
if not project_dir.is_dir():
|
| 202 |
+
continue
|
| 203 |
+
|
| 204 |
+
project_name = project_dir.name
|
| 205 |
+
meetings_dir = project_dir / "meetings"
|
| 206 |
+
|
| 207 |
+
if not meetings_dir.exists():
|
| 208 |
+
continue
|
| 209 |
+
|
| 210 |
+
for meeting_file in meetings_dir.glob("*.md"):
|
| 211 |
+
meeting = MeetingParser.parse(meeting_file, project_name)
|
| 212 |
+
if meeting:
|
| 213 |
+
meetings.append(meeting)
|
| 214 |
+
|
| 215 |
+
return meetings
|
src/rag.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
RAG (Retrieval Augmented Generation) implementation for project assistant.
|
| 3 |
+
"""
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import chromadb
|
| 8 |
+
from chromadb.config import Settings
|
| 9 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 10 |
+
from langchain_text_splitters import RecursiveCharacterTextSplitter
|
| 11 |
+
from src.parsers import MeetingNote, load_meetings_from_directory
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ProjectRAG:
|
| 15 |
+
"""RAG system for project meeting notes."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, data_dir: Path, persist_dir: Path = None):
|
| 18 |
+
"""Initialize the RAG system."""
|
| 19 |
+
self.data_dir = data_dir
|
| 20 |
+
self.persist_dir = persist_dir or Path("./chroma_db")
|
| 21 |
+
|
| 22 |
+
# Initialize embeddings
|
| 23 |
+
self.embeddings = HuggingFaceEmbeddings(
|
| 24 |
+
model_name="sentence-transformers/all-MiniLM-L6-v2"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Initialize ChromaDB
|
| 28 |
+
self.client = chromadb.PersistentClient(path=str(self.persist_dir))
|
| 29 |
+
self.collection = self.client.get_or_create_collection(
|
| 30 |
+
name="meeting_notes",
|
| 31 |
+
metadata={"hnsw:space": "cosine"}
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Text splitter for chunking
|
| 35 |
+
self.text_splitter = RecursiveCharacterTextSplitter(
|
| 36 |
+
chunk_size=500,
|
| 37 |
+
chunk_overlap=50,
|
| 38 |
+
separators=["\n\n", "\n", ". ", " ", ""]
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
self.meetings: List[MeetingNote] = []
|
| 42 |
+
|
| 43 |
+
def load_and_index(self):
|
| 44 |
+
"""Load all meetings and index them in the vector store."""
|
| 45 |
+
print("Loading meetings from directory...")
|
| 46 |
+
self.meetings = load_meetings_from_directory(self.data_dir)
|
| 47 |
+
print(f"Loaded {len(self.meetings)} meetings")
|
| 48 |
+
|
| 49 |
+
if not self.meetings:
|
| 50 |
+
print("No meetings found. Please add meeting notes to the data directory.")
|
| 51 |
+
return
|
| 52 |
+
|
| 53 |
+
# Clear existing collection
|
| 54 |
+
self.client.delete_collection("meeting_notes")
|
| 55 |
+
self.collection = self.client.create_collection(
|
| 56 |
+
name="meeting_notes",
|
| 57 |
+
metadata={"hnsw:space": "cosine"}
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
print("Indexing meetings...")
|
| 61 |
+
documents = []
|
| 62 |
+
metadatas = []
|
| 63 |
+
ids = []
|
| 64 |
+
|
| 65 |
+
for idx, meeting in enumerate(self.meetings):
|
| 66 |
+
# Create a rich document representation
|
| 67 |
+
doc_parts = [
|
| 68 |
+
f"Project: {meeting.project_name}",
|
| 69 |
+
f"Meeting: {meeting.title}",
|
| 70 |
+
f"Date: {meeting.date.strftime('%Y-%m-%d') if meeting.date else 'Unknown'}",
|
| 71 |
+
]
|
| 72 |
+
|
| 73 |
+
if meeting.participants:
|
| 74 |
+
doc_parts.append(f"Participants: {', '.join(meeting.participants)}")
|
| 75 |
+
|
| 76 |
+
if meeting.discussion:
|
| 77 |
+
doc_parts.append(f"Discussion:\n{meeting.discussion}")
|
| 78 |
+
|
| 79 |
+
if meeting.decisions:
|
| 80 |
+
doc_parts.append("Decisions:")
|
| 81 |
+
doc_parts.extend([f"- {d}" for d in meeting.decisions])
|
| 82 |
+
|
| 83 |
+
if meeting.action_items:
|
| 84 |
+
doc_parts.append("Action Items:")
|
| 85 |
+
for item in meeting.action_items:
|
| 86 |
+
status = "✓" if item.completed else "○"
|
| 87 |
+
assignee = f"{item.assignee}: " if item.assignee else ""
|
| 88 |
+
deadline = f" (by {item.deadline})" if item.deadline else ""
|
| 89 |
+
doc_parts.append(f"{status} {assignee}{item.task}{deadline}")
|
| 90 |
+
|
| 91 |
+
if meeting.blockers:
|
| 92 |
+
doc_parts.append("Blockers:")
|
| 93 |
+
doc_parts.extend([f"- {b}" for b in meeting.blockers])
|
| 94 |
+
|
| 95 |
+
full_doc = "\n".join(doc_parts)
|
| 96 |
+
|
| 97 |
+
# Chunk the document
|
| 98 |
+
chunks = self.text_splitter.split_text(full_doc)
|
| 99 |
+
|
| 100 |
+
for chunk_idx, chunk in enumerate(chunks):
|
| 101 |
+
documents.append(chunk)
|
| 102 |
+
metadatas.append({
|
| 103 |
+
"meeting_idx": idx,
|
| 104 |
+
"project": meeting.project_name,
|
| 105 |
+
"title": meeting.title,
|
| 106 |
+
"date": meeting.date.isoformat() if meeting.date else "",
|
| 107 |
+
"file_path": meeting.file_path,
|
| 108 |
+
"chunk_idx": chunk_idx
|
| 109 |
+
})
|
| 110 |
+
ids.append(f"meeting_{idx}_chunk_{chunk_idx}")
|
| 111 |
+
|
| 112 |
+
# Add to ChromaDB
|
| 113 |
+
if documents:
|
| 114 |
+
# Embed documents
|
| 115 |
+
embeddings_list = self.embeddings.embed_documents(documents)
|
| 116 |
+
|
| 117 |
+
self.collection.add(
|
| 118 |
+
embeddings=embeddings_list,
|
| 119 |
+
documents=documents,
|
| 120 |
+
metadatas=metadatas,
|
| 121 |
+
ids=ids
|
| 122 |
+
)
|
| 123 |
+
print(f"Indexed {len(documents)} chunks from {len(self.meetings)} meetings")
|
| 124 |
+
|
| 125 |
+
def search(self, query: str, n_results: int = 5, project_filter: str = None) -> List[Dict[str, Any]]:
|
| 126 |
+
"""Search for relevant meeting content."""
|
| 127 |
+
# Embed the query
|
| 128 |
+
query_embedding = self.embeddings.embed_query(query)
|
| 129 |
+
|
| 130 |
+
# Prepare where clause for filtering
|
| 131 |
+
where = None
|
| 132 |
+
if project_filter:
|
| 133 |
+
where = {"project": project_filter}
|
| 134 |
+
|
| 135 |
+
# Search in ChromaDB
|
| 136 |
+
results = self.collection.query(
|
| 137 |
+
query_embeddings=[query_embedding],
|
| 138 |
+
n_results=n_results,
|
| 139 |
+
where=where
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
# Format results
|
| 143 |
+
formatted_results = []
|
| 144 |
+
if results['documents'] and results['documents'][0]:
|
| 145 |
+
for i in range(len(results['documents'][0])):
|
| 146 |
+
formatted_results.append({
|
| 147 |
+
'content': results['documents'][0][i],
|
| 148 |
+
'metadata': results['metadatas'][0][i],
|
| 149 |
+
'distance': results['distances'][0][i] if 'distances' in results else None
|
| 150 |
+
})
|
| 151 |
+
|
| 152 |
+
return formatted_results
|
| 153 |
+
|
| 154 |
+
def get_all_projects(self) -> List[str]:
|
| 155 |
+
"""Get list of all project names."""
|
| 156 |
+
return list(set(m.project_name for m in self.meetings))
|
| 157 |
+
|
| 158 |
+
def get_open_action_items(self, project: str = None) -> List[Dict[str, Any]]:
|
| 159 |
+
"""Get all open action items, optionally filtered by project."""
|
| 160 |
+
action_items = []
|
| 161 |
+
|
| 162 |
+
for meeting in self.meetings:
|
| 163 |
+
if project and meeting.project_name != project:
|
| 164 |
+
continue
|
| 165 |
+
|
| 166 |
+
for item in meeting.action_items:
|
| 167 |
+
if not item.completed:
|
| 168 |
+
action_items.append({
|
| 169 |
+
'project': meeting.project_name,
|
| 170 |
+
'meeting': meeting.title,
|
| 171 |
+
'date': meeting.date,
|
| 172 |
+
'assignee': item.assignee,
|
| 173 |
+
'task': item.task,
|
| 174 |
+
'deadline': item.deadline
|
| 175 |
+
})
|
| 176 |
+
|
| 177 |
+
return action_items
|
| 178 |
+
|
| 179 |
+
def get_blockers(self, project: str = None) -> List[Dict[str, Any]]:
|
| 180 |
+
"""Get all blockers, optionally filtered by project."""
|
| 181 |
+
blockers = []
|
| 182 |
+
|
| 183 |
+
for meeting in self.meetings:
|
| 184 |
+
if project and meeting.project_name != project:
|
| 185 |
+
continue
|
| 186 |
+
|
| 187 |
+
for blocker in meeting.blockers:
|
| 188 |
+
blockers.append({
|
| 189 |
+
'project': meeting.project_name,
|
| 190 |
+
'meeting': meeting.title,
|
| 191 |
+
'date': meeting.date,
|
| 192 |
+
'blocker': blocker
|
| 193 |
+
})
|
| 194 |
+
|
| 195 |
+
return blockers
|
| 196 |
+
|
| 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,
|
| 208 |
+
'meeting': meeting.title,
|
| 209 |
+
'date': meeting.date,
|
| 210 |
+
'decision': decision
|
| 211 |
+
})
|
| 212 |
+
|
| 213 |
+
if len(decisions) >= limit:
|
| 214 |
+
return decisions
|
| 215 |
+
|
| 216 |
+
return decisions
|
src/utils.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions for the project assistant.
|
| 3 |
+
"""
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import List, Dict, Any
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def format_date(date: datetime) -> str:
|
| 10 |
+
"""Format a datetime object for display."""
|
| 11 |
+
if date is None:
|
| 12 |
+
return "Unknown date"
|
| 13 |
+
return date.strftime("%B %d, %Y")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def format_action_item(item: Dict[str, Any]) -> str:
|
| 17 |
+
"""Format an action item for display."""
|
| 18 |
+
parts = []
|
| 19 |
+
|
| 20 |
+
if item.get('assignee'):
|
| 21 |
+
parts.append(f"**{item['assignee']}**:")
|
| 22 |
+
|
| 23 |
+
parts.append(item['task'])
|
| 24 |
+
|
| 25 |
+
if item.get('deadline'):
|
| 26 |
+
parts.append(f"(by {item['deadline']})")
|
| 27 |
+
|
| 28 |
+
return " ".join(parts)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_project_stats(meetings: List[Any]) -> Dict[str, Any]:
|
| 32 |
+
"""Calculate statistics across all projects."""
|
| 33 |
+
stats = {
|
| 34 |
+
'total_meetings': len(meetings),
|
| 35 |
+
'total_action_items': 0,
|
| 36 |
+
'completed_action_items': 0,
|
| 37 |
+
'open_action_items': 0,
|
| 38 |
+
'total_blockers': 0,
|
| 39 |
+
'total_decisions': 0,
|
| 40 |
+
'projects': {}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
for meeting in meetings:
|
| 44 |
+
project = meeting.project_name
|
| 45 |
+
|
| 46 |
+
if project not in stats['projects']:
|
| 47 |
+
stats['projects'][project] = {
|
| 48 |
+
'meetings': 0,
|
| 49 |
+
'action_items': 0,
|
| 50 |
+
'blockers': 0,
|
| 51 |
+
'decisions': 0
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
stats['projects'][project]['meetings'] += 1
|
| 55 |
+
|
| 56 |
+
for item in meeting.action_items:
|
| 57 |
+
stats['total_action_items'] += 1
|
| 58 |
+
stats['projects'][project]['action_items'] += 1
|
| 59 |
+
|
| 60 |
+
if item.completed:
|
| 61 |
+
stats['completed_action_items'] += 1
|
| 62 |
+
else:
|
| 63 |
+
stats['open_action_items'] += 1
|
| 64 |
+
|
| 65 |
+
stats['total_blockers'] += len(meeting.blockers)
|
| 66 |
+
stats['projects'][project]['blockers'] += len(meeting.blockers)
|
| 67 |
+
|
| 68 |
+
stats['total_decisions'] += len(meeting.decisions)
|
| 69 |
+
stats['projects'][project]['decisions'] += len(meeting.decisions)
|
| 70 |
+
|
| 71 |
+
return stats
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def create_sample_meeting(project_name: str, output_path: Path) -> None:
|
| 75 |
+
"""Create a sample meeting note template."""
|
| 76 |
+
template = f"""# Meeting: [Meeting Title]
|
| 77 |
+
Date: {datetime.now().strftime('%Y-%m-%d')}
|
| 78 |
+
Participants: [Name1, Name2, Name3]
|
| 79 |
+
|
| 80 |
+
## Discussion
|
| 81 |
+
[What was discussed in the meeting]
|
| 82 |
+
|
| 83 |
+
## Decisions
|
| 84 |
+
- [Decision 1]
|
| 85 |
+
- [Decision 2]
|
| 86 |
+
|
| 87 |
+
## Action Items
|
| 88 |
+
- [ ] [Person]: [Task description] by [deadline]
|
| 89 |
+
- [ ] [Person]: [Task description]
|
| 90 |
+
|
| 91 |
+
## Blockers
|
| 92 |
+
- [Blocker description]
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 96 |
+
output_path.write_text(template)
|
| 97 |
+
print(f"Created sample meeting template at {output_path}")
|