├── .env.example
├── requirements.txt
├── langgraph.json
├── configuration.py
├── README.md
├── task_maistro.py
└── ntbk
└── audio_ux.ipynb
/.env.example:
--------------------------------------------------------------------------------
1 | OPENAI_API_KEY=sk-xxx
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | langgraph
2 | langchain-core
3 | langchain-community
4 | langchain-openai
5 | trustcall
6 | ipython
--------------------------------------------------------------------------------
/langgraph.json:
--------------------------------------------------------------------------------
1 | {
2 | "dockerfile_lines": [],
3 | "graphs": {
4 | "task_maistro": "./task_maistro.py:graph"
5 | },
6 | "env": "./.env",
7 | "python_version": "3.11",
8 | "dependencies": [
9 | "."
10 | ]
11 | }
--------------------------------------------------------------------------------
/configuration.py:
--------------------------------------------------------------------------------
1 | import os
2 | from dataclasses import dataclass, field, fields
3 | from typing import Any, Optional
4 |
5 | from langchain_core.runnables import RunnableConfig
6 | from typing_extensions import Annotated
7 | from dataclasses import dataclass
8 |
9 | @dataclass(kw_only=True)
10 | class Configuration:
11 | """The configurable fields for the chatbot."""
12 | user_id: str = "default-user"
13 | todo_category: str = "general"
14 | task_maistro_role: str = "You are a helpful task management assistant. You help you create, organize, and manage the user's ToDo list."
15 |
16 | @classmethod
17 | def from_runnable_config(
18 | cls, config: Optional[RunnableConfig] = None
19 | ) -> "Configuration":
20 | """Create a Configuration instance from a RunnableConfig."""
21 | configurable = (
22 | config["configurable"] if config and "configurable" in config else {}
23 | )
24 | values: dict[str, Any] = {
25 | f.name: os.environ.get(f.name.upper(), configurable.get(f.name))
26 | for f in fields(cls)
27 | if f.init
28 | }
29 | return cls(**{k: v for k, v in values.items() if v})
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Task mAIstro
2 |
3 | Managing tasks effectively is a universal challenge. Task mAIstro is an AI-powered task management agent that combines natural language processing with long-term memory to create a more intuitive and adaptive experience. This repo can be used to deploy Task mAIstro and interact with it through text or voice!
4 |
5 | Key features:
6 | * Natural conversation through text and / or voice to update or add tasks
7 | * Adaptive learning of your management style and preferences
8 | * Persistent memory of tasks, context, and preferences
9 | * Flexible deployment options - local or hosted
10 |
11 | 
12 |
13 | ## Quickstart
14 |
15 | 1. Populate the `.env` file:
16 | ```
17 | $ cp .env.example .env
18 | ```
19 |
20 | 2. Download the LangGraph Studio desktop app for Mac [here](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download).
21 |
22 | 3. Load this repository as a project in LangGraph Studio.
23 |
24 | 4. Start chatting with the task mAIstro through the text interface in LangGraph Studio.
25 |
26 | 5. Experiment with voice UX in `ntbk/audio_ux.ipynb`.
27 |
28 | ## Task mAIstro Application
29 |
30 | ### Architecture
31 |
32 | Task mAIstro leverages [LangGraph](https://langchain-ai.github.io/langgraph/) to maintain three memory types:
33 |
34 | 1. **ToDo List Memory**
35 | - Task descriptions and deadlines
36 | - Time estimates and status tracking
37 | - Actionable next steps
38 |
39 | 2. **User Profile Memory**
40 | - Personal preferences and context
41 | - Work/life patterns
42 | - Historical interactions
43 |
44 | 3. **Interaction Memory**
45 | - Task management style
46 | - Communication preferences
47 | - Organizational patterns
48 |
49 | The schema for each memory type as well as the graph flow is defined in `task_maistro.py`.
50 | The graph flow is orchestrated by a central `task_maistro` node that:
51 | - Chooses to update one of the three memories based on the user's input
52 | - Uses tool calling with the [Trustcall library](https://github.com/hinthornw/trustcall) to update the chosen memory type
53 |
54 | ### 📚 Learning Resources
55 |
56 | > **Want to learn more?** Check out [Module 5 of our LangGraph Course](https://academy.langchain.com/courses/intro-to-langgraph):
57 | > - All notebooks are available [here](https://github.com/langchain-ai/langchain-academy/tree/main/module-5)
58 | > - All notebooks have accompanying videos
59 |
60 | ### Deployment Options
61 |
62 | Task mAIstro offers three flexible deployment paths:
63 |
64 | #### 1. LangGraph Studio (Recommended for quickstart)
65 | The fastest way to get started is with [LangGraph Studio](https://github.com/langchain-ai/langgraph-studio):
66 | - Download the [desktop app](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download)
67 | - Ensure [Docker Desktop](https://docs.docker.com/engine/install/) is running
68 | - Load this repository as a project
69 |
70 | 
71 |
72 | #### 2. LangGraph CLI
73 | For developers who prefer the command line:
74 | - Use the [LangGraph CLI](https://langchain-ai.github.io/langgraph/concepts/langgraph_cli/#up)
75 | - Deploy locally with a single command
76 | - Full control over deployment configuration
77 |
78 | #### 3. LangGraph Cloud
79 | For production deployments:
80 | - Deploy to [LangGraph Cloud](https://langchain-ai.github.io/langgraph/concepts/langgraph_cloud/)
81 | - Manage through your LangSmith account
82 | - Access via LangGraph Studio web UI
83 |
84 | ### 🚀 Learning Resources
85 | > **Want to learn more about deployments?** Check out [Module 6 of our LangGraph Course](https://academy.langchain.com/courses/intro-to-langgraph)
86 | > - All notebooks are available [here](https://github.com/langchain-ai/langchain-academy/tree/main/module-6)
87 | > - All notebooks have accompanying videos
88 |
89 | ## Voice Interface
90 |
91 | Task mAIstro supports voice interactions using:
92 | - [OpenAI's Whisper](https://platform.openai.com/docs/guides/speech-to-text) for speech-to-text
93 | - [ElevenLabs](https://github.com/elevenlabs/elevenlabs-python) for text-to-speech
94 |
95 | ### Setup
96 |
97 | 1. Install FFmpeg (required for ElevenLabs):
98 | ```bash
99 | # macOS
100 | brew install ffmpeg
101 | ```
102 |
103 | 2. In `audio_ux.ipynb`, connect to your deployment using the URL endpoint:
104 | - **Studio**: Found in Studio UI
105 | - **CLI**: Printed to console (e.g., typically `http://localhost:8123`)
106 | - **Cloud**: Available in the LangSmith Deployment page
107 |
--------------------------------------------------------------------------------
/task_maistro.py:
--------------------------------------------------------------------------------
1 | import uuid
2 | from datetime import datetime
3 |
4 | from pydantic import BaseModel, Field
5 |
6 | from trustcall import create_extractor
7 |
8 | from typing import Literal, Optional, TypedDict
9 |
10 | from langchain_core.runnables import RunnableConfig
11 | from langchain_core.messages import merge_message_runs
12 | from langchain_core.messages import SystemMessage, HumanMessage
13 |
14 | from langchain_openai import ChatOpenAI
15 |
16 | from langgraph.checkpoint.memory import MemorySaver
17 | from langgraph.graph import StateGraph, MessagesState, START, END
18 | from langgraph.store.base import BaseStore
19 | from langgraph.store.memory import InMemoryStore
20 |
21 | import configuration
22 |
23 | ## Utilities
24 |
25 | # Inspect the tool calls for Trustcall
26 | class Spy:
27 | def __init__(self):
28 | self.called_tools = []
29 |
30 | def __call__(self, run):
31 | q = [run]
32 | while q:
33 | r = q.pop()
34 | if r.child_runs:
35 | q.extend(r.child_runs)
36 | if r.run_type == "chat_model":
37 | self.called_tools.append(
38 | r.outputs["generations"][0][0]["message"]["kwargs"]["tool_calls"]
39 | )
40 |
41 | # Extract information from tool calls for both patches and new memories in Trustcall
42 | def extract_tool_info(tool_calls, schema_name="Memory"):
43 | """Extract information from tool calls for both patches and new memories.
44 |
45 | Args:
46 | tool_calls: List of tool calls from the model
47 | schema_name: Name of the schema tool (e.g., "Memory", "ToDo", "Profile")
48 | """
49 | # Initialize list of changes
50 | changes = []
51 |
52 | for call_group in tool_calls:
53 | for call in call_group:
54 | if call['name'] == 'PatchDoc':
55 | # Check if there are any patches
56 | if call['args']['patches']:
57 | changes.append({
58 | 'type': 'update',
59 | 'doc_id': call['args']['json_doc_id'],
60 | 'planned_edits': call['args']['planned_edits'],
61 | 'value': call['args']['patches'][0]['value']
62 | })
63 | else:
64 | # Handle case where no changes were needed
65 | changes.append({
66 | 'type': 'no_update',
67 | 'doc_id': call['args']['json_doc_id'],
68 | 'planned_edits': call['args']['planned_edits']
69 | })
70 | elif call['name'] == schema_name:
71 | changes.append({
72 | 'type': 'new',
73 | 'value': call['args']
74 | })
75 |
76 | # Format results as a single string
77 | result_parts = []
78 | for change in changes:
79 | if change['type'] == 'update':
80 | result_parts.append(
81 | f"Document {change['doc_id']} updated:\n"
82 | f"Plan: {change['planned_edits']}\n"
83 | f"Added content: {change['value']}"
84 | )
85 | elif change['type'] == 'no_update':
86 | result_parts.append(
87 | f"Document {change['doc_id']} unchanged:\n"
88 | f"{change['planned_edits']}"
89 | )
90 | else:
91 | result_parts.append(
92 | f"New {schema_name} created:\n"
93 | f"Content: {change['value']}"
94 | )
95 |
96 | return "\n\n".join(result_parts)
97 |
98 | ## Schema definitions
99 |
100 | # User profile schema
101 | class Profile(BaseModel):
102 | """This is the profile of the user you are chatting with"""
103 | name: Optional[str] = Field(description="The user's name", default=None)
104 | location: Optional[str] = Field(description="The user's location", default=None)
105 | job: Optional[str] = Field(description="The user's job", default=None)
106 | connections: list[str] = Field(
107 | description="Personal connection of the user, such as family members, friends, or coworkers",
108 | default_factory=list
109 | )
110 | interests: list[str] = Field(
111 | description="Interests that the user has",
112 | default_factory=list
113 | )
114 |
115 | # ToDo schema
116 | class ToDo(BaseModel):
117 | task: str = Field(description="The task to be completed.")
118 | time_to_complete: Optional[int] = Field(description="Estimated time to complete the task (minutes).")
119 | deadline: Optional[datetime] = Field(
120 | description="When the task needs to be completed by (if applicable)",
121 | default=None
122 | )
123 | solutions: list[str] = Field(
124 | description="List of specific, actionable solutions (e.g., specific ideas, service providers, or concrete options relevant to completing the task)",
125 | min_items=1,
126 | default_factory=list
127 | )
128 | status: Literal["not started", "in progress", "done", "archived"] = Field(
129 | description="Current status of the task",
130 | default="not started"
131 | )
132 |
133 | ## Initialize the model and tools
134 |
135 | # Update memory tool
136 | class UpdateMemory(TypedDict):
137 | """ Decision on what memory type to update """
138 | update_type: Literal['user', 'todo', 'instructions']
139 |
140 | # Initialize the model
141 | model = ChatOpenAI(model="gpt-4o", temperature=0)
142 |
143 | ## Create the Trustcall extractors for updating the user profile and ToDo list
144 | profile_extractor = create_extractor(
145 | model,
146 | tools=[Profile],
147 | tool_choice="Profile",
148 | )
149 |
150 | ## Prompts
151 |
152 | # Chatbot instruction for choosing what to update and what tools to call
153 | MODEL_SYSTEM_MESSAGE = """{task_maistro_role}
154 |
155 | You have a long term memory which keeps track of three things:
156 | 1. The user's profile (general information about them)
157 | 2. The user's ToDo list
158 | 3. General instructions for updating the ToDo list
159 |
160 | Here is the current User Profile (may be empty if no information has been collected yet):
161 |
162 | {user_profile}
163 |
164 |
165 | Here is the current ToDo List (may be empty if no tasks have been added yet):
166 |
167 | {todo}
168 |
169 |
170 | Here are the current user-specified preferences for updating the ToDo list (may be empty if no preferences have been specified yet):
171 |
172 | {instructions}
173 |
174 |
175 | Here are your instructions for reasoning about the user's messages:
176 |
177 | 1. Reason carefully about the user's messages as presented below.
178 |
179 | 2. Decide whether any of the your long-term memory should be updated:
180 | - If personal information was provided about the user, update the user's profile by calling UpdateMemory tool with type `user`
181 | - If tasks are mentioned, update the ToDo list by calling UpdateMemory tool with type `todo`
182 | - If the user has specified preferences for how to update the ToDo list, update the instructions by calling UpdateMemory tool with type `instructions`
183 |
184 | 3. Tell the user that you have updated your memory, if appropriate:
185 | - Do not tell the user you have updated the user's profile
186 | - Tell the user them when you update the todo list
187 | - Do not tell the user that you have updated instructions
188 |
189 | 4. Err on the side of updating the todo list. No need to ask for explicit permission.
190 |
191 | 5. Respond naturally to user user after a tool call was made to save memories, or if no tool call was made."""
192 |
193 | # Trustcall instruction
194 | TRUSTCALL_INSTRUCTION = """Reflect on following interaction.
195 |
196 | Use the provided tools to retain any necessary memories about the user.
197 |
198 | Use parallel tool calling to handle updates and insertions simultaneously.
199 |
200 | System Time: {time}"""
201 |
202 | # Instructions for updating the ToDo list
203 | CREATE_INSTRUCTIONS = """Reflect on the following interaction.
204 |
205 | Based on this interaction, update your instructions for how to update ToDo list items. Use any feedback from the user to update how they like to have items added, etc.
206 |
207 | Your current instructions are:
208 |
209 |
210 | {current_instructions}
211 | """
212 |
213 | ## Node definitions
214 |
215 | def task_mAIstro(state: MessagesState, config: RunnableConfig, store: BaseStore):
216 |
217 | """Load memories from the store and use them to personalize the chatbot's response."""
218 |
219 | # Get the user ID from the config
220 | configurable = configuration.Configuration.from_runnable_config(config)
221 | user_id = configurable.user_id
222 | todo_category = configurable.todo_category
223 | task_maistro_role = configurable.task_maistro_role
224 |
225 | # Retrieve profile memory from the store
226 | namespace = ("profile", todo_category, user_id)
227 | memories = store.search(namespace)
228 | if memories:
229 | user_profile = memories[0].value
230 | else:
231 | user_profile = None
232 |
233 | # Retrieve people memory from the store
234 | namespace = ("todo", todo_category, user_id)
235 | memories = store.search(namespace)
236 | todo = "\n".join(f"{mem.value}" for mem in memories)
237 |
238 | # Retrieve custom instructions
239 | namespace = ("instructions", todo_category, user_id)
240 | memories = store.search(namespace)
241 | if memories:
242 | instructions = memories[0].value
243 | else:
244 | instructions = ""
245 |
246 | system_msg = MODEL_SYSTEM_MESSAGE.format(task_maistro_role=task_maistro_role, user_profile=user_profile, todo=todo, instructions=instructions)
247 |
248 | # Respond using memory as well as the chat history
249 | response = model.bind_tools([UpdateMemory], parallel_tool_calls=False).invoke([SystemMessage(content=system_msg)]+state["messages"])
250 |
251 | return {"messages": [response]}
252 |
253 | def update_profile(state: MessagesState, config: RunnableConfig, store: BaseStore):
254 |
255 | """Reflect on the chat history and update the memory collection."""
256 |
257 | # Get the user ID from the config
258 | configurable = configuration.Configuration.from_runnable_config(config)
259 | user_id = configurable.user_id
260 | todo_category = configurable.todo_category
261 |
262 | # Define the namespace for the memories
263 | namespace = ("profile", todo_category, user_id)
264 |
265 | # Retrieve the most recent memories for context
266 | existing_items = store.search(namespace)
267 |
268 | # Format the existing memories for the Trustcall extractor
269 | tool_name = "Profile"
270 | existing_memories = ([(existing_item.key, tool_name, existing_item.value)
271 | for existing_item in existing_items]
272 | if existing_items
273 | else None
274 | )
275 |
276 | # Merge the chat history and the instruction
277 | TRUSTCALL_INSTRUCTION_FORMATTED=TRUSTCALL_INSTRUCTION.format(time=datetime.now().isoformat())
278 | updated_messages=list(merge_message_runs(messages=[SystemMessage(content=TRUSTCALL_INSTRUCTION_FORMATTED)] + state["messages"][:-1]))
279 |
280 | # Invoke the extractor
281 | result = profile_extractor.invoke({"messages": updated_messages,
282 | "existing": existing_memories})
283 |
284 | # Save save the memories from Trustcall to the store
285 | for r, rmeta in zip(result["responses"], result["response_metadata"]):
286 | store.put(namespace,
287 | rmeta.get("json_doc_id", str(uuid.uuid4())),
288 | r.model_dump(mode="json"),
289 | )
290 | tool_calls = state['messages'][-1].tool_calls
291 | # Return tool message with update verification
292 | return {"messages": [{"role": "tool", "content": "updated profile", "tool_call_id":tool_calls[0]['id']}]}
293 |
294 | def update_todos(state: MessagesState, config: RunnableConfig, store: BaseStore):
295 |
296 | """Reflect on the chat history and update the memory collection."""
297 |
298 | # Get the user ID from the config
299 | configurable = configuration.Configuration.from_runnable_config(config)
300 | user_id = configurable.user_id
301 | todo_category = configurable.todo_category
302 |
303 | # Define the namespace for the memories
304 | namespace = ("todo", todo_category, user_id)
305 |
306 | # Retrieve the most recent memories for context
307 | existing_items = store.search(namespace)
308 |
309 | # Format the existing memories for the Trustcall extractor
310 | tool_name = "ToDo"
311 | existing_memories = ([(existing_item.key, tool_name, existing_item.value)
312 | for existing_item in existing_items]
313 | if existing_items
314 | else None
315 | )
316 |
317 | # Merge the chat history and the instruction
318 | TRUSTCALL_INSTRUCTION_FORMATTED=TRUSTCALL_INSTRUCTION.format(time=datetime.now().isoformat())
319 | updated_messages=list(merge_message_runs(messages=[SystemMessage(content=TRUSTCALL_INSTRUCTION_FORMATTED)] + state["messages"][:-1]))
320 |
321 | # Initialize the spy for visibility into the tool calls made by Trustcall
322 | spy = Spy()
323 |
324 | # Create the Trustcall extractor for updating the ToDo list
325 | todo_extractor = create_extractor(
326 | model,
327 | tools=[ToDo],
328 | tool_choice=tool_name,
329 | enable_inserts=True
330 | ).with_listeners(on_end=spy)
331 |
332 | # Invoke the extractor
333 | result = todo_extractor.invoke({"messages": updated_messages,
334 | "existing": existing_memories})
335 |
336 | # Save save the memories from Trustcall to the store
337 | for r, rmeta in zip(result["responses"], result["response_metadata"]):
338 | store.put(namespace,
339 | rmeta.get("json_doc_id", str(uuid.uuid4())),
340 | r.model_dump(mode="json"),
341 | )
342 |
343 | # Respond to the tool call made in task_mAIstro, confirming the update
344 | tool_calls = state['messages'][-1].tool_calls
345 |
346 | # Extract the changes made by Trustcall and add the the ToolMessage returned to task_mAIstro
347 | todo_update_msg = extract_tool_info(spy.called_tools, tool_name)
348 | return {"messages": [{"role": "tool", "content": todo_update_msg, "tool_call_id":tool_calls[0]['id']}]}
349 |
350 | def update_instructions(state: MessagesState, config: RunnableConfig, store: BaseStore):
351 |
352 | """Reflect on the chat history and update the memory collection."""
353 |
354 | # Get the user ID from the config
355 | configurable = configuration.Configuration.from_runnable_config(config)
356 | user_id = configurable.user_id
357 | todo_category = configurable.todo_category
358 |
359 | namespace = ("instructions", todo_category, user_id)
360 |
361 | existing_memory = store.get(namespace, "user_instructions")
362 |
363 | # Format the memory in the system prompt
364 | system_msg = CREATE_INSTRUCTIONS.format(current_instructions=existing_memory.value if existing_memory else None)
365 | new_memory = model.invoke([SystemMessage(content=system_msg)]+state['messages'][:-1] + [HumanMessage(content="Please update the instructions based on the conversation")])
366 |
367 | # Overwrite the existing memory in the store
368 | key = "user_instructions"
369 | store.put(namespace, key, {"memory": new_memory.content})
370 | tool_calls = state['messages'][-1].tool_calls
371 | # Return tool message with update verification
372 | return {"messages": [{"role": "tool", "content": "updated instructions", "tool_call_id":tool_calls[0]['id']}]}
373 |
374 | # Conditional edge
375 | def route_message(state: MessagesState, config: RunnableConfig, store: BaseStore) -> Literal[END, "update_todos", "update_instructions", "update_profile"]:
376 |
377 | """Reflect on the memories and chat history to decide whether to update the memory collection."""
378 | message = state['messages'][-1]
379 | if len(message.tool_calls) ==0:
380 | return END
381 | else:
382 | tool_call = message.tool_calls[0]
383 | if tool_call['args']['update_type'] == "user":
384 | return "update_profile"
385 | elif tool_call['args']['update_type'] == "todo":
386 | return "update_todos"
387 | elif tool_call['args']['update_type'] == "instructions":
388 | return "update_instructions"
389 | else:
390 | raise ValueError
391 |
392 | # Create the graph + all nodes
393 | builder = StateGraph(MessagesState, config_schema=configuration.Configuration)
394 |
395 | # Define the flow of the memory extraction process
396 | builder.add_node(task_mAIstro)
397 | builder.add_node(update_todos)
398 | builder.add_node(update_profile)
399 | builder.add_node(update_instructions)
400 |
401 | # Define the flow
402 | builder.add_edge(START, "task_mAIstro")
403 | builder.add_conditional_edges("task_mAIstro", route_message)
404 | builder.add_edge("update_todos", "task_mAIstro")
405 | builder.add_edge("update_profile", "task_mAIstro")
406 | builder.add_edge("update_instructions", "task_mAIstro")
407 |
408 | # Compile the graph
409 | graph = builder.compile()
--------------------------------------------------------------------------------
/ntbk/audio_ux.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "b4cd66e8",
6 | "metadata": {},
7 | "source": [
8 | "# Audio UX\n",
9 | "\n",
10 | "Task mAIstro supports voice interactions using:\n",
11 | "- [OpenAI's Whisper](https://platform.openai.com/docs/guides/speech-to-text) for speech-to-text\n",
12 | "- [ElevenLabs](https://github.com/elevenlabs/elevenlabs-python) for text-to-speech\n",
13 | "\n",
14 | "### Install dependencies\n",
15 | "\n",
16 | "Ensure you have `ffmpeg` installed for using ElevenLabs. \n",
17 | "\n",
18 | "On MacOS, you can install it with `brew install ffmpeg`. "
19 | ]
20 | },
21 | {
22 | "cell_type": "code",
23 | "execution_count": 1,
24 | "id": "d1fd88e4",
25 | "metadata": {},
26 | "outputs": [],
27 | "source": [
28 | "%%capture --no-stderr\n",
29 | "%pip install -U langchain_openai langgraph langchain_core sounddevice scipy elevenlabs "
30 | ]
31 | },
32 | {
33 | "cell_type": "markdown",
34 | "id": "44b724c3",
35 | "metadata": {},
36 | "source": [
37 | "### Set environment variables\n",
38 | "\n",
39 | "* Set your `OPENAI_API_KEY`\n",
40 | "* Set your `ELEVENLABS_API_KEY` (available [here](https://elevenlabs.io/api))\n",
41 | "* Optional: Set your `LANGCHAIN_API_KEY` (available [here](https://smith.langchain.com/)) if you want tracing or want to connect with a hosted deployment."
42 | ]
43 | },
44 | {
45 | "cell_type": "code",
46 | "execution_count": 1,
47 | "id": "c7311ebb",
48 | "metadata": {},
49 | "outputs": [],
50 | "source": [
51 | "import os, getpass\n",
52 | "\n",
53 | "def _set_env(var: str):\n",
54 | " # Check if the variable is set in the OS environment\n",
55 | " env_value = os.environ.get(var)\n",
56 | " if not env_value:\n",
57 | " # If not set, prompt the user for input\n",
58 | " env_value = getpass.getpass(f\"{var}: \")\n",
59 | " \n",
60 | " # Set the environment variable for the current process\n",
61 | " os.environ[var] = env_value\n",
62 | "\n",
63 | "_set_env(\"LANGCHAIN_API_KEY\")\n",
64 | "_set_env(\"ELEVENLABS_API_KEY\")\n",
65 | "_set_env(\"OPENAI_API_KEY\")\n",
66 | "\n",
67 | "import os\n",
68 | "os.environ[\"LANGCHAIN_TRACING_V2\"] = \"true\"\n",
69 | "os.environ[\"LANGCHAIN_PROJECT\"] = \"task-maistro-deployment\""
70 | ]
71 | },
72 | {
73 | "cell_type": "markdown",
74 | "id": "45f5e99c-9c95-4ec9-a879-45cb80c76179",
75 | "metadata": {},
76 | "source": [
77 | "### Connect to your deployment\n",
78 | "\n",
79 | "Connect to your deployment using the URL endpoint:\n",
80 | "- **Studio**: Found in Studio UI \n",
81 | "- **CLI**: Printed to console (typically `http://localhost:8123`)\n",
82 | "- **Cloud**: Available in LangGraph Deployment page\n",
83 | "\n",
84 | "We'll connect to the deployment as a [RemoteGraph](https://langchain-ai.github.io/langgraph/how-tos/use-remote-graph/#how-to-interact-with-the-deployment-using-remotegraph). \n"
85 | ]
86 | },
87 | {
88 | "cell_type": "code",
89 | "execution_count": 2,
90 | "id": "b2bdfbed-694a-4dbc-8ddf-e2649ec28181",
91 | "metadata": {},
92 | "outputs": [],
93 | "source": [
94 | "from langgraph.pregel.remote import RemoteGraph\n",
95 | "from langchain_core.messages import convert_to_messages\n",
96 | "from langchain_core.messages import HumanMessage, SystemMessage\n",
97 | "\n",
98 | "# Local deployment (via LangGraph Studio)\n",
99 | "local_deployment_url = \"http://localhost:63557\"\n",
100 | "\n",
101 | "# Deployment URL\n",
102 | "cloud_deployment_url = \"https://task-maistro-1b681add7a2b549499bb0cd21a7e5be4.default.us.langgraph.app\"\n",
103 | "\n",
104 | "# Graph name\n",
105 | "graph_name = \"task_maistro\" \n",
106 | "\n",
107 | "# Connect to the deployment\n",
108 | "remote_graph = RemoteGraph(graph_name, url=local_deployment_url)"
109 | ]
110 | },
111 | {
112 | "cell_type": "markdown",
113 | "id": "1770e138",
114 | "metadata": {},
115 | "source": [
116 | "You can test your deployment by running the following. "
117 | ]
118 | },
119 | {
120 | "cell_type": "code",
121 | "execution_count": 5,
122 | "id": "62c16a2c",
123 | "metadata": {},
124 | "outputs": [
125 | {
126 | "name": "stdout",
127 | "output_type": "stream",
128 | "text": [
129 | "================================\u001b[1m Human Message \u001b[0m=================================\n",
130 | "\n",
131 | "Hi I'm Lance. I live in San Francisco with my wife and have a 1 year old.\n",
132 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
133 | "Tool Calls:\n",
134 | " UpdateMemory (call_ubXYV29olc4XYbbrD1khqtNU)\n",
135 | " Call ID: call_ubXYV29olc4XYbbrD1khqtNU\n",
136 | " Args:\n",
137 | " update_type: user\n",
138 | "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
139 | "\n",
140 | "updated profile\n",
141 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
142 | "\n",
143 | "Hi Lance! It's great to meet you. If there's anything specific you'd like help with, feel free to let me know!\n"
144 | ]
145 | }
146 | ],
147 | "source": [
148 | "# Int\n",
149 | "user_input = \"Hi I'm Lance. I live in San Francisco with my wife and have a 1 year old.\"\n",
150 | "config = {\"configurable\": {\"user_id\": \"Test-Deployment-User\"}}\n",
151 | "for chunk in remote_graph.stream({\"messages\": [HumanMessage(content=user_input)]}, stream_mode=\"values\", config=config):\n",
152 | " convert_to_messages(chunk[\"messages\"])[-1].pretty_print()"
153 | ]
154 | },
155 | {
156 | "cell_type": "markdown",
157 | "id": "1169896f",
158 | "metadata": {},
159 | "source": [
160 | "### Add audio\n",
161 | "\n",
162 | "Our deployed graph has some benefits: \n",
163 | "* It has built-in support for long-term memory \n",
164 | "* It implements all the logic for task mAIstro \n",
165 | "\n",
166 | "But, we have a challenge:\n",
167 | "* It takes test as input and returns text as output\n",
168 | "\n",
169 | "We need to add audio input and output to the graph. So, we'll simply add two nodes to our graph:\n",
170 | "\n",
171 | "1. **Audio Input Node**\n",
172 | " * Records microphone input (stop with Enter)\n",
173 | " * Transcribes speech using Whisper\n",
174 | " * Passes text to Task mAIstro\n",
175 | "\n",
176 | "2. **Audio Output Node**\n",
177 | " * Takes Task mAIstro's text response\n",
178 | " * Converts to speech via ElevenLabs\n",
179 | " * Plays audio response\n",
180 | "\n",
181 | "We can achieve this by embedding our deployed graph [as a node](https://langchain-ai.github.io/langgraph/how-tos/use-remote-graph/#using-as-a-subgraph) in a new graph. "
182 | ]
183 | },
184 | {
185 | "cell_type": "code",
186 | "execution_count": 3,
187 | "id": "ea79d376-af11-4a85-af8e-9da2f8f1da2d",
188 | "metadata": {},
189 | "outputs": [
190 | {
191 | "data": {
192 | "image/jpeg": "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7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7HurY/Xf3jQrzup5z4TNDXBkc32PdWx+u/vHN9j3Vsfrv7xoV53U858JmhrgyOb7HurY/Xf3jm+x7q2P13940K87qec+EzQ1wZHN9j3Vsfrv7xzfY91bH67+8aFed1POfCZoa4Mjm+x7q2P13945vse6tj9d/eNCvO6nnPhM0NcGRzfY91bH67+8c32PdWx+u/vGhXndTznwmaGuDI5vse6tj9d/eOb7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",
193 | "text/plain": [
194 | ""
195 | ]
196 | },
197 | "metadata": {},
198 | "output_type": "display_data"
199 | }
200 | ],
201 | "source": [
202 | "import io\n",
203 | "import threading\n",
204 | "import numpy as np\n",
205 | "import sounddevice as sd\n",
206 | "from scipy.io.wavfile import write\n",
207 | "from IPython.display import Image, display\n",
208 | "\n",
209 | "from openai import OpenAI\n",
210 | "\n",
211 | "from elevenlabs import play, VoiceSettings\n",
212 | "from elevenlabs.client import ElevenLabs\n",
213 | "\n",
214 | "from langgraph.graph import StateGraph, MessagesState, END, START\n",
215 | "\n",
216 | "# Initialize OpenAI client\n",
217 | "openai_client = OpenAI()\n",
218 | "\n",
219 | "# Initialize ElevenLabs client\n",
220 | "elevenlabs_client = ElevenLabs(api_key=os.getenv(\"ELEVENLABS_API_KEY\"))\n",
221 | "\n",
222 | "def record_audio_until_stop(state: MessagesState):\n",
223 | "\n",
224 | " \"\"\"Records audio from the microphone until Enter is pressed, then saves it to a .wav file.\"\"\"\n",
225 | " \n",
226 | " audio_data = [] # List to store audio chunks\n",
227 | " recording = True # Flag to control recording\n",
228 | " sample_rate = 16000 # (kHz) Adequate for human voice frequency\n",
229 | "\n",
230 | " def record_audio():\n",
231 | " \"\"\"Continuously records audio until the recording flag is set to False.\"\"\"\n",
232 | " nonlocal audio_data, recording\n",
233 | " with sd.InputStream(samplerate=sample_rate, channels=1, dtype='int16') as stream:\n",
234 | " print(\"Recording your instruction! ... Press Enter to stop recording.\")\n",
235 | " while recording:\n",
236 | " audio_chunk, _ = stream.read(1024) # Read audio data in chunks\n",
237 | " audio_data.append(audio_chunk)\n",
238 | "\n",
239 | " def stop_recording():\n",
240 | " \"\"\"Waits for user input to stop the recording.\"\"\"\n",
241 | " input() # Wait for Enter key press\n",
242 | " nonlocal recording\n",
243 | " recording = False\n",
244 | "\n",
245 | " # Start recording in a separate thread\n",
246 | " recording_thread = threading.Thread(target=record_audio)\n",
247 | " recording_thread.start()\n",
248 | "\n",
249 | " # Start a thread to listen for the Enter key\n",
250 | " stop_thread = threading.Thread(target=stop_recording)\n",
251 | " stop_thread.start()\n",
252 | "\n",
253 | " # Wait for both threads to complete\n",
254 | " stop_thread.join()\n",
255 | " recording_thread.join()\n",
256 | "\n",
257 | " # Stack all audio chunks into a single NumPy array and write to file\n",
258 | " audio_data = np.concatenate(audio_data, axis=0)\n",
259 | " \n",
260 | " # Convert to WAV format in-memory\n",
261 | " audio_bytes = io.BytesIO()\n",
262 | " write(audio_bytes, sample_rate, audio_data) # Use scipy's write function to save to BytesIO\n",
263 | " audio_bytes.seek(0) # Go to the start of the BytesIO buffer\n",
264 | " audio_bytes.name = \"audio.wav\" # Set a filename for the in-memory file\n",
265 | "\n",
266 | " # Transcribe via Whisper\n",
267 | " transcription = openai_client.audio.transcriptions.create(\n",
268 | " model=\"whisper-1\", \n",
269 | " file=audio_bytes,\n",
270 | " )\n",
271 | "\n",
272 | " # Print the transcription\n",
273 | " print(\"Here is the transcription:\", transcription.text)\n",
274 | "\n",
275 | " # Write to messages \n",
276 | " return {\"messages\": [HumanMessage(content=transcription.text)]}\n",
277 | "\n",
278 | "def play_audio(state: MessagesState):\n",
279 | " \n",
280 | " \"\"\"Plays the audio response from the remote graph with ElevenLabs.\"\"\"\n",
281 | "\n",
282 | " # Response from the agent \n",
283 | " response = state['messages'][-1]\n",
284 | "\n",
285 | " # Prepare text by replacing ** with empty strings\n",
286 | " # These can cause unexpected behavior in ElevenLabs\n",
287 | " cleaned_text = response.content.replace(\"**\", \"\")\n",
288 | " \n",
289 | " # Call text_to_speech API with turbo model for low latency\n",
290 | " response = elevenlabs_client.text_to_speech.convert(\n",
291 | " voice_id=\"pNInz6obpgDQGcFmaJgB\", # Adam pre-made voice\n",
292 | " output_format=\"mp3_22050_32\",\n",
293 | " text=cleaned_text,\n",
294 | " model_id=\"eleven_turbo_v2_5\", \n",
295 | " voice_settings=VoiceSettings(\n",
296 | " stability=0.0,\n",
297 | " similarity_boost=1.0,\n",
298 | " style=0.0,\n",
299 | " use_speaker_boost=True,\n",
300 | " ),\n",
301 | " )\n",
302 | " \n",
303 | " # Play the audio back\n",
304 | " play(response)\n",
305 | "\n",
306 | "# Define parent graph\n",
307 | "builder = StateGraph(MessagesState)\n",
308 | "\n",
309 | "# Add remote graph directly as a node\n",
310 | "builder.add_node(\"audio_input\", record_audio_until_stop)\n",
311 | "builder.add_node(\"todo_app\", remote_graph)\n",
312 | "builder.add_node(\"audio_output\", play_audio)\n",
313 | "builder.add_edge(START, \"audio_input\")\n",
314 | "builder.add_edge(\"audio_input\", \"todo_app\")\n",
315 | "builder.add_edge(\"todo_app\",\"audio_output\")\n",
316 | "builder.add_edge(\"audio_output\",END)\n",
317 | "graph = builder.compile()\n",
318 | "\n",
319 | "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))"
320 | ]
321 | },
322 | {
323 | "cell_type": "markdown",
324 | "id": "d5b16223",
325 | "metadata": {},
326 | "source": [
327 | "Optionally, you can supply a thread ID to ensure that conversation history is persisted. "
328 | ]
329 | },
330 | {
331 | "cell_type": "code",
332 | "execution_count": 6,
333 | "id": "1b7277b5",
334 | "metadata": {},
335 | "outputs": [],
336 | "source": [
337 | "import uuid\n",
338 | "thread_id = str(uuid.uuid4())"
339 | ]
340 | },
341 | {
342 | "cell_type": "markdown",
343 | "id": "297defe6",
344 | "metadata": {},
345 | "source": [
346 | "Simply, run this cell and speak into your microphone. When you are finished, press Enter."
347 | ]
348 | },
349 | {
350 | "cell_type": "code",
351 | "execution_count": 9,
352 | "id": "a18659da-1c49-48c1-8838-554f85b7876d",
353 | "metadata": {},
354 | "outputs": [
355 | {
356 | "name": "stdout",
357 | "output_type": "stream",
358 | "text": [
359 | "================================\u001b[1m Human Message \u001b[0m=================================\n",
360 | "\n",
361 | "Follow the user's instructions:\n",
362 | "Recording your instruction! ... Press Enter to stop recording.\n"
363 | ]
364 | },
365 | {
366 | "name": "stdin",
367 | "output_type": "stream",
368 | "text": [
369 | " \n"
370 | ]
371 | },
372 | {
373 | "name": "stdout",
374 | "output_type": "stream",
375 | "text": [
376 | "Here is the transcription: Add a to-do to walk the dog tomorrow night and make sure I get it done by end of day tomorrow.\n",
377 | "================================\u001b[1m Human Message \u001b[0m=================================\n",
378 | "\n",
379 | "Add a to-do to walk the dog tomorrow night and make sure I get it done by end of day tomorrow.\n",
380 | "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
381 | "\n",
382 | "Awesome, Lance! I've added \"Walk the dog\" to your to-do list for tomorrow night, and it's set to be completed by the end of the day. 🐶✨ You're going to rock this! Remember to set a reminder on your phone, prepare the leash and dog bags in advance, and maybe plan a fun route in your neighborhood. You've got this! 🌟😊\n"
383 | ]
384 | }
385 | ],
386 | "source": [
387 | "# Set user ID for storing memories\n",
388 | "config = {\"configurable\": {\"user_id\": \"Test-Audio-UX\", \"thread_id\": thread_id}}\n",
389 | "\n",
390 | "# Kick off the graph, which will record user input until the user presses Enter\n",
391 | "for chunk in graph.stream({\"messages\":HumanMessage(content=\"Follow the user's instructions:\")}, stream_mode=\"values\", config=config):\n",
392 | " chunk[\"messages\"][-1].pretty_print()"
393 | ]
394 | },
395 | {
396 | "cell_type": "code",
397 | "execution_count": null,
398 | "id": "2e5532fe",
399 | "metadata": {},
400 | "outputs": [],
401 | "source": []
402 | }
403 | ],
404 | "metadata": {
405 | "kernelspec": {
406 | "display_name": "Python 3 (ipykernel)",
407 | "language": "python",
408 | "name": "python3"
409 | },
410 | "language_info": {
411 | "codemirror_mode": {
412 | "name": "ipython",
413 | "version": 3
414 | },
415 | "file_extension": ".py",
416 | "mimetype": "text/x-python",
417 | "name": "python",
418 | "nbconvert_exporter": "python",
419 | "pygments_lexer": "ipython3",
420 | "version": "3.11.6"
421 | }
422 | },
423 | "nbformat": 4,
424 | "nbformat_minor": 5
425 | }
426 |
--------------------------------------------------------------------------------