sebasmos commited on
Commit
d6f13c4
·
0 Parent(s):

Deploy Sherlock

Browse files
.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}")