From 5a0238e30e78ea09f2ed61b9525ff4c0f899284c Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Tue, 2 Dec 2025 15:28:44 -0800 Subject: [PATCH 01/11] feat: add personal assistant quickstart Add a new personal assistant quickstart that demonstrates email and calendar management capabilities. Includes: - Email assistant agent with human-in-the-loop support - Gmail integration tools with setup scripts - Calendar management tools - Configurable tool implementations (default and Gmail) - Test notebook and deployment configuration --- personal_assistant/README.md | 364 ++ personal_assistant/agent.py | 14 + personal_assistant/langgraph.json | 11 + personal_assistant/pyproject.toml | 34 + .../src/personal_assistant/__init__.py | 7 + .../src/personal_assistant/configuration.py | 27 + .../email_assistant_deepagents.py | 298 ++ .../email_assistant_hitl_memory.py | 501 +++ .../personal_assistant/middleware/__init__.py | 5 + .../middleware/email_assistant_hitl.py | 779 +++++ .../src/personal_assistant/ntbk_utils.py | 94 + .../src/personal_assistant/prompts.py | 283 ++ .../src/personal_assistant/schemas.py | 43 + .../src/personal_assistant/tools/__init__.py | 13 + .../src/personal_assistant/tools/base.py | 57 + .../tools/default/__init__.py | 22 + .../tools/default/calendar_tools.py | 17 + .../tools/default/email_tools.py | 24 + .../tools/default/prompt_templates.py | 37 + .../personal_assistant/tools/gmail/README.md | 280 ++ .../tools/gmail/__init__.py | 18 + .../tools/gmail/gmail_tools.py | 946 +++++ .../tools/gmail/prompt_templates.py | 23 + .../tools/gmail/run_ingest.py | 341 ++ .../tools/gmail/setup_cron.py | 109 + .../tools/gmail/setup_gmail.py | 87 + .../src/personal_assistant/utils.py | 367 ++ personal_assistant/test.ipynb | 1830 ++++++++++ personal_assistant/uv.lock | 3056 +++++++++++++++++ 29 files changed, 9687 insertions(+) create mode 100644 personal_assistant/README.md create mode 100644 personal_assistant/agent.py create mode 100644 personal_assistant/langgraph.json create mode 100644 personal_assistant/pyproject.toml create mode 100644 personal_assistant/src/personal_assistant/__init__.py create mode 100644 personal_assistant/src/personal_assistant/configuration.py create mode 100644 personal_assistant/src/personal_assistant/email_assistant_deepagents.py create mode 100644 personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py create mode 100644 personal_assistant/src/personal_assistant/middleware/__init__.py create mode 100644 personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py create mode 100644 personal_assistant/src/personal_assistant/ntbk_utils.py create mode 100644 personal_assistant/src/personal_assistant/prompts.py create mode 100644 personal_assistant/src/personal_assistant/schemas.py create mode 100644 personal_assistant/src/personal_assistant/tools/__init__.py create mode 100644 personal_assistant/src/personal_assistant/tools/base.py create mode 100644 personal_assistant/src/personal_assistant/tools/default/__init__.py create mode 100644 personal_assistant/src/personal_assistant/tools/default/calendar_tools.py create mode 100644 personal_assistant/src/personal_assistant/tools/default/email_tools.py create mode 100644 personal_assistant/src/personal_assistant/tools/default/prompt_templates.py create mode 100644 personal_assistant/src/personal_assistant/tools/gmail/README.md create mode 100644 personal_assistant/src/personal_assistant/tools/gmail/__init__.py create mode 100644 personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py create mode 100644 personal_assistant/src/personal_assistant/tools/gmail/prompt_templates.py create mode 100644 personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py create mode 100644 personal_assistant/src/personal_assistant/tools/gmail/setup_cron.py create mode 100644 personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py create mode 100644 personal_assistant/src/personal_assistant/utils.py create mode 100644 personal_assistant/test.ipynb create mode 100644 personal_assistant/uv.lock diff --git a/personal_assistant/README.md b/personal_assistant/README.md new file mode 100644 index 0000000..0be401a --- /dev/null +++ b/personal_assistant/README.md @@ -0,0 +1,364 @@ +# Personal Assistant + +Email assistant using deepagents library with two-tiered architecture, human-in-the-loop (HITL) interrupts, and persistent memory system. + +## Quickstart + +### Python Version + +* Ensure you're using Python 3.11 or later +* Required for optimal compatibility with LangGraph + +```shell +python3 --version +``` + +### API Keys + +* Sign up for Anthropic API key [here](https://console.anthropic.com/) +* Sign up for LangSmith [here](https://smith.langchain.com/) + +### Set Environment Variables + +Create a `.env` file in the root directory: + +```shell +ANTHROPIC_API_KEY=your_anthropic_api_key +LANGSMITH_API_KEY=your_langsmith_api_key +LANGSMITH_TRACING=true +LANGSMITH_PROJECT=personal-assistant +``` + +Or set them in your terminal: + +```shell +export ANTHROPIC_API_KEY=your_anthropic_api_key +export LANGSMITH_API_KEY=your_langsmith_api_key +export LANGSMITH_TRACING=true +``` + +### Package Installation + +**Using uv (recommended):** + +```shell +# Install uv if you haven't already +pip install uv + +# Install the package +uv sync + +# Activate the virtual environment +source .venv/bin/activate +``` + +### Local Deployment + +Start LangGraph Studio for interactive testing: + +```bash +langgraph dev +``` + +Then open [http://localhost:2024](http://localhost:2024) in your browser. + +### Using Agent Inbox + +[Agent Inbox](https://github.com/langchain-ai/agent-inbox) provides a user-friendly web interface for managing human-in-the-loop interactions. Instead of programmatically handling interrupts, you can use Agent Inbox's UI to review and respond to agent actions. + +**Quick Setup:** + +1. Start your local LangGraph deployment: + ```bash + langgraph dev + ``` + +2. Open Agent Inbox in your browser: + ``` + https://dev.agentinbox.ai/ + ``` + +3. Connect to your local graph: + - Click "Settings" (gear icon) in Agent Inbox + - Add your LangSmith API key + - Create a new inbox connection: + - **Graph ID**: `personal_assistant` (from `langgraph.json`) + - **Deployment URL**: `http://localhost:2024` (your local dev server) + +4. Process emails through the inbox: + - Send an email input to your graph via LangGraph Studio or API + - When the agent needs approval, the interrupt appears in Agent Inbox + - Review the email context and proposed action + - Choose your response: + - **Accept** - Execute the action as-is + - **Edit** - Modify the arguments and execute + - **Ignore** - Skip the action and end workflow + - **Response** - Provide feedback for the agent to incorporate + +**What You'll See:** + +When the email assistant interrupts (for `write_email`, `schedule_meeting`, or `Question`), Agent Inbox displays: +- Original email context (subject, sender, content) +- Proposed action (email draft or meeting invite) +- Action details (formatted for easy review) +- Available response options based on the tool + +**Memory Learning:** + +Agent Inbox responses automatically update the assistant's memory: +- **Edit responses** → Updates response/calendar preferences +- **Ignore responses** → Updates triage preferences +- **Feedback responses** → Incorporates into next action + +This allows the assistant to learn your preferences over time and improve future suggestions. + +## Architecture Overview + +### Two-Tiered Design + +The assistant uses a two-tiered architecture to efficiently handle emails: + +``` +Email Input → Triage Router → [Respond / Notify / Ignore] + ↓ + Response Agent (with HITL) +``` + +**Tier 1: Triage Router** +- **Purpose**: Classify emails to avoid wasting time on irrelevant messages +- **Classifications**: + - `respond` - Email requires a response → Routes to Response Agent + - `notify` - Important FYI email → Creates interrupt for user review + - `ignore` - Spam, marketing, or irrelevant → Ends workflow +- **Memory**: Learns from user corrections via `triage_preferences` namespace +- **Location**: `src/personal_assistant/email_assistant_deepagents.py:29-156` + +**Tier 2: Response Agent** +- **Purpose**: Generate email drafts and schedule meetings with HITL approval +- **Built with**: `create_deep_agent()` from deepagents library +- **Custom Middleware**: `EmailAssistantHITLMiddleware` for intelligent interrupts +- **Tools**: `write_email`, `schedule_meeting`, `check_calendar_availability`, `Question`, `Done` +- **Location**: `src/personal_assistant/email_assistant_deepagents.py:227-255` + +### HITL Middleware System + +Custom middleware that intercepts specific tool calls for human approval: + +**Interrupt Filtering**: +- Only interrupts for: `write_email`, `schedule_meeting`, `Question` +- Other tools execute directly (e.g., `check_calendar_availability`) + +**Four Response Types**: +1. **Accept** - Execute tool with original arguments +2. **Edit** - Execute with modified arguments, update AI message immutably +3. **Ignore** - Skip execution, end workflow, update triage memory +4. **Response** - Provide feedback, continue workflow with updated context + +**Memory Integration**: +- Injects learned preferences into system prompt before each LLM call +- Updates memory after user edits or feedback +- Uses runtime store in deployment, local store in testing + +**Location**: `src/personal_assistant/middleware/email_assistant_hitl.py` + +### Memory System + +Three persistent memory namespaces that learn from user behavior: + +1. **`triage_preferences`** - Email classification rules + - Updated when: User corrects triage decisions (respond vs. notify vs. ignore) + - Example: "Emails from Alice about API docs should be responded to" + +2. **`response_preferences`** - Email writing style + - Updated when: User edits email drafts or provides feedback + - Example: "Keep responses concise, avoid formalities" + +3. **`cal_preferences`** - Meeting scheduling preferences + - Updated when: User edits meeting invitations or provides feedback + - Example: "Prefer 30-minute meetings, avoid Fridays" + +**Storage**: +- Local testing: `InMemoryStore()` (ephemeral) +- Deployment: LangGraph platform store (persistent across sessions) +- Backend: `StoreBackend` for deepagents integration + +### Deployment Modes + +**Local Testing Mode** (`for_deployment=False`): +- Creates `InMemoryStore()` and `MemorySaver()` +- Passes store/checkpointer to both top-level workflow and response agent +- Use for: Notebook testing, standalone Python scripts + +**Deployment Mode** (`for_deployment=True`): +- Does NOT pass store/checkpointer to graph compilation +- LangGraph platform provides persistence infrastructure +- Use for: `langgraph dev`, LangGraph Cloud, production deployments + +## Code Structure + +``` +examples/personal_assistant/ +├── agent.py # Deployment entry point (used by langgraph.json) +├── langgraph.json # LangGraph deployment config +├── pyproject.toml # Package dependencies and metadata +├── test.ipynb # Interactive testing notebook +├── README.md # This file +│ +└── src/personal_assistant/ + ├── __init__.py # Package exports + ├── email_assistant_deepagents.py # Main: Two-tiered workflow + response agent + ├── email_assistant_hitl_memory.py # Legacy: Original implementation (reference) + ├── schemas.py # State schemas (State, EmailAssistantState, etc.) + ├── prompts.py # System prompts and memory instructions + ├── configuration.py # Config loading (minimal) + ├── utils.py # Utilities (memory, formatting, parsing) + ├── ntbk_utils.py # Notebook utilities (rich formatting) + │ + ├── middleware/ + │ ├── __init__.py + │ └── email_assistant_hitl.py # Custom HITL middleware with memory + │ + └── tools/ + ├── __init__.py + ├── base.py # Tool loading and registry + │ + ├── default/ # Default tools (no external APIs) + │ ├── __init__.py + │ ├── email_tools.py # write_email, Question, Done + │ ├── calendar_tools.py # schedule_meeting, check_calendar_availability + │ └── prompt_templates.py # Tool prompt templates + │ + └── gmail/ # Gmail integration (optional) + ├── __init__.py + ├── gmail_tools.py # Gmail API tools + └── gmail_utils.py # Gmail utilities +``` + +## Test Email Examples + +### Example 1: Direct Question (Will be RESPONDED to) + +This email will trigger the `respond` classification because it's addressed to Lance with a direct technical question: + +```json +{ + "author": "Sarah Chen ", + "to": "Lance Martin ", + "subject": "Question about LangGraph deployment", + "email_thread": "Hi Lance,\n\nI'm working on deploying a LangGraph agent to production and ran into an issue with the store configuration. When I try to use a custom store, I get an error about the platform providing its own persistence.\n\nCould you clarify when we should pass a store vs. letting the platform handle it?\n\nAlso, do you have any examples of deploying agents with custom HITL middleware?\n\nThanks!\nSarah" +} +``` + +**Why this will be responded to:** +- ✅ Addressed TO Lance (not CC'd) +- ✅ Contains direct questions requiring response +- ✅ Technical question about LangGraph (Lance's area) + +### Example 2: Meeting Request (Will be RESPONDED to) + +```json +{ + "author": "Alex Rodriguez ", + "to": "Lance Martin ", + "subject": "Sync on agent architecture", + "email_thread": "Hey Lance,\n\nCan we schedule 30 minutes next week to discuss the multi-agent architecture for our project? I have some questions about routing between agents and want to get your input.\n\nI'm free Tuesday afternoon or Thursday morning. Let me know what works!\n\nBest,\nAlex" +} +``` + +**Why this will be responded to:** +- ✅ Direct meeting request to Lance +- ✅ Requires scheduling action +- ✅ Will trigger `schedule_meeting` tool with HITL + +### Example 3: FYI Email (Will be IGNORED) + +```json +{ + "author": "Newsletter ", + "to": "Lance Martin ", + "subject": "Weekly AI Newsletter - Top Articles", + "email_thread": "This week's top articles:\n\n1. New advances in RAG systems\n2. Latest LLM benchmarks\n3. Multi-agent coordination patterns\n\n[Read more...]" +} +``` + +**Why this will be ignored:** +- ❌ Newsletter/marketing content +- ❌ No action required +- ❌ No direct questions + +## Usage + +### Python API + +```python +from personal_assistant import create_email_assistant + +# Create agent (local testing mode) +agent = create_email_assistant(for_deployment=False) + +# Example email +email_input = { + "author": "sarah.chen@techcorp.com", + "to": "lance@langchain.dev", + "subject": "Question about LangGraph deployment", + "email_thread": "Hi Lance,\n\nI'm working on deploying a LangGraph agent..." +} + +# Process email +config = {"configurable": {"thread_id": "thread-1"}} +result = agent.invoke({"email_input": email_input}, config=config) + +# Check for interrupts +if "__interrupt__" in result: + print("Agent is waiting for approval") + # Resume with decision + from langgraph.types import Command + result = agent.invoke( + Command(resume={"decisions": [{"type": "accept"}]}), + config=config + ) +``` + +### Notebook Testing + +See `test.ipynb` for interactive examples with rich formatting: +- Process emails step-by-step +- Visualize agent reasoning +- Test all HITL response types (accept, edit, ignore, response) +- View memory updates in real-time + +### Deployment + +The agent is configured for LangGraph deployment via `agent.py` and `langgraph.json`. + +**Key difference**: In deployment mode, the agent doesn't create its own store/checkpointer: + +```python +# agent.py uses deployment mode +graph = create_email_assistant(for_deployment=True) +``` + +This allows the LangGraph platform to provide persistent storage, checkpointing, and the LangGraph Studio UI. + +## Features + +- **Two-Tiered Architecture**: Triage router + response agent for efficient email handling +- **Custom HITL Middleware**: Sophisticated interrupt handling with tool filtering +- **Persistent Memory**: Learns from user feedback across 3 namespaces +- **Four Response Types**: Accept, edit, ignore, or provide feedback on agent actions +- **Async Support**: Works with both sync and async invocation (invoke/ainvoke, stream/astream) +- **Deployment Ready**: Optimized for LangGraph Cloud with platform-provided persistence + +## Testing + +Run the notebook for interactive testing: + +```bash +jupyter notebook test.ipynb +``` + +Or run the standalone script: + +```bash +python -m personal_assistant.email_assistant_deepagents +``` diff --git a/personal_assistant/agent.py b/personal_assistant/agent.py new file mode 100644 index 0000000..76888b5 --- /dev/null +++ b/personal_assistant/agent.py @@ -0,0 +1,14 @@ +"""Agent entry point for LangGraph deployment. + +This file is referenced by langgraph.json and provides the graph for deployment. +It uses absolute imports to avoid relative import issues when loaded by LangGraph. + +The for_deployment=True flag ensures we don't pass store/checkpointer to the graph, +allowing LangGraph platform to provide its own persistence infrastructure. +""" + +from personal_assistant import create_email_assistant + +# Export the graph for deployment +# Use for_deployment=True to let LangGraph platform provide store/checkpointer +graph = create_email_assistant(for_deployment=True) diff --git a/personal_assistant/langgraph.json b/personal_assistant/langgraph.json new file mode 100644 index 0000000..4878d27 --- /dev/null +++ b/personal_assistant/langgraph.json @@ -0,0 +1,11 @@ +{ + "dockerfile_lines": [], + "graphs": { + "personal_assistant": "./agent.py:graph" + }, + "python_version": "3.11", + "env": ".env", + "dependencies": [ + "." + ] + } \ No newline at end of file diff --git a/personal_assistant/pyproject.toml b/personal_assistant/pyproject.toml new file mode 100644 index 0000000..9d9ae4a --- /dev/null +++ b/personal_assistant/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "personal-assistant" +version = "0.1.0" +description = "Email assistant with HITL and memory using deepagents" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "deepagents", + "langchain-anthropic>=1.0.3", + "langgraph-cli[inmem]>=0.1.55", + "langgraph>=1.0.4", + "html2text>=2020.1.16", + "langchain>=1.1.0", + "rich>=10.0.0", +] + +[project.optional-dependencies] +dev = [ + "jupyter>=1.0.0", + "ipython>=8.0.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/personal_assistant"] + +[tool.uv] +dev-dependencies = [ + "jupyter>=1.0.0", + "ipython>=8.0.0", +] diff --git a/personal_assistant/src/personal_assistant/__init__.py b/personal_assistant/src/personal_assistant/__init__.py new file mode 100644 index 0000000..972a90f --- /dev/null +++ b/personal_assistant/src/personal_assistant/__init__.py @@ -0,0 +1,7 @@ +"""Personal assistant package with HITL and memory using deepagents.""" + +from .email_assistant_deepagents import create_email_assistant + +__version__ = "0.1.0" + +__all__ = ["create_email_assistant"] diff --git a/personal_assistant/src/personal_assistant/configuration.py b/personal_assistant/src/personal_assistant/configuration.py new file mode 100644 index 0000000..6a617bd --- /dev/null +++ b/personal_assistant/src/personal_assistant/configuration.py @@ -0,0 +1,27 @@ +"""Define the configurable parameters for the agent.""" + +import os +from dataclasses import dataclass, fields +from typing import Any, Optional + +from langchain_core.runnables import RunnableConfig + +@dataclass(kw_only=True) +class Configuration: + """Placeholder for configuration.""" + + @classmethod + def from_runnable_config( + cls, config: Optional[RunnableConfig] = None + ) -> "Configuration": + """Create a Configuration instance from a RunnableConfig.""" + configurable = ( + config["configurable"] if config and "configurable" in config else {} + ) + values: dict[str, Any] = { + f.name: os.environ.get(f.name.upper(), configurable.get(f.name)) + for f in fields(cls) + if f.init + } + + return cls(**{k: v for k, v in values.items() if v}) \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/email_assistant_deepagents.py b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py new file mode 100644 index 0000000..9d1d452 --- /dev/null +++ b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py @@ -0,0 +1,298 @@ +"""Email assistant using deepagents library with custom HITL middleware. + +This is the migration of email_assistant_hitl_memory.py to use the deepagents library's +create_deep_agent() pattern instead of manual graph construction. All functionality is +preserved including HITL logic, memory system, and custom tools. + +Usage: + python -m examples.personal_assistant.email_assistant_deepagents +""" + +from typing import Literal + +from langgraph.graph import StateGraph, START, END +from langchain_anthropic import ChatAnthropic +from langgraph.checkpoint.memory import MemorySaver +from langgraph.store.memory import InMemoryStore +from langgraph.store.base import BaseStore +from langgraph.types import interrupt, Command + +from deepagents import create_deep_agent +from deepagents.backends import StoreBackend + +from .middleware import EmailAssistantHITLMiddleware +from .schemas import State, StateInput, EmailAssistantState, UserPreferences, RouterSchema +from .tools import get_tools +from .utils import format_email_markdown, parse_email, get_memory, update_memory +from .prompts import triage_user_prompt, default_triage_instructions, triage_system_prompt, default_background + +def triage_router(state: State, store: BaseStore) -> Command[Literal["triage_interrupt_handler", "response_agent", "__end__"]]: + """Analyze email content to decide if we should respond, notify, or ignore. + + The triage step prevents the assistant from wasting time on: + - Marketing emails and spam + - Company-wide announcements + - Messages meant for other teams + """ + + # Parse the email input + author, to, subject, email_thread = parse_email(state["email_input"]) + user_prompt = triage_user_prompt.format( + author=author, to=to, subject=subject, email_thread=email_thread + ) + + # Create email markdown for Agent Inbox in case of notification + email_markdown = format_email_markdown(subject, author, to, email_thread) + + # Search for existing triage_preferences memory + triage_instructions = get_memory(store, ("email_assistant", "triage_preferences"), default_triage_instructions) + + # Format system prompt with background and triage instructions + system_prompt = triage_system_prompt.format( + background=default_background, + triage_instructions=triage_instructions, + ) + + llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0) + llm_router = llm.with_structured_output(RouterSchema) + + # Run the router LLM + result = llm_router.invoke( + [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + ) + + # Decision + classification = result.classification + + # Process the classification decision + if classification == "respond": + print("📧 Classification: RESPOND - This email requires a response") + # Next node + goto = "response_agent" + # Update the state + update = { + "classification_decision": result.classification, + "messages": [{"role": "user", + "content": f"Respond to the email: {email_markdown}" + }], + } + + elif classification == "ignore": + print("🚫 Classification: IGNORE - This email can be safely ignored") + + # Next node + goto = END + # Update the state + update = { + "classification_decision": classification, + } + + elif classification == "notify": + print("🔔 Classification: NOTIFY - This email contains important information") + + # Next node + goto = "triage_interrupt_handler" + # Update the state + update = { + "classification_decision": classification, + } + + else: + raise ValueError(f"Invalid classification: {classification}") + + return Command(goto=goto, update=update) + +def triage_interrupt_handler(state: State, store: BaseStore) -> Command[Literal["response_agent", "__end__"]]: + """Handles interrupts from the triage step""" + + # Parse the email input + author, to, subject, email_thread = parse_email(state["email_input"]) + + # Create email markdown for Agent Inbox in case of notification + email_markdown = format_email_markdown(subject, author, to, email_thread) + + # Create messages + messages = [{"role": "user", + "content": f"Email to notify user about: {email_markdown}" + }] + + # Create interrupt for Agent Inbox + request = { + "action_request": { + "action": f"Email Assistant: {state['classification_decision']}", + "args": {} + }, + "config": { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": False, + "allow_accept": False, + }, + # Email to show in Agent Inbox + "description": email_markdown, + } + + # Send to Agent Inbox and wait for response + response = interrupt([request])[0] + + # If user provides feedback, go to response agent and use feedback to respond to email + if response["type"] == "response": + # Add feedback to messages + user_input = response["args"] + messages.append({"role": "user", + "content": f"User wants to reply to the email. Use this feedback to respond: {user_input}" + }) + # Update memory with feedback + update_memory(store, ("email_assistant", "triage_preferences"), [{ + "role": "user", + "content": f"The user decided to respond to the email, so update the triage preferences to capture this." + }] + messages) + + goto = "response_agent" + + # If user ignores email, go to END + elif response["type"] == "ignore": + # Make note of the user's decision to ignore the email + messages.append({"role": "user", + "content": f"The user decided to ignore the email even though it was classified as notify. Update triage preferences to capture this." + }) + # Update memory with feedback + update_memory(store, ("email_assistant", "triage_preferences"), messages) + goto = END + + # Catch all other responses + else: + raise ValueError(f"Invalid response: {response}") + + # Update the state + update = { + "messages": messages, + } + + return Command(goto=goto, update=update) + +def create_email_assistant(for_deployment=False): + """Create and configure the email assistant agent. + + Args: + for_deployment: If True, don't pass store/checkpointer (for LangGraph deployment). + If False, create InMemoryStore and MemorySaver for local testing. + + Returns: + CompiledStateGraph: Configured email assistant agent + """ + # Initialize model + model = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0) + + # Get tools + tools = get_tools( + [ + "write_email", + "schedule_meeting", + "check_calendar_availability", + "Question", + "Done", + ] + ) + + # Initialize persistence based on deployment mode + if for_deployment: + # In deployment, LangGraph platform provides store and checkpointer + # We need to pass a store to middleware, but it will be overridden by platform + # Use a placeholder that the middleware can work with during initialization + store = InMemoryStore() # Placeholder - will be overridden by platform + store_kwarg = {} # Don't pass store to create_deep_agent + checkpointer_kwarg = {} # Don't pass checkpointer to create_deep_agent + else: + # Local testing mode - create and use our own store and checkpointer + store = InMemoryStore() + checkpointer = MemorySaver() + store_kwarg = {"store": store} + checkpointer_kwarg = {"checkpointer": checkpointer} + + # Create custom HITL middleware + hitl_middleware = EmailAssistantHITLMiddleware( + store=store, + interrupt_on={ + "write_email": True, + "schedule_meeting": True, + "Question": True, + }, + ) + + # Create agent with deepagents library + agent = create_deep_agent( + model=model, + tools=tools, + middleware=[hitl_middleware], # Custom middleware added to default stack + backend=lambda rt: StoreBackend(rt), # Persistent storage for memory + context_schema=EmailAssistantState, + **store_kwarg, + **checkpointer_kwarg, + ) + + + # Build overall workflow + overall_workflow = ( + StateGraph(State, input=StateInput) + .add_node(triage_router) + .add_node(triage_interrupt_handler) + .add_node("response_agent", agent) + .add_edge(START, "triage_router") + ) + + # Compile with store/checkpointer based on deployment mode + if for_deployment: + # In deployment, platform provides store/checkpointer + email_assistant = overall_workflow.compile() + else: + # In local testing, use our store/checkpointer + email_assistant = overall_workflow.compile(store=store, checkpointer=checkpointer) + + return email_assistant + + +def main(): + """Example usage of the email assistant.""" + # Create agent + agent = create_email_assistant() + + # Example email input + email_input = { + "author": "jane@example.com", + "to": "lance@langchain.dev", + "subject": "Quick question about next week", + "email_thread": "Hi Lance,\n\nCan we meet next Tuesday at 2pm to discuss the project roadmap?\n\nBest,\nJane", + } + + # Format email for message + author, to, subject, email_thread = parse_email(email_input) + email_markdown = format_email_markdown(subject, author, to, email_thread) + + # Configure thread + config = {"configurable": {"thread_id": "test-thread-1"}} + + # Invoke agent + print("=" * 80) + print("EMAIL ASSISTANT EXAMPLE") + print("=" * 80) + print("\nProcessing email:") + print(email_markdown) + print("=" * 80) + + # Pass email_input to top-level state (triage_router expects this) + result = agent.invoke( + {"email_input": email_input}, + config=config, + ) + + print("\nAgent result:") + print(result) + print("=" * 80) + + +if __name__ == "__main__": + main() diff --git a/personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py b/personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py new file mode 100644 index 0000000..1753ce2 --- /dev/null +++ b/personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py @@ -0,0 +1,501 @@ +from typing import Literal + +from langchain.chat_models import init_chat_model + +from langgraph.graph import StateGraph, START, END +from langgraph.store.base import BaseStore +from langgraph.types import interrupt, Command + +from email_assistant.tools import get_tools, get_tools_by_name +from email_assistant.tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT +from email_assistant.prompts import triage_system_prompt, triage_user_prompt, agent_system_prompt_hitl_memory, default_triage_instructions, default_background, default_response_preferences, default_cal_preferences, MEMORY_UPDATE_INSTRUCTIONS, MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT +from email_assistant.schemas import State, RouterSchema, StateInput, UserPreferences +from email_assistant.utils import parse_email, format_for_display, format_email_markdown +from dotenv import load_dotenv + +load_dotenv(".env") + +# Get tools +tools = get_tools(["write_email", "schedule_meeting", "check_calendar_availability", "Question", "Done"]) +tools_by_name = get_tools_by_name(tools) + +# Initialize the LLM for use with router / structured output +llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0) +llm_router = llm.with_structured_output(RouterSchema) + +# Initialize the LLM, enforcing tool use (of any available tools) for agent +llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0) +llm_with_tools = llm.bind_tools(tools, tool_choice="required") + +def get_memory(store, namespace, default_content=None): + """Get memory from the store or initialize with default if it doesn't exist. + + Args: + store: LangGraph BaseStore instance to search for existing memory + namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") + default_content: Default content to use if memory doesn't exist + + Returns: + str: The content of the memory profile, either from existing memory or the default + """ + # Search for existing memory with namespace and key + user_preferences = store.get(namespace, "user_preferences") + + # If memory exists, return its content (the value) + if user_preferences: + return user_preferences.value + + # If memory doesn't exist, add it to the store and return the default content + else: + # Namespace, key, value + store.put(namespace, "user_preferences", default_content) + user_preferences = default_content + + # Return the default content + return user_preferences + +def update_memory(store, namespace, messages): + """Update memory profile in the store. + + Args: + store: LangGraph BaseStore instance to update memory + namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") + messages: List of messages to update the memory with + """ + + # Get the existing memory + user_preferences = store.get(namespace, "user_preferences") + # Update the memory + llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0).with_structured_output(UserPreferences) + result = llm.invoke( + [ + {"role": "system", "content": MEMORY_UPDATE_INSTRUCTIONS.format(current_profile=user_preferences.value, namespace=namespace)}, + ] + messages + ) + # Save the updated memory to the store + store.put(namespace, "user_preferences", result.user_preferences) + +# Nodes +def triage_router(state: State, store: BaseStore) -> Command[Literal["triage_interrupt_handler", "response_agent", "__end__"]]: + """Analyze email content to decide if we should respond, notify, or ignore. + + The triage step prevents the assistant from wasting time on: + - Marketing emails and spam + - Company-wide announcements + - Messages meant for other teams + """ + + # Parse the email input + author, to, subject, email_thread = parse_email(state["email_input"]) + user_prompt = triage_user_prompt.format( + author=author, to=to, subject=subject, email_thread=email_thread + ) + + # Create email markdown for Agent Inbox in case of notification + email_markdown = format_email_markdown(subject, author, to, email_thread) + + # Search for existing triage_preferences memory + triage_instructions = get_memory(store, ("email_assistant", "triage_preferences"), default_triage_instructions) + + # Format system prompt with background and triage instructions + system_prompt = triage_system_prompt.format( + background=default_background, + triage_instructions=triage_instructions, + ) + + # Run the router LLM + result = llm_router.invoke( + [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + ) + + # Decision + classification = result.classification + + # Process the classification decision + if classification == "respond": + print("📧 Classification: RESPOND - This email requires a response") + # Next node + goto = "response_agent" + # Update the state + update = { + "classification_decision": result.classification, + "messages": [{"role": "user", + "content": f"Respond to the email: {email_markdown}" + }], + } + + elif classification == "ignore": + print("🚫 Classification: IGNORE - This email can be safely ignored") + + # Next node + goto = END + # Update the state + update = { + "classification_decision": classification, + } + + elif classification == "notify": + print("🔔 Classification: NOTIFY - This email contains important information") + + # Next node + goto = "triage_interrupt_handler" + # Update the state + update = { + "classification_decision": classification, + } + + else: + raise ValueError(f"Invalid classification: {classification}") + + return Command(goto=goto, update=update) + +def triage_interrupt_handler(state: State, store: BaseStore) -> Command[Literal["response_agent", "__end__"]]: + """Handles interrupts from the triage step""" + + # Parse the email input + author, to, subject, email_thread = parse_email(state["email_input"]) + + # Create email markdown for Agent Inbox in case of notification + email_markdown = format_email_markdown(subject, author, to, email_thread) + + # Create messages + messages = [{"role": "user", + "content": f"Email to notify user about: {email_markdown}" + }] + + # Create interrupt for Agent Inbox + request = { + "action_request": { + "action": f"Email Assistant: {state['classification_decision']}", + "args": {} + }, + "config": { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": False, + "allow_accept": False, + }, + # Email to show in Agent Inbox + "description": email_markdown, + } + + # Send to Agent Inbox and wait for response + response = interrupt([request])[0] + + # If user provides feedback, go to response agent and use feedback to respond to email + if response["type"] == "response": + # Add feedback to messages + user_input = response["args"] + messages.append({"role": "user", + "content": f"User wants to reply to the email. Use this feedback to respond: {user_input}" + }) + # Update memory with feedback + update_memory(store, ("email_assistant", "triage_preferences"), [{ + "role": "user", + "content": f"The user decided to respond to the email, so update the triage preferences to capture this." + }] + messages) + + goto = "response_agent" + + # If user ignores email, go to END + elif response["type"] == "ignore": + # Make note of the user's decision to ignore the email + messages.append({"role": "user", + "content": f"The user decided to ignore the email even though it was classified as notify. Update triage preferences to capture this." + }) + # Update memory with feedback + update_memory(store, ("email_assistant", "triage_preferences"), messages) + goto = END + + # Catch all other responses + else: + raise ValueError(f"Invalid response: {response}") + + # Update the state + update = { + "messages": messages, + } + + return Command(goto=goto, update=update) + +def llm_call(state: State, store: BaseStore): + """LLM decides whether to call a tool or not""" + + # Search for existing cal_preferences memory + cal_preferences = get_memory(store, ("email_assistant", "cal_preferences"), default_cal_preferences) + + # Search for existing response_preferences memory + response_preferences = get_memory(store, ("email_assistant", "response_preferences"), default_response_preferences) + + return { + "messages": [ + llm_with_tools.invoke( + [ + {"role": "system", "content": agent_system_prompt_hitl_memory.format( + tools_prompt=HITL_MEMORY_TOOLS_PROMPT, + background=default_background, + response_preferences=response_preferences, + cal_preferences=cal_preferences + )} + ] + + state["messages"] + ) + ] + } + +def interrupt_handler(state: State, store: BaseStore) -> Command[Literal["llm_call", "__end__"]]: + """Creates an interrupt for human review of tool calls""" + + # Store messages + result = [] + + # Go to the LLM call node next + goto = "llm_call" + + # Iterate over the tool calls in the last message + for tool_call in state["messages"][-1].tool_calls: + + # Allowed tools for HITL + hitl_tools = ["write_email", "schedule_meeting", "Question"] + + # If tool is not in our HITL list, execute it directly without interruption + if tool_call["name"] not in hitl_tools: + + # Execute search_memory and other tools without interruption + tool = tools_by_name[tool_call["name"]] + observation = tool.invoke(tool_call["args"]) + result.append({"role": "tool", "content": observation, "tool_call_id": tool_call["id"]}) + continue + + # Get original email from email_input in state + email_input = state["email_input"] + author, to, subject, email_thread = parse_email(email_input) + original_email_markdown = format_email_markdown(subject, author, to, email_thread) + + # Format tool call for display and prepend the original email + tool_display = format_for_display(tool_call) + description = original_email_markdown + tool_display + + # Configure what actions are allowed in Agent Inbox + if tool_call["name"] == "write_email": + config = { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": True, + "allow_accept": True, + } + elif tool_call["name"] == "schedule_meeting": + config = { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": True, + "allow_accept": True, + } + elif tool_call["name"] == "Question": + config = { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": False, + "allow_accept": False, + } + else: + raise ValueError(f"Invalid tool call: {tool_call['name']}") + + # Create the interrupt request + request = { + "action_request": { + "action": tool_call["name"], + "args": tool_call["args"] + }, + "config": config, + "description": description, + } + + # Send to Agent Inbox and wait for response + response = interrupt([request])[0] + + # Handle the responses + if response["type"] == "accept": + + # Execute the tool with original args + tool = tools_by_name[tool_call["name"]] + observation = tool.invoke(tool_call["args"]) + result.append({"role": "tool", "content": observation, "tool_call_id": tool_call["id"]}) + + elif response["type"] == "edit": + + # Tool selection + tool = tools_by_name[tool_call["name"]] + initial_tool_call = tool_call["args"] + + # Get edited args from Agent Inbox + edited_args = response["args"]["args"] + + # Update the AI message's tool call with edited content (reference to the message in the state) + ai_message = state["messages"][-1] # Get the most recent message from the state + current_id = tool_call["id"] # Store the ID of the tool call being edited + + # Create a new list of tool calls by filtering out the one being edited and adding the updated version + # This avoids modifying the original list directly (immutable approach) + updated_tool_calls = [tc for tc in ai_message.tool_calls if tc["id"] != current_id] + [ + {"type": "tool_call", "name": tool_call["name"], "args": edited_args, "id": current_id} + ] + + # Create a new copy of the message with updated tool calls rather than modifying the original + # This ensures state immutability and prevents side effects in other parts of the code + result.append(ai_message.model_copy(update={"tool_calls": updated_tool_calls})) + + # Save feedback in memory and update the write_email tool call with the edited content from Agent Inbox + if tool_call["name"] == "write_email": + + # Execute the tool with edited args + observation = tool.invoke(edited_args) + + # Add only the tool response message + result.append({"role": "tool", "content": observation, "tool_call_id": current_id}) + + # This is new: update the memory + update_memory(store, ("email_assistant", "response_preferences"), [{ + "role": "user", + "content": f"User edited the email response. Here is the initial email generated by the assistant: {initial_tool_call}. Here is the edited email: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." + }]) + + # Save feedback in memory and update the schedule_meeting tool call with the edited content from Agent Inbox + elif tool_call["name"] == "schedule_meeting": + + # Execute the tool with edited args + observation = tool.invoke(edited_args) + + # Add only the tool response message + result.append({"role": "tool", "content": observation, "tool_call_id": current_id}) + + # This is new: update the memory + update_memory(store, ("email_assistant", "cal_preferences"), [{ + "role": "user", + "content": f"User edited the calendar invitation. Here is the initial calendar invitation generated by the assistant: {initial_tool_call}. Here is the edited calendar invitation: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." + }]) + + # Catch all other tool calls + else: + raise ValueError(f"Invalid tool call: {tool_call['name']}") + + elif response["type"] == "ignore": + + if tool_call["name"] == "write_email": + # Don't execute the tool, and tell the agent how to proceed + result.append({"role": "tool", "content": "User ignored this email draft. Ignore this email and end the workflow.", "tool_call_id": tool_call["id"]}) + # Go to END + goto = END + # This is new: update the memory + update_memory(store, ("email_assistant", "triage_preferences"), state["messages"] + result + [{ + "role": "user", + "content": f"The user ignored the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." + }]) + + elif tool_call["name"] == "schedule_meeting": + # Don't execute the tool, and tell the agent how to proceed + result.append({"role": "tool", "content": "User ignored this calendar meeting draft. Ignore this email and end the workflow.", "tool_call_id": tool_call["id"]}) + # Go to END + goto = END + # This is new: update the memory + update_memory(store, ("email_assistant", "triage_preferences"), state["messages"] + result + [{ + "role": "user", + "content": f"The user ignored the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." + }]) + + elif tool_call["name"] == "Question": + # Don't execute the tool, and tell the agent how to proceed + result.append({"role": "tool", "content": "User ignored this question. Ignore this email and end the workflow.", "tool_call_id": tool_call["id"]}) + # Go to END + goto = END + # This is new: update the memory + update_memory(store, ("email_assistant", "triage_preferences"), state["messages"] + result + [{ + "role": "user", + "content": f"The user ignored the Question. That means they did not want to answer the question or deal with this email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." + }]) + + else: + raise ValueError(f"Invalid tool call: {tool_call['name']}") + + elif response["type"] == "response": + # User provided feedback + user_feedback = response["args"] + if tool_call["name"] == "write_email": + # Don't execute the tool, and add a message with the user feedback to incorporate into the email + result.append({"role": "tool", "content": f"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}", "tool_call_id": tool_call["id"]}) + # This is new: update the memory + update_memory(store, ("email_assistant", "response_preferences"), state["messages"] + result + [{ + "role": "user", + "content": f"User gave feedback, which we can use to update the response preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." + }]) + + elif tool_call["name"] == "schedule_meeting": + # Don't execute the tool, and add a message with the user feedback to incorporate into the email + result.append({"role": "tool", "content": f"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}", "tool_call_id": tool_call["id"]}) + # This is new: update the memory + update_memory(store, ("email_assistant", "cal_preferences"), state["messages"] + result + [{ + "role": "user", + "content": f"User gave feedback, which we can use to update the calendar preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." + }]) + + elif tool_call["name"] == "Question": + # Don't execute the tool, and add a message with the user feedback to incorporate into the email + result.append({"role": "tool", "content": f"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}", "tool_call_id": tool_call["id"]}) + + else: + raise ValueError(f"Invalid tool call: {tool_call['name']}") + + # Update the state + update = { + "messages": result, + } + + return Command(goto=goto, update=update) + +# Conditional edge function +def should_continue(state: State, store: BaseStore) -> Literal["interrupt_handler", "__end__"]: + """Route to tool handler, or end if Done tool called""" + messages = state["messages"] + last_message = messages[-1] + if last_message.tool_calls: + for tool_call in last_message.tool_calls: + if tool_call["name"] == "Done": + # TODO: Here, we could update the background memory with the email-response for follow up actions. + return END + else: + return "interrupt_handler" + +# Build workflow +agent_builder = StateGraph(State) + +# Add nodes - with store parameter +agent_builder.add_node("llm_call", llm_call) +agent_builder.add_node("interrupt_handler", interrupt_handler) + +# Add edges +agent_builder.add_edge(START, "llm_call") +agent_builder.add_conditional_edges( + "llm_call", + should_continue, + { + "interrupt_handler": "interrupt_handler", + END: END, + }, +) + +# Compile the agent +response_agent = agent_builder.compile() + +# Build overall workflow with store and checkpointer +overall_workflow = ( + StateGraph(State, input=StateInput) + .add_node(triage_router) + .add_node(triage_interrupt_handler) + .add_node("response_agent", response_agent) + .add_edge(START, "triage_router") +) + +email_assistant = overall_workflow.compile() \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/middleware/__init__.py b/personal_assistant/src/personal_assistant/middleware/__init__.py new file mode 100644 index 0000000..17fd16a --- /dev/null +++ b/personal_assistant/src/personal_assistant/middleware/__init__.py @@ -0,0 +1,5 @@ +"""Custom middleware for email assistant HITL workflow.""" + +from .email_assistant_hitl import EmailAssistantHITLMiddleware + +__all__ = ["EmailAssistantHITLMiddleware"] diff --git a/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py b/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py new file mode 100644 index 0000000..17cdcba --- /dev/null +++ b/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py @@ -0,0 +1,779 @@ +"""Custom HITL middleware for email assistant with memory updates.""" + +from typing import Callable + +from langchain.agents.middleware.types import AgentMiddleware, ModelRequest, ModelResponse +from langchain.tools import ToolRuntime +from langchain.tools.tool_node import ToolCallRequest +from langchain_core.messages import ToolMessage +from langgraph.constants import END +from langgraph.store.base import BaseStore +from langgraph.types import Command, interrupt + +from ..prompts import ( + MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT, + agent_system_prompt_hitl_memory, + default_background, + default_cal_preferences, + default_response_preferences, +) +from ..tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT +from ..utils import format_email_markdown, format_for_display, get_memory, parse_email, update_memory, aget_memory, aupdate_memory + + +class EmailAssistantHITLMiddleware(AgentMiddleware): + """Custom HITL middleware with memory updates and tool filtering. + + This middleware provides sophisticated human-in-the-loop functionality for the + email assistant, including: + - Tool filtering (only interrupts for specific tools) + - Custom display formatting with email context + - Per-tool action configurations + - Four response types: accept, edit, ignore, response + - Automatic memory updates based on user feedback + + Args: + store: LangGraph store for persistent memory + interrupt_on: Dict mapping tool names to whether they should trigger interrupts + email_input_key: State key containing email context (default: "email_input") + """ + + def __init__( + self, + store: BaseStore, + interrupt_on: dict[str, bool], + email_input_key: str = "email_input", + ): + self.store = store + self.interrupt_on = interrupt_on + self.email_input_key = email_input_key + self.tools_by_name = {} + + def _get_store(self, runtime=None): + """Get store from runtime if available, otherwise use instance store. + + In deployment, LangGraph platform provides store via runtime. + In local testing, we use the store passed during initialization. + + Args: + runtime: Optional runtime object with store attribute + + Returns: + BaseStore instance + """ + if runtime and hasattr(runtime, "store"): + return runtime.store + return self.store + + def wrap_model_call( + self, + request: ModelRequest, + handler: Callable[[ModelRequest], ModelResponse], + ) -> ModelResponse: + """Inject memory into system prompt before LLM call. + + Fetches memory from the three namespaces (triage_preferences, response_preferences, + cal_preferences) and injects them into the system prompt. + """ + # Get store (from runtime in deployment, or from self in local testing) + store = self._get_store(request.runtime if hasattr(request, "runtime") else None) + + # Fetch memory from store + triage_prefs = get_memory( + store, + ("email_assistant", "triage_preferences"), + default_response_preferences, + ) + response_prefs = get_memory( + store, + ("email_assistant", "response_preferences"), + default_response_preferences, + ) + cal_prefs = get_memory( + store, + ("email_assistant", "cal_preferences"), + default_cal_preferences, + ) + + # Format system prompt with memory + memory_prompt = agent_system_prompt_hitl_memory.format( + tools_prompt=HITL_MEMORY_TOOLS_PROMPT, + background=default_background, + response_preferences=response_prefs, + cal_preferences=cal_prefs, + ) + + # Append memory prompt to existing system prompt + new_system_prompt = ( + request.system_prompt + "\n\n" + memory_prompt + if request.system_prompt + else memory_prompt + ) + + # Update request with new system prompt + updated_request = request.override(system_prompt=new_system_prompt) + + return handler(updated_request) + + def wrap_tool_call( + self, + request: ToolCallRequest, + handler: Callable[[ToolCallRequest], ToolMessage | Command], + ) -> ToolMessage | Command: + """Intercept tool calls for HITL filtering and memory updates. + + This is the core HITL logic that: + 1. Filters tools (only interrupts for configured tools) + 2. Formats display with email context + 3. Creates interrupt with per-tool config + 4. Handles user response (accept/edit/ignore/response) + 5. Updates memory based on feedback + """ + tool_call = request.tool_call + tool_name = tool_call["name"] + tool = request.tool # Get the tool directly from the request + + # Cache tool for later use + if tool and tool_name not in self.tools_by_name: + self.tools_by_name[tool_name] = tool + + # STEP 1: Filter - Execute non-HITL tools directly without interruption + if tool_name not in self.interrupt_on or not self.interrupt_on[tool_name]: + # Use handler to ensure proper middleware chaining + return handler(request) + + # STEP 2: Format display - Get email context and format for display + email_input = request.runtime.state.get(self.email_input_key) + description = self._format_interrupt_display(email_input, tool_call) + + # STEP 3: Configure per-tool actions + config = self._get_action_config(tool_name) + + # STEP 4: Create interrupt + interrupt_request = { + "action_request": {"action": tool_name, "args": tool_call["args"]}, + "config": config, + "description": description, + } + + # Call interrupt() to create the interrupt and wait for user decision + # When creating a new interrupt: returns a list of responses [response] + # When resuming from an interrupt: returns a dict mapping interrupt IDs to decisions + result = interrupt([interrupt_request]) + print(f"DEBUG: interrupt() returned: {result}, type: {type(result)}") + + # Handle both new interrupt and resume cases + if isinstance(result, list) and len(result) > 0: + # New interrupt created, got user decision + response = result[0] + elif isinstance(result, dict): + # Check if result is the decision itself (has "type" key) or a mapping of interrupt IDs + if "type" in result: + # Result is the decision dict directly (e.g., {"type": "accept"}) + response = result + else: + # Result is a mapping of interrupt IDs to decisions + # Extract the decision (dict maps interrupt IDs to decisions) + decisions = list(result.values()) + if decisions: + response = decisions[0] + else: + # No decision found, execute tool with original args + tool = self.tools_by_name[tool_name] + observation = tool.invoke(tool_call["args"]) + return ToolMessage(content=observation, tool_call_id=tool_call["id"]) + else: + # Unexpected format, execute tool with original args + tool = self.tools_by_name[tool_name] + observation = tool.invoke(tool_call["args"]) + return ToolMessage(content=observation, tool_call_id=tool_call["id"]) + + # STEP 5: Handle response type + return self._handle_response(response, tool_call, request.runtime) + + def _format_interrupt_display(self, email_input: dict | None, tool_call: dict) -> str: + """Format interrupt display with email context and tool details. + + Args: + email_input: Email context from state + tool_call: Tool call dict with name and args + + Returns: + Formatted markdown string for interrupt display + """ + # Get original email context if available + if email_input: + author, to, subject, email_thread = parse_email(email_input) + email_markdown = format_email_markdown(subject, author, to, email_thread) + else: + email_markdown = "" + + # Format tool call for display + tool_display = format_for_display(tool_call) + + return email_markdown + tool_display + + def _get_action_config(self, tool_name: str) -> dict: + """Get per-tool action configuration. + + Args: + tool_name: Name of the tool being called + + Returns: + Config dict with allowed actions (allow_ignore, allow_respond, etc.) + """ + if tool_name == "write_email": + return { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": True, + "allow_accept": True, + } + elif tool_name == "schedule_meeting": + return { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": True, + "allow_accept": True, + } + elif tool_name == "Question": + return { + "allow_ignore": True, + "allow_respond": True, + "allow_edit": False, # Can't edit questions + "allow_accept": False, # Can't auto-accept questions + } + else: + raise ValueError(f"Invalid tool call: {tool_name}") + + def _handle_response( + self, response: dict, tool_call: dict, runtime: ToolRuntime + ) -> ToolMessage | Command: + """Route response to appropriate handler based on response type. + + Args: + response: Response dict from interrupt + tool_call: Tool call dict + runtime: Tool runtime context + + Returns: + ToolMessage or Command based on response type + """ + response_type = response["type"] + + if response_type == "accept": + return self._handle_accept(tool_call, runtime) + elif response_type == "edit": + return self._handle_edit(response, tool_call, runtime) + elif response_type == "ignore": + return self._handle_ignore(tool_call, runtime) + elif response_type == "response": + return self._handle_response_feedback(response, tool_call, runtime) + else: + raise ValueError(f"Invalid response type: {response_type}") + + def _handle_accept(self, tool_call: dict, _runtime: ToolRuntime) -> ToolMessage: + """Handle accept response - execute tool with original args. + + Args: + tool_call: Tool call dict + _runtime: Tool runtime context (unused, kept for signature compatibility) + + Returns: + ToolMessage with execution result + """ + tool = self.tools_by_name[tool_call["name"]] + observation = tool.invoke(tool_call["args"]) + return ToolMessage(content=observation, tool_call_id=tool_call["id"]) + + def _handle_edit( + self, response: dict, tool_call: dict, runtime: ToolRuntime + ) -> Command: + """Handle edit response - execute with edited args, update AI message, update memory. + + Args: + response: Response dict with edited args + tool_call: Tool call dict + runtime: Tool runtime context + + Returns: + Command with updated AI message and tool result + """ + tool = self.tools_by_name[tool_call["name"]] + tool_name = tool_call["name"] + initial_args = tool_call["args"] + edited_args = response["args"]["args"] + tool_call_id = tool_call["id"] + + # Execute tool with edited args + observation = tool.invoke(edited_args) + + # Update AI message immutably with edited tool calls + ai_message = runtime.state["messages"][-1] + updated_tool_calls = [ + tc if tc["id"] != tool_call_id + else {"type": "tool_call", "name": tool_name, "args": edited_args, "id": tool_call_id} + for tc in ai_message.tool_calls + ] + updated_ai_message = ai_message.model_copy(update={"tool_calls": updated_tool_calls}) + + # Update memory based on tool type + self._update_memory_for_edit(tool_name, initial_args, edited_args, runtime) + + # Return Command with both updated AI message and tool message + return Command( + update={ + "messages": [ + updated_ai_message, + ToolMessage(content=observation, tool_call_id=tool_call_id), + ] + } + ) + + def _handle_ignore(self, tool_call: dict, runtime: ToolRuntime) -> Command: + """Handle ignore response - skip execution, goto END, update triage memory. + + Args: + tool_call: Tool call dict + runtime: Tool runtime context + + Returns: + Command with goto END and feedback message + """ + tool_name = tool_call["name"] + tool_call_id = tool_call["id"] + + # Create feedback message + if tool_name == "write_email": + content = "User ignored this email draft. Ignore this email and end the workflow." + elif tool_name == "schedule_meeting": + content = "User ignored this calendar meeting draft. Ignore this email and end the workflow." + elif tool_name == "Question": + content = "User ignored this question. Ignore this email and end the workflow." + else: + raise ValueError(f"Invalid tool call: {tool_name}") + + # Update triage preferences to avoid future false positives + self._update_triage_preferences_ignore(tool_name, runtime) + + # Return Command with goto END + return Command( + goto=END, + update={ + "messages": [ToolMessage(content=content, tool_call_id=tool_call_id)] + }, + ) + + def _handle_response_feedback( + self, response: dict, tool_call: dict, runtime: ToolRuntime + ) -> ToolMessage: + """Handle response feedback - add feedback to messages, update memory. + + Args: + response: Response dict with user feedback + tool_call: Tool call dict + runtime: Tool runtime context + + Returns: + ToolMessage with user feedback + """ + tool_name = tool_call["name"] + tool_call_id = tool_call["id"] + user_feedback = response["args"] + + # Create feedback message based on tool type + if tool_name == "write_email": + content = f"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}" + elif tool_name == "schedule_meeting": + content = f"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}" + elif tool_name == "Question": + content = f"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}" + else: + raise ValueError(f"Invalid tool call: {tool_name}") + + # Update memory with feedback + self._update_memory_for_feedback(tool_name, user_feedback, runtime) + + return ToolMessage(content=content, tool_call_id=tool_call_id) + + def _update_memory_for_edit( + self, tool_name: str, initial_args: dict, edited_args: dict, _runtime: ToolRuntime + ): + """Update appropriate memory namespace when user edits. + + Args: + tool_name: Name of the tool + initial_args: Original tool arguments + edited_args: Edited tool arguments + _runtime: Tool runtime context (unused, kept for signature compatibility) + """ + if tool_name == "write_email": + namespace = ("email_assistant", "response_preferences") + messages = [ + { + "role": "user", + "content": f"User edited the email response. Here is the initial email generated by the assistant: {initial_args}. Here is the edited email: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + elif tool_name == "schedule_meeting": + namespace = ("email_assistant", "cal_preferences") + messages = [ + { + "role": "user", + "content": f"User edited the calendar invitation. Here is the initial calendar invitation generated by the assistant: {initial_args}. Here is the edited calendar invitation: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + else: + return # No memory update for other tools + + store = self._get_store(_runtime) + update_memory(store, namespace, messages) + + def _update_memory_for_feedback( + self, tool_name: str, user_feedback: str, runtime: ToolRuntime + ): + """Update memory with user feedback. + + Args: + tool_name: Name of the tool + user_feedback: User's feedback text + runtime: Tool runtime context + """ + if tool_name == "write_email": + namespace = ("email_assistant", "response_preferences") + elif tool_name == "schedule_meeting": + namespace = ("email_assistant", "cal_preferences") + else: + return # No memory update for Question feedback + + messages = runtime.state["messages"] + [ + { + "role": "user", + "content": f"User gave feedback: {user_feedback}. Use this to update the preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + + store = self._get_store(runtime) + update_memory(store, namespace, messages) + + def _update_triage_preferences_ignore(self, tool_name: str, runtime: ToolRuntime): + """Update triage preferences when user ignores. + + Args: + tool_name: Name of the tool + runtime: Tool runtime context + """ + namespace = ("email_assistant", "triage_preferences") + + if tool_name == "write_email": + feedback = "The user ignored the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." + elif tool_name == "schedule_meeting": + feedback = "The user ignored the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." + elif tool_name == "Question": + feedback = "The user ignored the Question. That means they did not want to answer the question or deal with this email. Update the triage preferences to ensure emails of this type are not classified as respond." + else: + return + + messages = runtime.state["messages"] + [ + { + "role": "user", + "content": f"{feedback} Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + + store = self._get_store(runtime) + update_memory(store, namespace, messages) + + # Async versions of middleware methods for async invocation (astream, ainvoke) + + async def awrap_model_call( + self, + request: ModelRequest, + handler: Callable[[ModelRequest], ModelResponse], + ) -> ModelResponse: + """Async version of wrap_model_call for async agent invocation. + + Identical logic to wrap_model_call but supports async context. + """ + # Get store (from runtime in deployment, or from self in local testing) + store = self._get_store(request.runtime if hasattr(request, "runtime") else None) + + # Fetch memory from store (using async methods) + triage_prefs = await aget_memory( + store, + ("email_assistant", "triage_preferences"), + default_response_preferences, + ) + response_prefs = await aget_memory( + store, + ("email_assistant", "response_preferences"), + default_response_preferences, + ) + cal_prefs = await aget_memory( + store, + ("email_assistant", "cal_preferences"), + default_cal_preferences, + ) + + # Format system prompt with memory + memory_prompt = agent_system_prompt_hitl_memory.format( + tools_prompt=HITL_MEMORY_TOOLS_PROMPT, + background=default_background, + response_preferences=response_prefs, + cal_preferences=cal_prefs, + ) + + # Append memory prompt to existing system prompt + new_system_prompt = ( + request.system_prompt + "\n\n" + memory_prompt + if request.system_prompt + else memory_prompt + ) + + # Update request with new system prompt + updated_request = request.override(system_prompt=new_system_prompt) + + # Call handler (may or may not be async) + return await handler(updated_request) + + async def awrap_tool_call( + self, + request: ToolCallRequest, + handler: Callable[[ToolCallRequest], ToolMessage | Command], + ) -> ToolMessage | Command: + """Async version of wrap_tool_call for async agent invocation. + + Identical logic to wrap_tool_call but supports async context and uses async store operations. + """ + tool_call = request.tool_call + tool_name = tool_call["name"] + tool = request.tool # Get the tool directly from the request + + # Cache tool for later use + if tool and tool_name not in self.tools_by_name: + self.tools_by_name[tool_name] = tool + + # STEP 1: Filter - Execute non-HITL tools directly without interruption + if tool_name not in self.interrupt_on or not self.interrupt_on[tool_name]: + # Use handler to ensure proper middleware chaining + return await handler(request) + + # STEP 2: Format display - Get email context and format for display + email_input = request.runtime.state.get(self.email_input_key) + description = self._format_interrupt_display(email_input, tool_call) + + # STEP 3: Configure per-tool actions + config = self._get_action_config(tool_name) + + # STEP 4: Create interrupt + interrupt_request = { + "action_request": {"action": tool_name, "args": tool_call["args"]}, + "config": config, + "description": description, + } + + # Call interrupt() to create the interrupt and wait for user decision + # When creating a new interrupt: returns a list of responses [response] + # When resuming from an interrupt: returns a dict mapping interrupt IDs to decisions + result = interrupt([interrupt_request]) + print(f"DEBUG: interrupt() returned: {result}, type: {type(result)}") + + # Handle both new interrupt and resume cases + if isinstance(result, list) and len(result) > 0: + # New interrupt created, got user decision + response = result[0] + elif isinstance(result, dict): + # Check if result is the decision itself (has "type" key) or a mapping of interrupt IDs + if "type" in result: + # Result is the decision dict directly (e.g., {"type": "accept"}) + response = result + else: + # Result is a mapping of interrupt IDs to decisions + # Extract the decision (dict maps interrupt IDs to decisions) + decisions = list(result.values()) + if decisions: + response = decisions[0] + else: + # No decision found, execute tool with original args + tool = self.tools_by_name[tool_name] + observation = tool.invoke(tool_call["args"]) + return ToolMessage(content=observation, tool_call_id=tool_call["id"]) + else: + # Unexpected format, execute tool with original args + tool = self.tools_by_name[tool_name] + observation = tool.invoke(tool_call["args"]) + return ToolMessage(content=observation, tool_call_id=tool_call["id"]) + + # STEP 5: Handle response type (async version with async store operations) + return await self._ahandle_response(response, tool_call, request.runtime) + + async def _ahandle_response( + self, response: dict, tool_call: dict, runtime: ToolRuntime + ) -> ToolMessage | Command: + """Async version of _handle_response for async store operations.""" + response_type = response["type"] + + if response_type == "accept": + return self._handle_accept(tool_call, runtime) + elif response_type == "edit": + return await self._ahandle_edit(response, tool_call, runtime) + elif response_type == "ignore": + return await self._ahandle_ignore(tool_call, runtime) + elif response_type == "response": + return await self._ahandle_response_feedback(response, tool_call, runtime) + else: + raise ValueError(f"Invalid response type: {response_type}") + + async def _ahandle_edit( + self, response: dict, tool_call: dict, runtime: ToolRuntime + ) -> Command: + """Async version of _handle_edit with async memory updates.""" + tool = self.tools_by_name[tool_call["name"]] + tool_name = tool_call["name"] + initial_args = tool_call["args"] + edited_args = response["args"]["args"] + tool_call_id = tool_call["id"] + + # Execute tool with edited args + observation = tool.invoke(edited_args) + + # Update AI message immutably with edited tool calls + ai_message = runtime.state["messages"][-1] + updated_tool_calls = [ + tc if tc["id"] != tool_call_id + else {"type": "tool_call", "name": tool_name, "args": edited_args, "id": tool_call_id} + for tc in ai_message.tool_calls + ] + updated_ai_message = ai_message.model_copy(update={"tool_calls": updated_tool_calls}) + + # Update memory based on tool type (async) + await self._aupdate_memory_for_edit(tool_name, initial_args, edited_args, runtime) + + # Return Command with both updated AI message and tool message + return Command( + update={ + "messages": [ + updated_ai_message, + ToolMessage(content=observation, tool_call_id=tool_call_id), + ] + } + ) + + async def _ahandle_ignore(self, tool_call: dict, runtime: ToolRuntime) -> Command: + """Async version of _handle_ignore with async memory updates.""" + tool_name = tool_call["name"] + tool_call_id = tool_call["id"] + + # Create feedback message + if tool_name == "write_email": + content = "User ignored this email draft. Ignore this email and end the workflow." + elif tool_name == "schedule_meeting": + content = "User ignored this calendar meeting draft. Ignore this email and end the workflow." + elif tool_name == "Question": + content = "User ignored this question. Ignore this email and end the workflow." + else: + raise ValueError(f"Invalid tool call: {tool_name}") + + # Update triage preferences to avoid future false positives (async) + await self._aupdate_triage_preferences_ignore(tool_name, runtime) + + # Return Command with goto END + return Command( + goto=END, + update={ + "messages": [ToolMessage(content=content, tool_call_id=tool_call_id)] + }, + ) + + async def _ahandle_response_feedback( + self, response: dict, tool_call: dict, runtime: ToolRuntime + ) -> ToolMessage: + """Async version of _handle_response_feedback with async memory updates.""" + tool_name = tool_call["name"] + tool_call_id = tool_call["id"] + user_feedback = response["args"] + + # Create feedback message based on tool type + if tool_name == "write_email": + content = f"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}" + elif tool_name == "schedule_meeting": + content = f"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}" + elif tool_name == "Question": + content = f"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}" + else: + raise ValueError(f"Invalid tool call: {tool_name}") + + # Update memory with feedback (async) + await self._aupdate_memory_for_feedback(tool_name, user_feedback, runtime) + + return ToolMessage(content=content, tool_call_id=tool_call_id) + + async def _aupdate_memory_for_edit( + self, tool_name: str, initial_args: dict, edited_args: dict, runtime: ToolRuntime + ): + """Async version of _update_memory_for_edit.""" + if tool_name == "write_email": + namespace = ("email_assistant", "response_preferences") + messages = [ + { + "role": "user", + "content": f"User edited the email response. Here is the initial email generated by the assistant: {initial_args}. Here is the edited email: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + elif tool_name == "schedule_meeting": + namespace = ("email_assistant", "cal_preferences") + messages = [ + { + "role": "user", + "content": f"User edited the calendar invitation. Here is the initial calendar invitation generated by the assistant: {initial_args}. Here is the edited calendar invitation: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + else: + return # No memory update for other tools + + store = self._get_store(runtime) + await aupdate_memory(store, namespace, messages) + + async def _aupdate_memory_for_feedback( + self, tool_name: str, user_feedback: str, runtime: ToolRuntime + ): + """Async version of _update_memory_for_feedback.""" + if tool_name == "write_email": + namespace = ("email_assistant", "response_preferences") + elif tool_name == "schedule_meeting": + namespace = ("email_assistant", "cal_preferences") + else: + return # No memory update for Question feedback + + messages = runtime.state["messages"] + [ + { + "role": "user", + "content": f"User gave feedback: {user_feedback}. Use this to update the preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + + store = self._get_store(runtime) + await aupdate_memory(store, namespace, messages) + + async def _aupdate_triage_preferences_ignore(self, tool_name: str, runtime: ToolRuntime): + """Async version of _update_triage_preferences_ignore.""" + namespace = ("email_assistant", "triage_preferences") + + if tool_name == "write_email": + feedback = "The user ignored the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." + elif tool_name == "schedule_meeting": + feedback = "The user ignored the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." + elif tool_name == "Question": + feedback = "The user ignored the Question. That means they did not want to answer the question or deal with this email. Update the triage preferences to ensure emails of this type are not classified as respond." + else: + return + + messages = runtime.state["messages"] + [ + { + "role": "user", + "content": f"{feedback} Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", + } + ] + + store = self._get_store(runtime) + await aupdate_memory(store, namespace, messages) diff --git a/personal_assistant/src/personal_assistant/ntbk_utils.py b/personal_assistant/src/personal_assistant/ntbk_utils.py new file mode 100644 index 0000000..48aecfb --- /dev/null +++ b/personal_assistant/src/personal_assistant/ntbk_utils.py @@ -0,0 +1,94 @@ +"""Utility functions for displaying messages and prompts in Jupyter notebooks.""" + +import json + +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + +console = Console() + + +def format_message_content(message): + """Convert message content to displayable string.""" + parts = [] + tool_calls_processed = False + + # Handle main content + if isinstance(message.content, str): + parts.append(message.content) + elif isinstance(message.content, list): + # Handle complex content like tool calls (Anthropic format) + for item in message.content: + if item.get("type") == "text": + parts.append(item["text"]) + elif item.get("type") == "tool_use": + parts.append(f"\n🔧 Tool Call: {item['name']}") + parts.append(f" Args: {json.dumps(item['input'], indent=2)}") + parts.append(f" ID: {item.get('id', 'N/A')}") + tool_calls_processed = True + else: + parts.append(str(message.content)) + + # Handle tool calls attached to the message (OpenAI format) - only if not already processed + if ( + not tool_calls_processed + and hasattr(message, "tool_calls") + and message.tool_calls + ): + for tool_call in message.tool_calls: + parts.append(f"\n🔧 Tool Call: {tool_call['name']}") + parts.append(f" Args: {json.dumps(tool_call['args'], indent=2)}") + parts.append(f" ID: {tool_call['id']}") + + return "\n".join(parts) + + +def format_messages(messages): + """Format and display a list of messages with Rich formatting.""" + for m in messages: + msg_type = m.__class__.__name__.replace("Message", "") + content = format_message_content(m) + + if msg_type == "Human": + console.print(Panel(content, title="🧑 Human", border_style="blue")) + elif msg_type == "Ai": + console.print(Panel(content, title="🤖 Assistant", border_style="green")) + elif msg_type == "Tool": + console.print(Panel(content, title="🔧 Tool Output", border_style="yellow")) + else: + console.print(Panel(content, title=f"📝 {msg_type}", border_style="white")) + + +def format_message(messages): + """Alias for format_messages for backward compatibility.""" + return format_messages(messages) + + +def show_prompt(prompt_text: str, title: str = "Prompt", border_style: str = "blue"): + """Display a prompt with rich formatting and XML tag highlighting. + + Args: + prompt_text: The prompt string to display + title: Title for the panel (default: "Prompt") + border_style: Border color style (default: "blue") + """ + # Create a formatted display of the prompt + formatted_text = Text(prompt_text) + formatted_text.highlight_regex(r"<[^>]+>", style="bold blue") # Highlight XML tags + formatted_text.highlight_regex( + r"##[^#\n]+", style="bold magenta" + ) # Highlight headers + formatted_text.highlight_regex( + r"###[^#\n]+", style="bold cyan" + ) # Highlight sub-headers + + # Display in a panel for better presentation + console.print( + Panel( + formatted_text, + title=f"[bold green]{title}[/bold green]", + border_style=border_style, + padding=(1, 2), + ) + ) diff --git a/personal_assistant/src/personal_assistant/prompts.py b/personal_assistant/src/personal_assistant/prompts.py new file mode 100644 index 0000000..475bc19 --- /dev/null +++ b/personal_assistant/src/personal_assistant/prompts.py @@ -0,0 +1,283 @@ +from datetime import datetime + +# Email assistant triage prompt +triage_system_prompt = """ + +< Role > +Your role is to triage incoming emails based upon instructs and background information below. + + +< Background > +{background}. + + +< Instructions > +Categorize each email into one of three categories: +1. IGNORE - Emails that are not worth responding to or tracking +2. NOTIFY - Important information that worth notification but doesn't require a response +3. RESPOND - Emails that need a direct response +Classify the below email into one of these categories. + + +< Rules > +{triage_instructions} + +""" + +# Email assistant triage user prompt +triage_user_prompt = """ +Please determine how to handle the below email thread: + +From: {author} +To: {to} +Subject: {subject} +{email_thread}""" + +# Email assistant prompt +agent_system_prompt = """ +< Role > +You are a top-notch executive assistant who cares about helping your executive perform as well as possible. + + +< Tools > +You have access to the following tools to help manage communications and schedule: +{tools_prompt} + + +< Instructions > +When handling emails, follow these steps: +1. Carefully analyze the email content and purpose +2. IMPORTANT --- always call a tool and call one tool at a time until the task is complete: +3. For responding to the email, draft a response email with the write_email tool +4. For meeting requests, use the check_calendar_availability tool to find open time slots +5. To schedule a meeting, use the schedule_meeting tool with a datetime object for the preferred_day parameter + - Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """ - use this for scheduling meetings accurately +6. If you scheduled a meeting, then draft a short response email using the write_email tool +7. After using the write_email tool, the task is complete +8. If you have sent the email, then use the Done tool to indicate that the task is complete + + +< Background > +{background} + + +< Response Preferences > +{response_preferences} + + +< Calendar Preferences > +{cal_preferences} + +""" + +# Email assistant with HITL prompt +agent_system_prompt_hitl = """ +< Role > +You are a top-notch executive assistant who cares about helping your executive perform as well as possible. + + +< Tools > +You have access to the following tools to help manage communications and schedule: +{tools_prompt} + + +< Instructions > +When handling emails, follow these steps: +1. Carefully analyze the email content and purpose +2. IMPORTANT --- always call a tool and call one tool at a time until the task is complete: +3. If the incoming email asks the user a direct question and you do not have context to answer the question, use the Question tool to ask the user for the answer +4. For responding to the email, draft a response email with the write_email tool +5. For meeting requests, use the check_calendar_availability tool to find open time slots +6. To schedule a meeting, use the schedule_meeting tool with a datetime object for the preferred_day parameter + - Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """ - use this for scheduling meetings accurately +7. If you scheduled a meeting, then draft a short response email using the write_email tool +8. After using the write_email tool, the task is complete +9. If you have sent the email, then use the Done tool to indicate that the task is complete + + +< Background > +{background} + + +< Response Preferences > +{response_preferences} + + +< Calendar Preferences > +{cal_preferences} + +""" + +# Email assistant with HITL and memory prompt +# Note: Currently, this is the same as the HITL prompt. However, memory specific tools (see https://langchain-ai.github.io/langmem/) can be added +agent_system_prompt_hitl_memory = """ +< Role > +You are a top-notch executive assistant. + + +< Tools > +You have access to the following tools to help manage communications and schedule: +{tools_prompt} + + +< Instructions > +When handling emails, follow these steps: +1. Carefully analyze the email content and purpose +2. IMPORTANT --- always call a tool and call one tool at a time until the task is complete: +3. If the incoming email asks the user a direct question and you do not have context to answer the question, use the Question tool to ask the user for the answer +4. For responding to the email, draft a response email with the write_email tool +5. For meeting requests, use the check_calendar_availability tool to find open time slots +6. To schedule a meeting, use the schedule_meeting tool with a datetime object for the preferred_day parameter + - Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """ - use this for scheduling meetings accurately +7. If you scheduled a meeting, then draft a short response email using the write_email tool +8. After using the write_email tool, the task is complete +9. If you have sent the email, then use the Done tool to indicate that the task is complete + + +< Background > +{background} + + +< Response Preferences > +{response_preferences} + + +< Calendar Preferences > +{cal_preferences} + +""" + +# Default background information +default_background = """ +I'm Lance, a software engineer at LangChain. +""" + +# Default response preferences +default_response_preferences = """ +Use professional and concise language. If the e-mail mentions a deadline, make sure to explicitly acknowledge and reference the deadline in your response. + +When responding to technical questions that require investigation: +- Clearly state whether you will investigate or who you will ask +- Provide an estimated timeline for when you'll have more information or complete the task + +When responding to event or conference invitations: +- Always acknowledge any mentioned deadlines (particularly registration deadlines) +- If workshops or specific topics are mentioned, ask for more specific details about them +- If discounts (group or early bird) are mentioned, explicitly request information about them +- Don't commit + +When responding to collaboration or project-related requests: +- Acknowledge any existing work or materials mentioned (drafts, slides, documents, etc.) +- Explicitly mention reviewing these materials before or during the meeting +- When scheduling meetings, clearly state the specific day, date, and time proposed + +When responding to meeting scheduling requests: +- If times are proposed, verify calendar availability for all time slots mentioned in the original email and then commit to one of the proposed times based on your availability by scheduling the meeting. Or, say you can't make it at the time proposed. +- If no times are proposed, then check your calendar for availability and propose multiple time options when available instead of selecting just one. +- Mention the meeting duration in your response to confirm you've noted it correctly. +- Reference the meeting's purpose in your response. +""" + +# Default calendar preferences +default_cal_preferences = """ +30 minute meetings are preferred, but 15 minute meetings are also acceptable. +""" + +# Default triage instructions +default_triage_instructions = """ +Emails that are not worth responding to: +- Marketing newsletters and promotional emails +- Spam or suspicious emails +- CC'd on FYI threads with no direct questions + +There are also other things that should be known about, but don't require an email response. For these, you should notify (using the `notify` response). Examples of this include: +- Team member out sick or on vacation +- Build system notifications or deployments +- Project status updates without action items +- Important company announcements +- FYI emails that contain relevant information for current projects +- HR Department deadline reminders +- Subscription status / renewal reminders +- GitHub notifications + +Emails that are worth responding to: +- Direct questions from team members requiring expertise +- Meeting requests requiring confirmation +- Critical bug reports related to team's projects +- Requests from management requiring acknowledgment +- Client inquiries about project status or features +- Technical questions about documentation, code, or APIs (especially questions about missing endpoints or features) +- Personal reminders related to family (wife / daughter) +- Personal reminder related to self-care (doctor appointments, etc) +""" + +MEMORY_UPDATE_INSTRUCTIONS = """ +# Role and Objective +You are a memory profile manager for an email assistant agent that selectively updates user preferences based on feedback messages from human-in-the-loop interactions with the email assistant. + +# Instructions +- NEVER overwrite the entire memory profile +- ONLY make targeted additions of new information +- ONLY update specific facts that are directly contradicted by feedback messages +- PRESERVE all other existing information in the profile +- Format the profile consistently with the original style +- Generate the profile as a string + +# Reasoning Steps +1. Analyze the current memory profile structure and content +2. Review feedback messages from human-in-the-loop interactions +3. Extract relevant user preferences from these feedback messages (such as edits to emails/calendar invites, explicit feedback on assistant performance, user decisions to ignore certain emails) +4. Compare new information against existing profile +5. Identify only specific facts to add or update +6. Preserve all other existing information +7. Output the complete updated profile + +# Example + +RESPOND: +- wife +- specific questions +- system admin notifications +NOTIFY: +- meeting invites +IGNORE: +- marketing emails +- company-wide announcements +- messages meant for other teams + + + +"The assistant shouldn't have responded to that system admin notification." + + + +RESPOND: +- wife +- specific questions +NOTIFY: +- meeting invites +- system admin notifications +IGNORE: +- marketing emails +- company-wide announcements +- messages meant for other teams + + +# Process current profile for {namespace} + +{current_profile} + + +Think step by step about what specific feedback is being provided and what specific information should be added or updated in the profile while preserving everything else. + +Think carefully and update the memory profile based upon these user messages:""" + +MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT = """ +Remember: +- NEVER overwrite the entire memory profile +- ONLY make targeted additions of new information +- ONLY update specific facts that are directly contradicted by feedback messages +- PRESERVE all other existing information in the profile +- Format the profile consistently with the original style +- Generate the profile as a string +""" \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/schemas.py b/personal_assistant/src/personal_assistant/schemas.py new file mode 100644 index 0000000..dfffdd3 --- /dev/null +++ b/personal_assistant/src/personal_assistant/schemas.py @@ -0,0 +1,43 @@ +from pydantic import BaseModel, Field +from typing_extensions import TypedDict, Literal, NotRequired +from langgraph.graph import MessagesState + +class RouterSchema(BaseModel): + """Analyze the unread email and route it according to its content.""" + + reasoning: str = Field( + description="Step-by-step reasoning behind the classification." + ) + classification: Literal["ignore", "respond", "notify"] = Field( + description="The classification of an email: 'ignore' for irrelevant emails, " + "'notify' for important information that doesn't need a response, " + "'respond' for emails that need a reply", + ) + +class StateInput(TypedDict): + # This is the input to the state + email_input: dict + +class State(MessagesState): + # This state class has the messages key build in + email_input: dict + classification_decision: Literal["ignore", "respond", "notify"] + +class EmailAssistantState(TypedDict): + """State for email assistant agent using deepagents library.""" + messages: list # Required by MessagesState + email_input: NotRequired[dict] # Email context for middleware display formatting + +class EmailData(TypedDict): + id: str + thread_id: str + from_email: str + subject: str + page_content: str + send_time: str + to_email: str + +class UserPreferences(BaseModel): + """Updated user preferences based on user's feedback.""" + chain_of_thought: str = Field(description="Reasoning about which user preferences need to add/update if required") + user_preferences: str = Field(description="Updated user preferences") \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/__init__.py b/personal_assistant/src/personal_assistant/tools/__init__.py new file mode 100644 index 0000000..7fe614f --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/__init__.py @@ -0,0 +1,13 @@ +from .base import get_tools, get_tools_by_name +from .default.email_tools import write_email, triage_email, Done +from .default.calendar_tools import schedule_meeting, check_calendar_availability + +__all__ = [ + "get_tools", + "get_tools_by_name", + "write_email", + "triage_email", + "Done", + "schedule_meeting", + "check_calendar_availability", +] \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/base.py b/personal_assistant/src/personal_assistant/tools/base.py new file mode 100644 index 0000000..6fb4019 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/base.py @@ -0,0 +1,57 @@ +from typing import Dict, List, Callable, Any, Optional +from langchain_core.tools import BaseTool + +def get_tools(tool_names: Optional[List[str]] = None, include_gmail: bool = False) -> List[BaseTool]: + """Get specified tools or all tools if tool_names is None. + + Args: + tool_names: Optional list of tool names to include. If None, returns all tools. + include_gmail: Whether to include Gmail tools. Defaults to False. + + Returns: + List of tool objects + """ + # Import default tools + from .default.email_tools import write_email, Done, Question + from .default.calendar_tools import schedule_meeting, check_calendar_availability + + # Base tools dictionary + all_tools = { + "write_email": write_email, + "Done": Done, + "Question": Question, + "schedule_meeting": schedule_meeting, + "check_calendar_availability": check_calendar_availability, + } + + # Add Gmail tools if requested + if include_gmail: + try: + from .gmail.gmail_tools import ( + fetch_emails_tool, + send_email_tool, + check_calendar_tool, + schedule_meeting_tool + ) + + all_tools.update({ + "fetch_emails_tool": fetch_emails_tool, + "send_email_tool": send_email_tool, + "check_calendar_tool": check_calendar_tool, + "schedule_meeting_tool": schedule_meeting_tool, + }) + except ImportError: + # If Gmail tools aren't available, continue without them + pass + + if tool_names is None: + return list(all_tools.values()) + + return [all_tools[name] for name in tool_names if name in all_tools] + +def get_tools_by_name(tools: Optional[List[BaseTool]] = None) -> Dict[str, BaseTool]: + """Get a dictionary of tools mapped by name.""" + if tools is None: + tools = get_tools() + + return {tool.name: tool for tool in tools} diff --git a/personal_assistant/src/personal_assistant/tools/default/__init__.py b/personal_assistant/src/personal_assistant/tools/default/__init__.py new file mode 100644 index 0000000..de73cae --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/default/__init__.py @@ -0,0 +1,22 @@ +"""Default tools for email assistant.""" + +from .email_tools import write_email, triage_email, Done +from .calendar_tools import schedule_meeting, check_calendar_availability +from .prompt_templates import ( + STANDARD_TOOLS_PROMPT, + AGENT_TOOLS_PROMPT, + HITL_TOOLS_PROMPT, + HITL_MEMORY_TOOLS_PROMPT +) + +__all__ = [ + "write_email", + "triage_email", + "Done", + "schedule_meeting", + "check_calendar_availability", + "STANDARD_TOOLS_PROMPT", + "AGENT_TOOLS_PROMPT", + "HITL_TOOLS_PROMPT", + "HITL_MEMORY_TOOLS_PROMPT" +] \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/default/calendar_tools.py b/personal_assistant/src/personal_assistant/tools/default/calendar_tools.py new file mode 100644 index 0000000..00eb563 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/default/calendar_tools.py @@ -0,0 +1,17 @@ +from datetime import datetime +from langchain_core.tools import tool + +@tool +def schedule_meeting( + attendees: list[str], subject: str, duration_minutes: int, preferred_day: datetime, start_time: int +) -> str: + """Schedule a calendar meeting.""" + # Placeholder response - in real app would check calendar and schedule + date_str = preferred_day.strftime("%A, %B %d, %Y") + return f"Meeting '{subject}' scheduled on {date_str} at {start_time} for {duration_minutes} minutes with {len(attendees)} attendees" + +@tool +def check_calendar_availability(day: str) -> str: + """Check calendar availability for a given day.""" + # Placeholder response - in real app would check actual calendar + return f"Available times on {day}: 9:00 AM, 2:00 PM, 4:00 PM" diff --git a/personal_assistant/src/personal_assistant/tools/default/email_tools.py b/personal_assistant/src/personal_assistant/tools/default/email_tools.py new file mode 100644 index 0000000..12f3de7 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/default/email_tools.py @@ -0,0 +1,24 @@ +from typing import Literal +from pydantic import BaseModel +from langchain_core.tools import tool + +@tool +def write_email(to: str, subject: str, content: str) -> str: + """Write and send an email.""" + # Placeholder response - in real app would send email + return f"Email sent to {to} with subject '{subject}' and content: {content}" + +@tool +def triage_email(category: Literal["ignore", "notify", "respond"]) -> str: + """Triage an email into one of three categories: ignore, notify, respond.""" + return f"Classification Decision: {category}" + +@tool +class Done(BaseModel): + """E-mail has been sent.""" + done: bool + +@tool +class Question(BaseModel): + """Question to ask user.""" + content: str diff --git a/personal_assistant/src/personal_assistant/tools/default/prompt_templates.py b/personal_assistant/src/personal_assistant/tools/default/prompt_templates.py new file mode 100644 index 0000000..1138b76 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/default/prompt_templates.py @@ -0,0 +1,37 @@ +"""Tool prompt templates for the email assistant.""" + +# Standard tool descriptions for insertion into prompts +STANDARD_TOOLS_PROMPT = """ +1. triage_email(ignore, notify, respond) - Triage emails into one of three categories +2. write_email(to, subject, content) - Send emails to specified recipients +3. schedule_meeting(attendees, subject, duration_minutes, preferred_day, start_time) - Schedule calendar meetings where preferred_day is a datetime object +4. check_calendar_availability(day) - Check available time slots for a given day +5. Done - E-mail has been sent +""" + +# Tool descriptions for HITL workflow +HITL_TOOLS_PROMPT = """ +1. write_email(to, subject, content) - Send emails to specified recipients +2. schedule_meeting(attendees, subject, duration_minutes, preferred_day, start_time) - Schedule calendar meetings where preferred_day is a datetime object +3. check_calendar_availability(day) - Check available time slots for a given day +4. Question(content) - Ask the user any follow-up questions +5. Done - E-mail has been sent +""" + +# Tool descriptions for HITL with memory workflow +# Note: Additional memory specific tools could be added here +HITL_MEMORY_TOOLS_PROMPT = """ +1. write_email(to, subject, content) - Send emails to specified recipients +2. schedule_meeting(attendees, subject, duration_minutes, preferred_day, start_time) - Schedule calendar meetings where preferred_day is a datetime object +3. check_calendar_availability(day) - Check available time slots for a given day +4. Question(content) - Ask the user any follow-up questions +5. Done - E-mail has been sent +""" + +# Tool descriptions for agent workflow without triage +AGENT_TOOLS_PROMPT = """ +1. write_email(to, subject, content) - Send emails to specified recipients +2. schedule_meeting(attendees, subject, duration_minutes, preferred_day, start_time) - Schedule calendar meetings where preferred_day is a datetime object +3. check_calendar_availability(day) - Check available time slots for a given day +4. Done - E-mail has been sent +""" \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/gmail/README.md b/personal_assistant/src/personal_assistant/tools/gmail/README.md new file mode 100644 index 0000000..61f0a52 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/gmail/README.md @@ -0,0 +1,280 @@ +# Gmail Integration Tools + +Connect your email assistant to Gmail and Google Calendar APIs. + +## Graph + +The `src/email_assistant/email_assistant_hitl_memory_gmail.py` graph is configured to use Gmail tools. + +You simply need to run the setup below to obtain the credentials needed to run the graph with your own email. + +## Setup Credentials + +### 1. Set up Google Cloud Project and Enable Required APIs + +#### Enable Gmail and Calendar APIs + +1. Go to the [Google APIs Library and enable the Gmail API](https://developers.google.com/workspace/gmail/api/quickstart/python#enable_the_api) +2. Go to the [Google APIs Library and enable the Google Calendar API](https://developers.google.com/workspace/calendar/api/quickstart/python#enable_the_api) + +#### Create OAuth Credentials + +1. Authorize credentials for a desktop application [here](https://developers.google.com/workspace/gmail/api/quickstart/python#authorize_credentials_for_a_desktop_application) +2. Go to Credentials → Create Credentials → OAuth Client ID +3. Set Application Type to "Desktop app" +4. Click "Create" + +> Note: If using a personal email (non-Google Workspace) select "External" under "Audience" + +Screenshot 2025-04-26 at 7 43 57 AM + +> Then, add yourself as a test user + +5. Save the downloaded JSON file (you'll need this in the next step) + +### 2. Set Up Authentication Files + +1. Move your downloaded client secret JSON file to the `.secrets` directory + +```bash +# Create a secrets directory +mkdir -p src/email_assistant/tools/gmail/.secrets + +# Move your downloaded client secret to the secrets directory +mv /path/to/downloaded/client_secret.json src/email_assistant/tools/gmail/.secrets/secrets.json +``` + +2. Run the Gmail setup script + +```bash +# Run the Gmail setup script +python src/email_assistant/tools/gmail/setup_gmail.py +``` + +- This will open a browser window for you to authenticate with your Google account +- This will create a `token.json` file in the `.secrets` directory +- This token will be used for Gmail API access + +## Use With A Local Deployment + +### 1. Run the Gmail Ingestion Script with Locally Running LangGraph Server + +1. Once you have authentication set up, run LangGraph server locally: + +``` +langgraph dev +``` + +2. Run the ingestion script in another terminal with desired parameters: + +```bash +python src/email_assistant/tools/gmail/run_ingest.py --email lance@langgraph.dev --minutes-since 1000 +``` + +- By default, this will use the local deployment URL (http://127.0.0.1:2024) and fetch emails from the past 1000 minutes. +- It will use the LangGraph SDK to pass each email to the locally running email assistant. +- It will use the `email_assistant_hitl_memory_gmail` graph, which is configured to use Gmail tools. + +#### Parameters: + +- `--graph-name`: Name of the LangGraph to use (default: "email_assistant_hitl_memory_gmail") +- `--email`: The email address to fetch messages from (alternative to setting EMAIL_ADDRESS) +- `--minutes-since`: Only process emails that are newer than this many minutes (default: 60) +- `--url`: URL of the LangGraph deployment (default: http://127.0.0.1:2024) +- `--rerun`: Process emails that have already been processed (default: false) +- `--early`: Stop after processing one email (default: false) +- `--include-read`: Include emails that have already been read (by default only unread emails are processed) +- `--skip-filters`: Process all emails without filtering (by default only latest messages in threads where you're not the sender are processed) + +#### Troubleshooting: + +- **Missing emails?** The Gmail API applies filters to show only important/primary emails by default. You can: + - Increase the `--minutes-since` parameter to a larger value (e.g., 1000) to fetch emails from a longer time period + - Use the `--include-read` flag to process emails marked as "read" (by default only unread emails are processed) + - Use the `--skip-filters` flag to include all messages (not just the latest in a thread, and including ones you sent) + - Try running with all options to process everything: `--include-read --skip-filters --minutes-since 1000` + - Use the `--mock` flag to test the system with simulated emails + +### 2. Connect to Agent Inbox + +After ingestion, you can access your all interrupted threads in Agent Inbox (https://dev.agentinbox.ai/): +* Deployment URL: http://127.0.0.1:2024 +* Assistant/Graph ID: `email_assistant_hitl_memory_gmail` +* Name: `Graph Name` + +## Run A Hosted Deployment + +### 1. Deploy to LangGraph Platform + +1. Navigate to the deployments page in LangSmith +2. Click New Deployment +3. Connect it to your fork of the [this repo](https://github.com/langchain-ai/agents-from-scratch) and desired branch +4. Give it a name like `Yourname-Email-Assistant` +5. Add the following environment variables: + * `OPENAI_API_KEY` + * `GMAIL_SECRET` - This is the full dictionary in `.secrets/secrets.json` + * `GMAIL_TOKEN` - This is the full dictionary in `.secrets/token.json` +6. Click Submit +7. Get the `API URL` (https://your-email-assistant-xxx.us.langgraph.app) from the deployment page + +### 2. Run Ingestion with Hosted Deployment + +Once your LangGraph deployment is up and running, you can test the email ingestion with: + +```bash +python src/email_assistant/tools/gmail/run_ingest.py --email lance@langchain.dev --minutes-since 2440 --include-read --url https://your-email-assistant-xxx.us.langgraph.app +``` + +### 3. Connect to Agent Inbox + +After ingestion, you can access your all interrupted threads in Agent Inbox (https://dev.agentinbox.ai/): +* Deployment URL: https://your-email-assistant-xxx.us.langgraph.app +* Assistant/Graph ID: `email_assistant_hitl_memory_gmail` +* Name: `Graph Name` +* LangSmith API Key: `LANGSMITH_API_KEY` + +### 4. Set up Cron Job + +With a hosted deployment, you can set up a cron job to run the ingestion script at a specified interval. + +To automate email ingestion, set up a scheduled cron job using the included setup script: + +```bash +python src/email_assistant/tools/gmail/setup_cron.py --email lance@langchain.dev --url https://lance-email-assistant-4681ae9646335abe9f39acebbde8680b.us.langgraph.app +``` + +#### Parameters: + +- `--email`: Email address to fetch messages for (required) +- `--url`: LangGraph deployment URL (required) +- `--minutes-since`: Only fetch emails newer than this many minutes (default: 60) +- `--schedule`: Cron schedule expression (default: "*/10 * * * *" = every 10 minutes) +- `--graph-name`: Name of the graph to use (default: "email_assistant_hitl_memory_gmail") +- `--include-read`: Include emails marked as read (by default only unread emails are processed) (default: false) + +#### How the Cron Works + +The cron consists of two main components: + +1. **`src/email_assistant/cron.py`**: Defines a simple LangGraph graph that: + - Calls the same `fetch_and_process_emails` function used by `run_ingest.py` + - Wraps this in a simple graph so that it can be run as a hosted cron using LangGraph Platform + +2. **`src/email_assistant/tools/gmail/setup_cron.py`**: Creates the scheduled cron job: + - Uses LangGraph SDK `client.crons.create` to create a cron job for the hosted `cron.py` graph + +#### Managing Cron Jobs + +To view, update, or delete existing cron jobs, you can use the LangGraph SDK: + +```python +from langgraph_sdk import get_client + +# Connect to deployment +client = get_client(url="https://your-deployment-url.us.langgraph.app") + +# List all cron jobs +cron_jobs = await client.crons.list() +print(cron_jobs) + +# Delete a cron job +await client.crons.delete(cron_job_id) +``` + +## How Gmail Ingestion Works + +The Gmail ingestion process works in three main stages: + +### 1. CLI Parameters → Gmail Search Query + +CLI parameters are translated into a Gmail search query: + +- `--minutes-since 1440` → `after:TIMESTAMP` (emails from the last 24 hours) +- `--email you@example.com` → `to:you@example.com OR from:you@example.com` (emails where you're sender or recipient) +- `--include-read` → removes `is:unread` filter (includes read messages) + +For example, running: +``` +python run_ingest.py --email you@example.com --minutes-since 1440 --include-read +``` + +Creates a Gmail API search query like: +``` +(to:you@example.com OR from:you@example.com) after:1745432245 +``` + +### 2. Search Results → Thread Processing + +For each message returned by the search: + +1. The script obtains the thread ID +2. Using this thread ID, it fetches the **complete thread** with all messages +3. Messages in the thread are sorted by date to identify the latest message +4. Depending on filtering options, it processes either: + - The specific message found in the search (default behavior) + - The latest message in the thread (when using `--skip-filters`) + +### 3. Default Filters and `--skip-filters` Behavior + +#### Default Filters Applied + +Without `--skip-filters`, the system applies these three filters in sequence: + +1. **Unread Filter** (controlled by `--include-read`): + - Default behavior: Only processes unread messages + - With `--include-read`: Processes both read and unread messages + - Implementation: Adds `is:unread` to the Gmail search query + - This filter happens at the search level before any messages are retrieved + +2. **Sender Filter**: + - Default behavior: Skips messages sent by your own email address + - Implementation: Checks if your email appears in the "From" header + - Logic: `is_from_user = email_address in from_header` + - This prevents the assistant from responding to your own emails + +3. **Thread-Position Filter**: + - Default behavior: Only processes the most recent message in each thread + - Implementation: Compares message ID with the last message in thread + - Logic: `is_latest_in_thread = message["id"] == last_message["id"]` + - Prevents processing older messages when a newer reply exists + +The combination of these filters means only the latest message in each thread that was not sent by you and is unread (unless `--include-read` is specified) will be processed. + +#### Effect of `--skip-filters` Flag + +When `--skip-filters` is enabled: + +1. **Bypasses Sender and Thread-Position Filters**: + - Messages sent by you will be processed + - Messages that aren't the latest in thread will be processed + - Logic: `should_process = skip_filters or (not is_from_user and is_latest_in_thread)` + +2. **Changes Which Message Is Processed**: + - Without `--skip-filters`: Uses the specific message found by search + - With `--skip-filters`: Always uses the latest message in the thread + - Even if the latest message wasn't found in the search results + +3. **Unread Filter Still Applies (unless overridden)**: + - `--skip-filters` does NOT bypass the unread filter + - To process read messages, you must still use `--include-read` + - This is because the unread filter happens at the search level + +In summary: +- Default: Process only unread messages where you're not the sender and that are the latest in their thread +- `--skip-filters`: Process all messages found by search, using the latest message in each thread +- `--include-read`: Include read messages in the search +- `--include-read --skip-filters`: Most comprehensive, processes the latest message in all threads found by search + +## Important Gmail API Limitations + +The Gmail API has several limitations that affect email ingestion: + +1. **Search-Based API**: Gmail doesn't provide a direct "get all emails from timeframe" endpoint + - All email retrieval relies on Gmail's search functionality + - Search results can be delayed for very recent messages (indexing lag) + - Search results might not include all messages that technically match criteria + +2. **Two-Stage Retrieval Process**: + - Initial search to find relevant message IDs + - Secondary thread retrieval to get complete conversations + - This two-stage process is necessary because search doesn't guarantee complete thread information \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/gmail/__init__.py b/personal_assistant/src/personal_assistant/tools/gmail/__init__.py new file mode 100644 index 0000000..1cae78b --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/gmail/__init__.py @@ -0,0 +1,18 @@ +"""Gmail tools for email assistant.""" + +from email_assistant.tools.gmail.gmail_tools import ( + fetch_emails_tool, + send_email_tool, + check_calendar_tool, + schedule_meeting_tool +) + +from email_assistant.tools.gmail.prompt_templates import GMAIL_TOOLS_PROMPT + +__all__ = [ + "fetch_emails_tool", + "send_email_tool", + "check_calendar_tool", + "schedule_meeting_tool", + "GMAIL_TOOLS_PROMPT" +] \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py b/personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py new file mode 100644 index 0000000..148aef8 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py @@ -0,0 +1,946 @@ +""" +Gmail tools implementation module. +This module formats the Gmail API functions into LangChain tools. +""" + +import os +import sys +import base64 +import email.utils +import json +import logging +from datetime import datetime +from typing import List, Optional, Dict, Any, Iterator +from pathlib import Path +from pydantic import Field, BaseModel +from langchain_core.tools import tool + +# Setup basic logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Define paths for credentials and tokens +_ROOT = Path(__file__).parent.absolute() +_SECRETS_DIR = _ROOT / ".secrets" + +# We need to try importing the Gmail API libraries +# If they're not available, we'll use a mock implementation +try: + import logging + from googleapiclient.discovery import build + from email.mime.text import MIMEText + from datetime import timedelta + from dateutil.parser import parse as parse_time + from google.oauth2.credentials import Credentials + from google_auth_oauthlib.flow import InstalledAppFlow + from google.auth.transport.requests import Request + + # Setup logging + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + # Email content extraction function + def extract_message_part(payload): + """Extract content from a message part.""" + if payload.get("body", {}).get("data"): + # Handle base64 encoded content + data = payload["body"]["data"] + decoded = base64.urlsafe_b64decode(data).decode("utf-8") + return decoded + + # Handle multipart messages + if payload.get("parts"): + text_parts = [] + for part in payload["parts"]: + # Recursively process parts + content = extract_message_part(part) + if content: + text_parts.append(content) + return "\n".join(text_parts) + + return "" + + # Function to get credentials from token.json or environment variables + def get_credentials(gmail_token=None, gmail_secret=None): + """ + Get Gmail API credentials from token.json or environment variables. + + This function attempts to load credentials from multiple sources in this order: + 1. Directly passed gmail_token and gmail_secret parameters + 2. Environment variables GMAIL_TOKEN and GMAIL_SECRET + 3. Local files at token_path (.secrets/token.json) and secrets_path (.secrets/secrets.json) + + Args: + gmail_token: Optional JSON string containing token data + gmail_secret: Optional JSON string containing credentials + + Returns: + Google OAuth2 Credentials object or None if credentials can't be loaded + """ + token_path = _SECRETS_DIR / "token.json" + token_data = None + + # Try to get token data from various sources + if gmail_token: + # 1. Use directly passed token parameter if available + try: + token_data = json.loads(gmail_token) if isinstance(gmail_token, str) else gmail_token + logger.info("Using directly provided gmail_token parameter") + except Exception as e: + logger.warning(f"Could not parse provided gmail_token: {str(e)}") + + if token_data is None: + # 2. Try environment variable + env_token = os.getenv("GMAIL_TOKEN") + if env_token: + try: + token_data = json.loads(env_token) + logger.info("Using GMAIL_TOKEN environment variable") + except Exception as e: + logger.warning(f"Could not parse GMAIL_TOKEN environment variable: {str(e)}") + + if token_data is None: + # 3. Try local file + if os.path.exists(token_path): + try: + with open(token_path, "r") as f: + token_data = json.load(f) + logger.info(f"Using token from {token_path}") + except Exception as e: + logger.warning(f"Could not load token from {token_path}: {str(e)}") + + # If we couldn't get token data from any source, return None + if token_data is None: + logger.error("Could not find valid token data in any location") + return None + + try: + from google.oauth2.credentials import Credentials + + # Create credentials object with specific format + credentials = Credentials( + token=token_data.get("token"), + refresh_token=token_data.get("refresh_token"), + token_uri=token_data.get("token_uri", "https://oauth2.googleapis.com/token"), + client_id=token_data.get("client_id"), + client_secret=token_data.get("client_secret"), + scopes=token_data.get("scopes", ["https://www.googleapis.com/auth/gmail.modify"]) + ) + + # Add authorize method to make it compatible with old code + credentials.authorize = lambda request: request + + return credentials + except Exception as e: + logger.error(f"Error creating credentials object: {str(e)}") + return None + + # Type alias for better readability + EmailData = Dict[str, Any] + + GMAIL_API_AVAILABLE = True + +except ImportError: + # If Gmail API libraries aren't available, set flag to use mock implementation + GMAIL_API_AVAILABLE = False + logger = logging.getLogger(__name__) + +# Helper function that is used by the tool and can be imported elsewhere +def fetch_group_emails( + email_address: str, + minutes_since: int = 30, + gmail_token: Optional[str] = None, + gmail_secret: Optional[str] = None, + include_read: bool = False, + skip_filters: bool = False, +) -> Iterator[Dict[str, Any]]: + """ + Fetch recent emails from Gmail that involve the specified email address. + + This function retrieves emails where the specified address is either a sender + or recipient, processes them, and returns them in a format suitable for the + email assistant to process. + + Args: + email_address: Email address to fetch messages for + minutes_since: Only retrieve emails newer than this many minutes + gmail_token: Optional token for Gmail API authentication + gmail_secret: Optional credentials for Gmail API authentication + include_read: Whether to include already read emails (default: False) + skip_filters: Skip thread and sender filtering (return all messages, default: False) + + Yields: + Dict objects containing processed email information + """ + use_mock = False + + # Check if we need to use mock implementation + if not GMAIL_API_AVAILABLE: + logger.info("Gmail API not available, using mock implementation") + use_mock = True + + # Check if required credential files exist + if not use_mock and not gmail_token and not gmail_secret: + token_path = str(_SECRETS_DIR / "token.json") + secrets_path = str(_SECRETS_DIR / "secrets.json") + + if not os.path.exists(token_path) and not os.path.exists(secrets_path): + logger.warning(f"No Gmail API credentials found. Looking for token.json or secrets.json in .secrets directory") + logger.warning("Using mock implementation instead") + use_mock = True + + # Return mock data if needed + if use_mock: + # For demo purposes, we return a mock email + mock_email = { + "from_email": "sender@example.com", + "to_email": email_address, + "subject": "Sample Email Subject", + "page_content": "This is a sample email body for testing the email assistant.", + "id": "mock-email-id-123", + "thread_id": "mock-thread-id-123", + "send_time": datetime.now().isoformat() + } + + yield mock_email + return + + try: + # Get Gmail API credentials from parameters, environment variables, or local files + creds = get_credentials(gmail_token, gmail_secret) + + # Check if credentials are valid + if not creds or not hasattr(creds, 'authorize'): + logger.warning("Invalid Gmail credentials, using mock implementation") + logger.warning("Ensure GMAIL_TOKEN environment variable is set or token.json file exists") + mock_email = { + "from_email": "sender@example.com", + "to_email": email_address, + "subject": "Sample Email Subject - Invalid Credentials", + "page_content": "This is a mock email because the Gmail credentials are invalid.", + "id": "mock-email-id-123", + "thread_id": "mock-thread-id-123", + "send_time": datetime.now().isoformat() + } + yield mock_email + return + + service = build("gmail", "v1", credentials=creds) + + # Calculate timestamp for filtering + after = int((datetime.now() - timedelta(minutes=minutes_since)).timestamp()) + + # Construct Gmail search query + # This query searches for: + # - Emails sent to or from the specified address + # - Emails after the specified timestamp + # - Including emails from all categories (inbox, updates, promotions, etc.) + + # Base query with time filter + query = f"(to:{email_address} OR from:{email_address}) after:{after}" + + # Only include unread emails unless include_read is True + if not include_read: + query += " is:unread" + else: + logger.info("Including read emails in search") + + # Log the final query for debugging + logger.info(f"Gmail search query: {query}") + + # Additional filter options (commented out by default) + # If you want to include emails from specific categories, use: + # query += " category:(primary OR updates OR promotions)" + + # Retrieve all matching messages (handling pagination) + messages = [] + nextPageToken = None + logger.info(f"Fetching emails for {email_address} from last {minutes_since} minutes") + + while True: + results = ( + service.users() + .messages() + .list(userId="me", q=query, pageToken=nextPageToken) + .execute() + ) + if "messages" in results: + new_messages = results["messages"] + messages.extend(new_messages) + logger.info(f"Found {len(new_messages)} messages in this page") + else: + logger.info("No messages found in this page") + + nextPageToken = results.get("nextPageToken") + if not nextPageToken: + logger.info(f"Total messages found: {len(messages)}") + break + + # Process each message + count = 0 + for message in messages: + try: + # Get full message details + msg = service.users().messages().get(userId="me", id=message["id"]).execute() + thread_id = msg["threadId"] + payload = msg["payload"] + headers = payload.get("headers", []) + + # Get thread details to determine conversation context + # Directly fetch the complete thread without any format restriction + # This matches the exact approach in the test code that successfully gets all messages + thread = service.users().threads().get(userId="me", id=thread_id).execute() + messages_in_thread = thread["messages"] + logger.info(f"Retrieved thread {thread_id} with {len(messages_in_thread)} messages") + + # Sort messages by internalDate to ensure proper chronological ordering + # This ensures we correctly identify the latest message + if all("internalDate" in msg for msg in messages_in_thread): + messages_in_thread.sort(key=lambda m: int(m.get("internalDate", 0))) + logger.info(f"Sorted {len(messages_in_thread)} messages by internalDate") + else: + # Fallback to ID-based sorting if internalDate is missing + messages_in_thread.sort(key=lambda m: m["id"]) + logger.info(f"Sorted {len(messages_in_thread)} messages by ID (internalDate missing)") + + # Log details about the messages in the thread for debugging + for idx, msg in enumerate(messages_in_thread): + headers = msg["payload"]["headers"] + subject = next((h["value"] for h in headers if h["name"] == "Subject"), "No Subject") + from_email = next((h["value"] for h in headers if h["name"] == "From"), "Unknown") + date = next((h["value"] for h in headers if h["name"] == "Date"), "Unknown") + logger.info(f" Message {idx+1}/{len(messages_in_thread)}: ID={msg['id']}, Date={date}, From={from_email}") + + # Log thread information for debugging + logger.info(f"Thread {thread_id} has {len(messages_in_thread)} messages") + + # Analyze the last message in the thread to determine if we need to process it + last_message = messages_in_thread[-1] + last_headers = last_message["payload"]["headers"] + + # Get sender of last message + from_header = next( + header["value"] for header in last_headers if header["name"] == "From" + ) + last_from_header = next( + header["value"] + for header in last_message["payload"].get("headers") + if header["name"] == "From" + ) + + # If the last message was sent by the user, mark this as a user response + # and don't process it further (assistant doesn't need to respond to user's own emails) + if email_address in last_from_header: + yield { + "id": message["id"], + "thread_id": message["threadId"], + "user_respond": True, + } + continue + + # Check if this is a message we should process + is_from_user = email_address in from_header + is_latest_in_thread = message["id"] == last_message["id"] + + # Modified logic for skip_filters: + # 1. When skip_filters is True, process all messages regardless of position in thread + # 2. When skip_filters is False, only process if it's not from user AND is latest in thread + should_process = skip_filters or (not is_from_user and is_latest_in_thread) + + if not should_process: + if is_from_user: + logger.debug(f"Skipping message {message['id']}: sent by the user") + elif not is_latest_in_thread: + logger.debug(f"Skipping message {message['id']}: not the latest in thread") + + # Process the message if it passes our filters (or if filters are skipped) + if should_process: + # Log detailed information about this message + logger.info(f"Processing message {message['id']} from thread {thread_id}") + logger.info(f" Is latest in thread: {is_latest_in_thread}") + logger.info(f" Skip filters enabled: {skip_filters}") + + # If the user wants to process the latest message in the thread, + # use the last_message from the thread API call instead of the original message + # that matched the search query + if not skip_filters: + # Use original message if skip_filters is False + process_message = message + process_payload = payload + process_headers = headers + else: + # Use the latest message in the thread if skip_filters is True + process_message = last_message + process_payload = last_message["payload"] + process_headers = process_payload.get("headers", []) + logger.info(f"Using latest message in thread: {process_message['id']}") + + # Extract email metadata from headers + subject = next( + header["value"] for header in process_headers if header["name"] == "Subject" + ) + from_email = next( + (header["value"] for header in process_headers if header["name"] == "From"), + "", + ).strip() + _to_email = next( + (header["value"] for header in process_headers if header["name"] == "To"), + "", + ).strip() + + # Use Reply-To header if present + if reply_to := next( + ( + header["value"] + for header in process_headers + if header["name"] == "Reply-To" + ), + "", + ).strip(): + from_email = reply_to + + # Extract and parse email timestamp + send_time = next( + header["value"] for header in process_headers if header["name"] == "Date" + ) + parsed_time = parse_time(send_time) + + # Extract email body content + body = extract_message_part(process_payload) + + # Yield the processed email data + yield { + "from_email": from_email, + "to_email": _to_email, + "subject": subject, + "page_content": body, + "id": process_message["id"], + "thread_id": process_message["threadId"], + "send_time": parsed_time.isoformat(), + } + count += 1 + + except Exception as e: + logger.warning(f"Failed to process message {message['id']}: {str(e)}") + + logger.info(f"Found {count} emails to process out of {len(messages)} total messages.") + + except Exception as e: + logger.error(f"Error accessing Gmail API: {str(e)}") + # Fall back to mock implementation + mock_email = { + "from_email": "sender@example.com", + "to_email": email_address, + "subject": "Sample Email Subject", + "page_content": "This is a sample email body for testing the email assistant.", + "id": "mock-email-id-123", + "thread_id": "mock-thread-id-123", + "send_time": datetime.now().isoformat() + } + + yield mock_email + +class FetchEmailsInput(BaseModel): + """ + Input schema for the fetch_emails_tool. + """ + email_address: str = Field( + description="Email address to fetch emails for" + ) + minutes_since: int = Field( + default=30, + description="Only retrieve emails newer than this many minutes" + ) + +@tool(args_schema=FetchEmailsInput) +def fetch_emails_tool(email_address: str, minutes_since: int = 30) -> str: + """ + Fetches recent emails from Gmail for the specified email address. + + Args: + email_address: Email address to fetch messages for + minutes_since: Only retrieve emails newer than this many minutes (default: 30) + + Returns: + String summary of fetched emails + """ + emails = list(fetch_group_emails(email_address, minutes_since)) + + if not emails: + return "No new emails found." + + result = f"Found {len(emails)} new emails:\n\n" + + for i, email in enumerate(emails, 1): + if email.get("user_respond", False): + result += f"{i}. You already responded to this email (Thread ID: {email['thread_id']})\n\n" + continue + + result += f"{i}. From: {email['from_email']}\n" + result += f" To: {email['to_email']}\n" + result += f" Subject: {email['subject']}\n" + result += f" Time: {email['send_time']}\n" + result += f" ID: {email['id']}\n" + result += f" Thread ID: {email['thread_id']}\n" + result += f" Content: {email['page_content'][:200]}...\n\n" + + return result + +class SendEmailInput(BaseModel): + """ + Input schema for the send_email_tool. + """ + email_id: str = Field( + description="Gmail message ID to reply to. This must be a valid Gmail message ID obtained from the fetch_emails_tool. If you're creating a new email (not replying), you can use any string like 'NEW_EMAIL'." + ) + response_text: str = Field( + description="Content of the reply" + ) + email_address: str = Field( + description="Current user's email address" + ) + additional_recipients: Optional[List[str]] = Field( + default=None, + description="Optional additional recipients to include" + ) + +# Helper function for sending emails +def send_email( + email_id: str, + response_text: str, + email_address: str, + addn_receipients: Optional[List[str]] = None +) -> bool: + """ + Send a reply to an existing email thread or create a new email. + + Args: + email_id: Gmail message ID to reply to. If this is not a valid Gmail ID (e.g., when creating a new email), + the function will create a new email instead of replying to an existing thread. + response_text: Content of the reply or new email + email_address: Current user's email address (the sender) + addn_receipients: Optional additional recipients + + Returns: + Success flag (True if email was sent) + """ + if not GMAIL_API_AVAILABLE: + logger.info("Gmail API not available, simulating email send") + logger.info(f"Would send: {response_text[:100]}...") + return True + + try: + # Get Gmail API credentials from environment variables or local files + creds = get_credentials( + gmail_token=os.getenv("GMAIL_TOKEN"), + gmail_secret=os.getenv("GMAIL_SECRET") + ) + service = build("gmail", "v1", credentials=creds) + + try: + # Try to get the original message to extract headers + message = service.users().messages().get(userId="me", id=email_id).execute() + headers = message["payload"]["headers"] + + # Extract subject with Re: prefix if not already present + subject = next(header["value"] for header in headers if header["name"] == "Subject") + if not subject.startswith("Re:"): + subject = f"Re: {subject}" + + # Create a reply message + original_from = next(header["value"] for header in headers if header["name"] == "From") + + # Get thread ID from message + thread_id = message["threadId"] + except Exception as e: + logger.warning(f"Could not retrieve original message with ID {email_id}. Error: {str(e)}") + # If we can't get the original message, create a new message with minimal info + subject = "Response" + original_from = "recipient@example.com" # Will be overridden by user input + thread_id = None + + # Create a message object + msg = MIMEText(response_text) + msg["to"] = original_from + msg["from"] = email_address + msg["subject"] = subject + + # Add additional recipients if specified + if addn_receipients: + msg["cc"] = ", ".join(addn_receipients) + + # Encode the message + raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("utf-8") + + # Prepare message body + body = {"raw": raw} + # Only add threadId if it exists + if thread_id: + body["threadId"] = thread_id + + # Send the message + sent_message = ( + service.users() + .messages() + .send( + userId="me", + body=body, + ) + .execute() + ) + + logger.info(f"Email sent: Message ID {sent_message['id']}") + return True + + except Exception as e: + logger.error(f"Error sending email: {str(e)}") + return False + +@tool(args_schema=SendEmailInput) +def send_email_tool( + email_id: str, + response_text: str, + email_address: str, + additional_recipients: Optional[List[str]] = None +) -> str: + """ + Send a reply to an existing email thread or create a new email in Gmail. + + Args: + email_id: Gmail message ID to reply to. This should be a valid Gmail message ID obtained from the fetch_emails_tool. + If creating a new email rather than replying, you can use any string identifier like "NEW_EMAIL". + response_text: Content of the reply or new email + email_address: Current user's email address (the sender) + additional_recipients: Optional additional recipients to include + + Returns: + Confirmation message + """ + try: + success = send_email( + email_id, + response_text, + email_address, + addn_receipients=additional_recipients + ) + if success: + return f"Email reply sent successfully to message ID: {email_id}" + else: + return "Failed to send email due to an API error" + except Exception as e: + return f"Failed to send email: {str(e)}" + +class CheckCalendarInput(BaseModel): + """ + Input schema for the check_calendar_tool. + """ + dates: List[str] = Field( + description="List of dates to check in DD-MM-YYYY format" + ) + +def get_calendar_events(dates: List[str]) -> str: + """ + Check Google Calendar for events on specified dates. + + Args: + dates: List of dates to check in DD-MM-YYYY format + + Returns: + Formatted calendar events for the specified dates + """ + if not GMAIL_API_AVAILABLE: + logger.info("Gmail API not available, simulating calendar check") + # Fallback: Return mock calendar data for demo/testing purposes + # In production, this should use the real Google Calendar API + result = "Calendar events:\n\n" + for date in dates: + result += f"Events for {date}:\n" + result += " - 9:00 AM - 10:00 AM: Team Meeting\n" + result += " - 2:00 PM - 3:00 PM: Project Review\n" + result += "Available slots: 10:00 AM - 2:00 PM, after 3:00 PM\n\n" + return result + + try: + # Get Gmail API credentials from environment variables or local files + creds = get_credentials( + gmail_token=os.getenv("GMAIL_TOKEN"), + gmail_secret=os.getenv("GMAIL_SECRET") + ) + service = build("calendar", "v3", credentials=creds) + + result = "Calendar events:\n\n" + + for date_str in dates: + # Parse date string (DD-MM-YYYY) + day, month, year = date_str.split("-") + + # Format start and end times for the API + start_time = f"{year}-{month}-{day}T00:00:00Z" + end_time = f"{year}-{month}-{day}T23:59:59Z" + + # Call the Calendar API + events_result = ( + service.events() + .list( + calendarId="primary", + timeMin=start_time, + timeMax=end_time, + singleEvents=True, + orderBy="startTime", + ) + .execute() + ) + + events = events_result.get("items", []) + + result += f"Events for {date_str}:\n" + + if not events: + result += " No events found for this day\n" + result += " Available all day\n\n" + continue + + # Process events + busy_slots = [] + for event in events: + start = event["start"].get("dateTime", event["start"].get("date")) + end = event["end"].get("dateTime", event["end"].get("date")) + + # Convert to datetime objects + if "T" in start: # dateTime format + start_dt = datetime.fromisoformat(start.replace("Z", "+00:00")) + end_dt = datetime.fromisoformat(end.replace("Z", "+00:00")) + + # Format for display + start_display = start_dt.strftime("%I:%M %p") + end_display = end_dt.strftime("%I:%M %p") + + result += f" - {start_display} - {end_display}: {event['summary']}\n" + busy_slots.append((start_dt, end_dt)) + else: # all-day event + result += f" - All day: {event['summary']}\n" + busy_slots.append(("all-day", "all-day")) + + # Calculate available slots + if "all-day" in [slot[0] for slot in busy_slots]: + result += " Available: No availability (all-day events)\n\n" + else: + # Sort busy slots by start time + busy_slots.sort(key=lambda x: x[0]) + + # Define working hours (9 AM to 5 PM) + # Note: Working hours are currently hardcoded for simplicity + # In production, this could be made configurable per user/organization + work_start = datetime( + year=int(year), + month=int(month), + day=int(day), + hour=9, + minute=0 + ) + work_end = datetime( + year=int(year), + month=int(month), + day=int(day), + hour=17, + minute=0 + ) + + # Calculate available slots + available_slots = [] + current = work_start + + for start, end in busy_slots: + if current < start: + available_slots.append((current, start)) + current = max(current, end) + + if current < work_end: + available_slots.append((current, work_end)) + + # Format available slots + if available_slots: + result += " Available: " + for i, (start, end) in enumerate(available_slots): + start_display = start.strftime("%I:%M %p") + end_display = end.strftime("%I:%M %p") + result += f"{start_display} - {end_display}" + if i < len(available_slots) - 1: + result += ", " + result += "\n\n" + else: + result += " Available: No availability during working hours\n\n" + + return result + + except Exception as e: + logger.error(f"Error checking calendar: {str(e)}") + # Return mock data in case of error + result = "Calendar events (mock due to error):\n\n" + for date in dates: + result += f"Events for {date}:\n" + result += " - 9:00 AM - 10:00 AM: Team Meeting\n" + result += " - 2:00 PM - 3:00 PM: Project Review\n" + result += "Available slots: 10:00 AM - 2:00 PM, after 3:00 PM\n\n" + return result + +@tool(args_schema=CheckCalendarInput) +def check_calendar_tool(dates: List[str]) -> str: + """ + Check Google Calendar for events on specified dates. + + Args: + dates: List of dates to check in DD-MM-YYYY format + + Returns: + Formatted calendar events for the specified dates + """ + try: + events = get_calendar_events(dates) + return events + except Exception as e: + return f"Failed to check calendar: {str(e)}" + +class ScheduleMeetingInput(BaseModel): + """ + Input schema for the schedule_meeting_tool. + """ + attendees: List[str] = Field( + description="Email addresses of meeting attendees" + ) + title: str = Field( + description="Meeting title/subject" + ) + start_time: str = Field( + description="Meeting start time in ISO format (YYYY-MM-DDTHH:MM:SS)" + ) + end_time: str = Field( + description="Meeting end time in ISO format (YYYY-MM-DDTHH:MM:SS)" + ) + organizer_email: str = Field( + description="Email address of the meeting organizer" + ) + timezone: str = Field( + default="America/Los_Angeles", + description="Timezone for the meeting" + ) + +def send_calendar_invite( + attendees: List[str], + title: str, + start_time: str, + end_time: str, + organizer_email: str, + timezone: str = "America/Los_Angeles" +) -> bool: + """ + Schedule a meeting with Google Calendar and send invites. + + Args: + attendees: Email addresses of meeting attendees + title: Meeting title/subject + start_time: Meeting start time in ISO format (YYYY-MM-DDTHH:MM:SS) + end_time: Meeting end time in ISO format (YYYY-MM-DDTHH:MM:SS) + organizer_email: Email address of the meeting organizer + timezone: Timezone for the meeting + + Returns: + Success flag (True if meeting was scheduled) + """ + if not GMAIL_API_AVAILABLE: + logger.info("Gmail API not available, simulating calendar invite") + logger.info(f"Would schedule: {title} from {start_time} to {end_time}") + logger.info(f"Attendees: {', '.join(attendees)}") + return True + + try: + # Get Gmail API credentials from environment variables or local files + creds = get_credentials( + gmail_token=os.getenv("GMAIL_TOKEN"), + gmail_secret=os.getenv("GMAIL_SECRET") + ) + service = build("calendar", "v3", credentials=creds) + + # Create event details + event = { + "summary": title, + "start": { + "dateTime": start_time, + "timeZone": timezone, + }, + "end": { + "dateTime": end_time, + "timeZone": timezone, + }, + "attendees": [{"email": email} for email in attendees], + "organizer": { + "email": organizer_email, + "self": True, + }, + "reminders": { + "useDefault": True, + }, + "sendUpdates": "all", # Send email notifications to attendees + } + + # Create the event + event = service.events().insert(calendarId="primary", body=event).execute() + + logger.info(f"Meeting created: {event.get('htmlLink')}") + return True + + except Exception as e: + logger.error(f"Error scheduling meeting: {str(e)}") + return False + +@tool(args_schema=ScheduleMeetingInput) +def schedule_meeting_tool( + attendees: List[str], + title: str, + start_time: str, + end_time: str, + organizer_email: str, + timezone: str = "America/Los_Angeles" +) -> str: + """ + Schedule a meeting with Google Calendar and send invites. + + Args: + attendees: Email addresses of meeting attendees + title: Meeting title/subject + start_time: Meeting start time in ISO format (YYYY-MM-DDTHH:MM:SS) + end_time: Meeting end time in ISO format (YYYY-MM-DDTHH:MM:SS) + organizer_email: Email address of the meeting organizer + timezone: Timezone for the meeting (default: America/Los_Angeles) + + Returns: + Success or failure message + """ + try: + success = send_calendar_invite( + attendees, + title, + start_time, + end_time, + organizer_email, + timezone + ) + + if success: + return f"Meeting '{title}' scheduled successfully from {start_time} to {end_time} with {len(attendees)} attendees" + else: + return "Failed to schedule meeting" + except Exception as e: + return f"Error scheduling meeting: {str(e)}" + +def mark_as_read( + message_id, + gmail_token: str | None = None, + gmail_secret: str | None = None, +): + creds = get_credentials(gmail_token, gmail_secret) + + service = build("gmail", "v1", credentials=creds) + service.users().messages().modify( + userId="me", id=message_id, body={"removeLabelIds": ["UNREAD"]} + ).execute() \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/gmail/prompt_templates.py b/personal_assistant/src/personal_assistant/tools/gmail/prompt_templates.py new file mode 100644 index 0000000..f8b004c --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/gmail/prompt_templates.py @@ -0,0 +1,23 @@ +"""Tool prompt templates for Gmail integration.""" + +# Gmail tools prompt for insertion into agent system prompts +GMAIL_TOOLS_PROMPT = """ +1. fetch_emails_tool(email_address, minutes_since) - Fetch recent emails from Gmail +2. send_email_tool(email_id, response_text, email_address, additional_recipients) - Send a reply to an email thread +3. check_calendar_tool(dates) - Check Google Calendar availability for specific dates +4. schedule_meeting_tool(attendees, title, start_time, end_time, organizer_email, timezone) - Schedule a meeting and send invites +5. triage_email(ignore, notify, respond) - Triage emails into one of three categories +6. Done - E-mail has been sent +""" + +# Combined tools prompt (default + Gmail) for full integration +COMBINED_TOOLS_PROMPT = """ +1. fetch_emails_tool(email_address, minutes_since) - Fetch recent emails from Gmail +2. send_email_tool(email_id, response_text, email_address, additional_recipients) - Send a reply to an email thread +3. check_calendar_tool(dates) - Check Google Calendar availability for specific dates +4. schedule_meeting_tool(attendees, title, start_time, end_time, organizer_email, timezone) - Schedule a meeting and send invites +5. write_email(to, subject, content) - Draft emails to specified recipients +6. triage_email(ignore, notify, respond) - Triage emails into one of three categories +7. check_calendar_availability(day) - Check available time slots for a given day +8. Done - E-mail has been sent +""" \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py b/personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py new file mode 100644 index 0000000..307c481 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python +""" +Simple Gmail ingestion script based directly on test.ipynb that works with LangSmith tracing. + +This script provides a minimal implementation for ingesting emails to the LangGraph server, +with reliable LangSmith tracing. +""" + +import base64 +import json +import uuid +import hashlib +import asyncio +import argparse +import os +from pathlib import Path +from datetime import datetime +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from langgraph_sdk import get_client +from dotenv import load_dotenv + +load_dotenv() + +# Setup paths +_ROOT = Path(__file__).parent.absolute() +_SECRETS_DIR = _ROOT / ".secrets" +TOKEN_PATH = _SECRETS_DIR / "token.json" + +def extract_message_part(payload): + """Extract content from a message part.""" + # If this is multipart, process with preference for text/plain + if payload.get("parts"): + # First try to find text/plain part + for part in payload["parts"]: + mime_type = part.get("mimeType", "") + if mime_type == "text/plain" and part.get("body", {}).get("data"): + data = part["body"]["data"] + return base64.urlsafe_b64decode(data).decode("utf-8") + + # If no text/plain found, try text/html + for part in payload["parts"]: + mime_type = part.get("mimeType", "") + if mime_type == "text/html" and part.get("body", {}).get("data"): + data = part["body"]["data"] + return base64.urlsafe_b64decode(data).decode("utf-8") + + # If we still haven't found content, recursively check for nested parts + for part in payload["parts"]: + content = extract_message_part(part) + if content: + return content + + # Not multipart, try to get content directly + if payload.get("body", {}).get("data"): + data = payload["body"]["data"] + return base64.urlsafe_b64decode(data).decode("utf-8") + + return "" + +def load_gmail_credentials(): + """ + Load Gmail credentials from token.json or environment variables. + + This function attempts to load credentials from multiple sources in this order: + 1. Environment variables GMAIL_TOKEN + 2. Local file at token_path (.secrets/token.json) + + Returns: + Google OAuth2 Credentials object or None if credentials can't be loaded + """ + token_data = None + + # 1. Try environment variable + env_token = os.getenv("GMAIL_TOKEN") + if env_token: + try: + token_data = json.loads(env_token) + print("Using GMAIL_TOKEN environment variable") + except Exception as e: + print(f"Could not parse GMAIL_TOKEN environment variable: {str(e)}") + + # 2. Try local file as fallback + if token_data is None: + if TOKEN_PATH.exists(): + try: + with open(TOKEN_PATH, "r") as f: + token_data = json.load(f) + print(f"Using token from {TOKEN_PATH}") + except Exception as e: + print(f"Could not load token from {TOKEN_PATH}: {str(e)}") + else: + print(f"Token file not found at {TOKEN_PATH}") + + # If we couldn't get token data from any source, return None + if token_data is None: + print("Could not find valid token data in any location") + return None + + try: + # Create credentials object + credentials = Credentials( + token=token_data.get("token"), + refresh_token=token_data.get("refresh_token"), + token_uri=token_data.get("token_uri", "https://oauth2.googleapis.com/token"), + client_id=token_data.get("client_id"), + client_secret=token_data.get("client_secret"), + scopes=token_data.get("scopes", ["https://www.googleapis.com/auth/gmail.modify"]) + ) + return credentials + except Exception as e: + print(f"Error creating credentials object: {str(e)}") + return None + +def extract_email_data(message): + """Extract key information from a Gmail message.""" + headers = message['payload']['headers'] + + # Extract key headers + subject = next((h['value'] for h in headers if h['name'] == 'Subject'), 'No Subject') + from_email = next((h['value'] for h in headers if h['name'] == 'From'), 'Unknown Sender') + to_email = next((h['value'] for h in headers if h['name'] == 'To'), 'Unknown Recipient') + date = next((h['value'] for h in headers if h['name'] == 'Date'), 'Unknown Date') + + # Extract message content + content = extract_message_part(message['payload']) + + # Create email data object + email_data = { + "from_email": from_email, + "to_email": to_email, + "subject": subject, + "page_content": content, + "id": message['id'], + "thread_id": message['threadId'], + "send_time": date + } + + return email_data + +async def ingest_email_to_langgraph(email_data, graph_name, url="http://127.0.0.1:2024"): + """Ingest an email to LangGraph.""" + # Connect to LangGraph server + client = get_client(url=url) + + # Create a consistent UUID for the thread + raw_thread_id = email_data["thread_id"] + thread_id = str( + uuid.UUID(hex=hashlib.md5(raw_thread_id.encode("UTF-8")).hexdigest()) + ) + print(f"Gmail thread ID: {raw_thread_id} → LangGraph thread ID: {thread_id}") + + thread_exists = False + try: + # Try to get existing thread info + thread_info = await client.threads.get(thread_id) + thread_exists = True + print(f"Found existing thread: {thread_id}") + except Exception as e: + # If thread doesn't exist, create it + print(f"Creating new thread: {thread_id}") + thread_info = await client.threads.create(thread_id=thread_id) + + # If thread exists, clean up previous runs + if thread_exists: + try: + # List all runs for this thread + runs = await client.runs.list(thread_id) + + # Delete all previous runs to avoid state accumulation + for run_info in runs: + run_id = run_info.id + print(f"Deleting previous run {run_id} from thread {thread_id}") + try: + await client.runs.delete(thread_id, run_id) + except Exception as e: + print(f"Failed to delete run {run_id}: {str(e)}") + except Exception as e: + print(f"Error listing/deleting runs: {str(e)}") + + # Update thread metadata with current email ID + await client.threads.update(thread_id, metadata={"email_id": email_data["id"]}) + + # Create a fresh run for this email + print(f"Creating run for thread {thread_id} with graph {graph_name}") + + run = await client.runs.create( + thread_id, + graph_name, + input={"email_input": { + "from": email_data["from_email"], + "to": email_data["to_email"], + "subject": email_data["subject"], + "body": email_data["page_content"], + "id": email_data["id"] + }}, + multitask_strategy="rollback", + ) + + print(f"Run created successfully with thread ID: {thread_id}") + + return thread_id, run + +async def fetch_and_process_emails(args): + """Fetch emails from Gmail and process them through LangGraph.""" + # Load Gmail credentials + credentials = load_gmail_credentials() + if not credentials: + print("Failed to load Gmail credentials") + return 1 + + # Build Gmail service + service = build("gmail", "v1", credentials=credentials) + + # Process emails + processed_count = 0 + + try: + # Get messages from the specified email address + email_address = args.email + + # Construct Gmail search query + query = f"to:{email_address} OR from:{email_address}" + + # Add time constraint if specified + if args.minutes_since > 0: + # Calculate timestamp for filtering + from datetime import timedelta + after = int((datetime.now() - timedelta(minutes=args.minutes_since)).timestamp()) + query += f" after:{after}" + + # Only include unread emails unless include_read is True + if not args.include_read: + query += " is:unread" + + print(f"Gmail search query: {query}") + + # Execute the search + results = service.users().messages().list(userId="me", q=query).execute() + messages = results.get("messages", []) + + if not messages: + print("No emails found matching the criteria") + return 0 + + print(f"Found {len(messages)} emails") + + # Process each email + for i, message_info in enumerate(messages): + # Stop early if requested + if args.early and i > 0: + print(f"Early stop after processing {i} emails") + break + + # Check if we should reprocess this email + if not args.rerun: + # TODO: Add check for already processed emails + pass + + # Get the full message + message = service.users().messages().get(userId="me", id=message_info["id"]).execute() + + # Extract email data + email_data = extract_email_data(message) + + print(f"\nProcessing email {i+1}/{len(messages)}:") + print(f"From: {email_data['from_email']}") + print(f"Subject: {email_data['subject']}") + + # Ingest to LangGraph + thread_id, run = await ingest_email_to_langgraph( + email_data, + args.graph_name, + url=args.url + ) + + processed_count += 1 + + print(f"\nProcessed {processed_count} emails successfully") + return 0 + + except Exception as e: + print(f"Error processing emails: {str(e)}") + return 1 + +def parse_args(): + """Parse command line arguments.""" + parser = argparse.ArgumentParser(description="Simple Gmail ingestion for LangGraph with reliable tracing") + + parser.add_argument( + "--email", + type=str, + required=True, + help="Email address to fetch messages for" + ) + parser.add_argument( + "--minutes-since", + type=int, + default=120, + help="Only retrieve emails newer than this many minutes" + ) + parser.add_argument( + "--graph-name", + type=str, + default="email_assistant_hitl_memory_gmail", + help="Name of the LangGraph to use" + ) + parser.add_argument( + "--url", + type=str, + default="http://127.0.0.1:2024", + help="URL of the LangGraph deployment" + ) + parser.add_argument( + "--early", + action="store_true", + help="Early stop after processing one email" + ) + parser.add_argument( + "--include-read", + action="store_true", + help="Include emails that have already been read" + ) + parser.add_argument( + "--rerun", + action="store_true", + help="Process the same emails again even if already processed" + ) + parser.add_argument( + "--skip-filters", + action="store_true", + help="Skip filtering of emails" + ) + return parser.parse_args() + +if __name__ == "__main__": + # Get command line arguments + args = parse_args() + + # Run the script + exit(asyncio.run(fetch_and_process_emails(args))) \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/gmail/setup_cron.py b/personal_assistant/src/personal_assistant/tools/gmail/setup_cron.py new file mode 100644 index 0000000..6f6d835 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/gmail/setup_cron.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +""" +Setup cron job for email ingestion in LangGraph. + +This script creates a scheduled cron job in LangGraph that periodically +runs the email ingestion graph to process new emails. +""" + +import argparse +import asyncio +from typing import Optional +from langgraph_sdk import get_client +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +async def main( + email: str, + url: Optional[str] = None, + minutes_since: int = 60, + schedule: str = "*/10 * * * *", + graph_name: str = "email_assistant_hitl_memory_gmail", + include_read: bool = False, +): + """Set up a cron job for email ingestion""" + # Connect to LangGraph server + if url is None: + client = get_client(url="http://127.0.0.1:2024") + else: + client = get_client(url=url) + + # Create cron job configuration + cron_input = { + "email": email, + "minutes_since": minutes_since, + "graph_name": graph_name, + "url": url if url else "http://127.0.0.1:2024", + "include_read": include_read, + "rerun": False, + "early": False, + "skip_filters": False + } + + # Register the cron job + cron = await client.crons.create( + "cron", # The graph name for the cron + schedule=schedule, # Cron schedule expression + input=cron_input # Input parameters for the cron graph + ) + + print(f"Cron job created successfully with schedule: {schedule}") + print(f"Email ingestion will run for: {email}") + print(f"Processing emails from the past {minutes_since} minutes") + print(f"Using graph: {graph_name}") + + return cron + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Set up a cron job for email ingestion in LangGraph") + + parser.add_argument( + "--email", + type=str, + required=True, + help="Email address to fetch messages for", + ) + parser.add_argument( + "--url", + type=str, + required=True, + help="URL to the LangGraph server", + ) + parser.add_argument( + "--minutes-since", + type=int, + default=60, + help="Only process emails that are less than this many minutes old", + ) + parser.add_argument( + "--schedule", + type=str, + default="*/10 * * * *", + help="Cron schedule expression (default: every 10 minutes)", + ) + parser.add_argument( + "--graph-name", + type=str, + default="email_assistant_hitl_memory_gmail", + help="Name of the graph to use for processing emails", + ) + parser.add_argument( + "--include-read", + action="store_true", + help="Include emails that have already been read", + ) + + args = parser.parse_args() + + asyncio.run( + main( + email=args.email, + url=args.url, + minutes_since=args.minutes_since, + schedule=args.schedule, + graph_name=args.graph_name, + include_read=args.include_read, + ) + ) \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py b/personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py new file mode 100644 index 0000000..a5a1a96 --- /dev/null +++ b/personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +""" +Setup script for Gmail API integration. + +This script handles the OAuth flow for Gmail API access by: +1. Creating a .secrets directory if it doesn't exist +2. Using credentials from .secrets/secrets.json to authenticate +3. Opening a browser window for user authentication +4. Storing the access token in .secrets/token.json +""" + +import os +import sys +import json +from pathlib import Path + +# Add project root to sys.path for imports to work correctly +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../"))) + +# Import required Google libraries +from google_auth_oauthlib.flow import InstalledAppFlow +from google.oauth2.credentials import Credentials + +def main(): + """Run Gmail authentication setup.""" + # Create .secrets directory + secrets_dir = Path(__file__).parent.absolute() / ".secrets" + secrets_dir.mkdir(parents=True, exist_ok=True) + + # Check for secrets.json + secrets_path = secrets_dir / "secrets.json" + if not secrets_path.exists(): + print(f"Error: Client secrets file not found at {secrets_path}") + print("Please download your OAuth client ID JSON from Google Cloud Console") + print("and save it as .secrets/secrets.json") + return 1 + + print("Starting Gmail API authentication flow...") + print("A browser window will open for you to authorize access.") + + # This will trigger the OAuth flow and create token.json + try: + # Define the scopes we need + SCOPES = [ + 'https://www.googleapis.com/auth/gmail.modify', + 'https://www.googleapis.com/auth/calendar' + ] + + # Load client secrets + with open(secrets_path, 'r') as f: + client_config = json.load(f) + + # Create the flow using the client_secrets.json format + flow = InstalledAppFlow.from_client_secrets_file( + str(secrets_path), + SCOPES + ) + + # Run the OAuth flow + credentials = flow.run_local_server(port=0) + + # Save the credentials to token.json + token_path = secrets_dir / "token.json" + token_data = { + 'token': credentials.token, + 'refresh_token': credentials.refresh_token, + 'token_uri': credentials.token_uri, + 'client_id': credentials.client_id, + 'client_secret': credentials.client_secret, + 'scopes': credentials.scopes, + 'universe_domain': 'googleapis.com', + 'account': '', + 'expiry': credentials.expiry.isoformat() + "Z" + } + + with open(token_path, 'w') as token_file: + json.dump(token_data, token_file) + + print("\nAuthentication successful!") + print(f"Access token stored at {token_path}") + return 0 + except Exception as e: + print(f"Authentication failed: {str(e)}") + return 1 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/personal_assistant/src/personal_assistant/utils.py b/personal_assistant/src/personal_assistant/utils.py new file mode 100644 index 0000000..6bee370 --- /dev/null +++ b/personal_assistant/src/personal_assistant/utils.py @@ -0,0 +1,367 @@ +from typing import List, Any +import json +import html2text +from langchain.chat_models import init_chat_model +from langgraph.store.base import BaseStore + +from .schemas import UserPreferences +from .prompts import MEMORY_UPDATE_INSTRUCTIONS + +def format_email_markdown(subject, author, to, email_thread, email_id=None): + """Format email details into a nicely formatted markdown string for display + + Args: + subject: Email subject + author: Email sender + to: Email recipient + email_thread: Email content + email_id: Optional email ID (for Gmail API) + """ + id_section = f"\n**ID**: {email_id}" if email_id else "" + + return f""" + +**Subject**: {subject} +**From**: {author} +**To**: {to}{id_section} + +{email_thread} + +--- +""" + +def format_gmail_markdown(subject, author, to, email_thread, email_id=None): + """Format Gmail email details into a nicely formatted markdown string for display, + with HTML to text conversion for HTML content + + Args: + subject: Email subject + author: Email sender + to: Email recipient + email_thread: Email content (possibly HTML) + email_id: Optional email ID (for Gmail API) + """ + id_section = f"\n**ID**: {email_id}" if email_id else "" + + # Check if email_thread is HTML content and convert to text if needed + if email_thread and (email_thread.strip().startswith(" dict: + """Parse an email input dictionary. + + Args: + email_input (dict): Dictionary containing email fields: + - author: Sender's name and email + - to: Recipient's name and email + - subject: Email subject line + - email_thread: Full email content + + Returns: + tuple[str, str, str, str]: Tuple containing: + - author: Sender's name and email + - to: Recipient's name and email + - subject: Email subject line + - email_thread: Full email content + """ + return ( + email_input["author"], + email_input["to"], + email_input["subject"], + email_input["email_thread"], + ) + +def parse_gmail(email_input: dict) -> tuple[str, str, str, str, str]: + """Parse an email input dictionary for Gmail, including the email ID. + + This function extends parse_email by also returning the email ID, + which is used specifically in the Gmail integration. + + Args: + email_input (dict): Dictionary containing email fields in any of these formats: + Gmail schema: + - From: Sender's email + - To: Recipient's email + - Subject: Email subject line + - Body: Full email content + - Id: Gmail message ID + + Returns: + tuple[str, str, str, str, str]: Tuple containing: + - author: Sender's name and email + - to: Recipient's name and email + - subject: Email subject line + - email_thread: Full email content + - email_id: Email ID (or None if not available) + """ + + print("!Email_input from Gmail!") + print(email_input) + + # Gmail schema + return ( + email_input["from"], + email_input["to"], + email_input["subject"], + email_input["body"], + email_input["id"], + ) + +def extract_message_content(message) -> str: + """Extract content from different message types as clean string. + + Args: + message: A message object (HumanMessage, AIMessage, ToolMessage) + + Returns: + str: Extracted content as clean string + """ + content = message.content + + # Check for recursion marker in string + if isinstance(content, str) and ' List[str]: + """Extract tool call names from messages, safely handling messages without tool_calls.""" + tool_call_names = [] + for message in messages: + # Check if message is a dict and has tool_calls + if isinstance(message, dict) and message.get("tool_calls"): + tool_call_names.extend([call["name"].lower() for call in message["tool_calls"]]) + # Check if message is an object with tool_calls attribute + elif hasattr(message, "tool_calls") and message.tool_calls: + tool_call_names.extend([call["name"].lower() for call in message.tool_calls]) + + return tool_call_names + +def format_messages_string(messages: List[Any]) -> str: + """Format messages into a single string for analysis.""" + return '\n'.join(message.pretty_repr() for message in messages) + +def show_graph(graph, xray=False): + """Display a LangGraph mermaid diagram with fallback rendering. + + Handles timeout errors from mermaid.ink by falling back to pyppeteer. + + Args: + graph: The LangGraph object that has a get_graph() method + """ + from IPython.display import Image + try: + # Try the default renderer first + return Image(graph.get_graph(xray=xray).draw_mermaid_png()) + except Exception as e: + # Fall back to pyppeteer if the default renderer fails + import nest_asyncio + nest_asyncio.apply() + from langchain_core.runnables.graph import MermaidDrawMethod + return Image(graph.get_graph().draw_mermaid_png(draw_method=MermaidDrawMethod.PYPPETEER)) + +def get_memory(store: BaseStore, namespace: tuple, default_content: str | None = None) -> str: + """Get memory from the store or initialize with default if it doesn't exist. + + Args: + store: LangGraph BaseStore instance to search for existing memory + namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") + default_content: Default content to use if memory doesn't exist + + Returns: + str: The content of the memory profile, either from existing memory or the default + """ + # Search for existing memory with namespace and key + user_preferences = store.get(namespace, "user_preferences") + + # If memory exists, return its content (the value) + if user_preferences: + return user_preferences.value + + # If memory doesn't exist, add it to the store and return the default content + else: + # Namespace, key, value + store.put(namespace, "user_preferences", default_content) + user_preferences = default_content + + # Return the default content + return user_preferences + +def update_memory(store: BaseStore, namespace: tuple, messages: list): + """Update memory profile in the store. + + Args: + store: LangGraph BaseStore instance to update memory + namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") + messages: List of messages to update the memory with + """ + + # Get the existing memory + user_preferences = store.get(namespace, "user_preferences") + # Update the memory + llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0).with_structured_output(UserPreferences) + result = llm.invoke( + [ + {"role": "system", "content": MEMORY_UPDATE_INSTRUCTIONS.format(current_profile=user_preferences.value, namespace=namespace)}, + ] + messages + ) + # Save the updated memory to the store + store.put(namespace, "user_preferences", result.user_preferences) + +async def aget_memory(store: BaseStore, namespace: tuple, default_content: str | None = None) -> str: + """Async version of get_memory for async contexts. + + Args: + store: LangGraph BaseStore instance to search for existing memory + namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") + default_content: Default content to use if memory doesn't exist + + Returns: + str: The content of the memory profile, either from existing memory or the default + """ + # Search for existing memory with namespace and key + user_preferences = await store.aget(namespace, "user_preferences") + + # If memory exists, return its content (the value) + if user_preferences: + return user_preferences.value + + # If memory doesn't exist, add it to the store and return the default content + else: + # Namespace, key, value + await store.aput(namespace, "user_preferences", default_content) + user_preferences = default_content + + # Return the default content + return user_preferences + +async def aupdate_memory(store: BaseStore, namespace: tuple, messages: list): + """Async version of update_memory for async contexts. + + Args: + store: LangGraph BaseStore instance to update memory + namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") + messages: List of messages to update the memory with + """ + + # Get the existing memory + user_preferences = await store.aget(namespace, "user_preferences") + # Update the memory + llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0).with_structured_output(UserPreferences) + result = await llm.ainvoke( + [ + {"role": "system", "content": MEMORY_UPDATE_INSTRUCTIONS.format(current_profile=user_preferences.value, namespace=namespace)}, + ] + messages + ) + # Save the updated memory to the store + await store.aput(namespace, "user_preferences", result.user_preferences) \ No newline at end of file diff --git a/personal_assistant/test.ipynb b/personal_assistant/test.ipynb new file mode 100644 index 0000000..1f121f0 --- /dev/null +++ b/personal_assistant/test.ipynb @@ -0,0 +1,1830 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "header", + "metadata": {}, + "source": [ + "# Personal Assistant Testing\n", + "\n", + "Test the email assistant with HITL and memory using deepagents." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "from personal_assistant import create_email_assistant\n", + "from personal_assistant.utils import format_email_markdown, parse_email\n", + "from personal_assistant.ntbk_utils import format_messages, show_prompt" + ] + }, + { + "cell_type": "markdown", + "id": "setup", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Create the email assistant agent." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "create_agent", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Agent created successfully\n" + ] + } + ], + "source": [ + "# Create agent\n", + "agent = create_email_assistant()\n", + "print(\"✓ Agent created successfully\")" + ] + }, + { + "cell_type": "markdown", + "id": "example1", + "metadata": {}, + "source": [ + "## Example 1: Meeting Request\n", + "\n", + "Test the agent with a meeting request email." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "0qvtwkqfbazq", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Helper function loaded\n" + ] + } + ], + "source": [ + "# Helper function to resume with a decision\n", + "from langgraph.types import Command\n", + "\n", + "def resume_with_decision(agent, config, decision_type=\"accept\", edited_args=None, feedback=None):\n", + " \"\"\"Resume the agent after an interrupt with a user decision.\n", + " \n", + " Args:\n", + " agent: The agent instance\n", + " config: Config with thread_id\n", + " decision_type: \"accept\", \"edit\", \"ignore\", or \"response\"\n", + " edited_args: For \"edit\" - the modified arguments\n", + " feedback: For \"response\" - user feedback text\n", + " \"\"\"\n", + " if decision_type == \"accept\":\n", + " decision = {\"type\": \"accept\"}\n", + " elif decision_type == \"edit\":\n", + " decision = {\"type\": \"edit\", \"args\": {\"args\": edited_args}}\n", + " elif decision_type == \"ignore\":\n", + " decision = {\"type\": \"ignore\"}\n", + " elif decision_type == \"response\":\n", + " decision = {\"type\": \"response\", \"args\": feedback}\n", + " else:\n", + " raise ValueError(f\"Invalid decision type: {decision_type}\")\n", + " \n", + " # Resume with the decision\n", + " result = agent.invoke(\n", + " Command(resume=[decision]),\n", + " config=config,\n", + " )\n", + " return result\n", + " \n", + "print(\"✓ Helper function loaded\")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "example1_email", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── 📧 Email to Process ──────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "  **Subject**: Quick question about next week                                                                    \n",
+       "  **From**: jane@example.com                                                                                     \n",
+       "  **To**: lance@langchain.dev                                                                                    \n",
+       "                                                                                                                 \n",
+       "  Hi Lance,                                                                                                      \n",
+       "                                                                                                                 \n",
+       "  Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                \n",
+       "                                                                                                                 \n",
+       "  Best,                                                                                                          \n",
+       "  Jane                                                                                                           \n",
+       "                                                                                                                 \n",
+       "  ---                                                                                                            \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Email to Process\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **Subject**: Quick question about next week \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **From**: jane@example.com \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **To**: lance@langchain.dev \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Hi Lance, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Best, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Jane \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m --- \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Example email input\n", + "email_input_1 = {\n", + " \"author\": \"jane@example.com\",\n", + " \"to\": \"lance@langchain.dev\",\n", + " \"subject\": \"Quick question about next week\",\n", + " \"email_thread\": \"Hi Lance,\\n\\nCan we meet next Tuesday at 2pm to discuss the project roadmap?\\n\\nBest,\\nJane\",\n", + "}\n", + "\n", + "# Format email\n", + "author, to, subject, email_thread = parse_email(email_input_1)\n", + "email_markdown = format_email_markdown(subject, author, to, email_thread)\n", + "\n", + "# Display email with rich formatting\n", + "show_prompt(email_markdown, title=\"📧 Email to Process\", border_style=\"cyan\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "example1_invoke", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "Agent Response\n", + "============================================================\n", + "\n", + "🛑 INTERRUPT: Agent is waiting for your approval\n", + "\n", + "Action: schedule_meeting\n", + "Args: {'attendees': ['jane@example.com'], 'subject': 'Project Roadmap Discussion', 'duration_minutes': 30, 'preferred_day': '2025-12-09T14:00:00', 'start_time': 14}\n", + "\n", + "Allowed actions: {'allow_ignore': True, 'allow_respond': True, 'allow_edit': True, 'allow_accept': True}\n", + "\n", + "Description:\n" + ] + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────── 📋 Action Details ───────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "  # Calendar Invite                                                                                              \n",
+       "                                                                                                                 \n",
+       "  **Meeting**: Project Roadmap Discussion                                                                        \n",
+       "  **Attendees**: jane@example.com                                                                                \n",
+       "  **Duration**: 30 minutes                                                                                       \n",
+       "  **Day**: 2025-12-09T14:00:00                                                                                   \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m \u001b[0m\u001b[1;32m📋 Action Details\u001b[0m\u001b[33m \u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m # Calendar Invite \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m **Meeting**: Project Roadmap Discussion \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m **Attendees**: jane@example.com \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m **Duration**: 30 minutes \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m **Day**: 2025-12-09T14:00:00 \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------------------------------------------------------------\n", + "\n", + "💡 To continue, you need to resume the agent with a decision.\n", + " Use: agent.invoke(None, config=config_1)\n" + ] + } + ], + "source": [ + "# Configure thread\n", + "config_1 = {\"configurable\": {\"thread_id\": \"test-thread-1\"}}\n", + "\n", + "# Invoke agent\n", + "result_1 = agent.invoke(\n", + " {\n", + " \"messages\": [{\"role\": \"user\", \"content\": email_markdown}],\n", + " \"email_input\": email_input_1,\n", + " },\n", + " config=config_1,\n", + ")\n", + "\n", + "# Display result with rich formatting\n", + "print(\"\\n\" + \"=\"*60)\n", + "print(\"Agent Response\")\n", + "print(\"=\"*60 + \"\\n\")\n", + "\n", + "# Check for interrupts (HITL)\n", + "if \"__interrupt__\" in result_1:\n", + " print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n", + " \n", + " for interrupt in result_1[\"__interrupt__\"]:\n", + " for request in interrupt.value:\n", + " print(\"Action:\", request[\"action_request\"][\"action\"])\n", + " print(\"Args:\", request[\"action_request\"][\"args\"])\n", + " print(\"\\nAllowed actions:\", request[\"config\"])\n", + " print(\"\\nDescription:\")\n", + " show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n", + " print(\"\\n\" + \"-\"*60)\n", + " \n", + " print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n", + " print(\" Use: agent.invoke(None, config=config_1)\")\n", + "else:\n", + " format_messages(result_1[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "15f8dc1d-8a22-46ad-8ee6-0d353936b514", + "metadata": {}, + "source": [ + "### Example: Resume by accepting the action" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "63fd68c2-b086-49b5-84fb-67b8a3e3e4dc", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       " **Subject**: Quick question about next week                                                                     \n",
+       " **From**: jane@example.com                                                                                      \n",
+       " **To**: lance@langchain.dev                                                                                     \n",
+       "                                                                                                                 \n",
+       " Hi Lance,                                                                                                       \n",
+       "                                                                                                                 \n",
+       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
+       "                                                                                                                 \n",
+       " Best,                                                                                                           \n",
+       " Jane                                                                                                            \n",
+       "                                                                                                                 \n",
+       " ---                                                                                                             \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01HLFiUonMHpa96sjmnVdTam                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01HLFiUonMHpa96sjmnVdTam \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to    \n",
+       " Jane.                                                                                                           \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Jane. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in       \n",
+       " before it could be completed.                                                                                   \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       " **Subject**: Quick question about next week                                                                     \n",
+       " **From**: jane@example.com                                                                                      \n",
+       " **To**: lance@langchain.dev                                                                                     \n",
+       "                                                                                                                 \n",
+       " Hi Lance,                                                                                                       \n",
+       "                                                                                                                 \n",
+       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
+       "                                                                                                                 \n",
+       " Best,                                                                                                           \n",
+       " Jane                                                                                                            \n",
+       "                                                                                                                 \n",
+       " ---                                                                                                             \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01We2XxG49E7jtJZhPJiVBGU                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01We2XxG49E7jtJZhPJiVBGU \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1        \n",
+       " attendees                                                                                                       \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1 \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m attendees \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Great! Now let me send a response email to Jane confirming the meeting.                                         \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: write_email                                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"to\": \"jane@example.com\",                                                                                     \n",
+       "   \"subject\": \"Re: Quick question about next week\",                                                              \n",
+       "   \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute  \n",
+       " meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01WmATRwvRPPmmZkoSDkYPxt                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Great! Now let me send a response email to Jane confirming the meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: write_email \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"to\": \"jane@example.com\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Re: Quick question about next week\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01WmATRwvRPPmmZkoSDkYPxt \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "result_1_continued = resume_with_decision(agent, config_1, decision_type=\"accept\")\n", + "format_messages(result_1_continued[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "99a2758e-d3a2-48ff-b365-7dfd2d4e1ac7", + "metadata": {}, + "source": [ + "### Example: Resume by editing" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "30f05395-a0ef-4507-bde9-25e240277ec3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       " **Subject**: Quick question about next week                                                                     \n",
+       " **From**: jane@example.com                                                                                      \n",
+       " **To**: lance@langchain.dev                                                                                     \n",
+       "                                                                                                                 \n",
+       " Hi Lance,                                                                                                       \n",
+       "                                                                                                                 \n",
+       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
+       "                                                                                                                 \n",
+       " Best,                                                                                                           \n",
+       " Jane                                                                                                            \n",
+       "                                                                                                                 \n",
+       " ---                                                                                                             \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01HLFiUonMHpa96sjmnVdTam                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01HLFiUonMHpa96sjmnVdTam \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to    \n",
+       " Jane.                                                                                                           \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Jane. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in       \n",
+       " before it could be completed.                                                                                   \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       " **Subject**: Quick question about next week                                                                     \n",
+       " **From**: jane@example.com                                                                                      \n",
+       " **To**: lance@langchain.dev                                                                                     \n",
+       "                                                                                                                 \n",
+       " Hi Lance,                                                                                                       \n",
+       "                                                                                                                 \n",
+       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
+       "                                                                                                                 \n",
+       " Best,                                                                                                           \n",
+       " Jane                                                                                                            \n",
+       "                                                                                                                 \n",
+       " ---                                                                                                             \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01We2XxG49E7jtJZhPJiVBGU                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01We2XxG49E7jtJZhPJiVBGU \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1        \n",
+       " attendees                                                                                                       \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1 \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m attendees \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Great! Now let me send a response email to Jane confirming the meeting.                                         \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: write_email                                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"to\": \"jane@example.com\",                                                                                     \n",
+       "   \"subject\": \"Re: Quick question about next week\",                                                              \n",
+       "   \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute  \n",
+       " meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01WmATRwvRPPmmZkoSDkYPxt                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Great! Now let me send a response email to Jane confirming the meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: write_email \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"to\": \"jane@example.com\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Re: Quick question about next week\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01WmATRwvRPPmmZkoSDkYPxt \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Tool call write_email with id toolu_01WmATRwvRPPmmZkoSDkYPxt was cancelled - another message came in before it  \n",
+       " could be completed.                                                                                             \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Tool call write_email with id toolu_01WmATRwvRPPmmZkoSDkYPxt was cancelled - another message came in before it \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       " **Subject**: Quick question about next week                                                                     \n",
+       " **From**: jane@example.com                                                                                      \n",
+       " **To**: lance@langchain.dev                                                                                     \n",
+       "                                                                                                                 \n",
+       " Hi Lance,                                                                                                       \n",
+       "                                                                                                                 \n",
+       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
+       "                                                                                                                 \n",
+       " Best,                                                                                                           \n",
+       " Jane                                                                                                            \n",
+       "                                                                                                                 \n",
+       " ---                                                                                                             \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01JGcGsEQ5jUtK1pHAv6M9Kw                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01JGcGsEQ5jUtK1pHAv6M9Kw \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_0126iavpuerW8BLF6jey4uKt                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_0126iavpuerW8BLF6jey4uKt \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Tool call schedule_meeting with id toolu_0126iavpuerW8BLF6jey4uKt was cancelled - another message came in       \n",
+       " before it could be completed.                                                                                   \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_0126iavpuerW8BLF6jey4uKt was cancelled - another message came in \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       " **Subject**: Quick question about next week                                                                     \n",
+       " **From**: jane@example.com                                                                                      \n",
+       " **To**: lance@langchain.dev                                                                                     \n",
+       "                                                                                                                 \n",
+       " Hi Lance,                                                                                                       \n",
+       "                                                                                                                 \n",
+       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
+       "                                                                                                                 \n",
+       " Best,                                                                                                           \n",
+       " Jane                                                                                                            \n",
+       "                                                                                                                 \n",
+       " ---                                                                                                             \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_015MCueda45cdMN5XkZyRxou                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_015MCueda45cdMN5XkZyRxou \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_017JvuuDJKQjTRcNbDrRURoY                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_017JvuuDJKQjTRcNbDrRURoY \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Meeting 'Project Roadmap Discussion - UPDATED' scheduled on Tuesday, December 09, 2025 at 14 for 45 minutes     \n",
+       " with 1 attendees                                                                                                \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion - UPDATED' scheduled on Tuesday, December 09, 2025 at 14 for 45 minutes \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m with 1 attendees \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Now let me send a response email to Jane confirming the meeting.                                                \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: write_email                                                                                       \n",
+       "    Args: {                                                                                                      \n",
+       "   \"to\": \"jane@example.com\",                                                                                     \n",
+       "   \"subject\": \"Re: Quick question about next week\",                                                              \n",
+       "   \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 45-minute  \n",
+       " meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01Ckz5kut6xXCxU1PrzU9rhS                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Now let me send a response email to Jane confirming the meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: write_email \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"to\": \"jane@example.com\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Re: Quick question about next week\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 45-minute \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01Ckz5kut6xXCxU1PrzU9rhS \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "edited_args = {\n", + " \"attendees\": [\"jane@example.com\"],\n", + " \"subject\": \"Project Roadmap Discussion - UPDATED\",\n", + " \"duration_minutes\": 45, # Changed from 30 to 45\n", + " \"preferred_day\": \"2025-12-09T14:00:00\",\n", + " \"start_time\": 14\n", + "}\n", + "\n", + "result_1_continued = resume_with_decision(agent, config_1, decision_type=\"edit\", edited_args=edited_args)\n", + "format_messages(result_1_continued[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "92831797-bf28-4901-9a7d-7568dd5d12fb", + "metadata": {}, + "source": [ + "### Example: Resume by ignoring" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "9ipquhkakha", + "metadata": {}, + "outputs": [ + { + "ename": "BadRequestError", + "evalue": "Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.24: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_015WrAQ82bzbKUbET1uhfixd. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'}, 'request_id': 'req_011CViX58yA9hzBebZGRqkgP'}", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mBadRequestError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[17]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m result_1_continued = \u001b[43mresume_with_decision\u001b[49m\u001b[43m(\u001b[49m\u001b[43magent\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig_1\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdecision_type\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mignore\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 2\u001b[39m format_messages(result_1_continued[\u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m])\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 26\u001b[39m, in \u001b[36mresume_with_decision\u001b[39m\u001b[34m(agent, config, decision_type, edited_args, feedback)\u001b[39m\n\u001b[32m 23\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mInvalid decision type: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdecision_type\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 25\u001b[39m \u001b[38;5;66;03m# Resume with the decision\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m26\u001b[39m result = \u001b[43magent\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43mCommand\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresume\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[43mdecision\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py:3068\u001b[39m, in \u001b[36mPregel.invoke\u001b[39m\u001b[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, **kwargs)\u001b[39m\n\u001b[32m 3065\u001b[39m chunks: \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any] | Any] = []\n\u001b[32m 3066\u001b[39m interrupts: \u001b[38;5;28mlist\u001b[39m[Interrupt] = []\n\u001b[32m-> \u001b[39m\u001b[32m3068\u001b[39m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 3069\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 3070\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3071\u001b[39m \u001b[43m \u001b[49m\u001b[43mcontext\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcontext\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3072\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mupdates\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[32m 3073\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m \u001b[49m\u001b[43m==\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\n\u001b[32m 3074\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3075\u001b[39m \u001b[43m \u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m=\u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3076\u001b[39m \u001b[43m \u001b[49m\u001b[43moutput_keys\u001b[49m\u001b[43m=\u001b[49m\u001b[43moutput_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3077\u001b[39m \u001b[43m \u001b[49m\u001b[43minterrupt_before\u001b[49m\u001b[43m=\u001b[49m\u001b[43minterrupt_before\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3078\u001b[39m \u001b[43m \u001b[49m\u001b[43minterrupt_after\u001b[49m\u001b[43m=\u001b[49m\u001b[43minterrupt_after\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3079\u001b[39m \u001b[43m \u001b[49m\u001b[43mdurability\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdurability\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3080\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3081\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 3082\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m \u001b[49m\u001b[43m==\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\n\u001b[32m 3083\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[43m==\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m2\u001b[39;49m\u001b[43m:\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py:2643\u001b[39m, in \u001b[36mPregel.stream\u001b[39m\u001b[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, subgraphs, debug, **kwargs)\u001b[39m\n\u001b[32m 2641\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m task \u001b[38;5;129;01min\u001b[39;00m loop.match_cached_writes():\n\u001b[32m 2642\u001b[39m loop.output_writes(task.id, task.writes, cached=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m-> \u001b[39m\u001b[32m2643\u001b[39m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrunner\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtick\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2644\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtasks\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalues\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m.\u001b[49m\u001b[43mwrites\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2645\u001b[39m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstep_timeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2646\u001b[39m \u001b[43m \u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m=\u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2647\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43maccept_push\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2648\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 2649\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# emit output\u001b[39;49;00m\n\u001b[32m 2650\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield from\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_output\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2651\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msubgraphs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mqueue\u001b[49m\u001b[43m.\u001b[49m\u001b[43mEmpty\u001b[49m\n\u001b[32m 2652\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 2653\u001b[39m loop.after_tick()\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py:167\u001b[39m, in \u001b[36mPregelRunner.tick\u001b[39m\u001b[34m(self, tasks, reraise, timeout, retry_policy, get_waiter, schedule_task)\u001b[39m\n\u001b[32m 165\u001b[39m t = tasks[\u001b[32m0\u001b[39m]\n\u001b[32m 166\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m167\u001b[39m \u001b[43mrun_with_retry\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 168\u001b[39m \u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 169\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 170\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfigurable\u001b[49m\u001b[43m=\u001b[49m\u001b[43m{\u001b[49m\n\u001b[32m 171\u001b[39m \u001b[43m \u001b[49m\u001b[43mCONFIG_KEY_CALL\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mpartial\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 172\u001b[39m \u001b[43m \u001b[49m\u001b[43m_call\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 173\u001b[39m \u001b[43m \u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mt\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 174\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m=\u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 175\u001b[39m \u001b[43m \u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m=\u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 176\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 177\u001b[39m \u001b[43m \u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 178\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 179\u001b[39m \u001b[43m \u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 180\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 181\u001b[39m \u001b[38;5;28mself\u001b[39m.commit(t, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 182\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py:42\u001b[39m, in \u001b[36mrun_with_retry\u001b[39m\u001b[34m(task, retry_policy, configurable)\u001b[39m\n\u001b[32m 40\u001b[39m task.writes.clear()\n\u001b[32m 41\u001b[39m \u001b[38;5;66;03m# run the task\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m42\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43mproc\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43minput\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 43\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m ParentCommand \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 44\u001b[39m ns: \u001b[38;5;28mstr\u001b[39m = config[CONF][CONFIG_KEY_CHECKPOINT_NS]\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py:656\u001b[39m, in \u001b[36mRunnableSeq.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 654\u001b[39m \u001b[38;5;66;03m# run in context\u001b[39;00m\n\u001b[32m 655\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m set_config_context(config, run) \u001b[38;5;28;01mas\u001b[39;00m context:\n\u001b[32m--> \u001b[39m\u001b[32m656\u001b[39m \u001b[38;5;28minput\u001b[39m = \u001b[43mcontext\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 657\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 658\u001b[39m \u001b[38;5;28minput\u001b[39m = step.invoke(\u001b[38;5;28minput\u001b[39m, config)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py:400\u001b[39m, in \u001b[36mRunnableCallable.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 398\u001b[39m run_manager.on_chain_end(ret)\n\u001b[32m 399\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m400\u001b[39m ret = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 401\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.recurse \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(ret, Runnable):\n\u001b[32m 402\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ret.invoke(\u001b[38;5;28minput\u001b[39m, config)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:799\u001b[39m, in \u001b[36mToolNode._func\u001b[39m\u001b[34m(self, input, config, runtime)\u001b[39m\n\u001b[32m 797\u001b[39m input_types = [input_type] * \u001b[38;5;28mlen\u001b[39m(tool_calls)\n\u001b[32m 798\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m get_executor_for_config(config) \u001b[38;5;28;01mas\u001b[39;00m executor:\n\u001b[32m--> \u001b[39m\u001b[32m799\u001b[39m outputs = \u001b[38;5;28;43mlist\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[32m 800\u001b[39m \u001b[43m \u001b[49m\u001b[43mexecutor\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmap\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_run_one\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_calls\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minput_types\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_runtimes\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 801\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 803\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._combine_tool_outputs(outputs, input_type)\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:619\u001b[39m, in \u001b[36mExecutor.map..result_iterator\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 616\u001b[39m \u001b[38;5;28;01mwhile\u001b[39;00m fs:\n\u001b[32m 617\u001b[39m \u001b[38;5;66;03m# Careful not to keep a reference to the popped future\u001b[39;00m\n\u001b[32m 618\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m timeout \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m619\u001b[39m \u001b[38;5;28;01myield\u001b[39;00m \u001b[43m_result_or_cancel\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfs\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 620\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 621\u001b[39m \u001b[38;5;28;01myield\u001b[39;00m _result_or_cancel(fs.pop(), end_time - time.monotonic())\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:317\u001b[39m, in \u001b[36m_result_or_cancel\u001b[39m\u001b[34m(***failed resolving arguments***)\u001b[39m\n\u001b[32m 315\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 316\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m317\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfut\u001b[49m\u001b[43m.\u001b[49m\u001b[43mresult\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 318\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 319\u001b[39m fut.cancel()\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:456\u001b[39m, in \u001b[36mFuture.result\u001b[39m\u001b[34m(self, timeout)\u001b[39m\n\u001b[32m 454\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m CancelledError()\n\u001b[32m 455\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._state == FINISHED:\n\u001b[32m--> \u001b[39m\u001b[32m456\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m__get_result\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 457\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 458\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTimeoutError\u001b[39;00m()\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:401\u001b[39m, in \u001b[36mFuture.__get_result\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 399\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._exception:\n\u001b[32m 400\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m401\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m._exception\n\u001b[32m 402\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 403\u001b[39m \u001b[38;5;66;03m# Break a reference cycle with the exception in self._exception\u001b[39;00m\n\u001b[32m 404\u001b[39m \u001b[38;5;28mself\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/thread.py:59\u001b[39m, in \u001b[36m_WorkItem.run\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 56\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 58\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m59\u001b[39m result = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfn\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 60\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 61\u001b[39m \u001b[38;5;28mself\u001b[39m.future.set_exception(exc)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/runnables/config.py:546\u001b[39m, in \u001b[36mContextThreadPoolExecutor.map.._wrapped_fn\u001b[39m\u001b[34m(*args)\u001b[39m\n\u001b[32m 545\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_wrapped_fn\u001b[39m(*args: Any) -> T:\n\u001b[32m--> \u001b[39m\u001b[32m546\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mcontexts\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:1025\u001b[39m, in \u001b[36mToolNode._run_one\u001b[39m\u001b[34m(self, call, input_type, tool_runtime)\u001b[39m\n\u001b[32m 1023\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m\n\u001b[32m 1024\u001b[39m \u001b[38;5;66;03m# Convert to error message\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1025\u001b[39m content = \u001b[43m_handle_tool_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43me\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflag\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_handle_tool_errors\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1026\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ToolMessage(\n\u001b[32m 1027\u001b[39m content=content,\n\u001b[32m 1028\u001b[39m name=tool_request.tool_call[\u001b[33m\"\u001b[39m\u001b[33mname\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 1029\u001b[39m tool_call_id=tool_request.tool_call[\u001b[33m\"\u001b[39m\u001b[33mid\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 1030\u001b[39m status=\u001b[33m\"\u001b[39m\u001b[33merror\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 1031\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:424\u001b[39m, in \u001b[36m_handle_tool_error\u001b[39m\u001b[34m(e, flag)\u001b[39m\n\u001b[32m 422\u001b[39m content = flag\n\u001b[32m 423\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mcallable\u001b[39m(flag):\n\u001b[32m--> \u001b[39m\u001b[32m424\u001b[39m content = \u001b[43mflag\u001b[49m\u001b[43m(\u001b[49m\u001b[43me\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# type: ignore [assignment, call-arg]\u001b[39;00m\n\u001b[32m 425\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 426\u001b[39m msg = (\n\u001b[32m 427\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGot unexpected type of `handle_tool_error`. Expected bool, str \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 428\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mor callable. Received: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mflag\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 429\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:381\u001b[39m, in \u001b[36m_default_handle_tool_errors\u001b[39m\u001b[34m(e)\u001b[39m\n\u001b[32m 379\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(e, ToolInvocationError):\n\u001b[32m 380\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m e.message\n\u001b[32m--> \u001b[39m\u001b[32m381\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m e\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:1019\u001b[39m, in \u001b[36mToolNode._run_one\u001b[39m\u001b[34m(self, call, input_type, tool_runtime)\u001b[39m\n\u001b[32m 1017\u001b[39m \u001b[38;5;66;03m# Call wrapper with request and execute callable\u001b[39;00m\n\u001b[32m 1018\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1019\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_wrap_tool_call\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_request\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexecute\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1020\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 1021\u001b[39m \u001b[38;5;66;03m# Wrapper threw an exception\u001b[39;00m\n\u001b[32m 1022\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m._handle_tool_errors:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain/agents/factory.py:465\u001b[39m, in \u001b[36m_chain_tool_call_wrappers..compose_two..composed\u001b[39m\u001b[34m(request, execute)\u001b[39m\n\u001b[32m 462\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m inner(req, execute)\n\u001b[32m 464\u001b[39m \u001b[38;5;66;03m# Outer can call call_inner multiple times\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m465\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mouter\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcall_inner\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py:902\u001b[39m, in \u001b[36mFilesystemMiddleware.wrap_tool_call\u001b[39m\u001b[34m(self, request, handler)\u001b[39m\n\u001b[32m 899\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.tool_token_limit_before_evict \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mor\u001b[39;00m request.tool_call[\u001b[33m\"\u001b[39m\u001b[33mname\u001b[39m\u001b[33m\"\u001b[39m] \u001b[38;5;129;01min\u001b[39;00m TOOL_GENERATORS:\n\u001b[32m 900\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m handler(request)\n\u001b[32m--> \u001b[39m\u001b[32m902\u001b[39m tool_result = \u001b[43mhandler\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 903\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._intercept_large_tool_result(tool_result, request.runtime)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain/agents/factory.py:462\u001b[39m, in \u001b[36m_chain_tool_call_wrappers..compose_two..composed..call_inner\u001b[39m\u001b[34m(req)\u001b[39m\n\u001b[32m 461\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcall_inner\u001b[39m(req: ToolCallRequest) -> ToolMessage | Command:\n\u001b[32m--> \u001b[39m\u001b[32m462\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43minner\u001b[49m\u001b[43m(\u001b[49m\u001b[43mreq\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexecute\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:167\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware.wrap_tool_call\u001b[39m\u001b[34m(self, request, handler)\u001b[39m\n\u001b[32m 164\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ToolMessage(content=observation, tool_call_id=tool_call[\u001b[33m\"\u001b[39m\u001b[33mid\u001b[39m\u001b[33m\"\u001b[39m])\n\u001b[32m 166\u001b[39m \u001b[38;5;66;03m# STEP 5: Handle response type\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m167\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_handle_response\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_call\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m.\u001b[49m\u001b[43mruntime\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:244\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware._handle_response\u001b[39m\u001b[34m(self, response, tool_call, runtime)\u001b[39m\n\u001b[32m 242\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._handle_edit(response, tool_call, runtime)\n\u001b[32m 243\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m response_type == \u001b[33m\"\u001b[39m\u001b[33mignore\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m244\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_handle_ignore\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_call\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mruntime\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 245\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m response_type == \u001b[33m\"\u001b[39m\u001b[33mresponse\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m 246\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._handle_response_feedback(response, tool_call, runtime)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:332\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware._handle_ignore\u001b[39m\u001b[34m(self, tool_call, runtime)\u001b[39m\n\u001b[32m 329\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mInvalid tool call: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtool_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 331\u001b[39m \u001b[38;5;66;03m# Update triage preferences to avoid future false positives\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m332\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_update_triage_preferences_ignore\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_name\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mruntime\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 334\u001b[39m \u001b[38;5;66;03m# Return Command with goto END\u001b[39;00m\n\u001b[32m 335\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m Command(\n\u001b[32m 336\u001b[39m goto=END,\n\u001b[32m 337\u001b[39m update={\n\u001b[32m 338\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m: [ToolMessage(content=content, tool_call_id=tool_call_id)]\n\u001b[32m 339\u001b[39m },\n\u001b[32m 340\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:457\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware._update_triage_preferences_ignore\u001b[39m\u001b[34m(self, tool_name, runtime)\u001b[39m\n\u001b[32m 448\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 450\u001b[39m messages = runtime.state[\u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m] + [\n\u001b[32m 451\u001b[39m {\n\u001b[32m 452\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mrole\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33muser\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 453\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mcontent\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfeedback\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m Follow all instructions above, and remember: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mMEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m.\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 454\u001b[39m }\n\u001b[32m 455\u001b[39m ]\n\u001b[32m--> \u001b[39m\u001b[32m457\u001b[39m \u001b[43mupdate_memory\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstore\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnamespace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/utils.py:313\u001b[39m, in \u001b[36mupdate_memory\u001b[39m\u001b[34m(store, namespace, messages)\u001b[39m\n\u001b[32m 311\u001b[39m \u001b[38;5;66;03m# Update the memory\u001b[39;00m\n\u001b[32m 312\u001b[39m llm = init_chat_model(\u001b[33m\"\u001b[39m\u001b[33manthropic:claude-sonnet-4-5-20250929\u001b[39m\u001b[33m\"\u001b[39m, temperature=\u001b[32m0.0\u001b[39m).with_structured_output(UserPreferences)\n\u001b[32m--> \u001b[39m\u001b[32m313\u001b[39m result = \u001b[43mllm\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 314\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\n\u001b[32m 315\u001b[39m \u001b[43m \u001b[49m\u001b[43m{\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrole\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43msystem\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcontent\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mMEMORY_UPDATE_INSTRUCTIONS\u001b[49m\u001b[43m.\u001b[49m\u001b[43mformat\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcurrent_profile\u001b[49m\u001b[43m=\u001b[49m\u001b[43muser_preferences\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnamespace\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnamespace\u001b[49m\u001b[43m)\u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 316\u001b[39m \u001b[43m \u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\n\u001b[32m 317\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 318\u001b[39m \u001b[38;5;66;03m# Save the updated memory to the store\u001b[39;00m\n\u001b[32m 319\u001b[39m store.put(namespace, \u001b[33m\"\u001b[39m\u001b[33muser_preferences\u001b[39m\u001b[33m\"\u001b[39m, result.user_preferences)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py:3127\u001b[39m, in \u001b[36mRunnableSequence.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 3125\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m set_config_context(config) \u001b[38;5;28;01mas\u001b[39;00m context:\n\u001b[32m 3126\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m i == \u001b[32m0\u001b[39m:\n\u001b[32m-> \u001b[39m\u001b[32m3127\u001b[39m input_ = \u001b[43mcontext\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minput_\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 3128\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 3129\u001b[39m input_ = context.run(step.invoke, input_, config)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py:5534\u001b[39m, in \u001b[36mRunnableBindingBase.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 5527\u001b[39m \u001b[38;5;129m@override\u001b[39m\n\u001b[32m 5528\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34minvoke\u001b[39m(\n\u001b[32m 5529\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 5532\u001b[39m **kwargs: Any | \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 5533\u001b[39m ) -> Output:\n\u001b[32m-> \u001b[39m\u001b[32m5534\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mbound\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 5535\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 5536\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_merge_configs\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 5537\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43m{\u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 5538\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:398\u001b[39m, in \u001b[36mBaseChatModel.invoke\u001b[39m\u001b[34m(self, input, config, stop, **kwargs)\u001b[39m\n\u001b[32m 384\u001b[39m \u001b[38;5;129m@override\u001b[39m\n\u001b[32m 385\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34minvoke\u001b[39m(\n\u001b[32m 386\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 391\u001b[39m **kwargs: Any,\n\u001b[32m 392\u001b[39m ) -> AIMessage:\n\u001b[32m 393\u001b[39m config = ensure_config(config)\n\u001b[32m 394\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m cast(\n\u001b[32m 395\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mAIMessage\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 396\u001b[39m cast(\n\u001b[32m 397\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mChatGeneration\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m--> \u001b[39m\u001b[32m398\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mgenerate_prompt\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 399\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_convert_input\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 400\u001b[39m \u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 401\u001b[39m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcallbacks\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 402\u001b[39m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtags\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 403\u001b[39m \u001b[43m \u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmetadata\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 404\u001b[39m \u001b[43m \u001b[49m\u001b[43mrun_name\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrun_name\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 405\u001b[39m \u001b[43m \u001b[49m\u001b[43mrun_id\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrun_id\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 406\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 407\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m.generations[\u001b[32m0\u001b[39m][\u001b[32m0\u001b[39m],\n\u001b[32m 408\u001b[39m ).message,\n\u001b[32m 409\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:1117\u001b[39m, in \u001b[36mBaseChatModel.generate_prompt\u001b[39m\u001b[34m(self, prompts, stop, callbacks, **kwargs)\u001b[39m\n\u001b[32m 1108\u001b[39m \u001b[38;5;129m@override\u001b[39m\n\u001b[32m 1109\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mgenerate_prompt\u001b[39m(\n\u001b[32m 1110\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 1114\u001b[39m **kwargs: Any,\n\u001b[32m 1115\u001b[39m ) -> LLMResult:\n\u001b[32m 1116\u001b[39m prompt_messages = [p.to_messages() \u001b[38;5;28;01mfor\u001b[39;00m p \u001b[38;5;129;01min\u001b[39;00m prompts]\n\u001b[32m-> \u001b[39m\u001b[32m1117\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mgenerate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprompt_messages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:927\u001b[39m, in \u001b[36mBaseChatModel.generate\u001b[39m\u001b[34m(self, messages, stop, callbacks, tags, metadata, run_name, run_id, **kwargs)\u001b[39m\n\u001b[32m 924\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m i, m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(input_messages):\n\u001b[32m 925\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 926\u001b[39m results.append(\n\u001b[32m--> \u001b[39m\u001b[32m927\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_generate_with_cache\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 928\u001b[39m \u001b[43m \u001b[49m\u001b[43mm\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 929\u001b[39m \u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 930\u001b[39m \u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrun_managers\u001b[49m\u001b[43m[\u001b[49m\u001b[43mi\u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrun_managers\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 931\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 932\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 933\u001b[39m )\n\u001b[32m 934\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 935\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m run_managers:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:1221\u001b[39m, in \u001b[36mBaseChatModel._generate_with_cache\u001b[39m\u001b[34m(self, messages, stop, run_manager, **kwargs)\u001b[39m\n\u001b[32m 1219\u001b[39m result = generate_from_stream(\u001b[38;5;28miter\u001b[39m(chunks))\n\u001b[32m 1220\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m inspect.signature(\u001b[38;5;28mself\u001b[39m._generate).parameters.get(\u001b[33m\"\u001b[39m\u001b[33mrun_manager\u001b[39m\u001b[33m\"\u001b[39m):\n\u001b[32m-> \u001b[39m\u001b[32m1221\u001b[39m result = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_generate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 1222\u001b[39m \u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\n\u001b[32m 1223\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1224\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 1225\u001b[39m result = \u001b[38;5;28mself\u001b[39m._generate(messages, stop=stop, **kwargs)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py:1917\u001b[39m, in \u001b[36mChatAnthropic._generate\u001b[39m\u001b[34m(self, messages, stop, run_manager, **kwargs)\u001b[39m\n\u001b[32m 1915\u001b[39m data = \u001b[38;5;28mself\u001b[39m._create(payload)\n\u001b[32m 1916\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m anthropic.BadRequestError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m-> \u001b[39m\u001b[32m1917\u001b[39m \u001b[43m_handle_anthropic_bad_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43me\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1918\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._format_output(data, **kwargs)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py:1915\u001b[39m, in \u001b[36mChatAnthropic._generate\u001b[39m\u001b[34m(self, messages, stop, run_manager, **kwargs)\u001b[39m\n\u001b[32m 1913\u001b[39m payload = \u001b[38;5;28mself\u001b[39m._get_request_payload(messages, stop=stop, **kwargs)\n\u001b[32m 1914\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1915\u001b[39m data = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_create\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpayload\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1916\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m anthropic.BadRequestError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 1917\u001b[39m _handle_anthropic_bad_request(e)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py:1777\u001b[39m, in \u001b[36mChatAnthropic._create\u001b[39m\u001b[34m(self, payload)\u001b[39m\n\u001b[32m 1775\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mbetas\u001b[39m\u001b[33m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m payload:\n\u001b[32m 1776\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._client.beta.messages.create(**payload)\n\u001b[32m-> \u001b[39m\u001b[32m1777\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_client\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mpayload\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py:282\u001b[39m, in \u001b[36mrequired_args..inner..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 280\u001b[39m msg = \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mMissing required argument: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquote(missing[\u001b[32m0\u001b[39m])\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 281\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(msg)\n\u001b[32m--> \u001b[39m\u001b[32m282\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py:930\u001b[39m, in \u001b[36mMessages.create\u001b[39m\u001b[34m(self, max_tokens, messages, model, metadata, service_tier, stop_sequences, stream, system, temperature, thinking, tool_choice, tools, top_k, top_p, extra_headers, extra_query, extra_body, timeout)\u001b[39m\n\u001b[32m 923\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m model \u001b[38;5;129;01min\u001b[39;00m DEPRECATED_MODELS:\n\u001b[32m 924\u001b[39m warnings.warn(\n\u001b[32m 925\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mThe model \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmodel\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m is deprecated and will reach end-of-life on \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mDEPRECATED_MODELS[model]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33mPlease migrate to a newer model. Visit https://docs.anthropic.com/en/docs/resources/model-deprecations for more information.\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 926\u001b[39m \u001b[38;5;167;01mDeprecationWarning\u001b[39;00m,\n\u001b[32m 927\u001b[39m stacklevel=\u001b[32m3\u001b[39m,\n\u001b[32m 928\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m930\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_post\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 931\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43m/v1/messages\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 932\u001b[39m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmaybe_transform\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 933\u001b[39m \u001b[43m \u001b[49m\u001b[43m{\u001b[49m\n\u001b[32m 934\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmax_tokens\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_tokens\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 935\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmessages\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 936\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmodel\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmodel\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 937\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmetadata\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 938\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mservice_tier\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mservice_tier\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 939\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mstop_sequences\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop_sequences\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 940\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mstream\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 941\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43msystem\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43msystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 942\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtemperature\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtemperature\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 943\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mthinking\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mthinking\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 944\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtool_choice\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_choice\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 945\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtools\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtools\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 946\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtop_k\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtop_k\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 947\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtop_p\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtop_p\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 948\u001b[39m \u001b[43m \u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 949\u001b[39m \u001b[43m \u001b[49m\u001b[43mmessage_create_params\u001b[49m\u001b[43m.\u001b[49m\u001b[43mMessageCreateParamsStreaming\u001b[49m\n\u001b[32m 950\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\n\u001b[32m 951\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mmessage_create_params\u001b[49m\u001b[43m.\u001b[49m\u001b[43mMessageCreateParamsNonStreaming\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 952\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 953\u001b[39m \u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmake_request_options\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 954\u001b[39m \u001b[43m \u001b[49m\u001b[43mextra_headers\u001b[49m\u001b[43m=\u001b[49m\u001b[43mextra_headers\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mextra_query\u001b[49m\u001b[43m=\u001b[49m\u001b[43mextra_query\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mextra_body\u001b[49m\u001b[43m=\u001b[49m\u001b[43mextra_body\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtimeout\u001b[49m\n\u001b[32m 955\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 956\u001b[39m \u001b[43m \u001b[49m\u001b[43mcast_to\u001b[49m\u001b[43m=\u001b[49m\u001b[43mMessage\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 957\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 958\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_cls\u001b[49m\u001b[43m=\u001b[49m\u001b[43mStream\u001b[49m\u001b[43m[\u001b[49m\u001b[43mRawMessageStreamEvent\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 959\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/_base_client.py:1326\u001b[39m, in \u001b[36mSyncAPIClient.post\u001b[39m\u001b[34m(self, path, cast_to, body, options, files, stream, stream_cls)\u001b[39m\n\u001b[32m 1312\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mpost\u001b[39m(\n\u001b[32m 1313\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 1314\u001b[39m path: \u001b[38;5;28mstr\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 1321\u001b[39m stream_cls: \u001b[38;5;28mtype\u001b[39m[_StreamT] | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 1322\u001b[39m ) -> ResponseT | _StreamT:\n\u001b[32m 1323\u001b[39m opts = FinalRequestOptions.construct(\n\u001b[32m 1324\u001b[39m method=\u001b[33m\"\u001b[39m\u001b[33mpost\u001b[39m\u001b[33m\"\u001b[39m, url=path, json_data=body, files=to_httpx_files(files), **options\n\u001b[32m 1325\u001b[39m )\n\u001b[32m-> \u001b[39m\u001b[32m1326\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m cast(ResponseT, \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcast_to\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mopts\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream_cls\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstream_cls\u001b[49m\u001b[43m)\u001b[49m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/_base_client.py:1114\u001b[39m, in \u001b[36mSyncAPIClient.request\u001b[39m\u001b[34m(self, cast_to, options, stream, stream_cls)\u001b[39m\n\u001b[32m 1111\u001b[39m err.response.read()\n\u001b[32m 1113\u001b[39m log.debug(\u001b[33m\"\u001b[39m\u001b[33mRe-raising status error\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m-> \u001b[39m\u001b[32m1114\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m._make_status_error_from_response(err.response) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m 1116\u001b[39m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[32m 1118\u001b[39m \u001b[38;5;28;01massert\u001b[39;00m response \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[33m\"\u001b[39m\u001b[33mcould not resolve response (should never happen)\u001b[39m\u001b[33m\"\u001b[39m\n", + "\u001b[31mBadRequestError\u001b[39m: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.24: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_015WrAQ82bzbKUbET1uhfixd. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'}, 'request_id': 'req_011CViX58yA9hzBebZGRqkgP'}", + "During task with name 'tools' and id 'd4296337-4ca9-f786-8a1d-b63b4da1765e'" + ] + } + ], + "source": [ + "result_1_continued = resume_with_decision(agent, config_1, decision_type=\"ignore\")\n", + "format_messages(result_1_continued[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "7hodvbagtn5", + "metadata": {}, + "source": [ + "### Resume Example (if interrupted)\n", + "\n", + "If the agent was interrupted, you can resume with a decision:" + ] + }, + { + "cell_type": "markdown", + "id": "example2", + "metadata": {}, + "source": [ + "## Example 2: Simple Question\n", + "\n", + "Test with a simple question that needs a direct response." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "example2_email", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── 📧 Email to Process ──────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "  **Subject**: LangGraph documentation link                                                                      \n",
+       "  **From**: bob@example.com                                                                                      \n",
+       "  **To**: lance@langchain.dev                                                                                    \n",
+       "                                                                                                                 \n",
+       "  Hey Lance,                                                                                                     \n",
+       "                                                                                                                 \n",
+       "  Could you send me the link to the LangGraph docs?                                                              \n",
+       "                                                                                                                 \n",
+       "  Thanks!                                                                                                        \n",
+       "  Bob                                                                                                            \n",
+       "                                                                                                                 \n",
+       "  ---                                                                                                            \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Email to Process\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **Subject**: LangGraph documentation link \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **From**: bob@example.com \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **To**: lance@langchain.dev \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Hey Lance, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Could you send me the link to the LangGraph docs? \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Thanks! \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Bob \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m --- \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "email_input_2 = {\n", + " \"author\": \"bob@example.com\",\n", + " \"to\": \"lance@langchain.dev\",\n", + " \"subject\": \"LangGraph documentation link\",\n", + " \"email_thread\": \"Hey Lance,\\n\\nCould you send me the link to the LangGraph docs?\\n\\nThanks!\\nBob\",\n", + "}\n", + "\n", + "author, to, subject, email_thread = parse_email(email_input_2)\n", + "email_markdown_2 = format_email_markdown(subject, author, to, email_thread)\n", + "\n", + "# Display email with rich formatting\n", + "show_prompt(email_markdown_2, title=\"📧 Email to Process\", border_style=\"cyan\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "example2_invoke", + "metadata": {}, + "outputs": [], + "source": [ + "config_2 = {\"configurable\": {\"thread_id\": \"test-thread-2\"}}\n", + "\n", + "result_2 = agent.invoke(\n", + " {\n", + " \"messages\": [{\"role\": \"user\", \"content\": email_markdown_2}],\n", + " \"email_input\": email_input_2,\n", + " },\n", + " config=config_2,\n", + ")\n", + "\n", + "# Display result with rich formatting\n", + "print(\"\\n\" + \"=\"*60)\n", + "print(\"Agent Response\")\n", + "print(\"=\"*60 + \"\\n\")\n", + "\n", + "# Check for interrupts (HITL)\n", + "if \"__interrupt__\" in result_2:\n", + " print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n", + " \n", + " for interrupt in result_2[\"__interrupt__\"]:\n", + " for request in interrupt.value:\n", + " print(\"Action:\", request[\"action_request\"][\"action\"])\n", + " print(\"Args:\", request[\"action_request\"][\"args\"])\n", + " print(\"\\nAllowed actions:\", request[\"config\"])\n", + " print(\"\\nDescription:\")\n", + " show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n", + " print(\"\\n\" + \"-\"*60)\n", + " \n", + " print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n", + " print(\" Use: agent.invoke(None, config=config_2)\")\n", + "else:\n", + " format_messages(result_2[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "test_memory", + "metadata": {}, + "source": [ + "## Test Memory Persistence\n", + "\n", + "Test that memory persists across invocations using the same thread ID." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "test_memory_code", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "Agent Response (with memory from previous conversation)\n", + "============================================================\n", + "\n" + ] + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to    \n",
+       " Jane.                                                                                                           \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_01Kp449tDf62QjfRcCwCT45C                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Jane. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01Kp449tDf62QjfRcCwCT45C \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
+       " Tool call schedule_meeting with id toolu_01Kp449tDf62QjfRcCwCT45C was cancelled - another message came in       \n",
+       " before it could be completed.                                                                                   \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_01Kp449tDf62QjfRcCwCT45C was cancelled - another message came in \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       " **Subject**: Follow-up meeting                                                                                  \n",
+       " **From**: jane@example.com                                                                                      \n",
+       " **To**: lance@langchain.dev                                                                                     \n",
+       "                                                                                                                 \n",
+       " Hi again,                                                                                                       \n",
+       "                                                                                                                 \n",
+       " Can we also schedule a follow-up meeting next week?                                                             \n",
+       "                                                                                                                 \n",
+       " Jane                                                                                                            \n",
+       "                                                                                                                 \n",
+       " ---                                                                                                             \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **Subject**: Follow-up meeting \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Hi again, \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Can we also schedule a follow-up meeting next week? \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", + "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", + "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
+       " I'll help you with both meeting requests from Jane. Let me schedule the first meeting for Tuesday at 2pm and    \n",
+       " then respond about the follow-up meeting.                                                                       \n",
+       "                                                                                                                 \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       "    Args: {                                                                                                      \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
+       " }                                                                                                               \n",
+       "    ID: toolu_018r9DgdPYxx7LeXxADVGuTS                                                                           \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", + "\u001b[37m│\u001b[0m I'll help you with both meeting requests from Jane. Let me schedule the first meeting for Tuesday at 2pm and \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m then respond about the follow-up meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_018r9DgdPYxx7LeXxADVGuTS \u001b[37m│\u001b[0m\n", + "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Use the same thread as example 1 to test memory\n", + "email_input_3 = {\n", + " \"author\": \"jane@example.com\",\n", + " \"to\": \"lance@langchain.dev\",\n", + " \"subject\": \"Follow-up meeting\",\n", + " \"email_thread\": \"Hi again,\\n\\nCan we also schedule a follow-up meeting next week?\\n\\nJane\",\n", + "}\n", + "\n", + "author, to, subject, email_thread = parse_email(email_input_3)\n", + "email_markdown_3 = format_email_markdown(subject, author, to, email_thread)\n", + "\n", + "# Use same config as example 1 (same thread_id)\n", + "result_3 = agent.invoke(\n", + " {\n", + " \"messages\": [{\"role\": \"user\", \"content\": email_markdown_3}],\n", + " \"email_input\": email_input_3,\n", + " },\n", + " config=config_1, # Same thread as example 1\n", + ")\n", + "\n", + "# Display result with rich formatting (show last 5 messages)\n", + "print(\"\\n\" + \"=\"*60)\n", + "print(\"Agent Response (with memory from previous conversation)\")\n", + "print(\"=\"*60 + \"\\n\")\n", + "format_messages(result_3[\"messages\"][-5:])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb4fc2a3-63ce-4f16-9003-c37294989355", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/personal_assistant/uv.lock b/personal_assistant/uv.lock new file mode 100644 index 0000000..19a00a3 --- /dev/null +++ b/personal_assistant/uv.lock @@ -0,0 +1,3056 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/1f/08e95f4b7e2d35205ae5dcbb4ae97e7d477fc521c275c02609e2931ece2d/anthropic-0.75.0.tar.gz", hash = "sha256:e8607422f4ab616db2ea5baacc215dd5f028da99ce2f022e33c7c535b29f3dfb", size = 439565, upload-time = "2025-11-24T20:41:45.28Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/1c/1cd02b7ae64302a6e06724bf80a96401d5313708651d277b1458504a1730/anthropic-0.75.0-py3-none-any.whl", hash = "sha256:ea8317271b6c15d80225a9f3c670152746e88805a7a61e14d4a374577164965b", size = 388164, upload-time = "2025-11-24T20:41:43.587Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, +] + +[[package]] +name = "arrow" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/33/032cdc44182491aa708d06a68b62434140d8c50820a087fac7af37703357/arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7", size = 152931, upload-time = "2025-10-18T17:46:46.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "async-lru" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + +[[package]] +name = "bleach" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "blockbuster" +version = "1.5.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "forbiddenfruit", marker = "implementation_name == 'cpython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/bc/57c49465decaeeedd58ce2d970b4cdfd93a74ba9993abff2dc498a31c283/blockbuster-1.5.25.tar.gz", hash = "sha256:b72f1d2aefdeecd2a820ddf1e1c8593bf00b96e9fdc4cd2199ebafd06f7cb8f0", size = 36058, upload-time = "2025-07-14T16:00:20.766Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/01/dccc277c014f171f61a6047bb22c684e16c7f2db6bb5c8cce1feaf41ec55/blockbuster-1.5.25-py3-none-any.whl", hash = "sha256:cb06229762273e0f5f3accdaed3d2c5a3b61b055e38843de202311ede21bb0f5", size = 13196, upload-time = "2025-07-14T16:00:19.396Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comm" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, +] + +[[package]] +name = "cryptography" +version = "44.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281, upload-time = "2025-05-02T19:34:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305, upload-time = "2025-05-02T19:34:53.042Z" }, + { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040, upload-time = "2025-05-02T19:34:54.675Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411, upload-time = "2025-05-02T19:34:56.61Z" }, + { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263, upload-time = "2025-05-02T19:34:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198, upload-time = "2025-05-02T19:35:00.988Z" }, + { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502, upload-time = "2025-05-02T19:35:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173, upload-time = "2025-05-02T19:35:05.018Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713, upload-time = "2025-05-02T19:35:07.187Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064, upload-time = "2025-05-02T19:35:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887, upload-time = "2025-05-02T19:35:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737, upload-time = "2025-05-02T19:35:12.12Z" }, + { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501, upload-time = "2025-05-02T19:35:13.775Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307, upload-time = "2025-05-02T19:35:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876, upload-time = "2025-05-02T19:35:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127, upload-time = "2025-05-02T19:35:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164, upload-time = "2025-05-02T19:35:21.449Z" }, + { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081, upload-time = "2025-05-02T19:35:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716, upload-time = "2025-05-02T19:35:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398, upload-time = "2025-05-02T19:35:27.678Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900, upload-time = "2025-05-02T19:35:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467, upload-time = "2025-05-02T19:35:33.805Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375, upload-time = "2025-05-02T19:35:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4b/c11ad0b6c061902de5223892d680e89c06c7c4d606305eb8de56c5427ae6/cryptography-44.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375", size = 3390230, upload-time = "2025-05-02T19:35:49.062Z" }, + { url = "https://files.pythonhosted.org/packages/58/11/0a6bf45d53b9b2290ea3cec30e78b78e6ca29dc101e2e296872a0ffe1335/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647", size = 3895216, upload-time = "2025-05-02T19:35:51.351Z" }, + { url = "https://files.pythonhosted.org/packages/0a/27/b28cdeb7270e957f0077a2c2bfad1b38f72f1f6d699679f97b816ca33642/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259", size = 4115044, upload-time = "2025-05-02T19:35:53.044Z" }, + { url = "https://files.pythonhosted.org/packages/35/b0/ec4082d3793f03cb248881fecefc26015813199b88f33e3e990a43f79835/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff", size = 3898034, upload-time = "2025-05-02T19:35:54.72Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7f/adf62e0b8e8d04d50c9a91282a57628c00c54d4ae75e2b02a223bd1f2613/cryptography-44.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5", size = 4114449, upload-time = "2025-05-02T19:35:57.139Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/d69eb4a8ee231f4bf733a92caf9da13f1c81a44e874b1d4080c25ecbb723/cryptography-44.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c", size = 3134369, upload-time = "2025-05-02T19:35:58.907Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/53/3af72b5c159278c4a0cf4cffa518675a0e73bdb7d1cac0239b815502d2ce/debugpy-1.8.17-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:d3fce3f0e3de262a3b67e69916d001f3e767661c6e1ee42553009d445d1cd840", size = 2207154, upload-time = "2025-09-17T16:33:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" }, + { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" }, + { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" }, + { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" }, + { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" }, + { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" }, + { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" }, + { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "deepagents" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/9a/f4c3f610bc11e15e87537b5b657ad7050a613849e955d4de03695a4eb798/deepagents-0.2.8.tar.gz", hash = "sha256:5fbf7f0db06b923409b79a6598784e0e9925958efabd4b702eed9238b459ae09", size = 49217, upload-time = "2025-11-24T04:06:16.493Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a7/a3993b58c013dae776f5880b4d83eca8d2804ea2a71793d2b96d22ca94a1/deepagents-0.2.8-py3-none-any.whl", hash = "sha256:d4c3d0df074be5e10f3a17005e68db3df18b3d51f48aca3e299d87c254fda797", size = 51973, upload-time = "2025-11-24T04:06:15.632Z" }, +] + +[[package]] +name = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, +] + +[[package]] +name = "forbiddenfruit" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756, upload-time = "2021-01-16T21:03:35.401Z" } + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + +[[package]] +name = "grpcio-tools" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "protobuf" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/76/0cd2a2bb379275c319544a3ab613dc3cea7a167503908c1b4de55f82bd9e/grpcio_tools-1.75.1.tar.gz", hash = "sha256:bb78960cf3d58941e1fec70cbdaccf255918beed13c34112a6915a6d8facebd1", size = 5390470, upload-time = "2025-09-26T09:10:11.948Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/28/71ab934662d41ded4e451d9af0ec6f9aade3525e470fdfd10bd20e588e44/grpcio_tools-1.75.1-cp311-cp311-linux_armv7l.whl", hash = "sha256:f0635231feb70a9d551452829943a1a5fa651283e7a300aadc22df5ea5da696f", size = 2545461, upload-time = "2025-09-26T09:08:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/69/40/d90f6fdb51f51b2a518401207b3920fcfdfa996ed7bca844096f111ed839/grpcio_tools-1.75.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:626293296ef7e2d87ab1a80b81a55eef91883c65b59a97576099a28b9535100b", size = 5842958, upload-time = "2025-09-26T09:08:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b7/52e6f32fd0101e3ac9c654a6441b254ba5874f146b543b20afbcb8246947/grpcio_tools-1.75.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:071339d90f1faab332ce4919c815a10b9c3ed2c09473f550f686bf9cc148579f", size = 2591669, upload-time = "2025-09-26T09:08:13.481Z" }, + { url = "https://files.pythonhosted.org/packages/0a/3c/115c59a5c0c8e9d7d99a40bac8d5e91c05b6735b3bb185265d40e9fc4346/grpcio_tools-1.75.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:44195f58c052fa935b78c7438c85cbcd4b273dd685028e4f6d4d7b30d47daad1", size = 2904952, upload-time = "2025-09-26T09:08:15.299Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cd/d2a3583a5b1d71da88f7998f20fb5a0b6fe5bb96bb916a610c29269063b6/grpcio_tools-1.75.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:860fafdb85726029d646c99859ff7bdca5aae61b5ff038c3bd355fc1ec6b2764", size = 2656311, upload-time = "2025-09-26T09:08:17.094Z" }, + { url = "https://files.pythonhosted.org/packages/aa/09/67b9215d39add550e430c9677bd43c9a315da07ab62fa3a5f44f1cf5bb75/grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4559547a0cb3d3db1b982eea87d4656036339b400f48127fef932210672fb59e", size = 3105583, upload-time = "2025-09-26T09:08:19.179Z" }, + { url = "https://files.pythonhosted.org/packages/98/d7/d400b90812470f3dc2466964e62fc03592de46b5c824c82ef5303be60167/grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9af65a310807d7f36a8f7cddea142fe97d6dffba74444f38870272f2e5a3a06b", size = 3654677, upload-time = "2025-09-26T09:08:21.227Z" }, + { url = "https://files.pythonhosted.org/packages/9c/93/edf6de71b4f936b3f09461a3286db1f902c6366c5de06ef19a8c2523034a/grpcio_tools-1.75.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c1de31aefc0585d2f915a7cd0994d153547495b8d79c44c58048a3ede0b65be", size = 3322147, upload-time = "2025-09-26T09:08:23.08Z" }, + { url = "https://files.pythonhosted.org/packages/80/00/0f8c6204e34070e7d4f344b27e4b1b0320dfdd94574f79738a43504d182e/grpcio_tools-1.75.1-cp311-cp311-win32.whl", hash = "sha256:efaf95fcaa5d3ac1bcfe44ceed9e2512eb95b5c8c476569bdbbe2bee4b59c8a9", size = 993388, upload-time = "2025-09-26T09:08:24.708Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/6f738154980f606293988a64ef4bb0ea2bb12029a4529464aac56fe2ab99/grpcio_tools-1.75.1-cp311-cp311-win_amd64.whl", hash = "sha256:7cefe76fc35c825f0148d60d2294a527053d0f5dd6a60352419214a8c53223c9", size = 1157907, upload-time = "2025-09-26T09:08:26.537Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a7/581bb204d19a347303ed5e25b19f7d8c6365a28c242fca013d1d6d78ad7e/grpcio_tools-1.75.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:49b68936cf212052eeafa50b824e17731b78d15016b235d36e0d32199000b14c", size = 2546099, upload-time = "2025-09-26T09:08:28.794Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/ab65998eba14ff9d292c880f6a276fe7d0571bba3bb4ddf66aca1f8438b5/grpcio_tools-1.75.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:08cb6e568e58b76a2178ad3b453845ff057131fff00f634d7e15dcd015cd455b", size = 5839838, upload-time = "2025-09-26T09:08:31.038Z" }, + { url = "https://files.pythonhosted.org/packages/7e/65/7027f71069b4c1e8c7b46de8c46c297c9d28ef6ed4ea0161e8c82c75d1d0/grpcio_tools-1.75.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:168402ad29a249092673079cf46266936ec2fb18d4f854d96e9c5fa5708efa39", size = 2592916, upload-time = "2025-09-26T09:08:33.216Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/1abfb3c679b78c7fca7524031cf9de4c4c509c441b48fd26291ac16dd1af/grpcio_tools-1.75.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:bbae11c29fcf450730f021bfc14b12279f2f985e2e493ccc2f133108728261db", size = 2905276, upload-time = "2025-09-26T09:08:35.691Z" }, + { url = "https://files.pythonhosted.org/packages/99/cd/7f9e05f1eddccb61bc0ead1e49eb2222441957b02ed11acfcd2f795b03a8/grpcio_tools-1.75.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38c6c7d5d4800f636ee691cd073db1606d1a6a76424ca75c9b709436c9c20439", size = 2656424, upload-time = "2025-09-26T09:08:38.255Z" }, + { url = "https://files.pythonhosted.org/packages/29/1d/8b7852771c2467728341f7b9c3ca4ebc76e4e23485c6a3e6d97a8323ad2a/grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:626f6a61a8f141dde9a657775854d1c0d99509f9a2762b82aa401a635f6ec73d", size = 3108985, upload-time = "2025-09-26T09:08:40.291Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6a/069da89cdf2e97e4558bfceef5b60bf0ef200c443b465e7691869006dd32/grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f61a8334ae38d4f98c744a732b89527e5af339d17180e25fff0676060f8709b7", size = 3657940, upload-time = "2025-09-26T09:08:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/ca8dae800c084beb89e2720346f70012d36dfb9df02d8eacd518c06cf4a0/grpcio_tools-1.75.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd0c3fb40d89a1e24a41974e77c7331e80396ab7cde39bc396a13d6b5e2a750b", size = 3324878, upload-time = "2025-09-26T09:08:45.083Z" }, + { url = "https://files.pythonhosted.org/packages/58/06/cbe923679309bf970923f4a11351ea9e485291b504d7243130fdcfdcb03f/grpcio_tools-1.75.1-cp312-cp312-win32.whl", hash = "sha256:004bc5327593eea48abd03be3188e757c3ca0039079587a6aac24275127cac20", size = 993071, upload-time = "2025-09-26T09:08:46.785Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0c/84d6be007262c5d88a590082f3a1fe62d4b0eeefa10c6cdb3548f3663e80/grpcio_tools-1.75.1-cp312-cp312-win_amd64.whl", hash = "sha256:23952692160b5fe7900653dfdc9858dc78c2c42e15c27e19ee780c8917ba6028", size = 1157506, upload-time = "2025-09-26T09:08:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/47/fa/624bbe1b2ccf4f6044bf3cd314fe2c35f78f702fcc2191dc65519baddca4/grpcio_tools-1.75.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:ca9e116aab0ecf4365fc2980f2e8ae1b22273c3847328b9a8e05cbd14345b397", size = 2545752, upload-time = "2025-09-26T09:08:51.433Z" }, + { url = "https://files.pythonhosted.org/packages/b9/4c/6d884e2337feff0a656e395338019adecc3aa1daeae9d7e8eb54340d4207/grpcio_tools-1.75.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:9fe87a926b65eb7f41f8738b6d03677cc43185ff77a9d9b201bdb2f673f3fa1e", size = 5838163, upload-time = "2025-09-26T09:08:53.858Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2a/2ba7b6911a754719643ed92ae816a7f989af2be2882b9a9e1f90f4b0e882/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45503a6094f91b3fd31c3d9adef26ac514f102086e2a37de797e220a6791ee87", size = 2592148, upload-time = "2025-09-26T09:08:55.86Z" }, + { url = "https://files.pythonhosted.org/packages/88/db/fa613a45c3c7b00f905bd5ad3a93c73194724d0a2dd72adae3be32983343/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b01b60b3de67be531a39fd869d7613fa8f178aff38c05e4d8bc2fc530fa58cb5", size = 2905215, upload-time = "2025-09-26T09:08:58.27Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0c/ee4786972bb82f60e4f313bb2227c79c2cd20eb13c94c0263067923cfd12/grpcio_tools-1.75.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e2b9b9488735514777d44c1e4eda813122d2c87aad219f98d5d49b359a8eab", size = 2656251, upload-time = "2025-09-26T09:09:00.249Z" }, + { url = "https://files.pythonhosted.org/packages/77/f1/cc5a50658d705d0b71ff8a4fbbfcc6279d3c95731a2ef7285e13dc40e2fe/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55e60300e62b220fabe6f062fe69f143abaeff3335f79b22b56d86254f3c3c80", size = 3108911, upload-time = "2025-09-26T09:09:02.515Z" }, + { url = "https://files.pythonhosted.org/packages/09/d8/43545f77c4918e778e90bc2c02b3462ac71cee14f29d85cdb69b089538eb/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:49ce00fcc6facbbf52bf376e55b8e08810cecd03dab0b3a2986d73117c6f6ee4", size = 3657021, upload-time = "2025-09-26T09:09:05.331Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0b/2ae5925374b66bc8df5b828eff1a5f9459349c83dae1773f0aa9858707e6/grpcio_tools-1.75.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71e95479aea868f8c8014d9dc4267f26ee75388a0d8a552e1648cfa0b53d24b4", size = 3324450, upload-time = "2025-09-26T09:09:07.867Z" }, + { url = "https://files.pythonhosted.org/packages/6e/53/9f887bacbecf892ac5b0b282477ca8cfa5b73911b04259f0d88b52e9a055/grpcio_tools-1.75.1-cp313-cp313-win32.whl", hash = "sha256:fff9d2297416eae8861e53154ccf70a19994e5935e6c8f58ebf431f81cbd8d12", size = 992434, upload-time = "2025-09-26T09:09:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/a5/f0/9979d97002edffdc2a88e5f2e0dccea396dd4a6eab34fa2f705fe43eae2f/grpcio_tools-1.75.1-cp313-cp313-win_amd64.whl", hash = "sha256:1849ddd508143eb48791e81d42ddc924c554d1b4900e06775a927573a8d4267f", size = 1157069, upload-time = "2025-09-26T09:09:12.287Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0b/4ff4ead293f2b016668628a240937828444094778c8037d2bbef700e9097/grpcio_tools-1.75.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:f281b594489184b1f9a337cdfed1fc1ddb8428f41c4b4023de81527e90b38e1e", size = 2545868, upload-time = "2025-09-26T09:09:14.716Z" }, + { url = "https://files.pythonhosted.org/packages/0e/78/aa6bf73a18de5357c01ef87eea92150931586b25196fa4df197a37bae11d/grpcio_tools-1.75.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:becf8332f391abc62bf4eea488b63be063d76a7cf2ef00b2e36c617d9ee9216b", size = 5838010, upload-time = "2025-09-26T09:09:20.415Z" }, + { url = "https://files.pythonhosted.org/packages/99/65/7eaad673bc971af45e079d3b13c20d9ba9842b8788d31953e3234c2e2cee/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a08330f24e5cd7b39541882a95a8ba04ffb4df79e2984aa0cd01ed26dcdccf49", size = 2593170, upload-time = "2025-09-26T09:09:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/57e1e29e9186c7ed223ce8a9b609d3f861c4db015efb643dfe60b403c137/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6bf3742bd8f102630072ed317d1496f31c454cd85ad19d37a68bd85bf9d5f8b9", size = 2905167, upload-time = "2025-09-26T09:09:25.96Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/894f891f3cf19812192f8bbf1e0e1c958055676ecf0a5466a350730a006d/grpcio_tools-1.75.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f26028949474feb380460ce52d9d090d00023940c65236294a66c42ac5850e8b", size = 2656210, upload-time = "2025-09-26T09:09:28.786Z" }, + { url = "https://files.pythonhosted.org/packages/99/76/8e48427da93ef243c09629969c7b5a2c59dceb674b6b623c1f5fbaa5c8c5/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1bd68fb98bf08f11b6c3210834a14eefe585bad959bdba38e78b4ae3b04ba5bd", size = 3109226, upload-time = "2025-09-26T09:09:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7e/ecf71c316c2a88c2478b7c6372d0f82d05f07edbf0f31b6da613df99ec7c/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f1496e21586193da62c3a73cd16f9c63c5b3efd68ff06dab96dbdfefa90d40bf", size = 3657139, upload-time = "2025-09-26T09:09:35.043Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f3/b2613e81da2085f40a989c0601ec9efc11e8b32fcb71b1234b64a18af830/grpcio_tools-1.75.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:14a78b1e36310cdb3516cdf9ee2726107875e0b247e2439d62fc8dc38cf793c1", size = 3324513, upload-time = "2025-09-26T09:09:37.44Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1f/2df4fa8634542524bc22442ffe045d41905dae62cc5dd14408b80c5ac1b8/grpcio_tools-1.75.1-cp314-cp314-win32.whl", hash = "sha256:0e6f916daf222002fb98f9a6f22de0751959e7e76a24941985cc8e43cea77b50", size = 1015283, upload-time = "2025-09-26T09:09:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/23/4f/f27c973ff50486a70be53a3978b6b0244398ca170a4e19d91988b5295d92/grpcio_tools-1.75.1-cp314-cp314-win_amd64.whl", hash = "sha256:878c3b362264588c45eba57ce088755f8b2b54893d41cc4a68cdeea62996da5c", size = 1189364, upload-time = "2025-09-26T09:09:42.036Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "html2text" +version = "2025.4.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/27/e158d86ba1e82967cc2f790b0cb02030d4a8bef58e0c79a8590e9678107f/html2text-2025.4.15.tar.gz", hash = "sha256:948a645f8f0bc3abe7fd587019a2197a12436cd73d0d4908af95bfc8da337588", size = 64316, upload-time = "2025-04-15T04:02:30.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/84/1a0f9555fd5f2b1c924ff932d99b40a0f8a6b12f6dd625e2a47f415b00ea/html2text-2025.4.15-py3-none-any.whl", hash = "sha256:00569167ffdab3d7767a4cdf589b7f57e777a5ed28d12907d8c58769ec734acc", size = 34656, upload-time = "2025-04-15T04:02:28.44Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "ipykernel" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" }, +] + +[[package]] +name = "ipython" +version = "9.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/ae/c5ce1edc1afe042eadb445e95b0671b03cee61895264357956e61c0d2ac0/ipywidgets-8.1.8.tar.gz", hash = "sha256:61f969306b95f85fba6b6986b7fe45d73124d1d9e3023a8068710d47a22ea668", size = 116739, upload-time = "2025-11-01T21:18:12.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, + { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, + { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, + { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, + { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, + { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, + { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" }, + { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" }, + { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, +] + +[[package]] +name = "json5" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/ae/929aee9619e9eba9015207a9d2c1c54db18311da7eb4dcf6d41ad6f0eb67/json5-0.12.1.tar.gz", hash = "sha256:b2743e77b3242f8d03c143dd975a6ec7c52e2f2afe76ed934e53503dd4ad4990", size = 52191, upload-time = "2025-08-12T19:47:42.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/e2/05328bd2621be49a6fed9e3030b1e51a2d04537d3f816d211b9cc53c5262/json5-0.12.1-py3-none-any.whl", hash = "sha256:d9c9b3bc34a5f54d43c35e11ef7cb87d8bdd098c6ace87117a7b7e83e705c1d5", size = 36119, upload-time = "2025-08-12T19:47:41.131Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "rfc3987-syntax" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-rs" +version = "0.29.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/b4/33a9b25cad41d1e533c1ab7ff30eaec50628dd1bcb92171b99a2e944d61f/jsonschema_rs-0.29.1.tar.gz", hash = "sha256:a9f896a9e4517630374f175364705836c22f09d5bd5bbb06ec0611332b6702fd", size = 1406679, upload-time = "2025-02-08T21:25:12.639Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e2/9c3af8c7d56ff1b6bac88137f60bf02f2814c60d1f658ef06b2ddc2a21b1/jsonschema_rs-0.29.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b4458f1a027ab0c64e91edcb23c48220d60a503e741030bcf260fbbe12979ad2", size = 3828925, upload-time = "2025-02-08T21:24:07.289Z" }, + { url = "https://files.pythonhosted.org/packages/3f/29/f9377e55f10ef173c4cf1c2c88bc30e4a1a4ea1c60659c524903cac85a07/jsonschema_rs-0.29.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:faf3d90b5473bf654fd6ffb490bd6fdd2e54f4034f652d1749bee963b3104ce3", size = 1968915, upload-time = "2025-02-08T21:24:09.123Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/8c514ebab1d312a2422bece0a1ccca45b82a36131d4cb63e01b4469ac99a/jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e96919483960737ea5cd8d36e0752c63b875459f31ae14b3a6e80df925b74947", size = 2066366, upload-time = "2025-02-08T21:24:10.469Z" }, + { url = "https://files.pythonhosted.org/packages/05/3e/04c6b25ae1b53c8c72eaf35cdda4f84558ca4df011d370b5906a6f56ba7f/jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e70f1ff7281810327b354ecaeba6cdce7fe498483338207fe7edfae1b21c212", size = 2067599, upload-time = "2025-02-08T21:24:12.006Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/b9b8934e4db4f43f61e65c5f285432c2d07cb1935ad9df88d5080a4a311b/jsonschema_rs-0.29.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07fef0706a5df7ba5f301a6920b28b0a4013ac06623aed96a6180e95c110b82a", size = 2084926, upload-time = "2025-02-08T21:24:14.544Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/676d67d2583cdd50b07b5a0989b501aebf003b12232d14f87fc7fb991f2c/jsonschema_rs-0.29.1-cp311-cp311-win32.whl", hash = "sha256:07524370bdce055d4f106b7fed1afdfc86facd7d004cbb71adeaff3e06861bf6", size = 1704339, upload-time = "2025-02-08T21:24:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3e/4767dce237d8ea2ff5f684699ef1b9dae5017dc41adaa6f3dc3a85b84608/jsonschema_rs-0.29.1-cp311-cp311-win_amd64.whl", hash = "sha256:36fa23c85333baa8ce5bf0564fb19de3d95b7640c0cab9e3205ddc44a62fdbf0", size = 1872253, upload-time = "2025-02-08T21:24:18.43Z" }, + { url = "https://files.pythonhosted.org/packages/7b/4a/67ea15558ab85e67d1438b2e5da63b8e89b273c457106cbc87f8f4959a3d/jsonschema_rs-0.29.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9fe7529faa6a84d23e31b1f45853631e4d4d991c85f3d50e6d1df857bb52b72d", size = 3825206, upload-time = "2025-02-08T21:24:19.985Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2e/bc75ed65d11ba47200ade9795ebd88eb2e64c2852a36d9be640172563430/jsonschema_rs-0.29.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5d7e385298f250ed5ce4928fd59fabf2b238f8167f2c73b9414af8143dfd12e", size = 1966302, upload-time = "2025-02-08T21:24:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/95/dd/4a90e96811f897de066c69d95bc0983138056b19cb169f2a99c736e21933/jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64a29be0504731a2e3164f66f609b9999aa66a2df3179ecbfc8ead88e0524388", size = 2062846, upload-time = "2025-02-08T21:24:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/21/91/61834396748a741021716751a786312b8a8319715e6c61421447a07c887c/jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e91defda5dfa87306543ee9b34d97553d9422c134998c0b64855b381f8b531d", size = 2065564, upload-time = "2025-02-08T21:24:24.574Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/920d92e88b9bdb6cb14867a55e5572e7b78bfc8554f9c625caa516aa13dd/jsonschema_rs-0.29.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96f87680a6a1c16000c851d3578534ae3c154da894026c2a09a50f727bd623d4", size = 2083055, upload-time = "2025-02-08T21:24:26.834Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0a/f4c1bea3193992fe4ff9ce330c6a594481caece06b1b67d30b15992bbf54/jsonschema_rs-0.29.1-cp312-cp312-win32.whl", hash = "sha256:bcfc0d52ecca6c1b2fbeede65c1ad1545de633045d42ad0c6699039f28b5fb71", size = 1701065, upload-time = "2025-02-08T21:24:28.282Z" }, + { url = "https://files.pythonhosted.org/packages/5e/89/3f89de071920208c0eb64b827a878d2e587f6a3431b58c02f63c3468b76e/jsonschema_rs-0.29.1-cp312-cp312-win_amd64.whl", hash = "sha256:a414c162d687ee19171e2d8aae821f396d2f84a966fd5c5c757bd47df0954452", size = 1871774, upload-time = "2025-02-08T21:24:30.824Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/d642024e8b39753b789598363fd5998eb3053b52755a5df6a021d53741d5/jsonschema_rs-0.29.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0afee5f31a940dec350a33549ec03f2d1eda2da3049a15cd951a266a57ef97ee", size = 3824864, upload-time = "2025-02-08T21:24:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3d/48a7baa2373b941e89a12e720dae123fd0a663c28c4e82213a29c89a4715/jsonschema_rs-0.29.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:c38453a5718bcf2ad1b0163d128814c12829c45f958f9407c69009d8b94a1232", size = 1966084, upload-time = "2025-02-08T21:24:33.8Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e4/f260917a17bb28bb1dec6fa5e869223341fac2c92053aa9bd23c1caaefa0/jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5dc8bdb1067bf4f6d2f80001a636202dc2cea027b8579f1658ce8e736b06557f", size = 2062430, upload-time = "2025-02-08T21:24:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/61353403b76768601d802afa5b7b5902d52c33d1dd0f3159aafa47463634/jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bcfe23992623a540169d0845ea8678209aa2fe7179941dc7c512efc0c2b6b46", size = 2065443, upload-time = "2025-02-08T21:24:36.778Z" }, + { url = "https://files.pythonhosted.org/packages/40/ed/40b971a09f46a22aa956071ea159413046e9d5fcd280a5910da058acdeb2/jsonschema_rs-0.29.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f2a526c0deacd588864d3400a0997421dffef6fe1df5cfda4513a453c01ad42", size = 2082606, upload-time = "2025-02-08T21:24:38.388Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/1c142e1bfb87d57c18fb189149f7aa8edf751725d238d787015278b07600/jsonschema_rs-0.29.1-cp313-cp313-win32.whl", hash = "sha256:68acaefb54f921243552d15cfee3734d222125584243ca438de4444c5654a8a3", size = 1700666, upload-time = "2025-02-08T21:24:40.573Z" }, + { url = "https://files.pythonhosted.org/packages/13/e8/f0ad941286cd350b879dd2b3c848deecd27f0b3fbc0ff44f2809ad59718d/jsonschema_rs-0.29.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c4e5a61ac760a2fc3856a129cc84aa6f8fba7b9bc07b19fe4101050a8ecc33c", size = 1871619, upload-time = "2025-02-08T21:24:42.286Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "pyzmq" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/5a/9066c9f8e94ee517133cd98dba393459a16cd48bba71a82f16a65415206c/jupyter_lsp-2.3.0.tar.gz", hash = "sha256:458aa59339dc868fb784d73364f17dbce8836e906cd75fd471a325cba02e0245", size = 54823, upload-time = "2025-08-27T17:47:34.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/60/1f6cee0c46263de1173894f0fafcb3475ded276c472c14d25e0280c18d6d/jupyter_lsp-2.3.0-py3-none-any.whl", hash = "sha256:e914a3cb2addf48b1c7710914771aaf1819d46b2e5a79b0f917b5478ec93f34f", size = 76687, upload-time = "2025-08-27T17:47:33.15Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/ac/e040ec363d7b6b1f11304cc9f209dac4517ece5d5e01821366b924a64a50/jupyter_server-2.17.0.tar.gz", hash = "sha256:c38ea898566964c888b4772ae1ed58eca84592e88251d2cfc4d171f81f7e99d5", size = 731949, upload-time = "2025-08-21T14:42:54.042Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/a24767e6ca280f5a49525d987bf3e4d7552bf67c8be07e8ccf20271f8568/jupyter_server-2.17.0-py3-none-any.whl", hash = "sha256:e8cb9c7db4251f51ed307e329b81b72ccf2056ff82d50524debde1ee1870e13f", size = 388221, upload-time = "2025-08-21T14:42:52.034Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656, upload-time = "2024-03-12T14:37:00.708Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/e5/4fa382a796a6d8e2cd867816b64f1ff27f906e43a7a83ad9eb389e448cd8/jupyterlab-4.5.0.tar.gz", hash = "sha256:aec33d6d8f1225b495ee2cf20f0514f45e6df8e360bdd7ac9bace0b7ac5177ea", size = 23989880, upload-time = "2025-11-18T13:19:00.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/1e/5a4d5498eba382fee667ed797cf64ae5d1b13b04356df62f067f48bb0f61/jupyterlab-4.5.0-py3-none-any.whl", hash = "sha256:88e157c75c1afff64c7dc4b801ec471450b922a4eae4305211ddd40da8201c8a", size = 12380641, upload-time = "2025-11-18T13:18:56.252Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/90153f189e421e93c4bb4f9e3f59802a1f01abd2ac5cf40b152d7f735232/jupyterlab_server-2.28.0.tar.gz", hash = "sha256:35baa81898b15f93573e2deca50d11ac0ae407ebb688299d3a5213265033712c", size = 76996, upload-time = "2025-10-22T13:59:18.37Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/07/a000fe835f76b7e1143242ab1122e6362ef1c03f23f83a045c38859c2ae0/jupyterlab_server-2.28.0-py3-none-any.whl", hash = "sha256:e4355b148fdcf34d312bbbc80f22467d6d20460e8b8736bf235577dd18506968", size = 59830, upload-time = "2025-10-22T13:59:16.767Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/2d/ef58fed122b268c69c0aa099da20bc67657cdfb2e222688d5731bd5b971d/jupyterlab_widgets-3.0.16.tar.gz", hash = "sha256:423da05071d55cf27a9e602216d35a3a65a3e41cdf9c5d3b643b814ce38c19e0", size = 897423, upload-time = "2025-11-01T21:11:29.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/b5/36c712098e6191d1b4e349304ef73a8d06aed77e56ceaac8c0a306c7bda1/jupyterlab_widgets-3.0.16-py3-none-any.whl", hash = "sha256:45fa36d9c6422cf2559198e4db481aa243c7a32d9926b500781c830c80f7ecf8", size = 914926, upload-time = "2025-11-01T21:11:28.008Z" }, +] + +[[package]] +name = "langchain" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/06/be7273c6c15f5a7e64788ed2aa6329dd019170a176977acff7bcde2cdea2/langchain-1.1.0.tar.gz", hash = "sha256:583c892f59873c0329dbe04169fb3234ac794c50780e7c6fb62a61c7b86a981b", size = 528416, upload-time = "2025-11-24T15:31:24.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/6f/889c01d22c84934615fa3f2dcf94c2fe76fd0afa7a7d01f9b798059f0ecc/langchain-1.1.0-py3-none-any.whl", hash = "sha256:af080f3a4a779bfa5925de7aacb6dfab83249d4aab9a08f7aa7b9bec3766d8ea", size = 101797, upload-time = "2025-11-24T15:31:23.401Z" }, +] + +[[package]] +name = "langchain-anthropic" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anthropic" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/f2/717dcadf0c96960154594409b68bdd5953ab95439e0b65de13cdd5c08785/langchain_anthropic-1.2.0.tar.gz", hash = "sha256:3f3cfad8c519ead2deb21c30dc538b18f4c094704c7874784320cbed7a199453", size = 688803, upload-time = "2025-11-24T14:17:17.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/f4/f684725bd375208130ff3e9878ff3e671d888eec89a834617f3d7bcc14c9/langchain_anthropic-1.2.0-py3-none-any.whl", hash = "sha256:f489df97833e12ca0360a098eb9d04e410752840416be87ab60b0a3e120a99fe", size = 49512, upload-time = "2025-11-24T14:17:16.048Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/17/67c1cc2ace919e2b02dd9d783154d7fb3f1495a4ef835d9cd163b7855ac2/langchain_core-1.1.0.tar.gz", hash = "sha256:2b76a82d427922c8bc51c08404af4fc2a29e9f161dfe2297cb05091e810201e7", size = 781995, upload-time = "2025-11-21T21:01:26.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/1e/e129fc471a2d2a7b3804480a937b5ab9319cab9f4142624fcb115f925501/langchain_core-1.1.0-py3-none-any.whl", hash = "sha256:2c9f27dadc6d21ed4aa46506a37a56e6a7e2d2f9141922dc5c251ba921822ee6", size = 473752, upload-time = "2025-11-21T21:01:25.841Z" }, +] + +[[package]] +name = "langgraph" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d6/3c/af87902d300c1f467165558c8966d8b1e1f896dace271d3f35a410a5c26a/langgraph-1.0.4.tar.gz", hash = "sha256:86d08e25d7244340f59c5200fa69fdd11066aa999b3164b531e2a20036fac156", size = 484397, upload-time = "2025-11-25T20:31:48.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/52/4eb25a3f60399da34ba34adff1b3e324cf0d87eb7a08cebf1882a9b5e0d5/langgraph-1.0.4-py3-none-any.whl", hash = "sha256:b1a835ceb0a8d69b9db48075e1939e28b1ad70ee23fa3fa8f90149904778bacf", size = 157271, upload-time = "2025-11-25T20:31:47.518Z" }, +] + +[[package]] +name = "langgraph-api" +version = "0.5.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "cryptography" }, + { name = "grpcio" }, + { name = "grpcio-tools" }, + { name = "httpx" }, + { name = "jsonschema-rs" }, + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-runtime-inmem" }, + { name = "langgraph-sdk" }, + { name = "langsmith" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-sdk" }, + { name = "orjson" }, + { name = "protobuf" }, + { name = "pyjwt" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "structlog" }, + { name = "tenacity" }, + { name = "truststore" }, + { name = "uvicorn" }, + { name = "watchfiles" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/f9/b0b6551ebd89d818bb31a83421708b8df8d79292f89350b01aaa4f324d51/langgraph_api-0.5.27.tar.gz", hash = "sha256:fb9e7ef0e2a110805e90d32030b5b0f5365dfaceca4aa69484690089d8df0475", size = 366023, upload-time = "2025-11-26T18:27:42.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/16/2325e588b8dcf005d6824b52a6d2a7145a18270e99beffe2e2ee8c422dfa/langgraph_api-0.5.27-py3-none-any.whl", hash = "sha256:c7fc4d33d1218e1d27bf14e73c7cd58d8a6b42b6eb7f8810f58ca7ba52455a1a", size = 294016, upload-time = "2025-11-26T18:27:41.278Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/07/2b1c042fa87d40cf2db5ca27dc4e8dd86f9a0436a10aa4361a8982718ae7/langgraph_checkpoint-3.0.1.tar.gz", hash = "sha256:59222f875f85186a22c494aedc65c4e985a3df27e696e5016ba0b98a5ed2cee0", size = 137785, upload-time = "2025-11-04T21:55:47.774Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/e3/616e3a7ff737d98c1bbb5700dd62278914e2a9ded09a79a1fa93cf24ce12/langgraph_checkpoint-3.0.1-py3-none-any.whl", hash = "sha256:9b04a8d0edc0474ce4eaf30c5d731cee38f11ddff50a6177eead95b5c4e4220b", size = 46249, upload-time = "2025-11-04T21:55:46.472Z" }, +] + +[[package]] +name = "langgraph-cli" +version = "0.4.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "langgraph-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/73/c581ff48ea039404619f56e0628eaa0e5327d9bee43ff2d3ae579b231d06/langgraph_cli-0.4.7.tar.gz", hash = "sha256:51dc5c7bfd0ce957162facea5ef93ffe9778e8d9ec993354f19aec9dd0161470", size = 801549, upload-time = "2025-11-03T23:45:34.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/b6/0d2d0ea664a74fa1b58aaf5e86f6cfa68a4173e601d5aef5f6fc2d83f99e/langgraph_cli-0.4.7-py3-none-any.whl", hash = "sha256:c24e1593c2cdffb658841999eccf71d8bd73025105c727ebcad7fafe35c983ab", size = 39482, upload-time = "2025-11-03T23:45:33.375Z" }, +] + +[package.optional-dependencies] +inmem = [ + { name = "langgraph-api" }, + { name = "langgraph-runtime-inmem" }, + { name = "python-dotenv" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/f9/54f8891b32159e4542236817aea2ee83de0de18bce28e9bdba08c7f93001/langgraph_prebuilt-1.0.5.tar.gz", hash = "sha256:85802675ad778cc7240fd02d47db1e0b59c0c86d8369447d77ce47623845db2d", size = 144453, upload-time = "2025-11-20T16:47:39.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/5e/aeba4a5b39fe6e874e0dd003a82da71c7153e671312671a8dacc5cb7c1af/langgraph_prebuilt-1.0.5-py3-none-any.whl", hash = "sha256:22369563e1848862ace53fbc11b027c28dd04a9ac39314633bb95f2a7e258496", size = 35072, upload-time = "2025-11-20T16:47:38.187Z" }, +] + +[[package]] +name = "langgraph-runtime-inmem" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "blockbuster" }, + { name = "langgraph" }, + { name = "langgraph-checkpoint" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "structlog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/9c/e0a499f3db30fa7f9656d54ec519371b1a01fa19551819d76fe06bbbc9e3/langgraph_runtime_inmem-0.19.0.tar.gz", hash = "sha256:6c57e3ce0282f7088f7f802844e95c01f94d518ced675b022ba9b5cac1b7dfdc", size = 98521, upload-time = "2025-11-22T03:55:42.81Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/c7/7a9ec31fdb960606f6c969a778d600d1c1c3b368e2f31b3986c8b2222745/langgraph_runtime_inmem-0.19.0-py3-none-any.whl", hash = "sha256:57b7ffb1accb0666cfbb51255b14067c6bb03f0d0ba428117e8cdbabe6be1b96", size = 34419, upload-time = "2025-11-22T03:55:41.705Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.2.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/7e/76e02b0242ce184fb9e43a867509aa024f6aebaea9695f53bff30714f0d0/langgraph_sdk-0.2.12.tar.gz", hash = "sha256:7776a95af1e2b084806ad815655fe6f287ead082cae629c106aed72d6e9dce29", size = 124683, upload-time = "2025-12-02T15:47:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/2f/5c97b3fc799730179f2061cca633c0dc03d9e74f0372a783d4d2be924110/langgraph_sdk-0.2.12-py3-none-any.whl", hash = "sha256:d3866a59dec225fdbfad4813399fcd9d2985ecb53a6acb9fe61fe0d8e9b5db7d", size = 60271, upload-time = "2025-12-02T15:47:32.025Z" }, +] + +[[package]] +name = "langsmith" +version = "0.4.50" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/57/d0b012e7afe7f1855c1f38f3358639a31aeff543d8a5e5573cce8720b2e7/langsmith-0.4.50.tar.gz", hash = "sha256:65b0c20e49fde4288c0c0bb049b465c54d49e49f9549587dca5e80c650187b5b", size = 987775, upload-time = "2025-12-02T16:11:34.719Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/f4/3650f87b922aa29984582466a89f09bd2549bc6acc63338ed3a4279add33/langsmith-0.4.50-py3-none-any.whl", hash = "sha256:d0133de1d2a0e81937458fc68ef9210d39ae9883d392519f85e7624d6a0025ad", size = 411061, upload-time = "2025-12-02T16:11:33.25Z" }, +] + +[[package]] +name = "lark" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mistune" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, +] + +[[package]] +name = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "notebook" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/ac/a97041621250a4fc5af379fb377942841eea2ca146aab166b8fcdfba96c2/notebook-7.5.0.tar.gz", hash = "sha256:3b27eaf9913033c28dde92d02139414c608992e1df4b969c843219acf2ff95e4", size = 14052074, upload-time = "2025-11-19T08:36:20.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/96/00df2a4760f10f5af0f45c4955573cae6189931f9a30265a35865f8c1031/notebook-7.5.0-py3-none-any.whl", hash = "sha256:3300262d52905ca271bd50b22617681d95f08a8360d099e097726e6d2efb5811", size = 14460968, upload-time = "2025-11-19T08:36:15.869Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/d8/0f354c375628e048bd0570645b310797299754730079853095bf000fba69/opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12", size = 65242, upload-time = "2025-10-16T08:35:50.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/a2/d86e01c28300bd41bab8f18afd613676e2bd63515417b77636fc1add426f/opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582", size = 65947, upload-time = "2025-10-16T08:35:30.23Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-proto" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/83/dd4660f2956ff88ed071e9e0e36e830df14b8c5dc06722dbde1841accbe8/opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c", size = 20431, upload-time = "2025-10-16T08:35:53.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359, upload-time = "2025-10-16T08:35:34.099Z" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/0a/debcdfb029fbd1ccd1563f7c287b89a6f7bef3b2902ade56797bfd020854/opentelemetry_exporter_otlp_proto_http-1.38.0.tar.gz", hash = "sha256:f16bd44baf15cbe07633c5112ffc68229d0edbeac7b37610be0b2def4e21e90b", size = 17282, upload-time = "2025-10-16T08:35:54.422Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/77/154004c99fb9f291f74aa0822a2f5bbf565a72d8126b3a1b63ed8e5f83c7/opentelemetry_exporter_otlp_proto_http-1.38.0-py3-none-any.whl", hash = "sha256:84b937305edfc563f08ec69b9cb2298be8188371217e867c1854d77198d0825b", size = 19579, upload-time = "2025-10-16T08:35:36.269Z" }, +] + +[[package]] +name = "opentelemetry-proto" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/14/f0c4f0f6371b9cb7f9fa9ee8918bfd59ac7040c7791f1e6da32a1839780d/opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468", size = 46152, upload-time = "2025-10-16T08:36:01.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/6a/82b68b14efca5150b2632f3692d627afa76b77378c4999f2648979409528/opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18", size = 72535, upload-time = "2025-10-16T08:35:45.749Z" }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/f0eee1445161faf4c9af3ba7b848cc22a50a3d3e2515051ad8628c35ff80/opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe", size = 171942, upload-time = "2025-10-16T08:36:02.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/2e/e93777a95d7d9c40d270a371392b6d6f1ff170c2a3cb32d6176741b5b723/opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b", size = 132349, upload-time = "2025-10-16T08:35:46.995Z" }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.59b0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/bc/8b9ad3802cd8ac6583a4eb7de7e5d7db004e89cb7efe7008f9c8a537ee75/opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0", size = 129861, upload-time = "2025-10-16T08:36:03.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954, upload-time = "2025-10-16T08:35:48.054Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188, upload-time = "2025-10-24T15:50:38.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/1d/1ea6005fffb56715fd48f632611e163d1604e8316a5bad2288bee9a1c9eb/orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", size = 243498, upload-time = "2025-10-24T15:48:48.101Z" }, + { url = "https://files.pythonhosted.org/packages/37/d7/ffed10c7da677f2a9da307d491b9eb1d0125b0307019c4ad3d665fd31f4f/orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", size = 128961, upload-time = "2025-10-24T15:48:49.571Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3e4d10a18866d1368f73c8c44b7fe37cc8a15c32f2a7620be3877d4c55a3/orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", size = 130321, upload-time = "2025-10-24T15:48:50.713Z" }, + { url = "https://files.pythonhosted.org/packages/eb/1f/465f66e93f434f968dd74d5b623eb62c657bdba2332f5a8be9f118bb74c7/orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", size = 129207, upload-time = "2025-10-24T15:48:52.193Z" }, + { url = "https://files.pythonhosted.org/packages/28/43/d1e94837543321c119dff277ae8e348562fe8c0fafbb648ef7cb0c67e521/orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", size = 136323, upload-time = "2025-10-24T15:48:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/04/93303776c8890e422a5847dd012b4853cdd88206b8bbd3edc292c90102d1/orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", size = 137440, upload-time = "2025-10-24T15:48:56.326Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ef/75519d039e5ae6b0f34d0336854d55544ba903e21bf56c83adc51cd8bf82/orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", size = 136680, upload-time = "2025-10-24T15:48:57.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/18/bf8581eaae0b941b44efe14fee7b7862c3382fbc9a0842132cfc7cf5ecf4/orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", size = 136160, upload-time = "2025-10-24T15:48:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/c4/35/a6d582766d351f87fc0a22ad740a641b0a8e6fc47515e8614d2e4790ae10/orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", size = 140318, upload-time = "2025-10-24T15:49:00.834Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/5a4801803ab2e2e2d703bce1a56540d9f99a9143fbec7bf63d225044fef8/orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", size = 406330, upload-time = "2025-10-24T15:49:02.327Z" }, + { url = "https://files.pythonhosted.org/packages/80/55/a8f682f64833e3a649f620eafefee175cbfeb9854fc5b710b90c3bca45df/orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", size = 149580, upload-time = "2025-10-24T15:49:03.517Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e4/c132fa0c67afbb3eb88274fa98df9ac1f631a675e7877037c611805a4413/orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", size = 139846, upload-time = "2025-10-24T15:49:04.761Z" }, + { url = "https://files.pythonhosted.org/packages/54/06/dc3491489efd651fef99c5908e13951abd1aead1257c67f16135f95ce209/orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", size = 135781, upload-time = "2025-10-24T15:49:05.969Z" }, + { url = "https://files.pythonhosted.org/packages/79/b7/5e5e8d77bd4ea02a6ac54c42c818afb01dd31961be8a574eb79f1d2cfb1e/orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", size = 131391, upload-time = "2025-10-24T15:49:07.355Z" }, + { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252, upload-time = "2025-10-24T15:49:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/63/51/6b556192a04595b93e277a9ff71cd0cc06c21a7df98bcce5963fa0f5e36f/orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", size = 243571, upload-time = "2025-10-24T15:49:10.008Z" }, + { url = "https://files.pythonhosted.org/packages/1c/2c/2602392ddf2601d538ff11848b98621cd465d1a1ceb9db9e8043181f2f7b/orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", size = 128891, upload-time = "2025-10-24T15:49:11.297Z" }, + { url = "https://files.pythonhosted.org/packages/4e/47/bf85dcf95f7a3a12bf223394a4f849430acd82633848d52def09fa3f46ad/orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", size = 130137, upload-time = "2025-10-24T15:49:12.544Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4d/a0cb31007f3ab6f1fd2a1b17057c7c349bc2baf8921a85c0180cc7be8011/orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", size = 129152, upload-time = "2025-10-24T15:49:13.754Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ef/2811def7ce3d8576b19e3929fff8f8f0d44bc5eb2e0fdecb2e6e6cc6c720/orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", size = 136834, upload-time = "2025-10-24T15:49:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/00/d4/9aee9e54f1809cec8ed5abd9bc31e8a9631d19460e3b8470145d25140106/orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", size = 137519, upload-time = "2025-10-24T15:49:16.557Z" }, + { url = "https://files.pythonhosted.org/packages/db/ea/67bfdb5465d5679e8ae8d68c11753aaf4f47e3e7264bad66dc2f2249e643/orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", size = 136749, upload-time = "2025-10-24T15:49:17.796Z" }, + { url = "https://files.pythonhosted.org/packages/01/7e/62517dddcfce6d53a39543cd74d0dccfcbdf53967017c58af68822100272/orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", size = 136325, upload-time = "2025-10-24T15:49:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/40516739f99ab4c7ec3aaa5cc242d341fcb03a45d89edeeaabc5f69cb2cf/orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", size = 140204, upload-time = "2025-10-24T15:49:20.545Z" }, + { url = "https://files.pythonhosted.org/packages/82/18/ff5734365623a8916e3a4037fcef1cd1782bfc14cf0992afe7940c5320bf/orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", size = 406242, upload-time = "2025-10-24T15:49:21.884Z" }, + { url = "https://files.pythonhosted.org/packages/e1/43/96436041f0a0c8c8deca6a05ebeaf529bf1de04839f93ac5e7c479807aec/orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", size = 150013, upload-time = "2025-10-24T15:49:23.185Z" }, + { url = "https://files.pythonhosted.org/packages/1b/48/78302d98423ed8780479a1e682b9aecb869e8404545d999d34fa486e573e/orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", size = 139951, upload-time = "2025-10-24T15:49:24.428Z" }, + { url = "https://files.pythonhosted.org/packages/4a/7b/ad613fdcdaa812f075ec0875143c3d37f8654457d2af17703905425981bf/orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", size = 136049, upload-time = "2025-10-24T15:49:25.973Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3c/9cf47c3ff5f39b8350fb21ba65d789b6a1129d4cbb3033ba36c8a9023520/orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", size = 131461, upload-time = "2025-10-24T15:49:27.259Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3b/e2425f61e5825dc5b08c2a5a2b3af387eaaca22a12b9c8c01504f8614c36/orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", size = 126167, upload-time = "2025-10-24T15:49:28.511Z" }, + { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525, upload-time = "2025-10-24T15:49:29.737Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871, upload-time = "2025-10-24T15:49:31.109Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055, upload-time = "2025-10-24T15:49:33.382Z" }, + { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061, upload-time = "2025-10-24T15:49:34.935Z" }, + { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541, upload-time = "2025-10-24T15:49:36.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535, upload-time = "2025-10-24T15:49:38.307Z" }, + { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703, upload-time = "2025-10-24T15:49:40.795Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293, upload-time = "2025-10-24T15:49:42.347Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131, upload-time = "2025-10-24T15:49:43.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164, upload-time = "2025-10-24T15:49:45.498Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859, upload-time = "2025-10-24T15:49:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926, upload-time = "2025-10-24T15:49:48.321Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007, upload-time = "2025-10-24T15:49:49.938Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314, upload-time = "2025-10-24T15:49:51.248Z" }, + { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152, upload-time = "2025-10-24T15:49:52.922Z" }, + { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501, upload-time = "2025-10-24T15:49:54.288Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862, upload-time = "2025-10-24T15:49:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047, upload-time = "2025-10-24T15:49:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073, upload-time = "2025-10-24T15:49:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597, upload-time = "2025-10-24T15:50:00.12Z" }, + { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515, upload-time = "2025-10-24T15:50:01.57Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703, upload-time = "2025-10-24T15:50:02.944Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311, upload-time = "2025-10-24T15:50:04.441Z" }, + { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127, upload-time = "2025-10-24T15:50:07.398Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201, upload-time = "2025-10-24T15:50:08.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872, upload-time = "2025-10-24T15:50:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931, upload-time = "2025-10-24T15:50:11.623Z" }, + { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065, upload-time = "2025-10-24T15:50:13.025Z" }, + { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310, upload-time = "2025-10-24T15:50:14.46Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151, upload-time = "2025-10-24T15:50:15.878Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/67/d5ef41c3b4a94400be801984ef7c7fc9623e1a82b643e74eeec367e7462b/ormsgpack-1.12.0.tar.gz", hash = "sha256:94be818fdbb0285945839b88763b269987787cb2f7ef280cad5d6ec815b7e608", size = 49959, upload-time = "2025-11-04T18:30:10.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/ba/3cae83cf36420c1c8dd294f16c852c03313aafe2439a165c4c6ac611b1d0/ormsgpack-1.12.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c40d86d77391b18dd34de5295e3de2b8ad818bcab9c9def4121c8ec5c9714ae4", size = 369159, upload-time = "2025-11-04T18:29:27.057Z" }, + { url = "https://files.pythonhosted.org/packages/97/d4/5e176309e01a8b9098d80201aac1eb7db9336c3b5b4fa6254a2bbb0d0fa0/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:777b7fab364dc0f200bb382a98a385c8222ffa6a2333d627d763797326202c86", size = 195744, upload-time = "2025-11-04T18:29:28.069Z" }, + { url = "https://files.pythonhosted.org/packages/4f/83/6d80c8c5571639c000a39f38f77752dfaf9d9e552d775331e8d280f66a4e/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b5089ad9dd5b3d3013b245a55e4abaea2f8ad70f4a78e1b002127b02340004", size = 206474, upload-time = "2025-11-04T18:29:29.034Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/940311e48dc0cfc3e212bd7007a21ed0825158638057687d804f2c5c2cca/ormsgpack-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaf0c87cace7bc08fbf68c5cc66605b593df6427e9f4de235b2da358787e008", size = 207959, upload-time = "2025-11-04T18:29:30.315Z" }, + { url = "https://files.pythonhosted.org/packages/1a/e3/fbe94b0a311815343b86a95a0627e4901b11ff6fd522679ca29a2a88c99b/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f62d476fe28bc5675d9aff30341bfa9f41d7de332c5b63fbbe9aaf6bb7ec74d4", size = 377666, upload-time = "2025-11-04T18:29:31.38Z" }, + { url = "https://files.pythonhosted.org/packages/a3/3b/229cfa28076798ffb619aaa854b842de3f2ed5ea4e6509bf34d14c038c4d/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ded7810095b887e28434f32f5a345d354e88cf851bab3c5435aeb86a718618d2", size = 471394, upload-time = "2025-11-04T18:29:32.521Z" }, + { url = "https://files.pythonhosted.org/packages/6b/bd/4eae4ab35586e4175c07acb5f98aec83aa9d8987f71ea0443aa900191bdf/ormsgpack-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f72a1dea0c4ae7c4101dcfbe8133f274a9d769d0b87fe5188db4fab07ffabaee", size = 381506, upload-time = "2025-11-04T18:29:33.533Z" }, + { url = "https://files.pythonhosted.org/packages/dd/51/f9d56d6d015cbfa1ce9a4358ca30a41744644f0cf606e060d7203efe5af8/ormsgpack-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f479bfef847255d7d0b12c7a198f6a21490155da2da3062e082ba370893d4a1", size = 112707, upload-time = "2025-11-04T18:29:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/f4/07/bb189ef7072979f2f96e8716e952172efdce9c54930aa0814bec73aee19b/ormsgpack-1.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:3583ca410e4502144b2594170542e4bbef7b15643fd1208703ae820f11029036", size = 106533, upload-time = "2025-11-04T18:29:36.112Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f2/c1036b2775fcc0cfa5fd618c53bcd3b862ee07298fb627f03af4c7982f84/ormsgpack-1.12.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0c1e08b64d99076fee155276097489b82cc56e8d5951c03c721a65a32f44494", size = 369538, upload-time = "2025-11-04T18:29:37.125Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ca/526c4ae02f3cb34621af91bf8282a10d666757c2e0c6ff391ff5d403d607/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fd43bcb299131690b8e0677af172020b2ada8e625169034b42ac0c13adf84aa", size = 195872, upload-time = "2025-11-04T18:29:38.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0f/83bb7968e9715f6a85be53d041b1e6324a05428f56b8b980dac866886871/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0149d595341e22ead340bf281b2995c4cc7dc8d522a6b5f575fe17aa407604", size = 206469, upload-time = "2025-11-04T18:29:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/02/e3/9e93ca1065f2d4af035804a842b1ff3025bab580c7918239bb225cd1fee2/ormsgpack-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f19a1b27d169deb553c80fd10b589fc2be1fc14cee779fae79fcaf40db04de2b", size = 208273, upload-time = "2025-11-04T18:29:40.769Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d8/6d6ef901b3a8b8f3ab8836b135a56eb7f66c559003e251d9530bedb12627/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6f28896942d655064940dfe06118b7ce1e3468d051483148bf02c99ec157483a", size = 377839, upload-time = "2025-11-04T18:29:42.092Z" }, + { url = "https://files.pythonhosted.org/packages/4c/72/fcb704bfa4c2c3a37b647d597cc45a13cffc9d50baac635a9ad620731d29/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9396efcfa48b4abbc06e44c5dbc3c4574a8381a80cb4cd01eea15d28b38c554e", size = 471446, upload-time = "2025-11-04T18:29:43.133Z" }, + { url = "https://files.pythonhosted.org/packages/84/f8/402e4e3eb997c2ee534c99bec4b5bb359c2a1f9edadf043e254a71e11378/ormsgpack-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96586ed537a5fb386a162c4f9f7d8e6f76e07b38a990d50c73f11131e00ff040", size = 381783, upload-time = "2025-11-04T18:29:44.466Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8d/5897b700360bc00911b70ae5ef1134ee7abf5baa81a92a4be005917d3dfd/ormsgpack-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e70387112fb3870e4844de090014212cdcf1342f5022047aecca01ec7de05d7a", size = 112943, upload-time = "2025-11-04T18:29:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5b/44/1e73649f79bb96d6cf9e5bcbac68b6216d238bba80af351c4c0cbcf7ee15/ormsgpack-1.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:d71290a23de5d4829610c42665d816c661ecad8979883f3f06b2e3ab9639962e", size = 106688, upload-time = "2025-11-04T18:29:46.411Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e8/35f11ce9313111488b26b3035e4cbe55caa27909c0b6c8b5b5cd59f9661e/ormsgpack-1.12.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:766f2f3b512d85cd375b26a8b1329b99843560b50b93d3880718e634ad4a5de5", size = 369574, upload-time = "2025-11-04T18:29:47.431Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/77461587f412d4e598d3687bafe23455ed0f26269f44be20252eddaa624e/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84b285b1f3f185aad7da45641b873b30acfd13084cf829cf668c4c6480a81583", size = 195893, upload-time = "2025-11-04T18:29:48.735Z" }, + { url = "https://files.pythonhosted.org/packages/c6/67/e197ceb04c3b550589e5407fc9fdae10f4e2e2eba5fdac921a269e02e974/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e23604fc79fe110292cb365f4c8232e64e63a34f470538be320feae3921f271b", size = 206503, upload-time = "2025-11-04T18:29:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b1/7fa8ba82a25cef678983c7976f85edeef5014f5c26495f338258e6a3cf1c/ormsgpack-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc32b156c113a0fae2975051417d8d9a7a5247c34b2d7239410c46b75ce9348a", size = 208257, upload-time = "2025-11-04T18:29:51.007Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/759e999390000d2589e6d0797f7265e6ec28378547075d28d3736248ab63/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:94ac500dd10c20fa8b8a23bc55606250bfe711bf9716828d9f3d44dfd1f25668", size = 377852, upload-time = "2025-11-04T18:29:52.103Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/0af737c94272494d9d84a3c29cc42c973ef7fd2342917020906596db863c/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:c5201ff7ec24f721f813a182885a17064cffdbe46b2412685a52e6374a872c8f", size = 471456, upload-time = "2025-11-04T18:29:53.336Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ba/c81f0aa4f19fbf457213395945b672e6fde3ce777e3587456e7f0fca2147/ormsgpack-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a9740bb3839c9368aacae1cbcfc474ee6976458f41cc135372b7255d5206c953", size = 381813, upload-time = "2025-11-04T18:29:54.394Z" }, + { url = "https://files.pythonhosted.org/packages/ce/15/429c72d64323503fd42cc4ca8398930ded8aa8b3470df8a86b3bbae7a35c/ormsgpack-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ed37f29772432048b58174e920a1d4c4cde0404a5d448d3d8bbcc95d86a6918", size = 112949, upload-time = "2025-11-04T18:29:55.371Z" }, + { url = "https://files.pythonhosted.org/packages/55/b9/e72c451a40f8c57bfc229e0b8e536ecea7203c8f0a839676df2ffb605c62/ormsgpack-1.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:b03994bbec5d6d42e03d6604e327863f885bde67aa61e06107ce1fa5bdd3e71d", size = 106689, upload-time = "2025-11-04T18:29:56.262Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/13eab1a75da531b359105fdee90dda0b6bd1ca0a09880250cf91d8bdfdea/ormsgpack-1.12.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0f3981ba3cba80656012090337e548e597799e14b41e3d0b595ab5ab05a23d7f", size = 369620, upload-time = "2025-11-04T18:29:57.255Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c1/cbcc38b7af4ce58d8893e56d3595c0c8dcd117093bf048f889cf351bdba0/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:901f6f55184d6776dbd5183cbce14caf05bf7f467eef52faf9b094686980bf71", size = 195925, upload-time = "2025-11-04T18:29:58.34Z" }, + { url = "https://files.pythonhosted.org/packages/5c/59/4fa4dc0681490e12b75333440a1c0fd9741b0ebff272b1db4a29d35c2021/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e13b15412571422b711b40f45e3fe6d993ea3314b5e97d1a853fe99226c5effc", size = 206594, upload-time = "2025-11-04T18:29:59.329Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/249770896bc32bb91b22c30256961f935d0915cbcf6e289a7fc961d9b14c/ormsgpack-1.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91fa8a452553a62e5fb3fbab471e7faf7b3bec3c87a2f355ebf3d7aab290fe4f", size = 208307, upload-time = "2025-11-04T18:30:00.377Z" }, + { url = "https://files.pythonhosted.org/packages/07/0a/e041a248cd72f2f4c07e155913e0a3ede4c86cf21a40ae6cd79f135f2847/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74ec101f69624695eec4ce7c953192d97748254abe78fb01b591f06d529e1952", size = 377844, upload-time = "2025-11-04T18:30:01.389Z" }, + { url = "https://files.pythonhosted.org/packages/d8/71/6f7773e4ffda73a358ce4bba69b3e8bee9d40a7a06315e4c1cd7a3ea9d02/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9bbf7896580848326c1f9bd7531f264e561f98db7e08e15aa75963d83832c717", size = 471572, upload-time = "2025-11-04T18:30:02.486Z" }, + { url = "https://files.pythonhosted.org/packages/65/29/af6769a4289c07acc71e7bda1d64fb31800563147d73142686e185e82348/ormsgpack-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7567917da613b8f8d591c1674e411fd3404bea41ef2b9a0e0a1e049c0f9406d7", size = 381842, upload-time = "2025-11-04T18:30:03.799Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/0a86195ee7a1a96c088aefc8504385e881cf56f4563ed81bafe21cbf1fb0/ormsgpack-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e418256c5d8622b8bc92861936f7c6a0131355e7bcad88a42102ae8227f8a1c", size = 113008, upload-time = "2025-11-04T18:30:04.777Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/fafc79e32f3087f6f26f509d80b8167516326bfea38d30502627c01617e0/ormsgpack-1.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:433ace29aa02713554f714c62a4e4dcad0c9e32674ba4f66742c91a4c3b1b969", size = 106648, upload-time = "2025-11-04T18:30:05.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cf/5d58d9b132128d2fe5d586355dde76af386554abef00d608f66b913bff1f/ormsgpack-1.12.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e57164be4ca34b64e210ec515059193280ac84df4d6f31a6fcbfb2fc8436de55", size = 369803, upload-time = "2025-11-04T18:30:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/67/42/968a2da361eaff2e4cbb17c82c7599787babf16684110ad70409646cc1e4/ormsgpack-1.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:904f96289deaa92fc6440b122edc27c5bdc28234edd63717f6d853d88c823a83", size = 195991, upload-time = "2025-11-04T18:30:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/03/f0/9696c6c6cf8ad35170f0be8d0ef3523cc258083535f6c8071cb8235ebb8b/ormsgpack-1.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b291d086e524a1062d57d1b7b5a8bcaaf29caebf0212fec12fd86240bd33633", size = 208316, upload-time = "2025-11-04T18:30:08.663Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "personal-assistant" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "deepagents" }, + { name = "html2text" }, + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langgraph" }, + { name = "langgraph-cli", extra = ["inmem"] }, + { name = "rich" }, +] + +[package.optional-dependencies] +dev = [ + { name = "ipython" }, + { name = "jupyter" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ipython" }, + { name = "jupyter" }, +] + +[package.metadata] +requires-dist = [ + { name = "deepagents" }, + { name = "html2text", specifier = ">=2020.1.16" }, + { name = "ipython", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "jupyter", marker = "extra == 'dev'", specifier = ">=1.0.0" }, + { name = "langchain", specifier = ">=1.1.0" }, + { name = "langchain-anthropic", specifier = ">=1.0.3" }, + { name = "langgraph", specifier = ">=1.0.4" }, + { name = "langgraph-cli", extras = ["inmem"], specifier = ">=0.1.55" }, + { name = "rich", specifier = ">=10.0.0" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "ipython", specifier = ">=8.0.0" }, + { name = "jupyter", specifier = ">=1.0.0" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/53/3edb5d68ecf6b38fcbcc1ad28391117d2a322d9a1a3eff04bfdb184d8c3b/prometheus_client-0.23.1.tar.gz", hash = "sha256:6ae8f9081eaaaf153a2e959d2e6c4f4fb57b12ef76c8c7980202f1e57b48b2ce", size = 80481, upload-time = "2025-09-18T20:47:25.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/db/14bafcb4af2139e046d03fd00dea7873e48eafe18b7d2797e73d6681f210/prometheus_client-0.23.1-py3-none-any.whl", hash = "sha256:dd1913e6e76b59cfe44e7a4b83e01afc9873c1bdfd2ed8739f1e76aeca115f99", size = 61145, upload-time = "2025-09-18T20:47:23.875Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/03/a1440979a3f74f16cab3b75b0da1a1a7f922d56a8ddea96092391998edc0/protobuf-6.33.1.tar.gz", hash = "sha256:97f65757e8d09870de6fd973aeddb92f85435607235d20b2dfed93405d00c85b", size = 443432, upload-time = "2025-11-13T16:44:18.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f1/446a9bbd2c60772ca36556bac8bfde40eceb28d9cc7838755bc41e001d8f/protobuf-6.33.1-cp310-abi3-win32.whl", hash = "sha256:f8d3fdbc966aaab1d05046d0240dd94d40f2a8c62856d41eaa141ff64a79de6b", size = 425593, upload-time = "2025-11-13T16:44:06.275Z" }, + { url = "https://files.pythonhosted.org/packages/a6/79/8780a378c650e3df849b73de8b13cf5412f521ca2ff9b78a45c247029440/protobuf-6.33.1-cp310-abi3-win_amd64.whl", hash = "sha256:923aa6d27a92bf44394f6abf7ea0500f38769d4b07f4be41cb52bd8b1123b9ed", size = 436883, upload-time = "2025-11-13T16:44:09.222Z" }, + { url = "https://files.pythonhosted.org/packages/cd/93/26213ff72b103ae55bb0d73e7fb91ea570ef407c3ab4fd2f1f27cac16044/protobuf-6.33.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:fe34575f2bdde76ac429ec7b570235bf0c788883e70aee90068e9981806f2490", size = 427522, upload-time = "2025-11-13T16:44:10.475Z" }, + { url = "https://files.pythonhosted.org/packages/c2/32/df4a35247923393aa6b887c3b3244a8c941c32a25681775f96e2b418f90e/protobuf-6.33.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:f8adba2e44cde2d7618996b3fc02341f03f5bc3f2748be72dc7b063319276178", size = 324445, upload-time = "2025-11-13T16:44:11.869Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d0/d796e419e2ec93d2f3fa44888861c3f88f722cde02b7c3488fcc6a166820/protobuf-6.33.1-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:0f4cf01222c0d959c2b399142deb526de420be8236f22c71356e2a544e153c53", size = 339161, upload-time = "2025-11-13T16:44:12.778Z" }, + { url = "https://files.pythonhosted.org/packages/1d/2a/3c5f05a4af06649547027d288747f68525755de692a26a7720dced3652c0/protobuf-6.33.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:8fd7d5e0eb08cd5b87fd3df49bc193f5cfd778701f47e11d127d0afc6c39f1d1", size = 323171, upload-time = "2025-11-13T16:44:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/08/b4/46310463b4f6ceef310f8348786f3cff181cea671578e3d9743ba61a459e/protobuf-6.33.1-py3-none-any.whl", hash = "sha256:d595a9fd694fdeb061a62fbe10eb039cc1e444df81ec9bb70c7fc59ebcb1eafa", size = 170477, upload-time = "2025-11-13T16:44:17.633Z" }, +] + +[[package]] +name = "psutil" +version = "7.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" }, + { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" }, + { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" }, + { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" }, + { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" }, + { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" }, + { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" }, + { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "pywinpty" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/bb/a7cc2967c5c4eceb6cc49cfe39447d4bfc56e6c865e7c2249b6eb978935f/pywinpty-3.0.2.tar.gz", hash = "sha256:1505cc4cb248af42cb6285a65c9c2086ee9e7e574078ee60933d5d7fa86fb004", size = 30669, upload-time = "2025-10-03T21:16:29.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/a1/409c1651c9f874d598c10f51ff586c416625601df4bca315d08baec4c3e3/pywinpty-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:327790d70e4c841ebd9d0f295a780177149aeb405bca44c7115a3de5c2054b23", size = 2050304, upload-time = "2025-10-03T21:19:29.466Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/1098484e042c9485f56f16eb2b69b43b874bd526044ee401512234cf9e04/pywinpty-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:99fdd9b455f0ad6419aba6731a7a0d2f88ced83c3c94a80ff9533d95fa8d8a9e", size = 2050391, upload-time = "2025-10-03T21:19:01.642Z" }, + { url = "https://files.pythonhosted.org/packages/fc/19/b757fe28008236a4a713e813283721b8a40aa60cd7d3f83549f2e25a3155/pywinpty-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:18f78b81e4cfee6aabe7ea8688441d30247b73e52cd9657138015c5f4ee13a51", size = 2050057, upload-time = "2025-10-03T21:19:26.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/cbae12ecf6f4fa4129c36871fd09c6bef4f98d5f625ecefb5e2449765508/pywinpty-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:663383ecfab7fc382cc97ea5c4f7f0bb32c2f889259855df6ea34e5df42d305b", size = 2049874, upload-time = "2025-10-03T21:18:53.923Z" }, + { url = "https://files.pythonhosted.org/packages/ca/15/f12c6055e2d7a617d4d5820e8ac4ceaff849da4cb124640ef5116a230771/pywinpty-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:28297cecc37bee9f24d8889e47231972d6e9e84f7b668909de54f36ca785029a", size = 2050386, upload-time = "2025-10-03T21:18:50.477Z" }, + { url = "https://files.pythonhosted.org/packages/de/24/c6907c5bb06043df98ad6a0a0ff5db2e0affcecbc3b15c42404393a3f72a/pywinpty-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:34b55ae9a1b671fe3eae071d86618110538e8eaad18fcb1531c0830b91a82767", size = 2049834, upload-time = "2025-10-03T21:19:25.688Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyzmq" +version = "27.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, + { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, + { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, + { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, + { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, + { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, + { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, + { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, + { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, + { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, + { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, + { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, + { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rfc3987-syntax" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "lark" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/06/37c1a5557acf449e8e406a830a05bf885ac47d33270aec454ef78675008d/rfc3987_syntax-1.1.0.tar.gz", hash = "sha256:717a62cbf33cffdd16dfa3a497d81ce48a660ea691b1ddd7be710c22f00b4a0d", size = 14239, upload-time = "2025-07-18T01:05:05.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/71/44ce230e1b7fadd372515a97e32a83011f906ddded8d03e3c6aafbdedbb7/rfc3987_syntax-1.1.0-py3-none-any.whl", hash = "sha256:6c3d97604e4c5ce9f714898e05401a0445a641cfa276432b0a648c80856f6a3f", size = 8046, upload-time = "2025-07-18T01:05:03.843Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, + { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, + { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, + { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, + { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, + { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, + { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, + { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, +] + +[[package]] +name = "send2trash" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394, upload-time = "2024-04-07T00:01:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072, upload-time = "2024-04-07T00:01:07.438Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, +] + +[[package]] +name = "sse-starlette" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/fc/56ab9f116b2133521f532fce8d03194cf04dcac25f583cf3d839be4c0496/sse_starlette-2.1.3.tar.gz", hash = "sha256:9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169", size = 19678, upload-time = "2024-08-01T08:52:50.248Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/aa/36b271bc4fa1d2796311ee7c7283a3a1c348bad426d37293609ca4300eef/sse_starlette-2.1.3-py3-none-any.whl", hash = "sha256:8ec846438b4665b9e8c560fcdea6bc8081a3abf7942faa95e5a744999d219772", size = 9383, upload-time = "2024-08-01T08:52:48.659Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "structlog" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, + { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, + { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, + { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, + { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "webcolors" +version = "25.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/7a/eb316761ec35664ea5174709a68bbd3389de60d4a1ebab8808bfc264ed67/webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf", size = 53491, upload-time = "2025-10-31T07:51:03.977Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/cc/e097523dd85c9cf5d354f78310927f1656c422bd7b2613b2db3e3f9a0f2c/webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d", size = 14905, upload-time = "2025-10-31T07:51:01.778Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/f4/c67440c7fb409a71b7404b7aefcd7569a9c0d6bd071299bf4198ae7a5d95/widgetsnbextension-4.0.15.tar.gz", hash = "sha256:de8610639996f1567952d763a5a41af8af37f2575a41f9852a38f947eb82a3b9", size = 1097402, upload-time = "2025-11-01T21:15:55.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl", hash = "sha256:8156704e4346a571d9ce73b84bee86a29906c9abfd7223b7228a28899ccf3366", size = 2196503, upload-time = "2025-11-01T21:15:53.565Z" }, +] + +[[package]] +name = "xxhash" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, + { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, + { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, + { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, + { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, + { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, + { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, + { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, + { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, + { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, + { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, + { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, + { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, + { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, + { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, + { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, + { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, + { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, + { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, + { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, + { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, + { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, + { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, + { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, + { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, + { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, + { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, + { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, + { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, + { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, + { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, + { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, + { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, + { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, + { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, + { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, + { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +] From 5502e158788d6a26659267dd70e41f9a42bb1238 Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Tue, 2 Dec 2025 21:42:15 -0800 Subject: [PATCH 02/11] refactor: simplify email assistant by moving triage to agent tool Replace the separate triage routing step with a triage_email tool that the agent calls directly. This simplifies the architecture from a multi-node workflow to a single deepagent. Key changes: - Add triage_email tool with structured schema (reasoning + classification) - Remove triage_router and triage_interrupt_handler nodes - Update agent prompt to require triage_email as first step - Simplify state schema to accept message strings instead of email_input dict - Update middleware to include triage_instructions in memory retrieval - Add triage_email to tool registry Benefits: - Simpler single-agent architecture vs multi-node workflow - More flexible with triage reasoning in agent context - Cleaner input interface (simple message strings) - Better token efficiency (no separate LLM routing calls) --- .../email_assistant_deepagents.py | 200 ++-------- .../middleware/email_assistant_hitl.py | 4 +- .../src/personal_assistant/prompts.py | 44 ++- .../src/personal_assistant/schemas.py | 32 +- .../src/personal_assistant/tools/base.py | 5 +- .../tools/default/email_tools.py | 27 +- personal_assistant/test.ipynb | 365 ++---------------- 7 files changed, 109 insertions(+), 568 deletions(-) diff --git a/personal_assistant/src/personal_assistant/email_assistant_deepagents.py b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py index 9d1d452..147d93a 100644 --- a/personal_assistant/src/personal_assistant/email_assistant_deepagents.py +++ b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py @@ -4,175 +4,24 @@ create_deep_agent() pattern instead of manual graph construction. All functionality is preserved including HITL logic, memory system, and custom tools. +The agent now handles triage directly through a tool call instead of a separate routing step. + Usage: python -m examples.personal_assistant.email_assistant_deepagents """ -from typing import Literal - -from langgraph.graph import StateGraph, START, END from langchain_anthropic import ChatAnthropic from langgraph.checkpoint.memory import MemorySaver from langgraph.store.memory import InMemoryStore -from langgraph.store.base import BaseStore -from langgraph.types import interrupt, Command from deepagents import create_deep_agent from deepagents.backends import StoreBackend from .middleware import EmailAssistantHITLMiddleware -from .schemas import State, StateInput, EmailAssistantState, UserPreferences, RouterSchema +from .schemas import EmailAssistantState from .tools import get_tools -from .utils import format_email_markdown, parse_email, get_memory, update_memory -from .prompts import triage_user_prompt, default_triage_instructions, triage_system_prompt, default_background - -def triage_router(state: State, store: BaseStore) -> Command[Literal["triage_interrupt_handler", "response_agent", "__end__"]]: - """Analyze email content to decide if we should respond, notify, or ignore. - - The triage step prevents the assistant from wasting time on: - - Marketing emails and spam - - Company-wide announcements - - Messages meant for other teams - """ - - # Parse the email input - author, to, subject, email_thread = parse_email(state["email_input"]) - user_prompt = triage_user_prompt.format( - author=author, to=to, subject=subject, email_thread=email_thread - ) - - # Create email markdown for Agent Inbox in case of notification - email_markdown = format_email_markdown(subject, author, to, email_thread) - - # Search for existing triage_preferences memory - triage_instructions = get_memory(store, ("email_assistant", "triage_preferences"), default_triage_instructions) - - # Format system prompt with background and triage instructions - system_prompt = triage_system_prompt.format( - background=default_background, - triage_instructions=triage_instructions, - ) - - llm = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0) - llm_router = llm.with_structured_output(RouterSchema) - - # Run the router LLM - result = llm_router.invoke( - [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ] - ) - - # Decision - classification = result.classification - - # Process the classification decision - if classification == "respond": - print("📧 Classification: RESPOND - This email requires a response") - # Next node - goto = "response_agent" - # Update the state - update = { - "classification_decision": result.classification, - "messages": [{"role": "user", - "content": f"Respond to the email: {email_markdown}" - }], - } - - elif classification == "ignore": - print("🚫 Classification: IGNORE - This email can be safely ignored") - - # Next node - goto = END - # Update the state - update = { - "classification_decision": classification, - } - - elif classification == "notify": - print("🔔 Classification: NOTIFY - This email contains important information") - - # Next node - goto = "triage_interrupt_handler" - # Update the state - update = { - "classification_decision": classification, - } - - else: - raise ValueError(f"Invalid classification: {classification}") - - return Command(goto=goto, update=update) - -def triage_interrupt_handler(state: State, store: BaseStore) -> Command[Literal["response_agent", "__end__"]]: - """Handles interrupts from the triage step""" - - # Parse the email input - author, to, subject, email_thread = parse_email(state["email_input"]) - - # Create email markdown for Agent Inbox in case of notification - email_markdown = format_email_markdown(subject, author, to, email_thread) - - # Create messages - messages = [{"role": "user", - "content": f"Email to notify user about: {email_markdown}" - }] - - # Create interrupt for Agent Inbox - request = { - "action_request": { - "action": f"Email Assistant: {state['classification_decision']}", - "args": {} - }, - "config": { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": False, - "allow_accept": False, - }, - # Email to show in Agent Inbox - "description": email_markdown, - } - - # Send to Agent Inbox and wait for response - response = interrupt([request])[0] - - # If user provides feedback, go to response agent and use feedback to respond to email - if response["type"] == "response": - # Add feedback to messages - user_input = response["args"] - messages.append({"role": "user", - "content": f"User wants to reply to the email. Use this feedback to respond: {user_input}" - }) - # Update memory with feedback - update_memory(store, ("email_assistant", "triage_preferences"), [{ - "role": "user", - "content": f"The user decided to respond to the email, so update the triage preferences to capture this." - }] + messages) - - goto = "response_agent" - - # If user ignores email, go to END - elif response["type"] == "ignore": - # Make note of the user's decision to ignore the email - messages.append({"role": "user", - "content": f"The user decided to ignore the email even though it was classified as notify. Update triage preferences to capture this." - }) - # Update memory with feedback - update_memory(store, ("email_assistant", "triage_preferences"), messages) - goto = END - - # Catch all other responses - else: - raise ValueError(f"Invalid response: {response}") - - # Update the state - update = { - "messages": messages, - } - - return Command(goto=goto, update=update) +from .utils import format_email_markdown, parse_email, get_memory +from .prompts import agent_system_prompt_hitl_memory, default_background, default_response_preferences, default_cal_preferences, default_triage_instructions def create_email_assistant(for_deployment=False): """Create and configure the email assistant agent. @@ -187,9 +36,10 @@ def create_email_assistant(for_deployment=False): # Initialize model model = ChatAnthropic(model="claude-sonnet-4-5-20250929", temperature=0) - # Get tools + # Get tools - now includes triage_email tools = get_tools( [ + "triage_email", "write_email", "schedule_meeting", "check_calendar_availability", @@ -223,6 +73,17 @@ def create_email_assistant(for_deployment=False): }, ) + # Build system prompt with default preferences + # Note: Memory-based preferences can be accessed via the store in middleware + tools_prompt = "\n".join([f"- {tool.name}: {tool.description}" for tool in tools]) + system_prompt = agent_system_prompt_hitl_memory.format( + tools_prompt=tools_prompt, + background=default_background, + triage_instructions=default_triage_instructions, + response_preferences=default_response_preferences, + cal_preferences=default_cal_preferences, + ) + # Create agent with deepagents library agent = create_deep_agent( model=model, @@ -230,29 +91,12 @@ def create_email_assistant(for_deployment=False): middleware=[hitl_middleware], # Custom middleware added to default stack backend=lambda rt: StoreBackend(rt), # Persistent storage for memory context_schema=EmailAssistantState, + system_prompt=system_prompt, **store_kwarg, **checkpointer_kwarg, ) - - # Build overall workflow - overall_workflow = ( - StateGraph(State, input=StateInput) - .add_node(triage_router) - .add_node(triage_interrupt_handler) - .add_node("response_agent", agent) - .add_edge(START, "triage_router") - ) - - # Compile with store/checkpointer based on deployment mode - if for_deployment: - # In deployment, platform provides store/checkpointer - email_assistant = overall_workflow.compile() - else: - # In local testing, use our store/checkpointer - email_assistant = overall_workflow.compile(store=store, checkpointer=checkpointer) - - return email_assistant + return agent def main(): @@ -283,9 +127,9 @@ def main(): print(email_markdown) print("=" * 80) - # Pass email_input to top-level state (triage_router expects this) + # Agent now accepts the email as a simple message string result = agent.invoke( - {"email_input": email_input}, + {"messages": [{"role": "user", "content": email_markdown}]}, config=config, ) diff --git a/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py b/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py index 17cdcba..33458d4 100644 --- a/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py +++ b/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py @@ -16,6 +16,7 @@ default_background, default_cal_preferences, default_response_preferences, + default_triage_instructions, ) from ..tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT from ..utils import format_email_markdown, format_for_display, get_memory, parse_email, update_memory, aget_memory, aupdate_memory @@ -82,7 +83,7 @@ def wrap_model_call( triage_prefs = get_memory( store, ("email_assistant", "triage_preferences"), - default_response_preferences, + default_triage_instructions, ) response_prefs = get_memory( store, @@ -99,6 +100,7 @@ def wrap_model_call( memory_prompt = agent_system_prompt_hitl_memory.format( tools_prompt=HITL_MEMORY_TOOLS_PROMPT, background=default_background, + triage_instructions=triage_prefs, response_preferences=response_prefs, cal_preferences=cal_prefs, ) diff --git a/personal_assistant/src/personal_assistant/prompts.py b/personal_assistant/src/personal_assistant/prompts.py index 475bc19..7879c0d 100644 --- a/personal_assistant/src/personal_assistant/prompts.py +++ b/personal_assistant/src/personal_assistant/prompts.py @@ -108,11 +108,11 @@ """ -# Email assistant with HITL and memory prompt -# Note: Currently, this is the same as the HITL prompt. However, memory specific tools (see https://langchain-ai.github.io/langmem/) can be added +# Email assistant with HITL and memory prompt +# Note: Currently, this is the same as the HITL prompt. However, memory specific tools (see https://langchain-ai.github.io/langmem/) can be added agent_system_prompt_hitl_memory = """ < Role > -You are a top-notch executive assistant. +You are a top-notch executive assistant. < Tools > @@ -121,23 +121,39 @@ < Instructions > -When handling emails, follow these steps: -1. Carefully analyze the email content and purpose -2. IMPORTANT --- always call a tool and call one tool at a time until the task is complete: -3. If the incoming email asks the user a direct question and you do not have context to answer the question, use the Question tool to ask the user for the answer -4. For responding to the email, draft a response email with the write_email tool -5. For meeting requests, use the check_calendar_availability tool to find open time slots -6. To schedule a meeting, use the schedule_meeting tool with a datetime object for the preferred_day parameter - - Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """ - use this for scheduling meetings accurately -7. If you scheduled a meeting, then draft a short response email using the write_email tool -8. After using the write_email tool, the task is complete -9. If you have sent the email, then use the Done tool to indicate that the task is complete +CRITICAL: Your FIRST action must ALWAYS be to call the triage_email tool to classify the email. + +Step 1 - TRIAGE (REQUIRED): +Call the triage_email tool to classify the email into one of three categories: +- 'ignore' for irrelevant emails (marketing, spam, FYI threads with no direct questions) +- 'notify' for important information that doesn't need a response (announcements, status updates, GitHub notifications, deadline reminders) +- 'respond' for emails that need a reply (direct questions, meeting requests, critical issues) + +Step 2 - ROUTE based on triage result: +- If 'ignore': Call the Done tool immediately +- If 'notify': Call the Done tool immediately (user will be notified through another channel) +- If 'respond': Proceed to Step 3 + +Step 3 - RESPOND (only if triage result is 'respond'): +- Carefully analyze the email content and purpose +- IMPORTANT: Always call one tool at a time until the task is complete +- If the email asks a direct question you cannot answer, use the Question tool +- For meeting requests, use check_calendar_availability to find open time slots +- To schedule a meeting, use schedule_meeting with a datetime object for preferred_day + (Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """) +- For responding to emails, draft a response using write_email +- If you scheduled a meeting, send a short confirmation email using write_email +- After sending the email, call the Done tool < Background > {background} +< Triage Rules > +{triage_instructions} + + < Response Preferences > {response_preferences} diff --git a/personal_assistant/src/personal_assistant/schemas.py b/personal_assistant/src/personal_assistant/schemas.py index dfffdd3..93ca7da 100644 --- a/personal_assistant/src/personal_assistant/schemas.py +++ b/personal_assistant/src/personal_assistant/schemas.py @@ -1,32 +1,14 @@ from pydantic import BaseModel, Field -from typing_extensions import TypedDict, Literal, NotRequired -from langgraph.graph import MessagesState - -class RouterSchema(BaseModel): - """Analyze the unread email and route it according to its content.""" - - reasoning: str = Field( - description="Step-by-step reasoning behind the classification." - ) - classification: Literal["ignore", "respond", "notify"] = Field( - description="The classification of an email: 'ignore' for irrelevant emails, " - "'notify' for important information that doesn't need a response, " - "'respond' for emails that need a reply", - ) - -class StateInput(TypedDict): - # This is the input to the state - email_input: dict - -class State(MessagesState): - # This state class has the messages key build in - email_input: dict - classification_decision: Literal["ignore", "respond", "notify"] +from typing_extensions import TypedDict, NotRequired class EmailAssistantState(TypedDict): - """State for email assistant agent using deepagents library.""" + """State for email assistant agent using deepagents library. + + The agent accepts email content as a simple message string and handles + triage through the triage_email tool. + """ messages: list # Required by MessagesState - email_input: NotRequired[dict] # Email context for middleware display formatting + email_input: NotRequired[dict] # Optional email context for middleware display formatting class EmailData(TypedDict): id: str diff --git a/personal_assistant/src/personal_assistant/tools/base.py b/personal_assistant/src/personal_assistant/tools/base.py index 6fb4019..2ef7ffd 100644 --- a/personal_assistant/src/personal_assistant/tools/base.py +++ b/personal_assistant/src/personal_assistant/tools/base.py @@ -12,11 +12,12 @@ def get_tools(tool_names: Optional[List[str]] = None, include_gmail: bool = Fals List of tool objects """ # Import default tools - from .default.email_tools import write_email, Done, Question + from .default.email_tools import write_email, triage_email, Done, Question from .default.calendar_tools import schedule_meeting, check_calendar_availability - + # Base tools dictionary all_tools = { + "triage_email": triage_email, "write_email": write_email, "Done": Done, "Question": Question, diff --git a/personal_assistant/src/personal_assistant/tools/default/email_tools.py b/personal_assistant/src/personal_assistant/tools/default/email_tools.py index 12f3de7..8932e09 100644 --- a/personal_assistant/src/personal_assistant/tools/default/email_tools.py +++ b/personal_assistant/src/personal_assistant/tools/default/email_tools.py @@ -1,5 +1,5 @@ from typing import Literal -from pydantic import BaseModel +from pydantic import BaseModel, Field from langchain_core.tools import tool @tool @@ -8,10 +8,27 @@ def write_email(to: str, subject: str, content: str) -> str: # Placeholder response - in real app would send email return f"Email sent to {to} with subject '{subject}' and content: {content}" -@tool -def triage_email(category: Literal["ignore", "notify", "respond"]) -> str: - """Triage an email into one of three categories: ignore, notify, respond.""" - return f"Classification Decision: {category}" +class TriageEmailInput(BaseModel): + """Input schema for triaging an email.""" + reasoning: str = Field( + description="Step-by-step reasoning behind the classification." + ) + classification: Literal["ignore", "respond", "notify"] = Field( + description="The classification of an email: 'ignore' for irrelevant emails, " + "'notify' for important information that doesn't need a response, " + "'respond' for emails that need a reply", + ) + +@tool(args_schema=TriageEmailInput) +def triage_email(reasoning: str, classification: Literal["ignore", "notify", "respond"]) -> str: + """Analyze the email content and classify it into one of three categories: + - 'ignore' for irrelevant emails (marketing, spam, FYI threads) + - 'notify' for important information that doesn't need a response (announcements, status updates) + - 'respond' for emails that need a reply (direct questions, meeting requests, critical issues) + + This tool MUST be called first to determine how to handle the email before taking any other actions. + """ + return f"Classification Decision: {classification}. Reasoning: {reasoning}" @tool class Done(BaseModel): diff --git a/personal_assistant/test.ipynb b/personal_assistant/test.ipynb index 1f121f0..e0b05db 100644 --- a/personal_assistant/test.ipynb +++ b/personal_assistant/test.ipynb @@ -186,110 +186,11 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "example1_invoke", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "============================================================\n", - "Agent Response\n", - "============================================================\n", - "\n", - "🛑 INTERRUPT: Agent is waiting for your approval\n", - "\n", - "Action: schedule_meeting\n", - "Args: {'attendees': ['jane@example.com'], 'subject': 'Project Roadmap Discussion', 'duration_minutes': 30, 'preferred_day': '2025-12-09T14:00:00', 'start_time': 14}\n", - "\n", - "Allowed actions: {'allow_ignore': True, 'allow_respond': True, 'allow_edit': True, 'allow_accept': True}\n", - "\n", - "Description:\n" - ] - }, - { - "data": { - "text/html": [ - "
╭─────────────────────────────────────────────── 📋 Action Details ───────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "  # Calendar Invite                                                                                              \n",
-       "                                                                                                                 \n",
-       "  **Meeting**: Project Roadmap Discussion                                                                        \n",
-       "  **Attendees**: jane@example.com                                                                                \n",
-       "  **Duration**: 30 minutes                                                                                       \n",
-       "  **Day**: 2025-12-09T14:00:00                                                                                   \n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[33m╭─\u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m \u001b[0m\u001b[1;32m📋 Action Details\u001b[0m\u001b[33m \u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m # Calendar Invite \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m **Meeting**: Project Roadmap Discussion \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m **Attendees**: jane@example.com \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m **Duration**: 30 minutes \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m **Day**: 2025-12-09T14:00:00 \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", - "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "------------------------------------------------------------\n", - "\n", - "💡 To continue, you need to resume the agent with a decision.\n", - " Use: agent.invoke(None, config=config_1)\n" - ] - } - ], - "source": [ - "# Configure thread\n", - "config_1 = {\"configurable\": {\"thread_id\": \"test-thread-1\"}}\n", - "\n", - "# Invoke agent\n", - "result_1 = agent.invoke(\n", - " {\n", - " \"messages\": [{\"role\": \"user\", \"content\": email_markdown}],\n", - " \"email_input\": email_input_1,\n", - " },\n", - " config=config_1,\n", - ")\n", - "\n", - "# Display result with rich formatting\n", - "print(\"\\n\" + \"=\"*60)\n", - "print(\"Agent Response\")\n", - "print(\"=\"*60 + \"\\n\")\n", - "\n", - "# Check for interrupts (HITL)\n", - "if \"__interrupt__\" in result_1:\n", - " print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n", - " \n", - " for interrupt in result_1[\"__interrupt__\"]:\n", - " for request in interrupt.value:\n", - " print(\"Action:\", request[\"action_request\"][\"action\"])\n", - " print(\"Args:\", request[\"action_request\"][\"args\"])\n", - " print(\"\\nAllowed actions:\", request[\"config\"])\n", - " print(\"\\nDescription:\")\n", - " show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n", - " print(\"\\n\" + \"-\"*60)\n", - " \n", - " print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n", - " print(\" Use: agent.invoke(None, config=config_1)\")\n", - "else:\n", - " format_messages(result_1[\"messages\"])" - ] + "outputs": [], + "source": "# Configure thread\nconfig_1 = {\"configurable\": {\"thread_id\": \"test-thread-1\"}}\n\n# Invoke agent with simplified interface\n# Note: email_input is optional, used by middleware for display formatting\nresult_1 = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown}],\n \"email_input\": email_input_1, # Optional: for middleware display formatting\n },\n config=config_1,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_1:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_1[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_1)\")\nelse:\n format_messages(result_1[\"messages\"])" }, { "cell_type": "markdown", @@ -1541,40 +1442,7 @@ "id": "example2_invoke", "metadata": {}, "outputs": [], - "source": [ - "config_2 = {\"configurable\": {\"thread_id\": \"test-thread-2\"}}\n", - "\n", - "result_2 = agent.invoke(\n", - " {\n", - " \"messages\": [{\"role\": \"user\", \"content\": email_markdown_2}],\n", - " \"email_input\": email_input_2,\n", - " },\n", - " config=config_2,\n", - ")\n", - "\n", - "# Display result with rich formatting\n", - "print(\"\\n\" + \"=\"*60)\n", - "print(\"Agent Response\")\n", - "print(\"=\"*60 + \"\\n\")\n", - "\n", - "# Check for interrupts (HITL)\n", - "if \"__interrupt__\" in result_2:\n", - " print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n", - " \n", - " for interrupt in result_2[\"__interrupt__\"]:\n", - " for request in interrupt.value:\n", - " print(\"Action:\", request[\"action_request\"][\"action\"])\n", - " print(\"Args:\", request[\"action_request\"][\"args\"])\n", - " print(\"\\nAllowed actions:\", request[\"config\"])\n", - " print(\"\\nDescription:\")\n", - " show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n", - " print(\"\\n\" + \"-\"*60)\n", - " \n", - " print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n", - " print(\" Use: agent.invoke(None, config=config_2)\")\n", - "else:\n", - " format_messages(result_2[\"messages\"])" - ] + "source": "config_2 = {\"configurable\": {\"thread_id\": \"test-thread-2\"}}\n\n# Invoke agent with simplified interface\nresult_2 = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown_2}],\n \"email_input\": email_input_2, # Optional: for middleware display formatting\n },\n config=config_2,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_2:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_2[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_2)\")\nelse:\n format_messages(result_2[\"messages\"])" }, { "cell_type": "markdown", @@ -1586,216 +1454,27 @@ "Test that memory persists across invocations using the same thread ID." ] }, + { + "cell_type": "markdown", + "id": "p8oc6m0fwzo", + "source": "## Example 3: Triage Functionality\n\nThe agent now uses a triage tool to classify emails before taking action. This example shows how the agent triages a marketing email.", + "metadata": {} + }, { "cell_type": "code", - "execution_count": 14, + "id": "yeib3g2uxlf", + "source": "# Marketing email that should be triaged as \"ignore\"\nemail_input_marketing = {\n \"author\": \"marketing@newsletter.com\",\n \"to\": \"lance@langchain.dev\",\n \"subject\": \"Limited Time Offer - 50% Off!\",\n \"email_thread\": \"Don't miss out on our amazing sale! Click here to shop now and save big on all your favorite items. This offer expires soon!\",\n}\n\nauthor, to, subject, email_thread = parse_email(email_input_marketing)\nemail_markdown_marketing = format_email_markdown(subject, author, to, email_thread)\n\n# Display email\nshow_prompt(email_markdown_marketing, title=\"📧 Marketing Email\", border_style=\"cyan\")\n\n# Invoke agent\nconfig_marketing = {\"configurable\": {\"thread_id\": \"test-marketing\"}}\nresult_marketing = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown_marketing}],\n \"email_input\": email_input_marketing,\n },\n config=config_marketing,\n)\n\n# Show triage decision\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Decision\")\nprint(\"=\"*60 + \"\\n\")\n\nmessages = result_marketing.get(\"messages\", [])\nfor i, msg in enumerate(messages):\n if hasattr(msg, 'tool_calls') and msg.tool_calls:\n for tool_call in msg.tool_calls:\n if tool_call.get('name') == 'triage_email':\n print(\"🔍 TRIAGE DECISION:\")\n print(f\" Classification: {tool_call.get('args', {}).get('classification', 'N/A')}\")\n print(f\" Reasoning: {tool_call.get('args', {}).get('reasoning', 'N/A')}\")\n print()\n if hasattr(msg, 'name') and msg.name == 'triage_email':\n print(f\"✓ Result: {msg.content}\")\n print()\n\nprint(\"Since this was classified as 'ignore', the agent calls Done immediately without drafting a response.\")", + "metadata": {}, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "execution_count": null, "id": "test_memory_code", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "============================================================\n", - "Agent Response (with memory from previous conversation)\n", - "============================================================\n", - "\n" - ] - }, - { - "data": { - "text/html": [ - "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", - "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to    \n",
-       " Jane.                                                                                                           \n",
-       "                                                                                                                 \n",
-       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
-       "    Args: {                                                                                                      \n",
-       "   \"attendees\": [                                                                                                \n",
-       "     \"jane@example.com\"                                                                                          \n",
-       "   ],                                                                                                            \n",
-       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
-       "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
-       "   \"start_time\": 14                                                                                              \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_01Kp449tDf62QjfRcCwCT45C                                                                           \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Jane. \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01Kp449tDf62QjfRcCwCT45C \u001b[37m│\u001b[0m\n", - "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Tool call schedule_meeting with id toolu_01Kp449tDf62QjfRcCwCT45C was cancelled - another message came in       \n",
-       " before it could be completed.                                                                                   \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_01Kp449tDf62QjfRcCwCT45C was cancelled - another message came in \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", - "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       " **Subject**: Follow-up meeting                                                                                  \n",
-       " **From**: jane@example.com                                                                                      \n",
-       " **To**: lance@langchain.dev                                                                                     \n",
-       "                                                                                                                 \n",
-       " Hi again,                                                                                                       \n",
-       "                                                                                                                 \n",
-       " Can we also schedule a follow-up meeting next week?                                                             \n",
-       "                                                                                                                 \n",
-       " Jane                                                                                                            \n",
-       "                                                                                                                 \n",
-       " ---                                                                                                             \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **Subject**: Follow-up meeting \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Hi again, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Can we also schedule a follow-up meeting next week? \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " I'll help you with both meeting requests from Jane. Let me schedule the first meeting for Tuesday at 2pm and    \n",
-       " then respond about the follow-up meeting.                                                                       \n",
-       "                                                                                                                 \n",
-       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
-       "    Args: {                                                                                                      \n",
-       "   \"attendees\": [                                                                                                \n",
-       "     \"jane@example.com\"                                                                                          \n",
-       "   ],                                                                                                            \n",
-       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
-       "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
-       "   \"start_time\": 14                                                                                              \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_018r9DgdPYxx7LeXxADVGuTS                                                                           \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m I'll help you with both meeting requests from Jane. Let me schedule the first meeting for Tuesday at 2pm and \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m then respond about the follow-up meeting. \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_018r9DgdPYxx7LeXxADVGuTS \u001b[37m│\u001b[0m\n", - "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Use the same thread as example 1 to test memory\n", - "email_input_3 = {\n", - " \"author\": \"jane@example.com\",\n", - " \"to\": \"lance@langchain.dev\",\n", - " \"subject\": \"Follow-up meeting\",\n", - " \"email_thread\": \"Hi again,\\n\\nCan we also schedule a follow-up meeting next week?\\n\\nJane\",\n", - "}\n", - "\n", - "author, to, subject, email_thread = parse_email(email_input_3)\n", - "email_markdown_3 = format_email_markdown(subject, author, to, email_thread)\n", - "\n", - "# Use same config as example 1 (same thread_id)\n", - "result_3 = agent.invoke(\n", - " {\n", - " \"messages\": [{\"role\": \"user\", \"content\": email_markdown_3}],\n", - " \"email_input\": email_input_3,\n", - " },\n", - " config=config_1, # Same thread as example 1\n", - ")\n", - "\n", - "# Display result with rich formatting (show last 5 messages)\n", - "print(\"\\n\" + \"=\"*60)\n", - "print(\"Agent Response (with memory from previous conversation)\")\n", - "print(\"=\"*60 + \"\\n\")\n", - "format_messages(result_3[\"messages\"][-5:])" - ] + "outputs": [], + "source": "# Use the same thread as example 1 to test memory\nemail_input_3 = {\n \"author\": \"jane@example.com\",\n \"to\": \"lance@langchain.dev\",\n \"subject\": \"Follow-up meeting\",\n \"email_thread\": \"Hi again,\\n\\nCan we also schedule a follow-up meeting next week?\\n\\nJane\",\n}\n\nauthor, to, subject, email_thread = parse_email(email_input_3)\nemail_markdown_3 = format_email_markdown(subject, author, to, email_thread)\n\n# Use same config as example 1 (same thread_id) to test memory persistence\nresult_3 = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown_3}],\n \"email_input\": email_input_3, # Optional: for middleware display formatting\n },\n config=config_1, # Same thread as example 1\n)\n\n# Display result with rich formatting (show last 5 messages)\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response (with memory from previous conversation)\")\nprint(\"=\"*60 + \"\\n\")\nformat_messages(result_3[\"messages\"][-5:])" }, { "cell_type": "code", @@ -1827,4 +1506,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file From 5b5b01f27310afe5f1418f2756a10032560cbf4e Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Tue, 2 Dec 2025 21:49:04 -0800 Subject: [PATCH 03/11] fix: remove sensitive path information from log messages Address CodeQL security warnings by avoiding logging of file paths to sensitive token and credentials files. Replace specific path logging with generic messages that don't expose filesystem structure. Changes: - gmail_tools.py: Use generic message for token file location - run_ingest.py: Remove TOKEN_PATH from log messages - setup_gmail.py: Remove secrets_path and token_path from logs This prevents potential information disclosure while maintaining useful debugging information. --- .../src/personal_assistant/tools/gmail/gmail_tools.py | 4 ++-- .../src/personal_assistant/tools/gmail/run_ingest.py | 8 ++++---- .../src/personal_assistant/tools/gmail/setup_gmail.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py b/personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py index 148aef8..112c89d 100644 --- a/personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py +++ b/personal_assistant/src/personal_assistant/tools/gmail/gmail_tools.py @@ -105,9 +105,9 @@ def get_credentials(gmail_token=None, gmail_secret=None): try: with open(token_path, "r") as f: token_data = json.load(f) - logger.info(f"Using token from {token_path}") + logger.info("Using token from local token file in .secrets directory") except Exception as e: - logger.warning(f"Could not load token from {token_path}: {str(e)}") + logger.warning(f"Could not load token from token file: {str(e)}") # If we couldn't get token data from any source, return None if token_data is None: diff --git a/personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py b/personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py index 307c481..0ba5d0f 100644 --- a/personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py +++ b/personal_assistant/src/personal_assistant/tools/gmail/run_ingest.py @@ -86,11 +86,11 @@ def load_gmail_credentials(): try: with open(TOKEN_PATH, "r") as f: token_data = json.load(f) - print(f"Using token from {TOKEN_PATH}") - except Exception as e: - print(f"Could not load token from {TOKEN_PATH}: {str(e)}") + print("Using token from local file.") + except Exception: + print("Could not load token due to an error.") else: - print(f"Token file not found at {TOKEN_PATH}") + print("Token file not found in secrets directory") # If we couldn't get token data from any source, return None if token_data is None: diff --git a/personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py b/personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py index a5a1a96..3ecdc1e 100644 --- a/personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py +++ b/personal_assistant/src/personal_assistant/tools/gmail/setup_gmail.py @@ -30,7 +30,7 @@ def main(): # Check for secrets.json secrets_path = secrets_dir / "secrets.json" if not secrets_path.exists(): - print(f"Error: Client secrets file not found at {secrets_path}") + print("Error: Client secrets file not found at .secrets/secrets.json") print("Please download your OAuth client ID JSON from Google Cloud Console") print("and save it as .secrets/secrets.json") return 1 @@ -77,7 +77,7 @@ def main(): json.dump(token_data, token_file) print("\nAuthentication successful!") - print(f"Access token stored at {token_path}") + print("Access token securely stored.") return 0 except Exception as e: print(f"Authentication failed: {str(e)}") From f395114693cc2f49b16ff5e99a85c51815dbd43b Mon Sep 17 00:00:00 2001 From: Nick Huang Date: Wed, 3 Dec 2025 14:56:10 -0500 Subject: [PATCH 04/11] Add triage instructions to memory prompt --- .../src/personal_assistant/middleware/email_assistant_hitl.py | 1 + 1 file changed, 1 insertion(+) diff --git a/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py b/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py index 33458d4..6e32c81 100644 --- a/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py +++ b/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py @@ -519,6 +519,7 @@ async def awrap_model_call( # Format system prompt with memory memory_prompt = agent_system_prompt_hitl_memory.format( + triage_instructions=triage_prefs, tools_prompt=HITL_MEMORY_TOOLS_PROMPT, background=default_background, response_preferences=response_prefs, From ecf28b3c7e9b2d669883383149065f497f878595 Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Wed, 3 Dec 2025 12:02:51 -0800 Subject: [PATCH 05/11] refactor: remove legacy email_assistant_hitl_memory.py file Remove the unused legacy implementation file that was superseded by the email_assistant_deepagents.py using the deepagents library. The custom HITL middleware in src/personal_assistant/middleware/ is still being used and provides the memory integration functionality. Also update README to remove reference to the legacy file and update the description to reflect the simplified triage architecture. --- personal_assistant/README.md | 5 +- .../email_assistant_hitl_memory.py | 501 ------------------ 2 files changed, 2 insertions(+), 504 deletions(-) delete mode 100644 personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py diff --git a/personal_assistant/README.md b/personal_assistant/README.md index 0be401a..d2169c8 100644 --- a/personal_assistant/README.md +++ b/personal_assistant/README.md @@ -206,9 +206,8 @@ examples/personal_assistant/ │ └── src/personal_assistant/ ├── __init__.py # Package exports - ├── email_assistant_deepagents.py # Main: Two-tiered workflow + response agent - ├── email_assistant_hitl_memory.py # Legacy: Original implementation (reference) - ├── schemas.py # State schemas (State, EmailAssistantState, etc.) + ├── email_assistant_deepagents.py # Main: Email assistant with triage tool + ├── schemas.py # State schemas (EmailAssistantState, etc.) ├── prompts.py # System prompts and memory instructions ├── configuration.py # Config loading (minimal) ├── utils.py # Utilities (memory, formatting, parsing) diff --git a/personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py b/personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py deleted file mode 100644 index 1753ce2..0000000 --- a/personal_assistant/src/personal_assistant/email_assistant_hitl_memory.py +++ /dev/null @@ -1,501 +0,0 @@ -from typing import Literal - -from langchain.chat_models import init_chat_model - -from langgraph.graph import StateGraph, START, END -from langgraph.store.base import BaseStore -from langgraph.types import interrupt, Command - -from email_assistant.tools import get_tools, get_tools_by_name -from email_assistant.tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT -from email_assistant.prompts import triage_system_prompt, triage_user_prompt, agent_system_prompt_hitl_memory, default_triage_instructions, default_background, default_response_preferences, default_cal_preferences, MEMORY_UPDATE_INSTRUCTIONS, MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT -from email_assistant.schemas import State, RouterSchema, StateInput, UserPreferences -from email_assistant.utils import parse_email, format_for_display, format_email_markdown -from dotenv import load_dotenv - -load_dotenv(".env") - -# Get tools -tools = get_tools(["write_email", "schedule_meeting", "check_calendar_availability", "Question", "Done"]) -tools_by_name = get_tools_by_name(tools) - -# Initialize the LLM for use with router / structured output -llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0) -llm_router = llm.with_structured_output(RouterSchema) - -# Initialize the LLM, enforcing tool use (of any available tools) for agent -llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0) -llm_with_tools = llm.bind_tools(tools, tool_choice="required") - -def get_memory(store, namespace, default_content=None): - """Get memory from the store or initialize with default if it doesn't exist. - - Args: - store: LangGraph BaseStore instance to search for existing memory - namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") - default_content: Default content to use if memory doesn't exist - - Returns: - str: The content of the memory profile, either from existing memory or the default - """ - # Search for existing memory with namespace and key - user_preferences = store.get(namespace, "user_preferences") - - # If memory exists, return its content (the value) - if user_preferences: - return user_preferences.value - - # If memory doesn't exist, add it to the store and return the default content - else: - # Namespace, key, value - store.put(namespace, "user_preferences", default_content) - user_preferences = default_content - - # Return the default content - return user_preferences - -def update_memory(store, namespace, messages): - """Update memory profile in the store. - - Args: - store: LangGraph BaseStore instance to update memory - namespace: Tuple defining the memory namespace, e.g. ("email_assistant", "triage_preferences") - messages: List of messages to update the memory with - """ - - # Get the existing memory - user_preferences = store.get(namespace, "user_preferences") - # Update the memory - llm = init_chat_model("anthropic:claude-sonnet-4-5-20250929", temperature=0.0).with_structured_output(UserPreferences) - result = llm.invoke( - [ - {"role": "system", "content": MEMORY_UPDATE_INSTRUCTIONS.format(current_profile=user_preferences.value, namespace=namespace)}, - ] + messages - ) - # Save the updated memory to the store - store.put(namespace, "user_preferences", result.user_preferences) - -# Nodes -def triage_router(state: State, store: BaseStore) -> Command[Literal["triage_interrupt_handler", "response_agent", "__end__"]]: - """Analyze email content to decide if we should respond, notify, or ignore. - - The triage step prevents the assistant from wasting time on: - - Marketing emails and spam - - Company-wide announcements - - Messages meant for other teams - """ - - # Parse the email input - author, to, subject, email_thread = parse_email(state["email_input"]) - user_prompt = triage_user_prompt.format( - author=author, to=to, subject=subject, email_thread=email_thread - ) - - # Create email markdown for Agent Inbox in case of notification - email_markdown = format_email_markdown(subject, author, to, email_thread) - - # Search for existing triage_preferences memory - triage_instructions = get_memory(store, ("email_assistant", "triage_preferences"), default_triage_instructions) - - # Format system prompt with background and triage instructions - system_prompt = triage_system_prompt.format( - background=default_background, - triage_instructions=triage_instructions, - ) - - # Run the router LLM - result = llm_router.invoke( - [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ] - ) - - # Decision - classification = result.classification - - # Process the classification decision - if classification == "respond": - print("📧 Classification: RESPOND - This email requires a response") - # Next node - goto = "response_agent" - # Update the state - update = { - "classification_decision": result.classification, - "messages": [{"role": "user", - "content": f"Respond to the email: {email_markdown}" - }], - } - - elif classification == "ignore": - print("🚫 Classification: IGNORE - This email can be safely ignored") - - # Next node - goto = END - # Update the state - update = { - "classification_decision": classification, - } - - elif classification == "notify": - print("🔔 Classification: NOTIFY - This email contains important information") - - # Next node - goto = "triage_interrupt_handler" - # Update the state - update = { - "classification_decision": classification, - } - - else: - raise ValueError(f"Invalid classification: {classification}") - - return Command(goto=goto, update=update) - -def triage_interrupt_handler(state: State, store: BaseStore) -> Command[Literal["response_agent", "__end__"]]: - """Handles interrupts from the triage step""" - - # Parse the email input - author, to, subject, email_thread = parse_email(state["email_input"]) - - # Create email markdown for Agent Inbox in case of notification - email_markdown = format_email_markdown(subject, author, to, email_thread) - - # Create messages - messages = [{"role": "user", - "content": f"Email to notify user about: {email_markdown}" - }] - - # Create interrupt for Agent Inbox - request = { - "action_request": { - "action": f"Email Assistant: {state['classification_decision']}", - "args": {} - }, - "config": { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": False, - "allow_accept": False, - }, - # Email to show in Agent Inbox - "description": email_markdown, - } - - # Send to Agent Inbox and wait for response - response = interrupt([request])[0] - - # If user provides feedback, go to response agent and use feedback to respond to email - if response["type"] == "response": - # Add feedback to messages - user_input = response["args"] - messages.append({"role": "user", - "content": f"User wants to reply to the email. Use this feedback to respond: {user_input}" - }) - # Update memory with feedback - update_memory(store, ("email_assistant", "triage_preferences"), [{ - "role": "user", - "content": f"The user decided to respond to the email, so update the triage preferences to capture this." - }] + messages) - - goto = "response_agent" - - # If user ignores email, go to END - elif response["type"] == "ignore": - # Make note of the user's decision to ignore the email - messages.append({"role": "user", - "content": f"The user decided to ignore the email even though it was classified as notify. Update triage preferences to capture this." - }) - # Update memory with feedback - update_memory(store, ("email_assistant", "triage_preferences"), messages) - goto = END - - # Catch all other responses - else: - raise ValueError(f"Invalid response: {response}") - - # Update the state - update = { - "messages": messages, - } - - return Command(goto=goto, update=update) - -def llm_call(state: State, store: BaseStore): - """LLM decides whether to call a tool or not""" - - # Search for existing cal_preferences memory - cal_preferences = get_memory(store, ("email_assistant", "cal_preferences"), default_cal_preferences) - - # Search for existing response_preferences memory - response_preferences = get_memory(store, ("email_assistant", "response_preferences"), default_response_preferences) - - return { - "messages": [ - llm_with_tools.invoke( - [ - {"role": "system", "content": agent_system_prompt_hitl_memory.format( - tools_prompt=HITL_MEMORY_TOOLS_PROMPT, - background=default_background, - response_preferences=response_preferences, - cal_preferences=cal_preferences - )} - ] - + state["messages"] - ) - ] - } - -def interrupt_handler(state: State, store: BaseStore) -> Command[Literal["llm_call", "__end__"]]: - """Creates an interrupt for human review of tool calls""" - - # Store messages - result = [] - - # Go to the LLM call node next - goto = "llm_call" - - # Iterate over the tool calls in the last message - for tool_call in state["messages"][-1].tool_calls: - - # Allowed tools for HITL - hitl_tools = ["write_email", "schedule_meeting", "Question"] - - # If tool is not in our HITL list, execute it directly without interruption - if tool_call["name"] not in hitl_tools: - - # Execute search_memory and other tools without interruption - tool = tools_by_name[tool_call["name"]] - observation = tool.invoke(tool_call["args"]) - result.append({"role": "tool", "content": observation, "tool_call_id": tool_call["id"]}) - continue - - # Get original email from email_input in state - email_input = state["email_input"] - author, to, subject, email_thread = parse_email(email_input) - original_email_markdown = format_email_markdown(subject, author, to, email_thread) - - # Format tool call for display and prepend the original email - tool_display = format_for_display(tool_call) - description = original_email_markdown + tool_display - - # Configure what actions are allowed in Agent Inbox - if tool_call["name"] == "write_email": - config = { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": True, - "allow_accept": True, - } - elif tool_call["name"] == "schedule_meeting": - config = { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": True, - "allow_accept": True, - } - elif tool_call["name"] == "Question": - config = { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": False, - "allow_accept": False, - } - else: - raise ValueError(f"Invalid tool call: {tool_call['name']}") - - # Create the interrupt request - request = { - "action_request": { - "action": tool_call["name"], - "args": tool_call["args"] - }, - "config": config, - "description": description, - } - - # Send to Agent Inbox and wait for response - response = interrupt([request])[0] - - # Handle the responses - if response["type"] == "accept": - - # Execute the tool with original args - tool = tools_by_name[tool_call["name"]] - observation = tool.invoke(tool_call["args"]) - result.append({"role": "tool", "content": observation, "tool_call_id": tool_call["id"]}) - - elif response["type"] == "edit": - - # Tool selection - tool = tools_by_name[tool_call["name"]] - initial_tool_call = tool_call["args"] - - # Get edited args from Agent Inbox - edited_args = response["args"]["args"] - - # Update the AI message's tool call with edited content (reference to the message in the state) - ai_message = state["messages"][-1] # Get the most recent message from the state - current_id = tool_call["id"] # Store the ID of the tool call being edited - - # Create a new list of tool calls by filtering out the one being edited and adding the updated version - # This avoids modifying the original list directly (immutable approach) - updated_tool_calls = [tc for tc in ai_message.tool_calls if tc["id"] != current_id] + [ - {"type": "tool_call", "name": tool_call["name"], "args": edited_args, "id": current_id} - ] - - # Create a new copy of the message with updated tool calls rather than modifying the original - # This ensures state immutability and prevents side effects in other parts of the code - result.append(ai_message.model_copy(update={"tool_calls": updated_tool_calls})) - - # Save feedback in memory and update the write_email tool call with the edited content from Agent Inbox - if tool_call["name"] == "write_email": - - # Execute the tool with edited args - observation = tool.invoke(edited_args) - - # Add only the tool response message - result.append({"role": "tool", "content": observation, "tool_call_id": current_id}) - - # This is new: update the memory - update_memory(store, ("email_assistant", "response_preferences"), [{ - "role": "user", - "content": f"User edited the email response. Here is the initial email generated by the assistant: {initial_tool_call}. Here is the edited email: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." - }]) - - # Save feedback in memory and update the schedule_meeting tool call with the edited content from Agent Inbox - elif tool_call["name"] == "schedule_meeting": - - # Execute the tool with edited args - observation = tool.invoke(edited_args) - - # Add only the tool response message - result.append({"role": "tool", "content": observation, "tool_call_id": current_id}) - - # This is new: update the memory - update_memory(store, ("email_assistant", "cal_preferences"), [{ - "role": "user", - "content": f"User edited the calendar invitation. Here is the initial calendar invitation generated by the assistant: {initial_tool_call}. Here is the edited calendar invitation: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." - }]) - - # Catch all other tool calls - else: - raise ValueError(f"Invalid tool call: {tool_call['name']}") - - elif response["type"] == "ignore": - - if tool_call["name"] == "write_email": - # Don't execute the tool, and tell the agent how to proceed - result.append({"role": "tool", "content": "User ignored this email draft. Ignore this email and end the workflow.", "tool_call_id": tool_call["id"]}) - # Go to END - goto = END - # This is new: update the memory - update_memory(store, ("email_assistant", "triage_preferences"), state["messages"] + result + [{ - "role": "user", - "content": f"The user ignored the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." - }]) - - elif tool_call["name"] == "schedule_meeting": - # Don't execute the tool, and tell the agent how to proceed - result.append({"role": "tool", "content": "User ignored this calendar meeting draft. Ignore this email and end the workflow.", "tool_call_id": tool_call["id"]}) - # Go to END - goto = END - # This is new: update the memory - update_memory(store, ("email_assistant", "triage_preferences"), state["messages"] + result + [{ - "role": "user", - "content": f"The user ignored the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." - }]) - - elif tool_call["name"] == "Question": - # Don't execute the tool, and tell the agent how to proceed - result.append({"role": "tool", "content": "User ignored this question. Ignore this email and end the workflow.", "tool_call_id": tool_call["id"]}) - # Go to END - goto = END - # This is new: update the memory - update_memory(store, ("email_assistant", "triage_preferences"), state["messages"] + result + [{ - "role": "user", - "content": f"The user ignored the Question. That means they did not want to answer the question or deal with this email. Update the triage preferences to ensure emails of this type are not classified as respond. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." - }]) - - else: - raise ValueError(f"Invalid tool call: {tool_call['name']}") - - elif response["type"] == "response": - # User provided feedback - user_feedback = response["args"] - if tool_call["name"] == "write_email": - # Don't execute the tool, and add a message with the user feedback to incorporate into the email - result.append({"role": "tool", "content": f"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}", "tool_call_id": tool_call["id"]}) - # This is new: update the memory - update_memory(store, ("email_assistant", "response_preferences"), state["messages"] + result + [{ - "role": "user", - "content": f"User gave feedback, which we can use to update the response preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." - }]) - - elif tool_call["name"] == "schedule_meeting": - # Don't execute the tool, and add a message with the user feedback to incorporate into the email - result.append({"role": "tool", "content": f"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}", "tool_call_id": tool_call["id"]}) - # This is new: update the memory - update_memory(store, ("email_assistant", "cal_preferences"), state["messages"] + result + [{ - "role": "user", - "content": f"User gave feedback, which we can use to update the calendar preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}." - }]) - - elif tool_call["name"] == "Question": - # Don't execute the tool, and add a message with the user feedback to incorporate into the email - result.append({"role": "tool", "content": f"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}", "tool_call_id": tool_call["id"]}) - - else: - raise ValueError(f"Invalid tool call: {tool_call['name']}") - - # Update the state - update = { - "messages": result, - } - - return Command(goto=goto, update=update) - -# Conditional edge function -def should_continue(state: State, store: BaseStore) -> Literal["interrupt_handler", "__end__"]: - """Route to tool handler, or end if Done tool called""" - messages = state["messages"] - last_message = messages[-1] - if last_message.tool_calls: - for tool_call in last_message.tool_calls: - if tool_call["name"] == "Done": - # TODO: Here, we could update the background memory with the email-response for follow up actions. - return END - else: - return "interrupt_handler" - -# Build workflow -agent_builder = StateGraph(State) - -# Add nodes - with store parameter -agent_builder.add_node("llm_call", llm_call) -agent_builder.add_node("interrupt_handler", interrupt_handler) - -# Add edges -agent_builder.add_edge(START, "llm_call") -agent_builder.add_conditional_edges( - "llm_call", - should_continue, - { - "interrupt_handler": "interrupt_handler", - END: END, - }, -) - -# Compile the agent -response_agent = agent_builder.compile() - -# Build overall workflow with store and checkpointer -overall_workflow = ( - StateGraph(State, input=StateInput) - .add_node(triage_router) - .add_node(triage_interrupt_handler) - .add_node("response_agent", response_agent) - .add_edge(START, "triage_router") -) - -email_assistant = overall_workflow.compile() \ No newline at end of file From 90b364e9e0837b644e9a25a0015a82ca045eb4e2 Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Wed, 3 Dec 2025 12:10:53 -0800 Subject: [PATCH 06/11] docs: update examples to use markdown email format Replace JSON email_input examples with markdown-formatted email strings in both README and test notebook. This better reflects the simplified agent interface where emails are passed as message strings. Changes: - README: Convert all 3 email examples to markdown format - README: Update Python API usage to show markdown string input - test.ipynb: Remove parse_email/format_email_markdown from imports - test.ipynb: Update all example cells to use markdown strings directly Note: parse_email and format_email_markdown utilities are retained in the codebase as they're still needed for processing actual Gmail API responses. --- personal_assistant/README.md | 94 +++++++++++++------- personal_assistant/test.ipynb | 156 +++------------------------------- 2 files changed, 74 insertions(+), 176 deletions(-) diff --git a/personal_assistant/README.md b/personal_assistant/README.md index d2169c8..469a847 100644 --- a/personal_assistant/README.md +++ b/personal_assistant/README.md @@ -239,13 +239,21 @@ examples/personal_assistant/ This email will trigger the `respond` classification because it's addressed to Lance with a direct technical question: -```json -{ - "author": "Sarah Chen ", - "to": "Lance Martin ", - "subject": "Question about LangGraph deployment", - "email_thread": "Hi Lance,\n\nI'm working on deploying a LangGraph agent to production and ran into an issue with the store configuration. When I try to use a custom store, I get an error about the platform providing its own persistence.\n\nCould you clarify when we should pass a store vs. letting the platform handle it?\n\nAlso, do you have any examples of deploying agents with custom HITL middleware?\n\nThanks!\nSarah" -} +```markdown +**Subject**: Question about LangGraph deployment +**From**: Sarah Chen +**To**: Lance Martin + +Hi Lance, + +I'm working on deploying a LangGraph agent to production and ran into an issue with the store configuration. When I try to use a custom store, I get an error about the platform providing its own persistence. + +Could you clarify when we should pass a store vs. letting the platform handle it? + +Also, do you have any examples of deploying agents with custom HITL middleware? + +Thanks! +Sarah ``` **Why this will be responded to:** @@ -255,13 +263,19 @@ This email will trigger the `respond` classification because it's addressed to L ### Example 2: Meeting Request (Will be RESPONDED to) -```json -{ - "author": "Alex Rodriguez ", - "to": "Lance Martin ", - "subject": "Sync on agent architecture", - "email_thread": "Hey Lance,\n\nCan we schedule 30 minutes next week to discuss the multi-agent architecture for our project? I have some questions about routing between agents and want to get your input.\n\nI'm free Tuesday afternoon or Thursday morning. Let me know what works!\n\nBest,\nAlex" -} +```markdown +**Subject**: Sync on agent architecture +**From**: Alex Rodriguez +**To**: Lance Martin + +Hey Lance, + +Can we schedule 30 minutes next week to discuss the multi-agent architecture for our project? I have some questions about routing between agents and want to get your input. + +I'm free Tuesday afternoon or Thursday morning. Let me know what works! + +Best, +Alex ``` **Why this will be responded to:** @@ -271,13 +285,18 @@ This email will trigger the `respond` classification because it's addressed to L ### Example 3: FYI Email (Will be IGNORED) -```json -{ - "author": "Newsletter ", - "to": "Lance Martin ", - "subject": "Weekly AI Newsletter - Top Articles", - "email_thread": "This week's top articles:\n\n1. New advances in RAG systems\n2. Latest LLM benchmarks\n3. Multi-agent coordination patterns\n\n[Read more...]" -} +```markdown +**Subject**: Weekly AI Newsletter - Top Articles +**From**: Newsletter +**To**: Lance Martin + +This week's top articles: + +1. New advances in RAG systems +2. Latest LLM benchmarks +3. Multi-agent coordination patterns + +[Read more...] ``` **Why this will be ignored:** @@ -295,25 +314,36 @@ from personal_assistant import create_email_assistant # Create agent (local testing mode) agent = create_email_assistant(for_deployment=False) -# Example email -email_input = { - "author": "sarah.chen@techcorp.com", - "to": "lance@langchain.dev", - "subject": "Question about LangGraph deployment", - "email_thread": "Hi Lance,\n\nI'm working on deploying a LangGraph agent..." -} +# Example email as markdown string +email_markdown = """ +**Subject**: Question about LangGraph deployment +**From**: sarah.chen@techcorp.com +**To**: lance@langchain.dev + +Hi Lance, + +I'm working on deploying a LangGraph agent to production and ran into an issue with the store configuration... + +Could you clarify when we should pass a store vs. letting the platform handle it? + +Thanks! +Sarah +""" -# Process email +# Process email - agent accepts message as simple string config = {"configurable": {"thread_id": "thread-1"}} -result = agent.invoke({"email_input": email_input}, config=config) +result = agent.invoke( + {"messages": [{"role": "user", "content": email_markdown}]}, + config=config +) -# Check for interrupts +# Check for interrupts (HITL) if "__interrupt__" in result: print("Agent is waiting for approval") # Resume with decision from langgraph.types import Command result = agent.invoke( - Command(resume={"decisions": [{"type": "accept"}]}), + Command(resume=[{"type": "accept"}]), config=config ) ``` diff --git a/personal_assistant/test.ipynb b/personal_assistant/test.ipynb index e0b05db..f49127b 100644 --- a/personal_assistant/test.ipynb +++ b/personal_assistant/test.ipynb @@ -12,15 +12,11 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "imports", "metadata": {}, "outputs": [], - "source": [ - "from personal_assistant import create_email_assistant\n", - "from personal_assistant.utils import format_email_markdown, parse_email\n", - "from personal_assistant.ntbk_utils import format_messages, show_prompt" - ] + "source": "from personal_assistant import create_email_assistant\nfrom personal_assistant.ntbk_utils import format_messages, show_prompt" }, { "cell_type": "markdown", @@ -113,76 +109,11 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "example1_email", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
╭────────────────────────────────────────────── 📧 Email to Process ──────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       "  **Subject**: Quick question about next week                                                                    \n",
-       "  **From**: jane@example.com                                                                                     \n",
-       "  **To**: lance@langchain.dev                                                                                    \n",
-       "                                                                                                                 \n",
-       "  Hi Lance,                                                                                                      \n",
-       "                                                                                                                 \n",
-       "  Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                \n",
-       "                                                                                                                 \n",
-       "  Best,                                                                                                          \n",
-       "  Jane                                                                                                           \n",
-       "                                                                                                                 \n",
-       "  ---                                                                                                            \n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Email to Process\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m **Subject**: Quick question about next week \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m **From**: jane@example.com \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m **To**: lance@langchain.dev \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Hi Lance, \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Best, \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Jane \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m --- \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Example email input\n", - "email_input_1 = {\n", - " \"author\": \"jane@example.com\",\n", - " \"to\": \"lance@langchain.dev\",\n", - " \"subject\": \"Quick question about next week\",\n", - " \"email_thread\": \"Hi Lance,\\n\\nCan we meet next Tuesday at 2pm to discuss the project roadmap?\\n\\nBest,\\nJane\",\n", - "}\n", - "\n", - "# Format email\n", - "author, to, subject, email_thread = parse_email(email_input_1)\n", - "email_markdown = format_email_markdown(subject, author, to, email_thread)\n", - "\n", - "# Display email with rich formatting\n", - "show_prompt(email_markdown, title=\"📧 Email to Process\", border_style=\"cyan\")" - ] + "outputs": [], + "source": "# Example email as markdown string\nemail_markdown = \"\"\"\n**Subject**: Quick question about next week\n**From**: jane@example.com\n**To**: lance@langchain.dev\n\nHi Lance,\n\nCan we meet next Tuesday at 2pm to discuss the project roadmap?\n\nBest,\nJane\n\n---\n\"\"\"\n\n# Display email with rich formatting\nshow_prompt(email_markdown, title=\"📧 Email to Process\", border_style=\"cyan\")" }, { "cell_type": "code", @@ -190,7 +121,7 @@ "id": "example1_invoke", "metadata": {}, "outputs": [], - "source": "# Configure thread\nconfig_1 = {\"configurable\": {\"thread_id\": \"test-thread-1\"}}\n\n# Invoke agent with simplified interface\n# Note: email_input is optional, used by middleware for display formatting\nresult_1 = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown}],\n \"email_input\": email_input_1, # Optional: for middleware display formatting\n },\n config=config_1,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_1:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_1[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_1)\")\nelse:\n format_messages(result_1[\"messages\"])" + "source": "# Configure thread\nconfig_1 = {\"configurable\": {\"thread_id\": \"test-thread-1\"}}\n\n# Invoke agent with markdown email string\nresult_1 = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown}]},\n config=config_1,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_1:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_1[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_1)\")\nelse:\n format_messages(result_1[\"messages\"])" }, { "cell_type": "markdown", @@ -1367,74 +1298,11 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "example2_email", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
╭────────────────────────────────────────────── 📧 Email to Process ──────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       "  **Subject**: LangGraph documentation link                                                                      \n",
-       "  **From**: bob@example.com                                                                                      \n",
-       "  **To**: lance@langchain.dev                                                                                    \n",
-       "                                                                                                                 \n",
-       "  Hey Lance,                                                                                                     \n",
-       "                                                                                                                 \n",
-       "  Could you send me the link to the LangGraph docs?                                                              \n",
-       "                                                                                                                 \n",
-       "  Thanks!                                                                                                        \n",
-       "  Bob                                                                                                            \n",
-       "                                                                                                                 \n",
-       "  ---                                                                                                            \n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Email to Process\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m **Subject**: LangGraph documentation link \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m **From**: bob@example.com \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m **To**: lance@langchain.dev \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Hey Lance, \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Could you send me the link to the LangGraph docs? \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Thanks! \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m Bob \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m --- \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", - "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "email_input_2 = {\n", - " \"author\": \"bob@example.com\",\n", - " \"to\": \"lance@langchain.dev\",\n", - " \"subject\": \"LangGraph documentation link\",\n", - " \"email_thread\": \"Hey Lance,\\n\\nCould you send me the link to the LangGraph docs?\\n\\nThanks!\\nBob\",\n", - "}\n", - "\n", - "author, to, subject, email_thread = parse_email(email_input_2)\n", - "email_markdown_2 = format_email_markdown(subject, author, to, email_thread)\n", - "\n", - "# Display email with rich formatting\n", - "show_prompt(email_markdown_2, title=\"📧 Email to Process\", border_style=\"cyan\")" - ] + "outputs": [], + "source": "# Example email as markdown string\nemail_markdown_2 = \"\"\"\n**Subject**: LangGraph documentation link\n**From**: bob@example.com\n**To**: lance@langchain.dev\n\nHey Lance,\n\nCould you send me the link to the LangGraph docs?\n\nThanks!\nBob\n\n---\n\"\"\"\n\n# Display email with rich formatting\nshow_prompt(email_markdown_2, title=\"📧 Email to Process\", border_style=\"cyan\")" }, { "cell_type": "code", @@ -1442,7 +1310,7 @@ "id": "example2_invoke", "metadata": {}, "outputs": [], - "source": "config_2 = {\"configurable\": {\"thread_id\": \"test-thread-2\"}}\n\n# Invoke agent with simplified interface\nresult_2 = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown_2}],\n \"email_input\": email_input_2, # Optional: for middleware display formatting\n },\n config=config_2,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_2:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_2[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_2)\")\nelse:\n format_messages(result_2[\"messages\"])" + "source": "config_2 = {\"configurable\": {\"thread_id\": \"test-thread-2\"}}\n\n# Invoke agent with markdown email string\nresult_2 = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_2}]},\n config=config_2,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_2:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_2[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_2)\")\nelse:\n format_messages(result_2[\"messages\"])" }, { "cell_type": "markdown", @@ -1463,7 +1331,7 @@ { "cell_type": "code", "id": "yeib3g2uxlf", - "source": "# Marketing email that should be triaged as \"ignore\"\nemail_input_marketing = {\n \"author\": \"marketing@newsletter.com\",\n \"to\": \"lance@langchain.dev\",\n \"subject\": \"Limited Time Offer - 50% Off!\",\n \"email_thread\": \"Don't miss out on our amazing sale! Click here to shop now and save big on all your favorite items. This offer expires soon!\",\n}\n\nauthor, to, subject, email_thread = parse_email(email_input_marketing)\nemail_markdown_marketing = format_email_markdown(subject, author, to, email_thread)\n\n# Display email\nshow_prompt(email_markdown_marketing, title=\"📧 Marketing Email\", border_style=\"cyan\")\n\n# Invoke agent\nconfig_marketing = {\"configurable\": {\"thread_id\": \"test-marketing\"}}\nresult_marketing = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown_marketing}],\n \"email_input\": email_input_marketing,\n },\n config=config_marketing,\n)\n\n# Show triage decision\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Decision\")\nprint(\"=\"*60 + \"\\n\")\n\nmessages = result_marketing.get(\"messages\", [])\nfor i, msg in enumerate(messages):\n if hasattr(msg, 'tool_calls') and msg.tool_calls:\n for tool_call in msg.tool_calls:\n if tool_call.get('name') == 'triage_email':\n print(\"🔍 TRIAGE DECISION:\")\n print(f\" Classification: {tool_call.get('args', {}).get('classification', 'N/A')}\")\n print(f\" Reasoning: {tool_call.get('args', {}).get('reasoning', 'N/A')}\")\n print()\n if hasattr(msg, 'name') and msg.name == 'triage_email':\n print(f\"✓ Result: {msg.content}\")\n print()\n\nprint(\"Since this was classified as 'ignore', the agent calls Done immediately without drafting a response.\")", + "source": "# Marketing email as markdown string\nemail_markdown_marketing = \"\"\"\n**Subject**: Limited Time Offer - 50% Off!\n**From**: marketing@newsletter.com\n**To**: lance@langchain.dev\n\nDon't miss out on our amazing sale! Click here to shop now and save big on all your favorite items. This offer expires soon!\n\n---\n\"\"\"\n\n# Display email\nshow_prompt(email_markdown_marketing, title=\"📧 Marketing Email\", border_style=\"cyan\")\n\n# Invoke agent\nconfig_marketing = {\"configurable\": {\"thread_id\": \"test-marketing\"}}\nresult_marketing = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_marketing}]},\n config=config_marketing,\n)\n\n# Show triage decision\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Decision\")\nprint(\"=\"*60 + \"\\n\")\n\nmessages = result_marketing.get(\"messages\", [])\nfor i, msg in enumerate(messages):\n if hasattr(msg, 'tool_calls') and msg.tool_calls:\n for tool_call in msg.tool_calls:\n if tool_call.get('name') == 'triage_email':\n print(\"🔍 TRIAGE DECISION:\")\n print(f\" Classification: {tool_call.get('args', {}).get('classification', 'N/A')}\")\n print(f\" Reasoning: {tool_call.get('args', {}).get('reasoning', 'N/A')}\")\n print()\n if hasattr(msg, 'name') and msg.name == 'triage_email':\n print(f\"✓ Result: {msg.content}\")\n print()\n\nprint(\"Since this was classified as 'ignore', the agent calls Done immediately without drafting a response.\")", "metadata": {}, "execution_count": null, "outputs": [] @@ -1474,7 +1342,7 @@ "id": "test_memory_code", "metadata": {}, "outputs": [], - "source": "# Use the same thread as example 1 to test memory\nemail_input_3 = {\n \"author\": \"jane@example.com\",\n \"to\": \"lance@langchain.dev\",\n \"subject\": \"Follow-up meeting\",\n \"email_thread\": \"Hi again,\\n\\nCan we also schedule a follow-up meeting next week?\\n\\nJane\",\n}\n\nauthor, to, subject, email_thread = parse_email(email_input_3)\nemail_markdown_3 = format_email_markdown(subject, author, to, email_thread)\n\n# Use same config as example 1 (same thread_id) to test memory persistence\nresult_3 = agent.invoke(\n {\n \"messages\": [{\"role\": \"user\", \"content\": email_markdown_3}],\n \"email_input\": email_input_3, # Optional: for middleware display formatting\n },\n config=config_1, # Same thread as example 1\n)\n\n# Display result with rich formatting (show last 5 messages)\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response (with memory from previous conversation)\")\nprint(\"=\"*60 + \"\\n\")\nformat_messages(result_3[\"messages\"][-5:])" + "source": "# Follow-up email as markdown string\nemail_markdown_3 = \"\"\"\n**Subject**: Follow-up meeting\n**From**: jane@example.com\n**To**: lance@langchain.dev\n\nHi again,\n\nCan we also schedule a follow-up meeting next week?\n\nJane\n\n---\n\"\"\"\n\n# Use same config as example 1 (same thread_id) to test memory persistence\nresult_3 = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_3}]},\n config=config_1, # Same thread as example 1\n)\n\n# Display result with rich formatting (show last 5 messages)\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response (with memory from previous conversation)\")\nprint(\"=\"*60 + \"\\n\")\nformat_messages(result_3[\"messages\"][-5:])" }, { "cell_type": "code", From b53af40ed69f96a10cf05b9291ac4a6cf4d5648f Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Thu, 4 Dec 2025 11:45:28 -0800 Subject: [PATCH 07/11] refactor: simplify memory middleware to reject-only with message capture Simplified PostInterruptMemoryMiddleware to focus solely on rejection detection, removing all edit detection logic since deepagents framework doesn't expose raw tool calls before edits. Key changes: - Remove edit detection (after_model, wrap_tool_call, update_memory_for_edit) - Use before_model hook to detect ToolMessages with status="error" (rejections) - Capture optional rejection message from ToolMessage content - Update triage_preferences with user's rejection feedback - Remove response_preferences and cal_preferences namespaces - Update interrupt config: remove "edit" from allowed_decisions - Add agent instruction to call Done tool immediately on rejection - Add state_schema to middleware for proper state access Middleware now properly detects rejections using before_model hook which runs before next model call, allowing it to see rejection ToolMessages created by built-in HITL middleware. Rejection messages are extracted and included in memory updates for better triage learning. --- personal_assistant/README.md | 210 +++++++++---- .../email_assistant_deepagents.py | 39 ++- .../personal_assistant/middleware/__init__.py | 10 +- .../middleware/email_genui.py | 66 ++++ .../middleware/email_memory_injection.py | 149 +++++++++ .../middleware/email_post_interrupt.py | 290 ++++++++++++++++++ .../src/personal_assistant/prompts.py | 173 ++--------- 7 files changed, 720 insertions(+), 217 deletions(-) create mode 100644 personal_assistant/src/personal_assistant/middleware/email_genui.py create mode 100644 personal_assistant/src/personal_assistant/middleware/email_memory_injection.py create mode 100644 personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py diff --git a/personal_assistant/README.md b/personal_assistant/README.md index 469a847..4411b89 100644 --- a/personal_assistant/README.md +++ b/personal_assistant/README.md @@ -54,17 +54,22 @@ source .venv/bin/activate ### Local Deployment -Start LangGraph Studio for interactive testing: +Start LangGraph dev server for local testing: ```bash langgraph dev ``` -Then open [http://localhost:2024](http://localhost:2024) in your browser. +This starts a local LangGraph server at [http://localhost:2024](http://localhost:2024) that you can connect to from various UIs. -### Using Agent Inbox +### Using the DeepAgents UI -[Agent Inbox](https://github.com/langchain-ai/agent-inbox) provides a user-friendly web interface for managing human-in-the-loop interactions. Instead of programmatically handling interrupts, you can use Agent Inbox's UI to review and respond to agent actions. +The [DeepAgents UI](https://deepagentsui.vercel.app/) ([GitHub repo](https://github.com/langchain-ai/deep-agents-ui)) provides a user-friendly web interface for interacting with LangGraph agents and managing human-in-the-loop interrupts. Instead of programmatically handling interrupts, you can use the UI to review and respond to agent actions. + +**Overall Flow:** +1. **Deploy your LangGraph app**: Start locally (`langgraph dev`) or deploy to LangGraph Platform +2. **Connect to the UI**: Open the DeepAgents UI and connect it to your deployment +3. **Interact with interrupts**: When your agent needs approval, review and respond via the UI **Quick Setup:** @@ -73,44 +78,50 @@ Then open [http://localhost:2024](http://localhost:2024) in your browser. langgraph dev ``` -2. Open Agent Inbox in your browser: +2. Open the DeepAgents UI in your browser: ``` - https://dev.agentinbox.ai/ + https://deepagentsui.vercel.app/ ``` 3. Connect to your local graph: - - Click "Settings" (gear icon) in Agent Inbox + - Click "Settings" or connection configuration in the UI - Add your LangSmith API key - - Create a new inbox connection: + - Configure your deployment: - **Graph ID**: `personal_assistant` (from `langgraph.json`) - **Deployment URL**: `http://localhost:2024` (your local dev server) + - Alternatively, use your LangGraph Cloud deployment URL -4. Process emails through the inbox: - - Send an email input to your graph via LangGraph Studio or API - - When the agent needs approval, the interrupt appears in Agent Inbox +4. Process emails through the UI: + - Send an email input to your graph + - When the agent generates a tool call that requires approval, an interrupt appears in the UI - Review the email context and proposed action - Choose your response: - - **Accept** - Execute the action as-is - - **Edit** - Modify the arguments and execute - - **Ignore** - Skip the action and end workflow - - **Response** - Provide feedback for the agent to incorporate + - **Approve** - Execute the action as-is + - **Reject** - Skip the action and end workflow **What You'll See:** -When the email assistant interrupts (for `write_email`, `schedule_meeting`, or `Question`), Agent Inbox displays: +When the email assistant interrupts (for `write_email`, `schedule_meeting`, or `Question`), the UI displays: - Original email context (subject, sender, content) - Proposed action (email draft or meeting invite) -- Action details (formatted for easy review) -- Available response options based on the tool +- Tool call arguments (editable JSON) +- Available response options based on the tool configuration **Memory Learning:** -Agent Inbox responses automatically update the assistant's memory: -- **Edit responses** → Updates response/calendar preferences -- **Ignore responses** → Updates triage preferences -- **Feedback responses** → Incorporates into next action +UI responses automatically update the assistant's memory profiles: +- **Reject** `write_email` or `schedule_meeting` → Updates `triage_preferences` (classification rules) using optional rejection message for better context +- **Approve** → No memory update (agent did the right thing) + +See the [Memory System](#memory-system) section below for detailed logic and examples. + +**Using LangGraph Studio:** -This allows the assistant to learn your preferences over time and improve future suggestions. +You can also use LangGraph Studio (included with `langgraph dev`) for testing: +- Navigate to [http://localhost:2024](http://localhost:2024) +- View the graph visualization and execution trace +- Handle interrupts programmatically or via the Studio UI +- Inspect memory state and conversation history ## Architecture Overview @@ -124,63 +135,132 @@ Email Input → Triage Router → [Respond / Notify / Ignore] Response Agent (with HITL) ``` -**Tier 1: Triage Router** +**Tier 1: Triage Tool** - **Purpose**: Classify emails to avoid wasting time on irrelevant messages - **Classifications**: - - `respond` - Email requires a response → Routes to Response Agent - - `notify` - Important FYI email → Creates interrupt for user review + - `respond` - Email requires a response → Continues to draft/schedule + - `notify` - Important FYI email → Ends workflow with notification - `ignore` - Spam, marketing, or irrelevant → Ends workflow -- **Memory**: Learns from user corrections via `triage_preferences` namespace -- **Location**: `src/personal_assistant/email_assistant_deepagents.py:29-156` +- **Memory**: Learns from reject decisions - when user rejects a draft, updates `triage_preferences` to avoid future misclassifications +- **Location**: `src/personal_assistant/tools/default/email_tools.py` (triage_email tool) **Tier 2: Response Agent** - **Purpose**: Generate email drafts and schedule meetings with HITL approval - **Built with**: `create_deep_agent()` from deepagents library -- **Custom Middleware**: `EmailAssistantHITLMiddleware` for intelligent interrupts +- **HITL Configuration**: Uses built-in `interrupt_on` parameter for tool interrupts +- **Custom Middleware**: `MemoryInjectionMiddleware` and `PostInterruptMemoryMiddleware` for memory management - **Tools**: `write_email`, `schedule_meeting`, `check_calendar_availability`, `Question`, `Done` -- **Location**: `src/personal_assistant/email_assistant_deepagents.py:227-255` +- **Location**: `src/personal_assistant/email_assistant_deepagents.py` -### HITL Middleware System +### HITL System -Custom middleware that intercepts specific tool calls for human approval: +The assistant uses `create_deep_agent`'s built-in `interrupt_on` parameter for HITL interrupts, with lightweight middleware for memory management: -**Interrupt Filtering**: -- Only interrupts for: `write_email`, `schedule_meeting`, `Question` -- Other tools execute directly (e.g., `check_calendar_availability`) +**Built-in Interrupt Handling** (via `interrupt_on` parameter): +- **Configured tools**: `write_email`, `schedule_meeting`, `Question` +- **Non-interrupted tools**: Execute directly (e.g., `check_calendar_availability`, `Done`) +- **Static descriptions**: Each tool has a plain text description explaining the action and available decisions +- **Per-tool decision configuration**: Different tools allow different decision types -**Four Response Types**: -1. **Accept** - Execute tool with original arguments -2. **Edit** - Execute with modified arguments, update AI message immutably -3. **Ignore** - Skip execution, end workflow, update triage memory -4. **Response** - Provide feedback, continue workflow with updated context +**Interrupt Configuration Format**: +```python +interrupt_on_config = { + "write_email": { + "allowed_decisions": ["approve", "reject"], + "description": "I've drafted an email response. Please review the content, recipients, and subject line below. Approve to send as-is, or Reject to cancel and end the workflow." + }, + "schedule_meeting": { + "allowed_decisions": ["approve", "reject"], + "description": "I've prepared a calendar invitation. Please review the meeting details below. Approve to send the invite as-is, or Reject to cancel and end the workflow." + }, + "Question": { + "allowed_decisions": ["approve", "reject"], + "description": "I need clarification before proceeding. Please review the question below and provide your response, or Reject to skip this action and end the workflow." + } +} +``` -**Memory Integration**: -- Injects learned preferences into system prompt before each LLM call -- Updates memory after user edits or feedback +**Two Decision Types**: +1. **Approve** - Execute tool with original arguments (no memory update) +2. **Reject** - Skip execution, end workflow, update `triage_preferences` to avoid future false positives + +**What the UI Displays**: +- The description text explaining the action and available decisions +- Tool arguments in JSON format (to, subject, content for emails; attendees, time for meetings) +- Decision buttons (Approve, Reject) based on `allowed_decisions` configuration + +**Memory Middleware**: +- **MemoryInjectionMiddleware**: Injects learned preferences into system prompt before each LLM call +- **PostInterruptMemoryMiddleware**: Detects rejected tool calls and updates memory profiles + - Uses `before_model()` hook to check for ToolMessages with `status="error"` (indicates rejection) + - Extracts optional rejection message from ToolMessage content + - **REJECT detection**: Updates `triage_preferences` with user's rejection feedback + - **APPROVE**: No memory update needed - Uses runtime store in deployment, local store in testing -**Location**: `src/personal_assistant/middleware/email_assistant_hitl.py` +**GenUI Middleware**: +- **GenUIMiddleware**: Pushes UI messages for tool calls to enable custom UI component rendering +- Maps tool names to UI component names for visualization in LangGraph Studio +- Runs after model generation to create UI messages for configured tools +- Example mapping: + ```python + tool_to_genui_map={ + "write_email": {"component_name": "write_email"}, + "schedule_meeting": {"component_name": "schedule_meeting"}, + } + ``` + +**Locations**: +- Interrupt config: `src/personal_assistant/email_assistant_deepagents.py:66-100` +- Memory injection: `src/personal_assistant/middleware/email_memory_injection.py` +- Post-interrupt updates: `src/personal_assistant/middleware/email_post_interrupt.py` +- GenUI: `src/personal_assistant/middleware/email_genui.py` ### Memory System -Three persistent memory namespaces that learn from user behavior: +The assistant maintains a persistent memory profile that learns from user interactions during HITL interrupts. The profile is stored in a LangGraph Store namespace and automatically updates based on user decisions. + +#### Memory Namespaces + +**1. `("email_assistant", "triage_preferences")`** - Email Classification Rules +- **Purpose**: Learns when to respond vs. notify vs. ignore emails +- **Updated by**: REJECT decisions on `write_email` or `schedule_meeting` +- **Update logic**: When user rejects a draft, it means the email shouldn't have been classified as "respond" +- **Example**: "Emails from newsletter@company.com should be ignored, not responded to" + +#### Memory Update Trigger Matrix + +| User Decision | Tool Call | Memory Namespace Updated | Update Reason | +|---------------|-----------|--------------------------|---------------| +| **REJECT** | `write_email` | `triage_preferences` | Email shouldn't have been classified as "respond" | +| **REJECT** | `schedule_meeting` | `triage_preferences` | Meeting request shouldn't have been classified as "respond" | +| **APPROVE** | any tool | _(none)_ | No update needed - agent did the right thing | + +#### How Memory Updates Work -1. **`triage_preferences`** - Email classification rules - - Updated when: User corrects triage decisions (respond vs. notify vs. ignore) - - Example: "Emails from Alice about API docs should be responded to" +**Memory Injection** (`MemoryInjectionMiddleware`): +- Runs **before each LLM call** via `wrap_model_call()` +- Fetches memory profile from the store +- Injects it into the system prompt using template variables +- Agent sees current preferences on every turn -2. **`response_preferences`** - Email writing style - - Updated when: User edits email drafts or provides feedback - - Example: "Keep responses concise, avoid formalities" +**Memory Update Detection** (`PostInterruptMemoryMiddleware`): +- **Before model generation** (`before_model`): Detects ToolMessages with `status="error"` (rejections) +- **REJECT detection**: Extracts optional rejection message from ToolMessage content and updates triage preferences with user's feedback for better learning +- **Agent behavior**: Agent is instructed to call Done tool immediately after receiving a rejection to end the workflow -3. **`cal_preferences`** - Meeting scheduling preferences - - Updated when: User edits meeting invitations or provides feedback - - Example: "Prefer 30-minute meetings, avoid Fridays" +**Memory Update Process**: +1. Build prompt with current memory profile + user's feedback (reject reason) +2. Call LLM with structured output to update profile +3. LLM returns updated profile with reasoning (via `UserPreferences` schema) +4. Save updated profile back to store namespace +5. Next LLM call will see the updated preferences -**Storage**: -- Local testing: `InMemoryStore()` (ephemeral) -- Deployment: LangGraph platform store (persistent across sessions) -- Backend: `StoreBackend` for deepagents integration +**Storage Backend**: +- **Local testing**: `InMemoryStore()` (ephemeral, resets on restart) +- **Deployment**: LangGraph platform store (persistent across sessions) +- **Integration**: `StoreBackend` for deepagents compatibility +- **Access**: Middleware uses `runtime.store` in deployment, `self.store` in local testing ### Deployment Modes @@ -215,7 +295,10 @@ examples/personal_assistant/ │ ├── middleware/ │ ├── __init__.py - │ └── email_assistant_hitl.py # Custom HITL middleware with memory + │ ├── email_assistant_hitl.py # DEPRECATED: Legacy HITL middleware (kept for reference) + │ ├── email_memory_injection.py # Memory injection into system prompts + │ ├── email_post_interrupt.py # Post-interrupt memory updates + │ └── email_genui.py # GenUI integration for tool visualization │ └── tools/ ├── __init__.py @@ -371,10 +454,11 @@ This allows the LangGraph platform to provide persistent storage, checkpointing, ## Features -- **Two-Tiered Architecture**: Triage router + response agent for efficient email handling -- **Custom HITL Middleware**: Sophisticated interrupt handling with tool filtering -- **Persistent Memory**: Learns from user feedback across 3 namespaces -- **Four Response Types**: Accept, edit, ignore, or provide feedback on agent actions +- **Integrated Triage**: Single deepagent with triage tool for efficient email classification +- **Built-in Interrupt System**: Uses `interrupt_on` parameter with clear descriptions for UI display +- **Persistent Memory**: Learns from user feedback to improve triage classification +- **Two Decision Types**: Approve or reject actions with automatic memory updates +- **GenUI Integration**: Custom UI components for tool visualization in LangGraph Studio - **Async Support**: Works with both sync and async invocation (invoke/ainvoke, stream/astream) - **Deployment Ready**: Optimized for LangGraph Cloud with platform-provided persistence diff --git a/personal_assistant/src/personal_assistant/email_assistant_deepagents.py b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py index 147d93a..d65c73c 100644 --- a/personal_assistant/src/personal_assistant/email_assistant_deepagents.py +++ b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py @@ -17,7 +17,7 @@ from deepagents import create_deep_agent from deepagents.backends import StoreBackend -from .middleware import EmailAssistantHITLMiddleware +from .middleware import MemoryInjectionMiddleware, PostInterruptMemoryMiddleware, GenUIMiddleware from .schemas import EmailAssistantState from .tools import get_tools from .utils import format_email_markdown, parse_email, get_memory @@ -63,15 +63,31 @@ def create_email_assistant(for_deployment=False): store_kwarg = {"store": store} checkpointer_kwarg = {"checkpointer": checkpointer} - # Create custom HITL middleware - hitl_middleware = EmailAssistantHITLMiddleware( - store=store, - interrupt_on={ - "write_email": True, - "schedule_meeting": True, - "Question": True, + # Define interrupt configurations with plain text descriptions + interrupt_on_config = { + "write_email": { + "allowed_decisions": ["approve", "reject"], + "description": "I've drafted an email response. Please review the content, recipients, and subject line below. Approve to send as-is, or Reject to cancel and end the workflow." }, - ) + "schedule_meeting": { + "allowed_decisions": ["approve", "reject"], + "description": "I've prepared a calendar invitation. Please review the meeting details below. Approve to send the invite as-is, or Reject to cancel and end the workflow." + }, + "Question": { + "allowed_decisions": ["approve", "reject"], + "description": "I need clarification before proceeding. Please review the question below and provide your response, or Reject to skip this action and end the workflow." + } + } + + # Create middleware instances + memory_injection = MemoryInjectionMiddleware(store=store) + post_interrupt_memory = PostInterruptMemoryMiddleware(store=store) + genui = GenUIMiddleware(tool_to_genui_map={ + "write_email": {"component_name": "write_email"}, + "schedule_meeting": {"component_name": "schedule_meeting"}, + "check_calendar_availability": {"component_name": "check_calendar_availability"}, + "Question": {"component_name": "question"}, + }) # Build system prompt with default preferences # Note: Memory-based preferences can be accessed via the store in middleware @@ -88,10 +104,11 @@ def create_email_assistant(for_deployment=False): agent = create_deep_agent( model=model, tools=tools, - middleware=[hitl_middleware], # Custom middleware added to default stack - backend=lambda rt: StoreBackend(rt), # Persistent storage for memory + middleware=[memory_injection, post_interrupt_memory, genui], + backend=lambda rt: StoreBackend(rt), context_schema=EmailAssistantState, system_prompt=system_prompt, + interrupt_on=interrupt_on_config, # NEW: Built-in interrupt handling **store_kwarg, **checkpointer_kwarg, ) diff --git a/personal_assistant/src/personal_assistant/middleware/__init__.py b/personal_assistant/src/personal_assistant/middleware/__init__.py index 17fd16a..7fe07bd 100644 --- a/personal_assistant/src/personal_assistant/middleware/__init__.py +++ b/personal_assistant/src/personal_assistant/middleware/__init__.py @@ -1,5 +1,13 @@ """Custom middleware for email assistant HITL workflow.""" from .email_assistant_hitl import EmailAssistantHITLMiddleware +from .email_memory_injection import MemoryInjectionMiddleware +from .email_post_interrupt import PostInterruptMemoryMiddleware +from .email_genui import GenUIMiddleware -__all__ = ["EmailAssistantHITLMiddleware"] +__all__ = [ + "EmailAssistantHITLMiddleware", + "MemoryInjectionMiddleware", + "PostInterruptMemoryMiddleware", + "GenUIMiddleware", +] diff --git a/personal_assistant/src/personal_assistant/middleware/email_genui.py b/personal_assistant/src/personal_assistant/middleware/email_genui.py new file mode 100644 index 0000000..6441153 --- /dev/null +++ b/personal_assistant/src/personal_assistant/middleware/email_genui.py @@ -0,0 +1,66 @@ +"""GenUI middleware for email assistant tool visualization.""" + +from typing import Annotated, Any, Sequence +from typing_extensions import TypedDict + +from langchain.agents.middleware.types import AgentMiddleware, AgentState +from langgraph.graph.ui import AnyUIMessage, ui_message_reducer, push_ui_message +from langgraph.runtime import Runtime + + +class UIState(AgentState): + """State schema with UI message support.""" + ui: Annotated[Sequence[AnyUIMessage], ui_message_reducer] + + +class ToolGenUI(TypedDict): + """Configuration for tool UI component mapping.""" + component_name: str + + +class GenUIMiddleware(AgentMiddleware): + """Middleware to push UI messages for tool calls. + + This middleware runs after the model generates tool calls and pushes + UI messages for configured tools. This enables custom UI components + to render tool calls in the interface. + + Args: + tool_to_genui_map: Dict mapping tool names to UI component configurations + """ + + state_schema = UIState + + def __init__(self, tool_to_genui_map: dict[str, ToolGenUI]): + self.tool_to_genui_map = tool_to_genui_map + + def after_model(self, state: UIState, runtime: Runtime) -> dict[str, Any] | None: + """Push UI messages for tool calls after model generation. + + Args: + state: Agent state with messages + runtime: Runtime context + + Returns: + None (UI messages are pushed via side effect) + """ + messages = state.get("messages", []) + if not messages: + return + + last_message = messages[-1] + if last_message.type != "ai": + return + + if last_message.tool_calls: + for tool_call in last_message.tool_calls: + if tool_call["name"] in self.tool_to_genui_map: + component_name = self.tool_to_genui_map[tool_call["name"]]["component_name"] + push_ui_message( + component_name, + {}, + metadata={ + "tool_call_id": tool_call["id"] + }, + message=last_message + ) diff --git a/personal_assistant/src/personal_assistant/middleware/email_memory_injection.py b/personal_assistant/src/personal_assistant/middleware/email_memory_injection.py new file mode 100644 index 0000000..509fb75 --- /dev/null +++ b/personal_assistant/src/personal_assistant/middleware/email_memory_injection.py @@ -0,0 +1,149 @@ +"""Middleware for injecting memory into system prompts.""" + +from typing import Callable + +from langchain.agents.middleware.types import AgentMiddleware, ModelRequest, ModelResponse +from langgraph.store.base import BaseStore + +from ..prompts import ( + agent_system_prompt_hitl_memory, + default_background, + default_cal_preferences, + default_response_preferences, + default_triage_instructions, +) +from ..tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT +from ..utils import aget_memory, get_memory + + +class MemoryInjectionMiddleware(AgentMiddleware): + """Middleware for injecting memory preferences into system prompts. + + This middleware fetches memory from three namespaces (triage_preferences, + response_preferences, cal_preferences) and injects them into the system prompt + before each LLM call. + + Args: + store: LangGraph store for persistent memory + """ + + def __init__(self, store: BaseStore): + self.store = store + + def _get_store(self, runtime=None) -> BaseStore: + """Get store from runtime if available, otherwise use instance store. + + In deployment, LangGraph platform provides store via runtime. + In local testing, we use the store passed during initialization. + + Args: + runtime: Optional runtime object with store attribute + + Returns: + BaseStore instance + """ + if runtime and hasattr(runtime, "store"): + return runtime.store + return self.store + + def wrap_model_call( + self, + request: ModelRequest, + handler: Callable[[ModelRequest], ModelResponse], + ) -> ModelResponse: + """Inject memory into system prompt before LLM call. + + Fetches memory from the three namespaces (triage_preferences, response_preferences, + cal_preferences) and injects them into the system prompt. + """ + # Get store (from runtime in deployment, or from self in local testing) + store = self._get_store(request.runtime if hasattr(request, "runtime") else None) + + # Fetch memory from store + triage_prefs = get_memory( + store, + ("email_assistant", "triage_preferences"), + default_triage_instructions, + ) + response_prefs = get_memory( + store, + ("email_assistant", "response_preferences"), + default_response_preferences, + ) + cal_prefs = get_memory( + store, + ("email_assistant", "cal_preferences"), + default_cal_preferences, + ) + + # Format system prompt with memory + memory_prompt = agent_system_prompt_hitl_memory.format( + tools_prompt=HITL_MEMORY_TOOLS_PROMPT, + background=default_background, + triage_instructions=triage_prefs, + response_preferences=response_prefs, + cal_preferences=cal_prefs, + ) + + # Append memory prompt to existing system prompt + new_system_prompt = ( + request.system_prompt + "\n\n" + memory_prompt + if request.system_prompt + else memory_prompt + ) + + # Update request with new system prompt + updated_request = request.override(system_prompt=new_system_prompt) + + return handler(updated_request) + + async def awrap_model_call( + self, + request: ModelRequest, + handler: Callable[[ModelRequest], ModelResponse], + ) -> ModelResponse: + """Async version of wrap_model_call for async agent invocation. + + Identical logic to wrap_model_call but supports async context. + """ + # Get store (from runtime in deployment, or from self in local testing) + store = self._get_store(request.runtime if hasattr(request, "runtime") else None) + + # Fetch memory from store (using async methods) + triage_prefs = await aget_memory( + store, + ("email_assistant", "triage_preferences"), + default_triage_instructions, + ) + response_prefs = await aget_memory( + store, + ("email_assistant", "response_preferences"), + default_response_preferences, + ) + cal_prefs = await aget_memory( + store, + ("email_assistant", "cal_preferences"), + default_cal_preferences, + ) + + # Format system prompt with memory + memory_prompt = agent_system_prompt_hitl_memory.format( + tools_prompt=HITL_MEMORY_TOOLS_PROMPT, + background=default_background, + triage_instructions=triage_prefs, + response_preferences=response_prefs, + cal_preferences=cal_prefs, + ) + + # Append memory prompt to existing system prompt + new_system_prompt = ( + request.system_prompt + "\n\n" + memory_prompt + if request.system_prompt + else memory_prompt + ) + + # Update request with new system prompt + updated_request = request.override(system_prompt=new_system_prompt) + + # Call handler (may or may not be async) + return await handler(updated_request) diff --git a/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py b/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py new file mode 100644 index 0000000..9b3ca96 --- /dev/null +++ b/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py @@ -0,0 +1,290 @@ +"""Middleware for updating memory based on interrupt responses.""" + +from langchain.agents.middleware.types import AgentMiddleware +from langchain_core.messages import AIMessage +from langgraph.store.base import BaseStore + +from ..prompts import MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT +from ..utils import aupdate_memory, update_memory + + +class PostInterruptMemoryMiddleware(AgentMiddleware): + """Middleware for updating memory when user rejects tool calls. + + This middleware detects when tool calls are rejected (never executed) and + updates triage preferences to learn which emails should not trigger responses. + + Memory updates: + - reject write_email: Updates triage_preferences + - reject schedule_meeting: Updates triage_preferences + - approve: No memory update + + Args: + store: LangGraph store for persistent memory + """ + + # Import state schema at the top of the file + from ..schemas import EmailAssistantState + state_schema = EmailAssistantState + + def __init__(self, store: BaseStore): + self.store = store + # Track which tools should trigger memory updates on reject + self.interrupt_tools = {"write_email", "schedule_meeting"} + # Track which tool calls we've already processed (to avoid duplicates) + self._processed_tool_calls = set() + + def before_model(self, state, runtime) -> dict | None: + """Check for rejected tool calls before next model generation. + + After a rejection, the built-in HITL middleware adds a ToolMessage with + status="error". We check for these BEFORE the next model call to update memory. + """ + print("DEBUG before_model: Hook called") + messages = state.get("messages", []) + print(f"DEBUG before_model: Found {len(messages)} messages") + + # Debug: Print message types + for idx, msg in enumerate(messages): + msg_type = type(msg).__name__ + status = getattr(msg, "status", None) + print(f"DEBUG before_model: Message {idx}: {msg_type}, status={status}") + + # Look for ToolMessages with status="error" (rejections) + for message in reversed(messages): + # Check if this is a ToolMessage with error status (indicates rejection) + if (hasattr(message, "status") and + message.status == "error" and + hasattr(message, "tool_call_id")): + + tool_call_id = message.tool_call_id + tool_name = message.name + rejection_message = message.content + print(f"DEBUG before_model: Found rejection - tool_name={tool_name}, tool_call_id={tool_call_id}") + + # Check if this is a tool we care about and haven't processed yet + if tool_name in self.interrupt_tools and tool_call_id not in self._processed_tool_calls: + print(f"DEBUG before_model: Processing rejection for {tool_name}") + # Find the original tool call args from the AIMessage + original_args = {} + for msg in reversed(messages): + if isinstance(msg, AIMessage) and hasattr(msg, "tool_calls"): + for tc in msg.tool_calls: + if tc.get("id") == tool_call_id: + original_args = dict(tc.get("args", {})) + break + if original_args: + break + + # Update memory with rejection message + print(f"DEBUG before_model: Updating memory for rejection") + self._update_memory_for_reject(tool_name, original_args, rejection_message, state, runtime) + # Mark as processed + self._processed_tool_calls.add(tool_call_id) + print(f"DEBUG before_model: Memory update complete") + + return None + + def _get_store(self, runtime=None) -> BaseStore: + """Get store from runtime if available, otherwise use instance store. + + In deployment, LangGraph platform provides store via runtime. + In local testing, we use the store passed during initialization. + + Args: + runtime: Optional runtime object with store attribute + + Returns: + BaseStore instance + """ + if runtime and hasattr(runtime, "store"): + return runtime.store + return self.store + + def after_tool(self, state, runtime) -> dict | None: + """Check for rejected tool calls. + + When a user rejects a tool call with built-in interrupt_on, a ToolMessage + with status="error" is created containing the rejection reason. + """ + print("DEBUG after_tool: Hook called") + messages = state.get("messages", []) + print(f"DEBUG after_tool: Found {len(messages)} messages") + if not messages: + return None + + # Debug: Print message types + for idx, msg in enumerate(messages): + msg_type = type(msg).__name__ + status = getattr(msg, "status", None) + print(f"DEBUG after_tool: Message {idx}: {msg_type}, status={status}") + + # Look for ToolMessages with status="error" (rejections) + for message in reversed(messages): + # Check if this is a ToolMessage with error status (indicates rejection) + if (hasattr(message, "status") and + message.status == "error" and + hasattr(message, "tool_call_id")): + + tool_call_id = message.tool_call_id + tool_name = message.name + rejection_message = message.content # This contains the user's rejection reason + print(f"DEBUG after_tool: Found rejection - tool_name={tool_name}, tool_call_id={tool_call_id}") + + # Check if this is a tool we care about and haven't processed yet + if tool_name in self.interrupt_tools and tool_call_id not in self._processed_tool_calls: + print(f"DEBUG after_tool: Processing rejection for {tool_name}") + # Find the original tool call args from the AIMessage + original_args = {} + for msg in reversed(messages): + if isinstance(msg, AIMessage) and hasattr(msg, "tool_calls"): + for tc in msg.tool_calls: + if tc.get("id") == tool_call_id: + original_args = dict(tc.get("args", {})) + break + if original_args: + break + + # Update memory with rejection message + print(f"DEBUG after_tool: Updating memory for rejection") + self._update_memory_for_reject(tool_name, original_args, rejection_message, state, runtime) + # Mark as processed + self._processed_tool_calls.add(tool_call_id) + print(f"DEBUG after_tool: Memory update complete") + else: + print(f"DEBUG after_tool: Skipping - tool_name in interrupt_tools: {tool_name in self.interrupt_tools}, already processed: {tool_call_id in self._processed_tool_calls}") + + return None + + def _update_memory_for_reject( + self, tool_name: str, original_args: dict, rejection_message: str, state, runtime + ): + """Update triage preferences when user rejects a tool call. + + Args: + tool_name: Name of the tool that was rejected + original_args: Original tool arguments that were rejected + rejection_message: User's optional feedback explaining why they rejected + state: Agent state containing messages + runtime: Runtime context + """ + namespace = ("email_assistant", "triage_preferences") + + # Create feedback based on tool type + if tool_name == "write_email": + feedback = "The user rejected the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." + elif tool_name == "schedule_meeting": + feedback = "The user rejected the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." + else: + return # No memory update for other tools + + # Get conversation context for memory update from state + messages = state.get("messages", []) + + # Format args as JSON for context + import json + args_str = json.dumps(original_args, indent=2) + + # Include user's rejection message if provided + rejection_context = f"\n\nUser's rejection reason: {rejection_message}" if rejection_message else "" + + messages_for_update = messages + [ + { + "role": "user", + "content": f"{feedback}\n\nRejected tool call: {tool_name}\nArguments: {args_str}{rejection_context}\n\nFollow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}", + } + ] + + store = self._get_store(runtime) + update_memory(store, namespace, messages_for_update) + + # Async versions + + async def abefore_model(self, state, runtime) -> dict | None: + """Async version - check for rejected tool calls before next model generation.""" + print("DEBUG abefore_model: Hook called") + messages = state.get("messages", []) + print(f"DEBUG abefore_model: Found {len(messages)} messages") + + # Debug: Print message types + for idx, msg in enumerate(messages): + msg_type = type(msg).__name__ + status = getattr(msg, "status", None) + print(f"DEBUG abefore_model: Message {idx}: {msg_type}, status={status}") + + # Look for ToolMessages with status="error" (rejections) + for message in reversed(messages): + # Check if this is a ToolMessage with error status (indicates rejection) + if (hasattr(message, "status") and + message.status == "error" and + hasattr(message, "tool_call_id")): + + tool_call_id = message.tool_call_id + tool_name = message.name + rejection_message = message.content + print(f"DEBUG abefore_model: Found rejection - tool_name={tool_name}, tool_call_id={tool_call_id}") + + # Check if this is a tool we care about and haven't processed yet + if tool_name in self.interrupt_tools and tool_call_id not in self._processed_tool_calls: + print(f"DEBUG abefore_model: Processing rejection for {tool_name}") + # Find the original tool call args from the AIMessage + original_args = {} + for msg in reversed(messages): + if isinstance(msg, AIMessage) and hasattr(msg, "tool_calls"): + for tc in msg.tool_calls: + if tc.get("id") == tool_call_id: + original_args = dict(tc.get("args", {})) + break + if original_args: + break + + # Update memory with rejection message + print(f"DEBUG abefore_model: Updating memory for rejection") + await self._aupdate_memory_for_reject(tool_name, original_args, rejection_message, state, runtime) + # Mark as processed + self._processed_tool_calls.add(tool_call_id) + print(f"DEBUG abefore_model: Memory update complete") + + return None + + async def _aupdate_memory_for_reject( + self, tool_name: str, original_args: dict, rejection_message: str, state, runtime + ): + """Async version of _update_memory_for_reject. + + Args: + tool_name: Name of the tool that was rejected + original_args: Original tool arguments that were rejected + rejection_message: User's optional feedback explaining why they rejected + state: Agent state containing messages + runtime: Runtime context + """ + namespace = ("email_assistant", "triage_preferences") + + # Create feedback based on tool type + if tool_name == "write_email": + feedback = "The user rejected the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." + elif tool_name == "schedule_meeting": + feedback = "The user rejected the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." + else: + return # No memory update for other tools + + # Get conversation context for memory update from state + messages = state.get("messages", []) + + # Format args as JSON for context + import json + args_str = json.dumps(original_args, indent=2) + + # Include user's rejection message if provided + rejection_context = f"\n\nUser's rejection reason: {rejection_message}" if rejection_message else "" + + messages_for_update = messages + [ + { + "role": "user", + "content": f"{feedback}\n\nRejected tool call: {tool_name}\nArguments: {args_str}{rejection_context}\n\nFollow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}", + } + ] + + store = self._get_store(runtime) + await aupdate_memory(store, namespace, messages_for_update) diff --git a/personal_assistant/src/personal_assistant/prompts.py b/personal_assistant/src/personal_assistant/prompts.py index 7879c0d..c48e060 100644 --- a/personal_assistant/src/personal_assistant/prompts.py +++ b/personal_assistant/src/personal_assistant/prompts.py @@ -1,115 +1,6 @@ from datetime import datetime -# Email assistant triage prompt -triage_system_prompt = """ - -< Role > -Your role is to triage incoming emails based upon instructs and background information below. - - -< Background > -{background}. - - -< Instructions > -Categorize each email into one of three categories: -1. IGNORE - Emails that are not worth responding to or tracking -2. NOTIFY - Important information that worth notification but doesn't require a response -3. RESPOND - Emails that need a direct response -Classify the below email into one of these categories. - - -< Rules > -{triage_instructions} - -""" - -# Email assistant triage user prompt -triage_user_prompt = """ -Please determine how to handle the below email thread: - -From: {author} -To: {to} -Subject: {subject} -{email_thread}""" - -# Email assistant prompt -agent_system_prompt = """ -< Role > -You are a top-notch executive assistant who cares about helping your executive perform as well as possible. - - -< Tools > -You have access to the following tools to help manage communications and schedule: -{tools_prompt} - - -< Instructions > -When handling emails, follow these steps: -1. Carefully analyze the email content and purpose -2. IMPORTANT --- always call a tool and call one tool at a time until the task is complete: -3. For responding to the email, draft a response email with the write_email tool -4. For meeting requests, use the check_calendar_availability tool to find open time slots -5. To schedule a meeting, use the schedule_meeting tool with a datetime object for the preferred_day parameter - - Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """ - use this for scheduling meetings accurately -6. If you scheduled a meeting, then draft a short response email using the write_email tool -7. After using the write_email tool, the task is complete -8. If you have sent the email, then use the Done tool to indicate that the task is complete - - -< Background > -{background} - - -< Response Preferences > -{response_preferences} - - -< Calendar Preferences > -{cal_preferences} - -""" - -# Email assistant with HITL prompt -agent_system_prompt_hitl = """ -< Role > -You are a top-notch executive assistant who cares about helping your executive perform as well as possible. - - -< Tools > -You have access to the following tools to help manage communications and schedule: -{tools_prompt} - - -< Instructions > -When handling emails, follow these steps: -1. Carefully analyze the email content and purpose -2. IMPORTANT --- always call a tool and call one tool at a time until the task is complete: -3. If the incoming email asks the user a direct question and you do not have context to answer the question, use the Question tool to ask the user for the answer -4. For responding to the email, draft a response email with the write_email tool -5. For meeting requests, use the check_calendar_availability tool to find open time slots -6. To schedule a meeting, use the schedule_meeting tool with a datetime object for the preferred_day parameter - - Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """ - use this for scheduling meetings accurately -7. If you scheduled a meeting, then draft a short response email using the write_email tool -8. After using the write_email tool, the task is complete -9. If you have sent the email, then use the Done tool to indicate that the task is complete - - -< Background > -{background} - - -< Response Preferences > -{response_preferences} - - -< Calendar Preferences > -{cal_preferences} - -""" - # Email assistant with HITL and memory prompt -# Note: Currently, this is the same as the HITL prompt. However, memory specific tools (see https://langchain-ai.github.io/langmem/) can be added agent_system_prompt_hitl_memory = """ < Role > You are a top-notch executive assistant. @@ -143,6 +34,7 @@ (Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """) - For responding to emails, draft a response using write_email - If you scheduled a meeting, send a short confirmation email using write_email +- CRITICAL: If the user rejects your tool call (write_email or schedule_meeting), you will receive a ToolMessage with status="error" containing their feedback. When this happens, call the Done tool immediately to end the workflow - do NOT retry or generate new drafts - After sending the email, call the Done tool @@ -229,54 +121,53 @@ MEMORY_UPDATE_INSTRUCTIONS = """ # Role and Objective -You are a memory profile manager for an email assistant agent that selectively updates user preferences based on feedback messages from human-in-the-loop interactions with the email assistant. +You are a memory profile manager for an email assistant agent that selectively updates user preferences based on edits made during human-in-the-loop interactions. + +# Context +When users edit tool calls (emails or calendar invitations), you receive: +- The ORIGINAL tool call generated by the assistant +- The EDITED tool call after the user made changes + +Your job is to learn from these edits and update the user's preference profile. # Instructions - NEVER overwrite the entire memory profile -- ONLY make targeted additions of new information -- ONLY update specific facts that are directly contradicted by feedback messages +- ONLY make targeted additions of new information based on the edits +- ONLY update specific facts that are directly contradicted by the edits - PRESERVE all other existing information in the profile - Format the profile consistently with the original style - Generate the profile as a string # Reasoning Steps 1. Analyze the current memory profile structure and content -2. Review feedback messages from human-in-the-loop interactions -3. Extract relevant user preferences from these feedback messages (such as edits to emails/calendar invites, explicit feedback on assistant performance, user decisions to ignore certain emails) -4. Compare new information against existing profile -5. Identify only specific facts to add or update +2. Compare the ORIGINAL tool call with the EDITED tool call +3. Identify what the user changed (subject lines, tone, content, timing, etc.) +4. Extract the underlying preference from the change +5. Add or update the relevant preference in the profile 6. Preserve all other existing information 7. Output the complete updated profile # Example -RESPOND: -- wife -- specific questions -- system admin notifications -NOTIFY: -- meeting invites -IGNORE: -- marketing emails -- company-wide announcements -- messages meant for other teams +Email responses should be: +- Professional and concise +- Include acknowledgment of deadlines - -"The assistant shouldn't have responded to that system admin notification." - + +{{"to": "sarah@example.com", "subject": "Re: Question", "content": "Thanks for reaching out. I'll look into this and get back to you soon."}} + + + +{{"to": "sarah@example.com", "subject": "Re: Question about deployment", "content": "Thanks for reaching out. I'll investigate the deployment issue and get back to you by end of day tomorrow."}} + -RESPOND: -- wife -- specific questions -NOTIFY: -- meeting invites -- system admin notifications -IGNORE: -- marketing emails -- company-wide announcements -- messages meant for other teams +Email responses should be: +- Professional and concise +- Include acknowledgment of deadlines +- Include specific timelines for follow-up +- Repeat key details from the original email in the subject line # Process current profile for {namespace} @@ -284,9 +175,7 @@ {current_profile} -Think step by step about what specific feedback is being provided and what specific information should be added or updated in the profile while preserving everything else. - -Think carefully and update the memory profile based upon these user messages:""" +The original and edited tool calls will be provided in the user message. Think step by step about what the user changed and what preference this reveals. Update the memory profile to reflect this preference while preserving all other existing information.""" MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT = """ Remember: From 05e79b4586af182cff9f3634c2f7729942978a17 Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Thu, 4 Dec 2025 11:46:37 -0800 Subject: [PATCH 08/11] refactor: remove debug logging from PostInterruptMemoryMiddleware --- .../middleware/email_post_interrupt.py | 46 ++----------------- 1 file changed, 4 insertions(+), 42 deletions(-) diff --git a/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py b/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py index 9b3ca96..c3fa2af 100644 --- a/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py +++ b/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py @@ -40,15 +40,7 @@ def before_model(self, state, runtime) -> dict | None: After a rejection, the built-in HITL middleware adds a ToolMessage with status="error". We check for these BEFORE the next model call to update memory. """ - print("DEBUG before_model: Hook called") messages = state.get("messages", []) - print(f"DEBUG before_model: Found {len(messages)} messages") - - # Debug: Print message types - for idx, msg in enumerate(messages): - msg_type = type(msg).__name__ - status = getattr(msg, "status", None) - print(f"DEBUG before_model: Message {idx}: {msg_type}, status={status}") # Look for ToolMessages with status="error" (rejections) for message in reversed(messages): @@ -60,11 +52,9 @@ def before_model(self, state, runtime) -> dict | None: tool_call_id = message.tool_call_id tool_name = message.name rejection_message = message.content - print(f"DEBUG before_model: Found rejection - tool_name={tool_name}, tool_call_id={tool_call_id}") # Check if this is a tool we care about and haven't processed yet if tool_name in self.interrupt_tools and tool_call_id not in self._processed_tool_calls: - print(f"DEBUG before_model: Processing rejection for {tool_name}") # Find the original tool call args from the AIMessage original_args = {} for msg in reversed(messages): @@ -77,11 +67,9 @@ def before_model(self, state, runtime) -> dict | None: break # Update memory with rejection message - print(f"DEBUG before_model: Updating memory for rejection") self._update_memory_for_reject(tool_name, original_args, rejection_message, state, runtime) # Mark as processed self._processed_tool_calls.add(tool_call_id) - print(f"DEBUG before_model: Memory update complete") return None @@ -102,23 +90,15 @@ def _get_store(self, runtime=None) -> BaseStore: return self.store def after_tool(self, state, runtime) -> dict | None: - """Check for rejected tool calls. + """Check for rejected tool calls (fallback hook). - When a user rejects a tool call with built-in interrupt_on, a ToolMessage - with status="error" is created containing the rejection reason. + Note: Rejections are primarily detected in before_model hook. + This hook serves as a fallback in case after_tool runs after rejections. """ - print("DEBUG after_tool: Hook called") messages = state.get("messages", []) - print(f"DEBUG after_tool: Found {len(messages)} messages") if not messages: return None - # Debug: Print message types - for idx, msg in enumerate(messages): - msg_type = type(msg).__name__ - status = getattr(msg, "status", None) - print(f"DEBUG after_tool: Message {idx}: {msg_type}, status={status}") - # Look for ToolMessages with status="error" (rejections) for message in reversed(messages): # Check if this is a ToolMessage with error status (indicates rejection) @@ -128,12 +108,10 @@ def after_tool(self, state, runtime) -> dict | None: tool_call_id = message.tool_call_id tool_name = message.name - rejection_message = message.content # This contains the user's rejection reason - print(f"DEBUG after_tool: Found rejection - tool_name={tool_name}, tool_call_id={tool_call_id}") + rejection_message = message.content # Check if this is a tool we care about and haven't processed yet if tool_name in self.interrupt_tools and tool_call_id not in self._processed_tool_calls: - print(f"DEBUG after_tool: Processing rejection for {tool_name}") # Find the original tool call args from the AIMessage original_args = {} for msg in reversed(messages): @@ -146,13 +124,9 @@ def after_tool(self, state, runtime) -> dict | None: break # Update memory with rejection message - print(f"DEBUG after_tool: Updating memory for rejection") self._update_memory_for_reject(tool_name, original_args, rejection_message, state, runtime) # Mark as processed self._processed_tool_calls.add(tool_call_id) - print(f"DEBUG after_tool: Memory update complete") - else: - print(f"DEBUG after_tool: Skipping - tool_name in interrupt_tools: {tool_name in self.interrupt_tools}, already processed: {tool_call_id in self._processed_tool_calls}") return None @@ -202,15 +176,7 @@ def _update_memory_for_reject( async def abefore_model(self, state, runtime) -> dict | None: """Async version - check for rejected tool calls before next model generation.""" - print("DEBUG abefore_model: Hook called") messages = state.get("messages", []) - print(f"DEBUG abefore_model: Found {len(messages)} messages") - - # Debug: Print message types - for idx, msg in enumerate(messages): - msg_type = type(msg).__name__ - status = getattr(msg, "status", None) - print(f"DEBUG abefore_model: Message {idx}: {msg_type}, status={status}") # Look for ToolMessages with status="error" (rejections) for message in reversed(messages): @@ -222,11 +188,9 @@ async def abefore_model(self, state, runtime) -> dict | None: tool_call_id = message.tool_call_id tool_name = message.name rejection_message = message.content - print(f"DEBUG abefore_model: Found rejection - tool_name={tool_name}, tool_call_id={tool_call_id}") # Check if this is a tool we care about and haven't processed yet if tool_name in self.interrupt_tools and tool_call_id not in self._processed_tool_calls: - print(f"DEBUG abefore_model: Processing rejection for {tool_name}") # Find the original tool call args from the AIMessage original_args = {} for msg in reversed(messages): @@ -239,11 +203,9 @@ async def abefore_model(self, state, runtime) -> dict | None: break # Update memory with rejection message - print(f"DEBUG abefore_model: Updating memory for rejection") await self._aupdate_memory_for_reject(tool_name, original_args, rejection_message, state, runtime) # Mark as processed self._processed_tool_calls.add(tool_call_id) - print(f"DEBUG abefore_model: Memory update complete") return None From 3cbb600b4595e5b09afa5ad398b93c3eede9fd7b Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Thu, 4 Dec 2025 12:03:28 -0800 Subject: [PATCH 09/11] refactor: remove deprecated EmailAssistantHITLMiddleware - Remove email_assistant_hitl.py (legacy Agent Inbox UI middleware) - Clean up middleware/__init__.py exports - Update README code structure to reflect current middleware The deprecated middleware was used with the old Agent Inbox UI and handled interrupts manually with 4 decision types (accept/edit/ignore/response). Current architecture uses built-in interrupt_on parameter with 3 focused middleware classes: - MemoryInjectionMiddleware: inject learned preferences before LLM calls - PostInterruptMemoryMiddleware: detect rejections and update memory - GenUIMiddleware: push UI messages for tool visualization --- personal_assistant/README.md | 1 - .../personal_assistant/middleware/__init__.py | 2 - .../middleware/email_assistant_hitl.py | 782 ------------------ 3 files changed, 785 deletions(-) delete mode 100644 personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py diff --git a/personal_assistant/README.md b/personal_assistant/README.md index 4411b89..83b714f 100644 --- a/personal_assistant/README.md +++ b/personal_assistant/README.md @@ -295,7 +295,6 @@ examples/personal_assistant/ │ ├── middleware/ │ ├── __init__.py - │ ├── email_assistant_hitl.py # DEPRECATED: Legacy HITL middleware (kept for reference) │ ├── email_memory_injection.py # Memory injection into system prompts │ ├── email_post_interrupt.py # Post-interrupt memory updates │ └── email_genui.py # GenUI integration for tool visualization diff --git a/personal_assistant/src/personal_assistant/middleware/__init__.py b/personal_assistant/src/personal_assistant/middleware/__init__.py index 7fe07bd..66b49a2 100644 --- a/personal_assistant/src/personal_assistant/middleware/__init__.py +++ b/personal_assistant/src/personal_assistant/middleware/__init__.py @@ -1,12 +1,10 @@ """Custom middleware for email assistant HITL workflow.""" -from .email_assistant_hitl import EmailAssistantHITLMiddleware from .email_memory_injection import MemoryInjectionMiddleware from .email_post_interrupt import PostInterruptMemoryMiddleware from .email_genui import GenUIMiddleware __all__ = [ - "EmailAssistantHITLMiddleware", "MemoryInjectionMiddleware", "PostInterruptMemoryMiddleware", "GenUIMiddleware", diff --git a/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py b/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py deleted file mode 100644 index 6e32c81..0000000 --- a/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py +++ /dev/null @@ -1,782 +0,0 @@ -"""Custom HITL middleware for email assistant with memory updates.""" - -from typing import Callable - -from langchain.agents.middleware.types import AgentMiddleware, ModelRequest, ModelResponse -from langchain.tools import ToolRuntime -from langchain.tools.tool_node import ToolCallRequest -from langchain_core.messages import ToolMessage -from langgraph.constants import END -from langgraph.store.base import BaseStore -from langgraph.types import Command, interrupt - -from ..prompts import ( - MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT, - agent_system_prompt_hitl_memory, - default_background, - default_cal_preferences, - default_response_preferences, - default_triage_instructions, -) -from ..tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT -from ..utils import format_email_markdown, format_for_display, get_memory, parse_email, update_memory, aget_memory, aupdate_memory - - -class EmailAssistantHITLMiddleware(AgentMiddleware): - """Custom HITL middleware with memory updates and tool filtering. - - This middleware provides sophisticated human-in-the-loop functionality for the - email assistant, including: - - Tool filtering (only interrupts for specific tools) - - Custom display formatting with email context - - Per-tool action configurations - - Four response types: accept, edit, ignore, response - - Automatic memory updates based on user feedback - - Args: - store: LangGraph store for persistent memory - interrupt_on: Dict mapping tool names to whether they should trigger interrupts - email_input_key: State key containing email context (default: "email_input") - """ - - def __init__( - self, - store: BaseStore, - interrupt_on: dict[str, bool], - email_input_key: str = "email_input", - ): - self.store = store - self.interrupt_on = interrupt_on - self.email_input_key = email_input_key - self.tools_by_name = {} - - def _get_store(self, runtime=None): - """Get store from runtime if available, otherwise use instance store. - - In deployment, LangGraph platform provides store via runtime. - In local testing, we use the store passed during initialization. - - Args: - runtime: Optional runtime object with store attribute - - Returns: - BaseStore instance - """ - if runtime and hasattr(runtime, "store"): - return runtime.store - return self.store - - def wrap_model_call( - self, - request: ModelRequest, - handler: Callable[[ModelRequest], ModelResponse], - ) -> ModelResponse: - """Inject memory into system prompt before LLM call. - - Fetches memory from the three namespaces (triage_preferences, response_preferences, - cal_preferences) and injects them into the system prompt. - """ - # Get store (from runtime in deployment, or from self in local testing) - store = self._get_store(request.runtime if hasattr(request, "runtime") else None) - - # Fetch memory from store - triage_prefs = get_memory( - store, - ("email_assistant", "triage_preferences"), - default_triage_instructions, - ) - response_prefs = get_memory( - store, - ("email_assistant", "response_preferences"), - default_response_preferences, - ) - cal_prefs = get_memory( - store, - ("email_assistant", "cal_preferences"), - default_cal_preferences, - ) - - # Format system prompt with memory - memory_prompt = agent_system_prompt_hitl_memory.format( - tools_prompt=HITL_MEMORY_TOOLS_PROMPT, - background=default_background, - triage_instructions=triage_prefs, - response_preferences=response_prefs, - cal_preferences=cal_prefs, - ) - - # Append memory prompt to existing system prompt - new_system_prompt = ( - request.system_prompt + "\n\n" + memory_prompt - if request.system_prompt - else memory_prompt - ) - - # Update request with new system prompt - updated_request = request.override(system_prompt=new_system_prompt) - - return handler(updated_request) - - def wrap_tool_call( - self, - request: ToolCallRequest, - handler: Callable[[ToolCallRequest], ToolMessage | Command], - ) -> ToolMessage | Command: - """Intercept tool calls for HITL filtering and memory updates. - - This is the core HITL logic that: - 1. Filters tools (only interrupts for configured tools) - 2. Formats display with email context - 3. Creates interrupt with per-tool config - 4. Handles user response (accept/edit/ignore/response) - 5. Updates memory based on feedback - """ - tool_call = request.tool_call - tool_name = tool_call["name"] - tool = request.tool # Get the tool directly from the request - - # Cache tool for later use - if tool and tool_name not in self.tools_by_name: - self.tools_by_name[tool_name] = tool - - # STEP 1: Filter - Execute non-HITL tools directly without interruption - if tool_name not in self.interrupt_on or not self.interrupt_on[tool_name]: - # Use handler to ensure proper middleware chaining - return handler(request) - - # STEP 2: Format display - Get email context and format for display - email_input = request.runtime.state.get(self.email_input_key) - description = self._format_interrupt_display(email_input, tool_call) - - # STEP 3: Configure per-tool actions - config = self._get_action_config(tool_name) - - # STEP 4: Create interrupt - interrupt_request = { - "action_request": {"action": tool_name, "args": tool_call["args"]}, - "config": config, - "description": description, - } - - # Call interrupt() to create the interrupt and wait for user decision - # When creating a new interrupt: returns a list of responses [response] - # When resuming from an interrupt: returns a dict mapping interrupt IDs to decisions - result = interrupt([interrupt_request]) - print(f"DEBUG: interrupt() returned: {result}, type: {type(result)}") - - # Handle both new interrupt and resume cases - if isinstance(result, list) and len(result) > 0: - # New interrupt created, got user decision - response = result[0] - elif isinstance(result, dict): - # Check if result is the decision itself (has "type" key) or a mapping of interrupt IDs - if "type" in result: - # Result is the decision dict directly (e.g., {"type": "accept"}) - response = result - else: - # Result is a mapping of interrupt IDs to decisions - # Extract the decision (dict maps interrupt IDs to decisions) - decisions = list(result.values()) - if decisions: - response = decisions[0] - else: - # No decision found, execute tool with original args - tool = self.tools_by_name[tool_name] - observation = tool.invoke(tool_call["args"]) - return ToolMessage(content=observation, tool_call_id=tool_call["id"]) - else: - # Unexpected format, execute tool with original args - tool = self.tools_by_name[tool_name] - observation = tool.invoke(tool_call["args"]) - return ToolMessage(content=observation, tool_call_id=tool_call["id"]) - - # STEP 5: Handle response type - return self._handle_response(response, tool_call, request.runtime) - - def _format_interrupt_display(self, email_input: dict | None, tool_call: dict) -> str: - """Format interrupt display with email context and tool details. - - Args: - email_input: Email context from state - tool_call: Tool call dict with name and args - - Returns: - Formatted markdown string for interrupt display - """ - # Get original email context if available - if email_input: - author, to, subject, email_thread = parse_email(email_input) - email_markdown = format_email_markdown(subject, author, to, email_thread) - else: - email_markdown = "" - - # Format tool call for display - tool_display = format_for_display(tool_call) - - return email_markdown + tool_display - - def _get_action_config(self, tool_name: str) -> dict: - """Get per-tool action configuration. - - Args: - tool_name: Name of the tool being called - - Returns: - Config dict with allowed actions (allow_ignore, allow_respond, etc.) - """ - if tool_name == "write_email": - return { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": True, - "allow_accept": True, - } - elif tool_name == "schedule_meeting": - return { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": True, - "allow_accept": True, - } - elif tool_name == "Question": - return { - "allow_ignore": True, - "allow_respond": True, - "allow_edit": False, # Can't edit questions - "allow_accept": False, # Can't auto-accept questions - } - else: - raise ValueError(f"Invalid tool call: {tool_name}") - - def _handle_response( - self, response: dict, tool_call: dict, runtime: ToolRuntime - ) -> ToolMessage | Command: - """Route response to appropriate handler based on response type. - - Args: - response: Response dict from interrupt - tool_call: Tool call dict - runtime: Tool runtime context - - Returns: - ToolMessage or Command based on response type - """ - response_type = response["type"] - - if response_type == "accept": - return self._handle_accept(tool_call, runtime) - elif response_type == "edit": - return self._handle_edit(response, tool_call, runtime) - elif response_type == "ignore": - return self._handle_ignore(tool_call, runtime) - elif response_type == "response": - return self._handle_response_feedback(response, tool_call, runtime) - else: - raise ValueError(f"Invalid response type: {response_type}") - - def _handle_accept(self, tool_call: dict, _runtime: ToolRuntime) -> ToolMessage: - """Handle accept response - execute tool with original args. - - Args: - tool_call: Tool call dict - _runtime: Tool runtime context (unused, kept for signature compatibility) - - Returns: - ToolMessage with execution result - """ - tool = self.tools_by_name[tool_call["name"]] - observation = tool.invoke(tool_call["args"]) - return ToolMessage(content=observation, tool_call_id=tool_call["id"]) - - def _handle_edit( - self, response: dict, tool_call: dict, runtime: ToolRuntime - ) -> Command: - """Handle edit response - execute with edited args, update AI message, update memory. - - Args: - response: Response dict with edited args - tool_call: Tool call dict - runtime: Tool runtime context - - Returns: - Command with updated AI message and tool result - """ - tool = self.tools_by_name[tool_call["name"]] - tool_name = tool_call["name"] - initial_args = tool_call["args"] - edited_args = response["args"]["args"] - tool_call_id = tool_call["id"] - - # Execute tool with edited args - observation = tool.invoke(edited_args) - - # Update AI message immutably with edited tool calls - ai_message = runtime.state["messages"][-1] - updated_tool_calls = [ - tc if tc["id"] != tool_call_id - else {"type": "tool_call", "name": tool_name, "args": edited_args, "id": tool_call_id} - for tc in ai_message.tool_calls - ] - updated_ai_message = ai_message.model_copy(update={"tool_calls": updated_tool_calls}) - - # Update memory based on tool type - self._update_memory_for_edit(tool_name, initial_args, edited_args, runtime) - - # Return Command with both updated AI message and tool message - return Command( - update={ - "messages": [ - updated_ai_message, - ToolMessage(content=observation, tool_call_id=tool_call_id), - ] - } - ) - - def _handle_ignore(self, tool_call: dict, runtime: ToolRuntime) -> Command: - """Handle ignore response - skip execution, goto END, update triage memory. - - Args: - tool_call: Tool call dict - runtime: Tool runtime context - - Returns: - Command with goto END and feedback message - """ - tool_name = tool_call["name"] - tool_call_id = tool_call["id"] - - # Create feedback message - if tool_name == "write_email": - content = "User ignored this email draft. Ignore this email and end the workflow." - elif tool_name == "schedule_meeting": - content = "User ignored this calendar meeting draft. Ignore this email and end the workflow." - elif tool_name == "Question": - content = "User ignored this question. Ignore this email and end the workflow." - else: - raise ValueError(f"Invalid tool call: {tool_name}") - - # Update triage preferences to avoid future false positives - self._update_triage_preferences_ignore(tool_name, runtime) - - # Return Command with goto END - return Command( - goto=END, - update={ - "messages": [ToolMessage(content=content, tool_call_id=tool_call_id)] - }, - ) - - def _handle_response_feedback( - self, response: dict, tool_call: dict, runtime: ToolRuntime - ) -> ToolMessage: - """Handle response feedback - add feedback to messages, update memory. - - Args: - response: Response dict with user feedback - tool_call: Tool call dict - runtime: Tool runtime context - - Returns: - ToolMessage with user feedback - """ - tool_name = tool_call["name"] - tool_call_id = tool_call["id"] - user_feedback = response["args"] - - # Create feedback message based on tool type - if tool_name == "write_email": - content = f"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}" - elif tool_name == "schedule_meeting": - content = f"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}" - elif tool_name == "Question": - content = f"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}" - else: - raise ValueError(f"Invalid tool call: {tool_name}") - - # Update memory with feedback - self._update_memory_for_feedback(tool_name, user_feedback, runtime) - - return ToolMessage(content=content, tool_call_id=tool_call_id) - - def _update_memory_for_edit( - self, tool_name: str, initial_args: dict, edited_args: dict, _runtime: ToolRuntime - ): - """Update appropriate memory namespace when user edits. - - Args: - tool_name: Name of the tool - initial_args: Original tool arguments - edited_args: Edited tool arguments - _runtime: Tool runtime context (unused, kept for signature compatibility) - """ - if tool_name == "write_email": - namespace = ("email_assistant", "response_preferences") - messages = [ - { - "role": "user", - "content": f"User edited the email response. Here is the initial email generated by the assistant: {initial_args}. Here is the edited email: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - elif tool_name == "schedule_meeting": - namespace = ("email_assistant", "cal_preferences") - messages = [ - { - "role": "user", - "content": f"User edited the calendar invitation. Here is the initial calendar invitation generated by the assistant: {initial_args}. Here is the edited calendar invitation: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - else: - return # No memory update for other tools - - store = self._get_store(_runtime) - update_memory(store, namespace, messages) - - def _update_memory_for_feedback( - self, tool_name: str, user_feedback: str, runtime: ToolRuntime - ): - """Update memory with user feedback. - - Args: - tool_name: Name of the tool - user_feedback: User's feedback text - runtime: Tool runtime context - """ - if tool_name == "write_email": - namespace = ("email_assistant", "response_preferences") - elif tool_name == "schedule_meeting": - namespace = ("email_assistant", "cal_preferences") - else: - return # No memory update for Question feedback - - messages = runtime.state["messages"] + [ - { - "role": "user", - "content": f"User gave feedback: {user_feedback}. Use this to update the preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - - store = self._get_store(runtime) - update_memory(store, namespace, messages) - - def _update_triage_preferences_ignore(self, tool_name: str, runtime: ToolRuntime): - """Update triage preferences when user ignores. - - Args: - tool_name: Name of the tool - runtime: Tool runtime context - """ - namespace = ("email_assistant", "triage_preferences") - - if tool_name == "write_email": - feedback = "The user ignored the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." - elif tool_name == "schedule_meeting": - feedback = "The user ignored the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." - elif tool_name == "Question": - feedback = "The user ignored the Question. That means they did not want to answer the question or deal with this email. Update the triage preferences to ensure emails of this type are not classified as respond." - else: - return - - messages = runtime.state["messages"] + [ - { - "role": "user", - "content": f"{feedback} Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - - store = self._get_store(runtime) - update_memory(store, namespace, messages) - - # Async versions of middleware methods for async invocation (astream, ainvoke) - - async def awrap_model_call( - self, - request: ModelRequest, - handler: Callable[[ModelRequest], ModelResponse], - ) -> ModelResponse: - """Async version of wrap_model_call for async agent invocation. - - Identical logic to wrap_model_call but supports async context. - """ - # Get store (from runtime in deployment, or from self in local testing) - store = self._get_store(request.runtime if hasattr(request, "runtime") else None) - - # Fetch memory from store (using async methods) - triage_prefs = await aget_memory( - store, - ("email_assistant", "triage_preferences"), - default_response_preferences, - ) - response_prefs = await aget_memory( - store, - ("email_assistant", "response_preferences"), - default_response_preferences, - ) - cal_prefs = await aget_memory( - store, - ("email_assistant", "cal_preferences"), - default_cal_preferences, - ) - - # Format system prompt with memory - memory_prompt = agent_system_prompt_hitl_memory.format( - triage_instructions=triage_prefs, - tools_prompt=HITL_MEMORY_TOOLS_PROMPT, - background=default_background, - response_preferences=response_prefs, - cal_preferences=cal_prefs, - ) - - # Append memory prompt to existing system prompt - new_system_prompt = ( - request.system_prompt + "\n\n" + memory_prompt - if request.system_prompt - else memory_prompt - ) - - # Update request with new system prompt - updated_request = request.override(system_prompt=new_system_prompt) - - # Call handler (may or may not be async) - return await handler(updated_request) - - async def awrap_tool_call( - self, - request: ToolCallRequest, - handler: Callable[[ToolCallRequest], ToolMessage | Command], - ) -> ToolMessage | Command: - """Async version of wrap_tool_call for async agent invocation. - - Identical logic to wrap_tool_call but supports async context and uses async store operations. - """ - tool_call = request.tool_call - tool_name = tool_call["name"] - tool = request.tool # Get the tool directly from the request - - # Cache tool for later use - if tool and tool_name not in self.tools_by_name: - self.tools_by_name[tool_name] = tool - - # STEP 1: Filter - Execute non-HITL tools directly without interruption - if tool_name not in self.interrupt_on or not self.interrupt_on[tool_name]: - # Use handler to ensure proper middleware chaining - return await handler(request) - - # STEP 2: Format display - Get email context and format for display - email_input = request.runtime.state.get(self.email_input_key) - description = self._format_interrupt_display(email_input, tool_call) - - # STEP 3: Configure per-tool actions - config = self._get_action_config(tool_name) - - # STEP 4: Create interrupt - interrupt_request = { - "action_request": {"action": tool_name, "args": tool_call["args"]}, - "config": config, - "description": description, - } - - # Call interrupt() to create the interrupt and wait for user decision - # When creating a new interrupt: returns a list of responses [response] - # When resuming from an interrupt: returns a dict mapping interrupt IDs to decisions - result = interrupt([interrupt_request]) - print(f"DEBUG: interrupt() returned: {result}, type: {type(result)}") - - # Handle both new interrupt and resume cases - if isinstance(result, list) and len(result) > 0: - # New interrupt created, got user decision - response = result[0] - elif isinstance(result, dict): - # Check if result is the decision itself (has "type" key) or a mapping of interrupt IDs - if "type" in result: - # Result is the decision dict directly (e.g., {"type": "accept"}) - response = result - else: - # Result is a mapping of interrupt IDs to decisions - # Extract the decision (dict maps interrupt IDs to decisions) - decisions = list(result.values()) - if decisions: - response = decisions[0] - else: - # No decision found, execute tool with original args - tool = self.tools_by_name[tool_name] - observation = tool.invoke(tool_call["args"]) - return ToolMessage(content=observation, tool_call_id=tool_call["id"]) - else: - # Unexpected format, execute tool with original args - tool = self.tools_by_name[tool_name] - observation = tool.invoke(tool_call["args"]) - return ToolMessage(content=observation, tool_call_id=tool_call["id"]) - - # STEP 5: Handle response type (async version with async store operations) - return await self._ahandle_response(response, tool_call, request.runtime) - - async def _ahandle_response( - self, response: dict, tool_call: dict, runtime: ToolRuntime - ) -> ToolMessage | Command: - """Async version of _handle_response for async store operations.""" - response_type = response["type"] - - if response_type == "accept": - return self._handle_accept(tool_call, runtime) - elif response_type == "edit": - return await self._ahandle_edit(response, tool_call, runtime) - elif response_type == "ignore": - return await self._ahandle_ignore(tool_call, runtime) - elif response_type == "response": - return await self._ahandle_response_feedback(response, tool_call, runtime) - else: - raise ValueError(f"Invalid response type: {response_type}") - - async def _ahandle_edit( - self, response: dict, tool_call: dict, runtime: ToolRuntime - ) -> Command: - """Async version of _handle_edit with async memory updates.""" - tool = self.tools_by_name[tool_call["name"]] - tool_name = tool_call["name"] - initial_args = tool_call["args"] - edited_args = response["args"]["args"] - tool_call_id = tool_call["id"] - - # Execute tool with edited args - observation = tool.invoke(edited_args) - - # Update AI message immutably with edited tool calls - ai_message = runtime.state["messages"][-1] - updated_tool_calls = [ - tc if tc["id"] != tool_call_id - else {"type": "tool_call", "name": tool_name, "args": edited_args, "id": tool_call_id} - for tc in ai_message.tool_calls - ] - updated_ai_message = ai_message.model_copy(update={"tool_calls": updated_tool_calls}) - - # Update memory based on tool type (async) - await self._aupdate_memory_for_edit(tool_name, initial_args, edited_args, runtime) - - # Return Command with both updated AI message and tool message - return Command( - update={ - "messages": [ - updated_ai_message, - ToolMessage(content=observation, tool_call_id=tool_call_id), - ] - } - ) - - async def _ahandle_ignore(self, tool_call: dict, runtime: ToolRuntime) -> Command: - """Async version of _handle_ignore with async memory updates.""" - tool_name = tool_call["name"] - tool_call_id = tool_call["id"] - - # Create feedback message - if tool_name == "write_email": - content = "User ignored this email draft. Ignore this email and end the workflow." - elif tool_name == "schedule_meeting": - content = "User ignored this calendar meeting draft. Ignore this email and end the workflow." - elif tool_name == "Question": - content = "User ignored this question. Ignore this email and end the workflow." - else: - raise ValueError(f"Invalid tool call: {tool_name}") - - # Update triage preferences to avoid future false positives (async) - await self._aupdate_triage_preferences_ignore(tool_name, runtime) - - # Return Command with goto END - return Command( - goto=END, - update={ - "messages": [ToolMessage(content=content, tool_call_id=tool_call_id)] - }, - ) - - async def _ahandle_response_feedback( - self, response: dict, tool_call: dict, runtime: ToolRuntime - ) -> ToolMessage: - """Async version of _handle_response_feedback with async memory updates.""" - tool_name = tool_call["name"] - tool_call_id = tool_call["id"] - user_feedback = response["args"] - - # Create feedback message based on tool type - if tool_name == "write_email": - content = f"User gave feedback, which can we incorporate into the email. Feedback: {user_feedback}" - elif tool_name == "schedule_meeting": - content = f"User gave feedback, which can we incorporate into the meeting request. Feedback: {user_feedback}" - elif tool_name == "Question": - content = f"User answered the question, which can we can use for any follow up actions. Feedback: {user_feedback}" - else: - raise ValueError(f"Invalid tool call: {tool_name}") - - # Update memory with feedback (async) - await self._aupdate_memory_for_feedback(tool_name, user_feedback, runtime) - - return ToolMessage(content=content, tool_call_id=tool_call_id) - - async def _aupdate_memory_for_edit( - self, tool_name: str, initial_args: dict, edited_args: dict, runtime: ToolRuntime - ): - """Async version of _update_memory_for_edit.""" - if tool_name == "write_email": - namespace = ("email_assistant", "response_preferences") - messages = [ - { - "role": "user", - "content": f"User edited the email response. Here is the initial email generated by the assistant: {initial_args}. Here is the edited email: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - elif tool_name == "schedule_meeting": - namespace = ("email_assistant", "cal_preferences") - messages = [ - { - "role": "user", - "content": f"User edited the calendar invitation. Here is the initial calendar invitation generated by the assistant: {initial_args}. Here is the edited calendar invitation: {edited_args}. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - else: - return # No memory update for other tools - - store = self._get_store(runtime) - await aupdate_memory(store, namespace, messages) - - async def _aupdate_memory_for_feedback( - self, tool_name: str, user_feedback: str, runtime: ToolRuntime - ): - """Async version of _update_memory_for_feedback.""" - if tool_name == "write_email": - namespace = ("email_assistant", "response_preferences") - elif tool_name == "schedule_meeting": - namespace = ("email_assistant", "cal_preferences") - else: - return # No memory update for Question feedback - - messages = runtime.state["messages"] + [ - { - "role": "user", - "content": f"User gave feedback: {user_feedback}. Use this to update the preferences. Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - - store = self._get_store(runtime) - await aupdate_memory(store, namespace, messages) - - async def _aupdate_triage_preferences_ignore(self, tool_name: str, runtime: ToolRuntime): - """Async version of _update_triage_preferences_ignore.""" - namespace = ("email_assistant", "triage_preferences") - - if tool_name == "write_email": - feedback = "The user ignored the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." - elif tool_name == "schedule_meeting": - feedback = "The user ignored the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." - elif tool_name == "Question": - feedback = "The user ignored the Question. That means they did not want to answer the question or deal with this email. Update the triage preferences to ensure emails of this type are not classified as respond." - else: - return - - messages = runtime.state["messages"] + [ - { - "role": "user", - "content": f"{feedback} Follow all instructions above, and remember: {MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT}.", - } - ] - - store = self._get_store(runtime) - await aupdate_memory(store, namespace, messages) From 771cc5098a5b9e2122a6378e6621b66514142d3d Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Thu, 4 Dec 2025 12:28:49 -0800 Subject: [PATCH 10/11] refactor: consolidate memory system to single unified profile Simplify memory architecture from 3 separate namespaces to 1 unified profile: - Single namespace: ("email_assistant", "user_profile") - Updates on ANY tool rejection (not just write_email/schedule_meeting) - Profile contains: user context, triage criteria, response style, scheduling Changes: - prompts.py: Add default_user_profile (~18 lines), update template to single {user_profile} variable, update MEMORY_UPDATE_INSTRUCTIONS for unified context - email_memory_injection.py: Simplify to 1 fetch instead of 3, update both sync/async methods - email_post_interrupt.py: Expand interrupt_tools to include Question, update namespace to user_profile, add tool-agnostic feedback with section guidance - email_assistant_deepagents.py: Simplify imports and formatting to single variable - README.md: Update Memory System section, trigger matrix, and middleware docs Benefits: - Performance: 1 store fetch vs 3 per model call - Flexibility: ANY tool rejection triggers learning - Maintainability: Single profile easier to understand - Learnability: Background now part of learnable profile --- personal_assistant/README.md | 48 ++++--- .../email_assistant_deepagents.py | 11 +- .../middleware/email_memory_injection.py | 61 ++------ .../middleware/email_post_interrupt.py | 65 +++++++-- .../src/personal_assistant/prompts.py | 131 +++++++++++------- 5 files changed, 177 insertions(+), 139 deletions(-) diff --git a/personal_assistant/README.md b/personal_assistant/README.md index 83b714f..3d34994 100644 --- a/personal_assistant/README.md +++ b/personal_assistant/README.md @@ -109,8 +109,8 @@ When the email assistant interrupts (for `write_email`, `schedule_meeting`, or ` **Memory Learning:** -UI responses automatically update the assistant's memory profiles: -- **Reject** `write_email` or `schedule_meeting` → Updates `triage_preferences` (classification rules) using optional rejection message for better context +UI responses automatically update the assistant's unified user profile: +- **Reject** ANY tool (`write_email`, `schedule_meeting`, `Question`) → Updates `user_profile` using optional rejection message for better context - **Approve** → No memory update (agent did the right thing) See the [Memory System](#memory-system) section below for detailed logic and examples. @@ -220,34 +220,44 @@ interrupt_on_config = { The assistant maintains a persistent memory profile that learns from user interactions during HITL interrupts. The profile is stored in a LangGraph Store namespace and automatically updates based on user decisions. -#### Memory Namespaces +#### Memory Profile -**1. `("email_assistant", "triage_preferences")`** - Email Classification Rules -- **Purpose**: Learns when to respond vs. notify vs. ignore emails -- **Updated by**: REJECT decisions on `write_email` or `schedule_meeting` -- **Update logic**: When user rejects a draft, it means the email shouldn't have been classified as "respond" -- **Example**: "Emails from newsletter@company.com should be ignored, not responded to" +**Single namespace:** `("email_assistant", "user_profile")` + +The assistant maintains a unified user profile containing: +1. **User context**: Identity and role information +2. **Triage criteria**: When to respond/notify/ignore emails +3. **Response style**: How to draft emails and handle different scenarios +4. **Meeting scheduling**: Calendar and scheduling preferences + +**Storage:** +- Profile size: 15-25 lines of moderate detail +- Updated by: ANY tool rejection (not just specific tools) +- Update logic: LLM analyzes rejection and updates relevant profile section +- Preserves unrelated information during updates #### Memory Update Trigger Matrix -| User Decision | Tool Call | Memory Namespace Updated | Update Reason | -|---------------|-----------|--------------------------|---------------| -| **REJECT** | `write_email` | `triage_preferences` | Email shouldn't have been classified as "respond" | -| **REJECT** | `schedule_meeting` | `triage_preferences` | Meeting request shouldn't have been classified as "respond" | +| User Decision | Tool Call | Profile Section Updated | Update Reason | +|---------------|-----------|-------------------------|---------------| +| **REJECT** | `write_email` | Triage and/or response style | Email shouldn't have been drafted or style was wrong | +| **REJECT** | `schedule_meeting` | Triage and/or scheduling | Meeting shouldn't have been scheduled or timing was wrong | +| **REJECT** | `Question` | Triage and/or response style | Question shouldn't have been asked | | **APPROVE** | any tool | _(none)_ | No update needed - agent did the right thing | #### How Memory Updates Work **Memory Injection** (`MemoryInjectionMiddleware`): - Runs **before each LLM call** via `wrap_model_call()` -- Fetches memory profile from the store -- Injects it into the system prompt using template variables -- Agent sees current preferences on every turn +- Fetches unified user profile from single namespace +- Injects it into the system prompt's `< User Profile >` section +- Agent sees current profile on every turn **Memory Update Detection** (`PostInterruptMemoryMiddleware`): - **Before model generation** (`before_model`): Detects ToolMessages with `status="error"` (rejections) -- **REJECT detection**: Extracts optional rejection message from ToolMessage content and updates triage preferences with user's feedback for better learning -- **Agent behavior**: Agent is instructed to call Done tool immediately after receiving a rejection to end the workflow +- **ANY tool rejection**: Extracts optional rejection message and updates user profile +- **Update logic**: Tool-specific feedback guides LLM on which profile section to update +- **Agent behavior**: Agent is instructed to call Done tool immediately after receiving any rejection **Memory Update Process**: 1. Build prompt with current memory profile + user's feedback (reject reason) @@ -295,8 +305,8 @@ examples/personal_assistant/ │ ├── middleware/ │ ├── __init__.py - │ ├── email_memory_injection.py # Memory injection into system prompts - │ ├── email_post_interrupt.py # Post-interrupt memory updates + │ ├── email_memory_injection.py # Memory injection from unified profile + │ ├── email_post_interrupt.py # Profile updates on ANY tool rejection │ └── email_genui.py # GenUI integration for tool visualization │ └── tools/ diff --git a/personal_assistant/src/personal_assistant/email_assistant_deepagents.py b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py index d65c73c..5c58cae 100644 --- a/personal_assistant/src/personal_assistant/email_assistant_deepagents.py +++ b/personal_assistant/src/personal_assistant/email_assistant_deepagents.py @@ -21,7 +21,7 @@ from .schemas import EmailAssistantState from .tools import get_tools from .utils import format_email_markdown, parse_email, get_memory -from .prompts import agent_system_prompt_hitl_memory, default_background, default_response_preferences, default_cal_preferences, default_triage_instructions +from .prompts import agent_system_prompt_hitl_memory, default_user_profile def create_email_assistant(for_deployment=False): """Create and configure the email assistant agent. @@ -89,15 +89,12 @@ def create_email_assistant(for_deployment=False): "Question": {"component_name": "question"}, }) - # Build system prompt with default preferences - # Note: Memory-based preferences can be accessed via the store in middleware + # Build system prompt with default user profile + # Note: Memory-based profile can be accessed via the store in middleware tools_prompt = "\n".join([f"- {tool.name}: {tool.description}" for tool in tools]) system_prompt = agent_system_prompt_hitl_memory.format( tools_prompt=tools_prompt, - background=default_background, - triage_instructions=default_triage_instructions, - response_preferences=default_response_preferences, - cal_preferences=default_cal_preferences, + user_profile=default_user_profile, ) # Create agent with deepagents library diff --git a/personal_assistant/src/personal_assistant/middleware/email_memory_injection.py b/personal_assistant/src/personal_assistant/middleware/email_memory_injection.py index 509fb75..9750821 100644 --- a/personal_assistant/src/personal_assistant/middleware/email_memory_injection.py +++ b/personal_assistant/src/personal_assistant/middleware/email_memory_injection.py @@ -7,21 +7,17 @@ from ..prompts import ( agent_system_prompt_hitl_memory, - default_background, - default_cal_preferences, - default_response_preferences, - default_triage_instructions, + default_user_profile, ) from ..tools.default.prompt_templates import HITL_MEMORY_TOOLS_PROMPT from ..utils import aget_memory, get_memory class MemoryInjectionMiddleware(AgentMiddleware): - """Middleware for injecting memory preferences into system prompts. + """Middleware for injecting user profile memory into system prompts. - This middleware fetches memory from three namespaces (triage_preferences, - response_preferences, cal_preferences) and injects them into the system prompt - before each LLM call. + This middleware fetches the unified user profile from a single namespace + (user_profile) and injects it into the system prompt before each LLM call. Args: store: LangGraph store for persistent memory @@ -53,36 +49,22 @@ def wrap_model_call( ) -> ModelResponse: """Inject memory into system prompt before LLM call. - Fetches memory from the three namespaces (triage_preferences, response_preferences, - cal_preferences) and injects them into the system prompt. + Fetches unified user profile from single namespace and injects into system prompt. """ # Get store (from runtime in deployment, or from self in local testing) store = self._get_store(request.runtime if hasattr(request, "runtime") else None) - # Fetch memory from store - triage_prefs = get_memory( + # Fetch user profile from store + user_profile = get_memory( store, - ("email_assistant", "triage_preferences"), - default_triage_instructions, - ) - response_prefs = get_memory( - store, - ("email_assistant", "response_preferences"), - default_response_preferences, - ) - cal_prefs = get_memory( - store, - ("email_assistant", "cal_preferences"), - default_cal_preferences, + ("email_assistant", "user_profile"), + default_user_profile, ) # Format system prompt with memory memory_prompt = agent_system_prompt_hitl_memory.format( tools_prompt=HITL_MEMORY_TOOLS_PROMPT, - background=default_background, - triage_instructions=triage_prefs, - response_preferences=response_prefs, - cal_preferences=cal_prefs, + user_profile=user_profile, ) # Append memory prompt to existing system prompt @@ -109,30 +91,17 @@ async def awrap_model_call( # Get store (from runtime in deployment, or from self in local testing) store = self._get_store(request.runtime if hasattr(request, "runtime") else None) - # Fetch memory from store (using async methods) - triage_prefs = await aget_memory( - store, - ("email_assistant", "triage_preferences"), - default_triage_instructions, - ) - response_prefs = await aget_memory( - store, - ("email_assistant", "response_preferences"), - default_response_preferences, - ) - cal_prefs = await aget_memory( + # Fetch user profile from store (using async methods) + user_profile = await aget_memory( store, - ("email_assistant", "cal_preferences"), - default_cal_preferences, + ("email_assistant", "user_profile"), + default_user_profile, ) # Format system prompt with memory memory_prompt = agent_system_prompt_hitl_memory.format( tools_prompt=HITL_MEMORY_TOOLS_PROMPT, - background=default_background, - triage_instructions=triage_prefs, - response_preferences=response_prefs, - cal_preferences=cal_prefs, + user_profile=user_profile, ) # Append memory prompt to existing system prompt diff --git a/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py b/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py index c3fa2af..c0b2c18 100644 --- a/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py +++ b/personal_assistant/src/personal_assistant/middleware/email_post_interrupt.py @@ -9,14 +9,15 @@ class PostInterruptMemoryMiddleware(AgentMiddleware): - """Middleware for updating memory when user rejects tool calls. + """Middleware for updating memory when user rejects ANY tool call. This middleware detects when tool calls are rejected (never executed) and - updates triage preferences to learn which emails should not trigger responses. + updates the user profile to learn from the rejection feedback. Memory updates: - - reject write_email: Updates triage_preferences - - reject schedule_meeting: Updates triage_preferences + - reject write_email: Updates user_profile (triage and/or response style) + - reject schedule_meeting: Updates user_profile (triage and/or scheduling) + - reject Question: Updates user_profile (triage and/or response style) - approve: No memory update Args: @@ -29,8 +30,8 @@ class PostInterruptMemoryMiddleware(AgentMiddleware): def __init__(self, store: BaseStore): self.store = store - # Track which tools should trigger memory updates on reject - self.interrupt_tools = {"write_email", "schedule_meeting"} + # Track ALL interrupt tools (not just specific ones) + self.interrupt_tools = {"write_email", "schedule_meeting", "Question"} # Track which tool calls we've already processed (to avoid duplicates) self._processed_tool_calls = set() @@ -133,7 +134,7 @@ def after_tool(self, state, runtime) -> dict | None: def _update_memory_for_reject( self, tool_name: str, original_args: dict, rejection_message: str, state, runtime ): - """Update triage preferences when user rejects a tool call. + """Update user profile when user rejects a tool call. Args: tool_name: Name of the tool that was rejected @@ -142,15 +143,32 @@ def _update_memory_for_reject( state: Agent state containing messages runtime: Runtime context """ - namespace = ("email_assistant", "triage_preferences") + namespace = ("email_assistant", "user_profile") # Create feedback based on tool type if tool_name == "write_email": - feedback = "The user rejected the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." + feedback = ( + "The user rejected the email draft, meaning they did not want to respond to this email. " + "Update the User Profile to refine triage criteria (which emails warrant responses) and/or " + "response style preferences based on what was wrong with the draft." + ) elif tool_name == "schedule_meeting": - feedback = "The user rejected the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." + feedback = ( + "The user rejected the meeting invitation, meaning they did not want to schedule this meeting. " + "Update the User Profile to refine triage criteria (which meeting requests warrant scheduling) and/or " + "meeting scheduling preferences based on what was wrong with the invitation." + ) + elif tool_name == "Question": + feedback = ( + "The user rejected the clarification question, meaning they did not want to ask this question. " + "Update the User Profile to refine when questions should be asked or how they should be phrased." + ) else: - return # No memory update for other tools + # Generic fallback for any future interrupt tools + feedback = ( + f"The user rejected the {tool_name} tool call. " + "Update the User Profile to learn from this feedback and avoid similar rejections in the future." + ) # Get conversation context for memory update from state messages = state.get("messages", []) @@ -221,15 +239,32 @@ async def _aupdate_memory_for_reject( state: Agent state containing messages runtime: Runtime context """ - namespace = ("email_assistant", "triage_preferences") + namespace = ("email_assistant", "user_profile") # Create feedback based on tool type if tool_name == "write_email": - feedback = "The user rejected the email draft. That means they did not want to respond to the email. Update the triage preferences to ensure emails of this type are not classified as respond." + feedback = ( + "The user rejected the email draft, meaning they did not want to respond to this email. " + "Update the User Profile to refine triage criteria (which emails warrant responses) and/or " + "response style preferences based on what was wrong with the draft." + ) elif tool_name == "schedule_meeting": - feedback = "The user rejected the calendar meeting draft. That means they did not want to schedule a meeting for this email. Update the triage preferences to ensure emails of this type are not classified as respond." + feedback = ( + "The user rejected the meeting invitation, meaning they did not want to schedule this meeting. " + "Update the User Profile to refine triage criteria (which meeting requests warrant scheduling) and/or " + "meeting scheduling preferences based on what was wrong with the invitation." + ) + elif tool_name == "Question": + feedback = ( + "The user rejected the clarification question, meaning they did not want to ask this question. " + "Update the User Profile to refine when questions should be asked or how they should be phrased." + ) else: - return # No memory update for other tools + # Generic fallback for any future interrupt tools + feedback = ( + f"The user rejected the {tool_name} tool call. " + "Update the User Profile to learn from this feedback and avoid similar rejections in the future." + ) # Get conversation context for memory update from state messages = state.get("messages", []) diff --git a/personal_assistant/src/personal_assistant/prompts.py b/personal_assistant/src/personal_assistant/prompts.py index c48e060..be77158 100644 --- a/personal_assistant/src/personal_assistant/prompts.py +++ b/personal_assistant/src/personal_assistant/prompts.py @@ -34,27 +34,34 @@ (Today's date is """ + datetime.now().strftime("%Y-%m-%d") + """) - For responding to emails, draft a response using write_email - If you scheduled a meeting, send a short confirmation email using write_email -- CRITICAL: If the user rejects your tool call (write_email or schedule_meeting), you will receive a ToolMessage with status="error" containing their feedback. When this happens, call the Done tool immediately to end the workflow - do NOT retry or generate new drafts +- CRITICAL: If the user rejects ANY tool call, you will receive a ToolMessage with status="error" containing their feedback. When this happens, call the Done tool immediately to end the workflow - do NOT retry or generate new drafts - After sending the email, call the Done tool -< Background > -{background} - - -< Triage Rules > -{triage_instructions} - - -< Response Preferences > -{response_preferences} - - -< Calendar Preferences > -{cal_preferences} - +< User Profile > +{user_profile} + """ +# Consolidated user profile (replaces default_background, default_triage_instructions, default_response_preferences, default_cal_preferences) +default_user_profile = """I'm Lance, a software engineer at LangChain. When triaging emails: +- Respond to: Direct questions about technical work, meeting requests, project inquiries, family/self-care reminders +- Notify: GitHub notifications, deadline reminders, FYI project updates, team status messages +- Ignore: Marketing newsletters, promotional emails, CC'd FYI threads + +Response style: +- Professional and concise tone +- Acknowledge deadlines explicitly in responses +- For technical questions: State investigation approach and timeline +- For event invitations: Ask about workshops, discounts, and deadlines; don't commit immediately +- For collaboration requests: Acknowledge existing materials, mention reviewing them + +Meeting scheduling: +- Prefer 30-minute meetings (15 minutes acceptable) +- If times proposed: Verify all options, commit to one or decline +- If no times proposed: Offer multiple options instead of picking one +- Reference meeting purpose and duration in response""" + # Default background information default_background = """ I'm Lance, a software engineer at LangChain. @@ -121,68 +128,88 @@ MEMORY_UPDATE_INSTRUCTIONS = """ # Role and Objective -You are a memory profile manager for an email assistant agent that selectively updates user preferences based on edits made during human-in-the-loop interactions. +You are a memory profile manager for an email assistant agent that selectively updates the user's profile based on feedback from human-in-the-loop interactions. # Context -When users edit tool calls (emails or calendar invitations), you receive: -- The ORIGINAL tool call generated by the assistant -- The EDITED tool call after the user made changes +When users reject tool calls (emails, calendar invitations, questions), you receive: +- The CURRENT user profile +- The REJECTED tool call and its arguments +- Optional user feedback explaining why they rejected -Your job is to learn from these edits and update the user's preference profile. +Your job is to learn from these rejections and update the user's profile. # Instructions -- NEVER overwrite the entire memory profile -- ONLY make targeted additions of new information based on the edits -- ONLY update specific facts that are directly contradicted by the edits +- NEVER overwrite the entire user profile +- ONLY make targeted additions of new information based on the rejection +- ONLY update specific facts that are directly contradicted by the feedback - PRESERVE all other existing information in the profile -- Format the profile consistently with the original style +- Keep the profile structure consistent (triage criteria, response style, scheduling preferences) - Generate the profile as a string +- Target length: 15-25 lines of moderate detail + +# Profile Structure +The user profile contains three main sections: +1. **Triage criteria**: When to respond/notify/ignore emails +2. **Response style**: How to draft emails and handle different scenarios +3. **Meeting scheduling**: Calendar and scheduling preferences + +Focus your updates on the section most relevant to the rejected tool call. # Reasoning Steps -1. Analyze the current memory profile structure and content -2. Compare the ORIGINAL tool call with the EDITED tool call -3. Identify what the user changed (subject lines, tone, content, timing, etc.) -4. Extract the underlying preference from the change -5. Add or update the relevant preference in the profile +1. Analyze the current user profile structure and content +2. Identify which tool was rejected and why (from feedback) +3. Determine which profile section(s) need updating (triage, response, or scheduling) +4. Extract the underlying preference from the rejection +5. Add or update the relevant preference in the appropriate section 6. Preserve all other existing information -7. Output the complete updated profile +7. Ensure the updated profile remains concise (15-25 lines) +8. Output the complete updated profile # Example - -Email responses should be: -- Professional and concise -- Include acknowledgment of deadlines - + +I'm Lance, a software engineer at LangChain. When triaging emails: +- Respond to: Direct questions about technical work, meeting requests +- Notify: GitHub notifications, deadline reminders +- Ignore: Marketing newsletters + +Response style: +- Professional and concise tone +- Acknowledge deadlines explicitly + - -{{"to": "sarah@example.com", "subject": "Re: Question", "content": "Thanks for reaching out. I'll look into this and get back to you soon."}} - + +write_email(to="newsletter@company.com", subject="Re: Monthly Update", content="Thanks for the update...") + - -{{"to": "sarah@example.com", "subject": "Re: Question about deployment", "content": "Thanks for reaching out. I'll investigate the deployment issue and get back to you by end of day tomorrow."}} - + +This is just a company newsletter, I don't need to respond to these. + -Email responses should be: -- Professional and concise -- Include acknowledgment of deadlines -- Include specific timelines for follow-up -- Repeat key details from the original email in the subject line +I'm Lance, a software engineer at LangChain. When triaging emails: +- Respond to: Direct questions about technical work, meeting requests +- Notify: GitHub notifications, deadline reminders, company newsletters +- Ignore: Marketing newsletters, external promotional emails + +Response style: +- Professional and concise tone +- Acknowledge deadlines explicitly -# Process current profile for {namespace} - +# Process current profile + {current_profile} - + -The original and edited tool calls will be provided in the user message. Think step by step about what the user changed and what preference this reveals. Update the memory profile to reflect this preference while preserving all other existing information.""" +The rejected tool call and user feedback will be provided in the user message. Think step by step about what the user rejected and what preference this reveals. Update the user profile to reflect this preference while preserving all other existing information.""" MEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT = """ Remember: -- NEVER overwrite the entire memory profile +- NEVER overwrite the entire user profile - ONLY make targeted additions of new information - ONLY update specific facts that are directly contradicted by feedback messages - PRESERVE all other existing information in the profile - Format the profile consistently with the original style - Generate the profile as a string +- Target length: 15-25 lines of moderate detail """ \ No newline at end of file From f4e19233b170d777fcc28163b2cc394207838e03 Mon Sep 17 00:00:00 2001 From: Lance Martin Date: Thu, 4 Dec 2025 14:11:43 -0800 Subject: [PATCH 11/11] test: update notebook for unified memory system with rejection testing Update test.ipynb to work with simplified memory architecture: - Fix interrupt display format for built-in interrupt_on system (action_requests instead of action_request structure) - Add resume_with_approve_or_reject() helper for new approve/reject decisions - Add get_user_profile_from_store() helper to inspect unified profile - Remove deprecated resume_with_decision() (old 4-decision system) - Update all example cells to use new interrupt format - Add comprehensive memory testing workflow (Steps 1-5): * Check initial profile * Send newsletter email and reject with feedback * Verify profile updates with rejection context * Test rejecting Question tool (ANY tool rejection triggers learning) * Display final profile showing cumulative learning - Add verification steps to confirm rejection messages captured correctly The notebook now demonstrates the single unified profile approach with memory learning from ANY tool rejection (write_email, schedule_meeting, Question). --- personal_assistant/test.ipynb | 1651 +++++++++++++++++++-------------- 1 file changed, 972 insertions(+), 679 deletions(-) diff --git a/personal_assistant/test.ipynb b/personal_assistant/test.ipynb index f49127b..57314b5 100644 --- a/personal_assistant/test.ipynb +++ b/personal_assistant/test.ipynb @@ -12,11 +12,14 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 36, "id": "imports", "metadata": {}, "outputs": [], - "source": "from personal_assistant import create_email_assistant\nfrom personal_assistant.ntbk_utils import format_messages, show_prompt" + "source": [ + "from personal_assistant import create_email_assistant\n", + "from personal_assistant.ntbk_utils import format_messages, show_prompt" + ] }, { "cell_type": "markdown", @@ -30,7 +33,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 37, "id": "create_agent", "metadata": {}, "outputs": [ @@ -60,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 38, "id": "0qvtwkqfbazq", "metadata": {}, "outputs": [ @@ -68,72 +71,242 @@ "name": "stdout", "output_type": "stream", "text": [ - "✓ Helper function loaded\n" + "✓ Helper functions loaded\n" ] } ], "source": [ - "# Helper function to resume with a decision\n", + "# Helper functions for testing\n", "from langgraph.types import Command\n", "\n", - "def resume_with_decision(agent, config, decision_type=\"accept\", edited_args=None, feedback=None):\n", - " \"\"\"Resume the agent after an interrupt with a user decision.\n", + "def resume_with_approve_or_reject(agent, config, decision_type=\"approve\", message=None):\n", + " \"\"\"Resume agent with approve or reject decision.\n", " \n", " Args:\n", " agent: The agent instance\n", " config: Config with thread_id\n", - " decision_type: \"accept\", \"edit\", \"ignore\", or \"response\"\n", - " edited_args: For \"edit\" - the modified arguments\n", - " feedback: For \"response\" - user feedback text\n", + " decision_type: \"approve\" or \"reject\"\n", + " message: Optional rejection message with feedback\n", " \"\"\"\n", - " if decision_type == \"accept\":\n", - " decision = {\"type\": \"accept\"}\n", - " elif decision_type == \"edit\":\n", - " decision = {\"type\": \"edit\", \"args\": {\"args\": edited_args}}\n", - " elif decision_type == \"ignore\":\n", - " decision = {\"type\": \"ignore\"}\n", - " elif decision_type == \"response\":\n", - " decision = {\"type\": \"response\", \"args\": feedback}\n", + " if decision_type == \"approve\":\n", + " decision = {\"type\": \"approve\"}\n", + " elif decision_type == \"reject\":\n", + " # Include optional rejection message\n", + " decision = {\"type\": \"reject\", \"message\": message if message else \"User rejected this action\"}\n", " else:\n", " raise ValueError(f\"Invalid decision type: {decision_type}\")\n", " \n", - " # Resume with the decision\n", + " # Resume with decisions wrapped in dict (required by built-in HITL middleware)\n", " result = agent.invoke(\n", - " Command(resume=[decision]),\n", + " Command(resume={\"decisions\": [decision]}),\n", " config=config,\n", " )\n", " return result\n", + "\n", + "def get_user_profile_from_store(agent):\n", + " \"\"\"Get the current user profile from the agent's store.\"\"\"\n", + " # Access the store from the agent's compiled graph\n", + " store = agent.store\n", + " namespace = (\"email_assistant\", \"user_profile\")\n", + " key = \"user_preferences\"\n", + " \n", + " # Get the item from store\n", + " item = store.get(namespace, key)\n", + " if item:\n", + " return item.value\n", + " return None\n", " \n", - "print(\"✓ Helper function loaded\")" + "print(\"✓ Helper functions loaded\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 39, "id": "example1_email", "metadata": {}, - "outputs": [], - "source": "# Example email as markdown string\nemail_markdown = \"\"\"\n**Subject**: Quick question about next week\n**From**: jane@example.com\n**To**: lance@langchain.dev\n\nHi Lance,\n\nCan we meet next Tuesday at 2pm to discuss the project roadmap?\n\nBest,\nJane\n\n---\n\"\"\"\n\n# Display email with rich formatting\nshow_prompt(email_markdown, title=\"📧 Email to Process\", border_style=\"cyan\")" + "outputs": [ + { + "data": { + "text/html": [ + "
╭────────────────────────────────────────────── 📧 Email to Process ──────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "  **Subject**: Quick question about next week                                                                    \n",
+       "  **From**: jane@example.com                                                                                     \n",
+       "  **To**: lance@langchain.dev                                                                                    \n",
+       "                                                                                                                 \n",
+       "  Hi Lance,                                                                                                      \n",
+       "                                                                                                                 \n",
+       "  Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                \n",
+       "                                                                                                                 \n",
+       "  Best,                                                                                                          \n",
+       "  Jane                                                                                                           \n",
+       "                                                                                                                 \n",
+       "  ---                                                                                                            \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Email to Process\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **Subject**: Quick question about next week \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **From**: jane@example.com \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **To**: lance@langchain.dev \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Hi Lance, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Best, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Jane \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m --- \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Example email as markdown string\n", + "email_markdown = \"\"\"\n", + "**Subject**: Quick question about next week\n", + "**From**: jane@example.com\n", + "**To**: lance@langchain.dev\n", + "\n", + "Hi Lance,\n", + "\n", + "Can we meet next Tuesday at 2pm to discuss the project roadmap?\n", + "\n", + "Best,\n", + "Jane\n", + "\n", + "---\n", + "\"\"\"\n", + "\n", + "# Display email with rich formatting\n", + "show_prompt(email_markdown, title=\"📧 Email to Process\", border_style=\"cyan\")" + ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 40, "id": "example1_invoke", "metadata": {}, - "outputs": [], - "source": "# Configure thread\nconfig_1 = {\"configurable\": {\"thread_id\": \"test-thread-1\"}}\n\n# Invoke agent with markdown email string\nresult_1 = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown}]},\n config=config_1,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_1:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_1[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_1)\")\nelse:\n format_messages(result_1[\"messages\"])" + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "Agent Response\n", + "============================================================\n", + "\n", + "🛑 INTERRUPT: Agent is waiting for your approval\n", + "\n", + "Action: schedule_meeting\n", + "Args: {'attendees': ['jane@example.com'], 'subject': 'Project Roadmap Discussion', 'duration_minutes': 30, 'preferred_day': '2025-12-10T14:00:00', 'start_time': 14}\n", + "\n", + "Description:\n" + ] + }, + { + "data": { + "text/html": [ + "
╭─────────────────────────────────────────────── 📋 Action Details ───────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "  I've prepared a calendar invitation. Please review the meeting details below. Approve to send the invite       \n",
+       "  as-is, or Reject to cancel and end the workflow.                                                               \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[33m╭─\u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m \u001b[0m\u001b[1;32m📋 Action Details\u001b[0m\u001b[33m \u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m I've prepared a calendar invitation. Please review the meeting details below. Approve to send the invite \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m as-is, or Reject to cancel and end the workflow. \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------------------------------------------------------------\n", + "\n", + "Allowed decisions: ['approve', 'reject']\n", + "\n", + "💡 To continue, you need to resume the agent with a decision.\n", + " Use: resume_with_approve_or_reject(agent, config_1, decision_type='approve')\n" + ] + } + ], + "source": [ + "# Configure thread\n", + "config_1 = {\"configurable\": {\"thread_id\": \"test-thread-1\"}}\n", + "\n", + "# Invoke agent with markdown email string\n", + "result_1 = agent.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": email_markdown}]},\n", + " config=config_1,\n", + ")\n", + "\n", + "# Display result with rich formatting\n", + "print(\"\\n\" + \"=\"*60)\n", + "print(\"Agent Response\")\n", + "print(\"=\"*60 + \"\\n\")\n", + "\n", + "# Check for interrupts (HITL)\n", + "if \"__interrupt__\" in result_1:\n", + " print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n", + " \n", + " for interrupt in result_1[\"__interrupt__\"]:\n", + " # New interrupt format with action_requests\n", + " action_requests = interrupt.value.get('action_requests', [])\n", + " review_configs = interrupt.value.get('review_configs', [])\n", + " \n", + " for action_req in action_requests:\n", + " print(\"Action:\", action_req['name'])\n", + " print(\"Args:\", action_req['args'])\n", + " print(\"\\nDescription:\")\n", + " show_prompt(action_req.get('description', ''), title=\"📋 Action Details\", border_style=\"yellow\")\n", + " print(\"\\n\" + \"-\"*60)\n", + " \n", + " if review_configs:\n", + " print(\"\\nAllowed decisions:\", review_configs[0].get('allowed_decisions', []))\n", + " \n", + " print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n", + " print(\" Use: resume_with_approve_or_reject(agent, config_1, decision_type='approve')\")\n", + "else:\n", + " format_messages(result_1[\"messages\"])" + ] }, { "cell_type": "markdown", "id": "15f8dc1d-8a22-46ad-8ee6-0d353936b514", "metadata": {}, "source": [ - "### Example: Resume by accepting the action" + "### Example: Resume by approving the action\n", + "\n", + "**Note:** The new interrupt system uses `resume_with_approve_or_reject()` with \"approve\" or \"reject\" decisions only." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 41, "id": "63fd68c2-b086-49b5-84fb-67b8a3e3e4dc", "metadata": {}, "outputs": [ @@ -142,7 +315,6 @@ "text/html": [ "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
        "                                                                                                                 \n",
-       "                                                                                                                 \n",
        " **Subject**: Quick question about next week                                                                     \n",
        " **From**: jane@example.com                                                                                      \n",
        " **To**: lance@langchain.dev                                                                                     \n",
@@ -162,7 +334,6 @@
       "text/plain": [
        "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n",
        "\u001b[34m│\u001b[0m                                                                                                                 \u001b[34m│\u001b[0m\n",
-       "\u001b[34m│\u001b[0m                                                                                                                 \u001b[34m│\u001b[0m\n",
        "\u001b[34m│\u001b[0m **Subject**: Quick question about next week                                                                     \u001b[34m│\u001b[0m\n",
        "\u001b[34m│\u001b[0m **From**: jane@example.com                                                                                      \u001b[34m│\u001b[0m\n",
        "\u001b[34m│\u001b[0m **To**: lance@langchain.dev                                                                                     \u001b[34m│\u001b[0m\n",
@@ -186,85 +357,31 @@
      "data": {
       "text/html": [
        "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
-       "                                                                                                                 \n",
-       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
-       "    Args: {                                                                                                      \n",
-       "   \"day\": \"2025-12-09\"                                                                                           \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_01HLFiUonMHpa96sjmnVdTam                                                                           \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01HLFiUonMHpa96sjmnVdTam \u001b[37m│\u001b[0m\n", - "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", - "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to    \n",
-       " Jane.                                                                                                           \n",
        "                                                                                                                 \n",
-       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       " 🔧 Tool Call: triage_email                                                                                      \n",
        "    Args: {                                                                                                      \n",
-       "   \"attendees\": [                                                                                                \n",
-       "     \"jane@example.com\"                                                                                          \n",
-       "   ],                                                                                                            \n",
-       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
-       "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
-       "   \"start_time\": 14                                                                                              \n",
+       "   \"reasoning\": \"This email is from Jane asking to meet next Tuesday at 2pm to discuss the project roadmap. This \n",
+       " is a direct meeting request with a specific time proposed. According to the user profile, meeting requests      \n",
+       " should be classified as 'respond'. The email requires a response to either confirm the meeting time or propose  \n",
+       " alternatives.\",                                                                                                 \n",
+       "   \"classification\": \"respond\"                                                                                   \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA                                                                           \n",
+       "    ID: toolu_01BkfgaUTdJgcpawMwqTMeYr                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Jane. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: triage_email \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"reasoning\": \"This email is from Jane asking to meet next Tuesday at 2pm to discuss the project roadmap. This \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m is a direct meeting request with a specific time proposed. According to the user profile, meeting requests \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m should be classified as 'respond'. The email requires a response to either confirm the meeting time or propose \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m alternatives.\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"classification\": \"respond\" \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01BkfgaUTdJgcpawMwqTMeYr \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -275,89 +392,48 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in       \n",
-       " before it could be completed.                                                                                   \n",
+       " Classification Decision: respond. Reasoning: This email is from Jane asking to meet next Tuesday at 2pm to      \n",
+       " discuss the project roadmap. This is a direct meeting request with a specific time proposed. According to the   \n",
+       " user profile, meeting requests should be classified as 'respond'. The email requires a response to either       \n",
+       " confirm the meeting time or propose alternatives.                                                               \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m Classification Decision: respond. Reasoning: This email is from Jane asking to meet next Tuesday at 2pm to \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m discuss the project roadmap. This is a direct meeting request with a specific time proposed. According to the \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m user profile, meeting requests should be classified as 'respond'. The email requires a response to either \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m confirm the meeting time or propose alternatives. \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, - { - "data": { - "text/html": [ - "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       " **Subject**: Quick question about next week                                                                     \n",
-       " **From**: jane@example.com                                                                                      \n",
-       " **To**: lance@langchain.dev                                                                                     \n",
-       "                                                                                                                 \n",
-       " Hi Lance,                                                                                                       \n",
-       "                                                                                                                 \n",
-       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
-       "                                                                                                                 \n",
-       " Best,                                                                                                           \n",
-       " Jane                                                                                                            \n",
-       "                                                                                                                 \n",
-       " ---                                                                                                             \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       " Now I need to check calendar availability for next Tuesday (December 10, 2025) to see if 2pm is available.      \n",
        "                                                                                                                 \n",
        " 🔧 Tool Call: check_calendar_availability                                                                       \n",
        "    Args: {                                                                                                      \n",
-       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       "   \"day\": \"2025-12-10\"                                                                                           \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01We2XxG49E7jtJZhPJiVBGU                                                                           \n",
+       "    ID: toolu_01WNeCsBFLNnK34k7A5QrtNv                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Now I need to check calendar availability for next Tuesday (December 10, 2025) to see if 2pm is available. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-10\" \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01We2XxG49E7jtJZhPJiVBGU \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01WNeCsBFLNnK34k7A5QrtNv \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -368,13 +444,13 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       " Available times on 2025-12-10: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-10: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -385,7 +461,8 @@ "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
+       " Great! 2pm is available on Tuesday, December 10th. Now I'll schedule the meeting for 30 minutes (as per the     \n",
+       " user profile preference).                                                                                       \n",
        "                                                                                                                 \n",
        " 🔧 Tool Call: schedule_meeting                                                                                  \n",
        "    Args: {                                                                                                      \n",
@@ -394,16 +471,17 @@
        "   ],                                                                                                            \n",
        "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
        "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
+       "   \"preferred_day\": \"2025-12-10T14:00:00\",                                                                       \n",
        "   \"start_time\": 14                                                                                              \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST                                                                           \n",
+       "    ID: toolu_01AsrXaBC6K3At2YNzBJ7wSg                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Great! 2pm is available on Tuesday, December 10th. Now I'll schedule the meeting for 30 minutes (as per the \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m user profile preference). \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", @@ -412,10 +490,10 @@ "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-10T14:00:00\", \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01AsrXaBC6K3At2YNzBJ7wSg \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -426,14 +504,14 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1        \n",
+       " Meeting 'Project Roadmap Discussion' scheduled on Wednesday, December 10, 2025 at 14 for 30 minutes with 1      \n",
        " attendees                                                                                                       \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1 \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion' scheduled on Wednesday, December 10, 2025 at 14 for 30 minutes with 1 \u001b[33m│\u001b[0m\n", "\u001b[33m│\u001b[0m attendees \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] @@ -445,31 +523,31 @@ "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Great! Now let me send a response email to Jane confirming the meeting.                                         \n",
+       " Perfect! The meeting has been scheduled. Now I'll send a confirmation email to Jane.                            \n",
        "                                                                                                                 \n",
        " 🔧 Tool Call: write_email                                                                                       \n",
        "    Args: {                                                                                                      \n",
        "   \"to\": \"jane@example.com\",                                                                                     \n",
        "   \"subject\": \"Re: Quick question about next week\",                                                              \n",
-       "   \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute  \n",
-       " meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                              \n",
+       "   \"content\": \"Hi Jane,\\n\\nTuesday at 2pm works perfectly. I've scheduled 30 minutes for us to discuss the       \n",
+       " project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                                                     \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01WmATRwvRPPmmZkoSDkYPxt                                                                           \n",
+       "    ID: toolu_01WUudqPrLWfUb2wGZDorAHz                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Great! Now let me send a response email to Jane confirming the meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Perfect! The meeting has been scheduled. Now I'll send a confirmation email to Jane. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m 🔧 Tool Call: write_email \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \"to\": \"jane@example.com\", \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \"subject\": \"Re: Quick question about next week\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nTuesday at 2pm works perfectly. I've scheduled 30 minutes for us to discuss the \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01WmATRwvRPPmmZkoSDkYPxt \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01WUudqPrLWfUb2wGZDorAHz \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -478,22 +556,24 @@ } ], "source": [ - "result_1_continued = resume_with_decision(agent, config_1, decision_type=\"accept\")\n", + "result_1_continued = resume_with_approve_or_reject(agent, config_1, decision_type=\"approve\")\n", "format_messages(result_1_continued[\"messages\"])" ] }, { "cell_type": "markdown", - "id": "99a2758e-d3a2-48ff-b365-7dfd2d4e1ac7", + "id": "92831797-bf28-4901-9a7d-7568dd5d12fb", "metadata": {}, "source": [ - "### Example: Resume by editing" + "### Example: Resume by rejecting \n", + "\n", + "The interrupt system uses \"reject\" Rejecting will update the user profile to learn from your feedback." ] }, { "cell_type": "code", - "execution_count": 15, - "id": "30f05395-a0ef-4507-bde9-25e240277ec3", + "execution_count": 42, + "id": "9ipquhkakha", "metadata": {}, "outputs": [ { @@ -501,7 +581,6 @@ "text/html": [ "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
        "                                                                                                                 \n",
-       "                                                                                                                 \n",
        " **Subject**: Quick question about next week                                                                     \n",
        " **From**: jane@example.com                                                                                      \n",
        " **To**: lance@langchain.dev                                                                                     \n",
@@ -521,7 +600,6 @@
       "text/plain": [
        "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n",
        "\u001b[34m│\u001b[0m                                                                                                                 \u001b[34m│\u001b[0m\n",
-       "\u001b[34m│\u001b[0m                                                                                                                 \u001b[34m│\u001b[0m\n",
        "\u001b[34m│\u001b[0m **Subject**: Quick question about next week                                                                     \u001b[34m│\u001b[0m\n",
        "\u001b[34m│\u001b[0m **From**: jane@example.com                                                                                      \u001b[34m│\u001b[0m\n",
        "\u001b[34m│\u001b[0m **To**: lance@langchain.dev                                                                                     \u001b[34m│\u001b[0m\n",
@@ -545,25 +623,31 @@
      "data": {
       "text/html": [
        "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
        "                                                                                                                 \n",
-       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       " 🔧 Tool Call: triage_email                                                                                      \n",
        "    Args: {                                                                                                      \n",
-       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       "   \"reasoning\": \"This email is from Jane asking to meet next Tuesday at 2pm to discuss the project roadmap. This \n",
+       " is a direct meeting request with a specific time proposed. According to the user profile, meeting requests      \n",
+       " should be classified as 'respond'. The email requires a response to either confirm the meeting time or propose  \n",
+       " alternatives.\",                                                                                                 \n",
+       "   \"classification\": \"respond\"                                                                                   \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01HLFiUonMHpa96sjmnVdTam                                                                           \n",
+       "    ID: toolu_01BkfgaUTdJgcpawMwqTMeYr                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: triage_email \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"reasoning\": \"This email is from Jane asking to meet next Tuesday at 2pm to discuss the project roadmap. This \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m is a direct meeting request with a specific time proposed. According to the user profile, meeting requests \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m should be classified as 'respond'. The email requires a response to either confirm the meeting time or propose \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m alternatives.\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"classification\": \"respond\" \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01HLFiUonMHpa96sjmnVdTam \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01BkfgaUTdJgcpawMwqTMeYr \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -574,13 +658,19 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       " Classification Decision: respond. Reasoning: This email is from Jane asking to meet next Tuesday at 2pm to      \n",
+       " discuss the project roadmap. This is a direct meeting request with a specific time proposed. According to the   \n",
+       " user profile, meeting requests should be classified as 'respond'. The email requires a response to either       \n",
+       " confirm the meeting time or propose alternatives.                                                               \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m Classification Decision: respond. Reasoning: This email is from Jane asking to meet next Tuesday at 2pm to \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m discuss the project roadmap. This is a direct meeting request with a specific time proposed. According to the \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m user profile, meeting requests should be classified as 'respond'. The email requires a response to either \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m confirm the meeting time or propose alternatives. \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -591,39 +681,25 @@ "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to    \n",
-       " Jane.                                                                                                           \n",
+       " Now I need to check calendar availability for next Tuesday (December 10, 2025) to see if 2pm is available.      \n",
        "                                                                                                                 \n",
-       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
+       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
        "    Args: {                                                                                                      \n",
-       "   \"attendees\": [                                                                                                \n",
-       "     \"jane@example.com\"                                                                                          \n",
-       "   ],                                                                                                            \n",
-       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
-       "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
-       "   \"start_time\": 14                                                                                              \n",
+       "   \"day\": \"2025-12-10\"                                                                                           \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA                                                                           \n",
+       "    ID: toolu_01WNeCsBFLNnK34k7A5QrtNv                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Great! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting and send a response to \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Jane. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Now I need to check calendar availability for next Tuesday (December 10, 2025) to see if 2pm is available. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"day\": \"2025-12-10\" \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01W4G5PFq7Et4TPsXxbKVvxA \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01WNeCsBFLNnK34k7A5QrtNv \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -634,89 +710,56 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in       \n",
-       " before it could be completed.                                                                                   \n",
+       " Available times on 2025-12-10: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_01W4G5PFq7Et4TPsXxbKVvxA was cancelled - another message came in \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m Available times on 2025-12-10: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, - { - "data": { - "text/html": [ - "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       " **Subject**: Quick question about next week                                                                     \n",
-       " **From**: jane@example.com                                                                                      \n",
-       " **To**: lance@langchain.dev                                                                                     \n",
-       "                                                                                                                 \n",
-       " Hi Lance,                                                                                                       \n",
-       "                                                                                                                 \n",
-       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
-       "                                                                                                                 \n",
-       " Best,                                                                                                           \n",
-       " Jane                                                                                                            \n",
-       "                                                                                                                 \n",
-       " ---                                                                                                             \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
+       " Great! 2pm is available on Tuesday, December 10th. Now I'll schedule the meeting for 30 minutes (as per the     \n",
+       " user profile preference).                                                                                       \n",
        "                                                                                                                 \n",
-       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
        "    Args: {                                                                                                      \n",
-       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       "   \"attendees\": [                                                                                                \n",
+       "     \"jane@example.com\"                                                                                          \n",
+       "   ],                                                                                                            \n",
+       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
+       "   \"duration_minutes\": 30,                                                                                       \n",
+       "   \"preferred_day\": \"2025-12-10T14:00:00\",                                                                       \n",
+       "   \"start_time\": 14                                                                                              \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01We2XxG49E7jtJZhPJiVBGU                                                                           \n",
+       "    ID: toolu_01AsrXaBC6K3At2YNzBJ7wSg                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Great! 2pm is available on Tuesday, December 10th. Now I'll schedule the meeting for 30 minutes (as per the \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m user profile preference). \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-10T14:00:00\", \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01We2XxG49E7jtJZhPJiVBGU \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01AsrXaBC6K3At2YNzBJ7wSg \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -727,13 +770,15 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       " Meeting 'Project Roadmap Discussion' scheduled on Wednesday, December 10, 2025 at 14 for 30 minutes with 1      \n",
+       " attendees                                                                                                       \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion' scheduled on Wednesday, December 10, 2025 at 14 for 30 minutes with 1 \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m attendees \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -744,91 +789,31 @@ "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
+       " Perfect! The meeting has been scheduled. Now I'll send a confirmation email to Jane.                            \n",
        "                                                                                                                 \n",
-       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
-       "    Args: {                                                                                                      \n",
-       "   \"attendees\": [                                                                                                \n",
-       "     \"jane@example.com\"                                                                                          \n",
-       "   ],                                                                                                            \n",
-       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
-       "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
-       "   \"start_time\": 14                                                                                              \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST                                                                           \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01DPWcDw2dYc3CgiU68Gh8ST \u001b[37m│\u001b[0m\n", - "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1        \n",
-       " attendees                                                                                                       \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion' scheduled on Tuesday, December 09, 2025 at 14 for 30 minutes with 1 \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m attendees \u001b[33m│\u001b[0m\n", - "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Great! Now let me send a response email to Jane confirming the meeting.                                         \n",
-       "                                                                                                                 \n",
-       " 🔧 Tool Call: write_email                                                                                       \n",
+       " 🔧 Tool Call: write_email                                                                                       \n",
        "    Args: {                                                                                                      \n",
        "   \"to\": \"jane@example.com\",                                                                                     \n",
        "   \"subject\": \"Re: Quick question about next week\",                                                              \n",
-       "   \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute  \n",
-       " meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                              \n",
+       "   \"content\": \"Hi Jane,\\n\\nTuesday at 2pm works perfectly. I've scheduled 30 minutes for us to discuss the       \n",
+       " project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                                                     \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01WmATRwvRPPmmZkoSDkYPxt                                                                           \n",
+       "    ID: toolu_01WUudqPrLWfUb2wGZDorAHz                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Great! Now let me send a response email to Jane confirming the meeting. \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Perfect! The meeting has been scheduled. Now I'll send a confirmation email to Jane. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m 🔧 Tool Call: write_email \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \"to\": \"jane@example.com\", \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \"subject\": \"Re: Quick question about next week\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 30-minute \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nTuesday at 2pm works perfectly. I've scheduled 30 minutes for us to discuss the \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01WmATRwvRPPmmZkoSDkYPxt \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01WUudqPrLWfUb2wGZDorAHz \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -839,89 +824,40 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Tool call write_email with id toolu_01WmATRwvRPPmmZkoSDkYPxt was cancelled - another message came in before it  \n",
-       " could be completed.                                                                                             \n",
+       " Not interested in this meeting                                                                                  \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Tool call write_email with id toolu_01WmATRwvRPPmmZkoSDkYPxt was cancelled - another message came in before it \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m could be completed. \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m Not interested in this meeting \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, - { - "data": { - "text/html": [ - "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       " **Subject**: Quick question about next week                                                                     \n",
-       " **From**: jane@example.com                                                                                      \n",
-       " **To**: lance@langchain.dev                                                                                     \n",
-       "                                                                                                                 \n",
-       " Hi Lance,                                                                                                       \n",
-       "                                                                                                                 \n",
-       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
-       "                                                                                                                 \n",
-       " Best,                                                                                                           \n",
-       " Jane                                                                                                            \n",
-       "                                                                                                                 \n",
-       " ---                                                                                                             \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, { "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
        "                                                                                                                 \n",
-       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
+       " 🔧 Tool Call: Done                                                                                              \n",
        "    Args: {                                                                                                      \n",
-       "   \"day\": \"2025-12-09\"                                                                                           \n",
+       "   \"done\": true                                                                                                  \n",
        " }                                                                                                               \n",
-       "    ID: toolu_01JGcGsEQ5jUtK1pHAv6M9Kw                                                                           \n",
+       "    ID: toolu_01XC8968ks1RHk9bpu4euhbG                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m 🔧 Tool Call: Done \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m \"done\": true \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01JGcGsEQ5jUtK1pHAv6M9Kw \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m ID: toolu_01XC8968ks1RHk9bpu4euhbG \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -932,13 +868,13 @@ "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
+       " done=True                                                                                                       \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m done=True \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -949,207 +885,617 @@ "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
        "                                                                                                                 \n",
-       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
-       "    Args: {                                                                                                      \n",
-       "   \"attendees\": [                                                                                                \n",
-       "     \"jane@example.com\"                                                                                          \n",
-       "   ],                                                                                                            \n",
-       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
-       "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
-       "   \"start_time\": 14                                                                                              \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_0126iavpuerW8BLF6jey4uKt                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_0126iavpuerW8BLF6jey4uKt \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" - }, + } + ], + "source": [ + "result_1_continued = resume_with_approve_or_reject(agent, config_1, decision_type=\"reject\", message=\"Not interested in this meeting\")\n", + "format_messages(result_1_continued[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "example2", + "metadata": {}, + "source": [ + "## Example 2: Simple Question\n", + "\n", + "Test with a simple question that needs a direct response." + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "example2_email", + "metadata": {}, + "outputs": [ { "data": { "text/html": [ - "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Tool call schedule_meeting with id toolu_0126iavpuerW8BLF6jey4uKt was cancelled - another message came in       \n",
-       " before it could be completed.                                                                                   \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
╭────────────────────────────────────────────── 📧 Email to Process ──────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "  **Subject**: LangGraph documentation link                                                                      \n",
+       "  **From**: bob@example.com                                                                                      \n",
+       "  **To**: lance@langchain.dev                                                                                    \n",
+       "                                                                                                                 \n",
+       "  Hey Lance,                                                                                                     \n",
+       "                                                                                                                 \n",
+       "  Could you send me the link to the LangGraph docs?                                                              \n",
+       "                                                                                                                 \n",
+       "  Thanks!                                                                                                        \n",
+       "  Bob                                                                                                            \n",
+       "                                                                                                                 \n",
+       "  ---                                                                                                            \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ - "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Tool call schedule_meeting with id toolu_0126iavpuerW8BLF6jey4uKt was cancelled - another message came in \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m before it could be completed. \u001b[33m│\u001b[0m\n", - "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Email to Process\u001b[0m\u001b[36m \u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **Subject**: LangGraph documentation link \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **From**: bob@example.com \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **To**: lance@langchain.dev \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Hey Lance, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Could you send me the link to the LangGraph docs? \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Thanks! \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Bob \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m --- \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" + } + ], + "source": [ + "# Example email as markdown string\n", + "email_markdown_2 = \"\"\"\n", + "**Subject**: LangGraph documentation link\n", + "**From**: bob@example.com\n", + "**To**: lance@langchain.dev\n", + "\n", + "Hey Lance,\n", + "\n", + "Could you send me the link to the LangGraph docs?\n", + "\n", + "Thanks!\n", + "Bob\n", + "\n", + "---\n", + "\"\"\"\n", + "\n", + "# Display email with rich formatting\n", + "show_prompt(email_markdown_2, title=\"📧 Email to Process\", border_style=\"cyan\")" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "example2_invoke", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "Agent Response\n", + "============================================================\n", + "\n", + "🛑 INTERRUPT: Agent is waiting for your approval\n", + "\n", + "Action: write_email\n", + "Args: {'to': 'bob@example.com', 'subject': 'Re: LangGraph documentation link', 'content': \"Hi Bob,\\n\\nHere's the link to the LangGraph documentation: https://langchain-ai.github.io/langgraph/\\n\\nLet me know if you need anything else!\\n\\nBest,\\nLance\"}\n", + "\n", + "Description:\n" + ] }, { "data": { "text/html": [ - "
╭─────────────────────────────────────────────────── 🧑 Human ────────────────────────────────────────────────────╮\n",
-       "                                                                                                                 \n",
-       "                                                                                                                 \n",
-       " **Subject**: Quick question about next week                                                                     \n",
-       " **From**: jane@example.com                                                                                      \n",
-       " **To**: lance@langchain.dev                                                                                     \n",
-       "                                                                                                                 \n",
-       " Hi Lance,                                                                                                       \n",
-       "                                                                                                                 \n",
-       " Can we meet next Tuesday at 2pm to discuss the project roadmap?                                                 \n",
-       "                                                                                                                 \n",
-       " Best,                                                                                                           \n",
-       " Jane                                                                                                            \n",
-       "                                                                                                                 \n",
-       " ---                                                                                                             \n",
-       "                                                                                                                 \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
╭─────────────────────────────────────────────── 📋 Action Details ───────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "  I've drafted an email response. Please review the content, recipients, and subject line below. Approve to      \n",
+       "  send as-is, or Reject to cancel and end the workflow.                                                          \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ - "\u001b[34m╭─\u001b[0m\u001b[34m──────────────────────────────────────────────────\u001b[0m\u001b[34m 🧑 Human \u001b[0m\u001b[34m───────────────────────────────────────────────────\u001b[0m\u001b[34m─╮\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **Subject**: Quick question about next week \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **From**: jane@example.com \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m **To**: lance@langchain.dev \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Hi Lance, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Can we meet next Tuesday at 2pm to discuss the project roadmap? \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Best, \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m Jane \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m --- \u001b[34m│\u001b[0m\n", - "\u001b[34m│\u001b[0m \u001b[34m│\u001b[0m\n", - "\u001b[34m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + "\u001b[33m╭─\u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m \u001b[0m\u001b[1;32m📋 Action Details\u001b[0m\u001b[33m \u001b[0m\u001b[33m──────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m I've drafted an email response. Please review the content, recipients, and subject line below. Approve to \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m send as-is, or Reject to cancel and end the workflow. \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m \u001b[33m│\u001b[0m\n", + "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "------------------------------------------------------------\n", + "\n", + "Allowed decisions: ['approve', 'reject']\n", + "\n", + "💡 To continue, you need to resume the agent with a decision.\n", + " Use: resume_with_approve_or_reject(agent, config_2, decision_type='approve')\n" + ] + } + ], + "source": [ + "config_2 = {\"configurable\": {\"thread_id\": \"test-thread-2\"}}\n", + "\n", + "# Invoke agent with markdown email string\n", + "result_2 = agent.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_2}]},\n", + " config=config_2,\n", + ")\n", + "\n", + "# Display result with rich formatting\n", + "print(\"\\n\" + \"=\"*60)\n", + "print(\"Agent Response\")\n", + "print(\"=\"*60 + \"\\n\")\n", + "\n", + "# Check for interrupts (HITL)\n", + "if \"__interrupt__\" in result_2:\n", + " print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n", + " \n", + " for interrupt in result_2[\"__interrupt__\"]:\n", + " # New interrupt format with action_requests\n", + " action_requests = interrupt.value.get('action_requests', [])\n", + " review_configs = interrupt.value.get('review_configs', [])\n", + " \n", + " for action_req in action_requests:\n", + " print(\"Action:\", action_req['name'])\n", + " print(\"Args:\", action_req['args'])\n", + " print(\"\\nDescription:\")\n", + " show_prompt(action_req.get('description', ''), title=\"📋 Action Details\", border_style=\"yellow\")\n", + " print(\"\\n\" + \"-\"*60)\n", + " \n", + " if review_configs:\n", + " print(\"\\nAllowed decisions:\", review_configs[0].get('allowed_decisions', []))\n", + " \n", + " print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n", + " print(\" Use: resume_with_approve_or_reject(agent, config_2, decision_type='approve')\")\n", + "else:\n", + " format_messages(result_2[\"messages\"])" + ] + }, + { + "cell_type": "markdown", + "id": "test_memory", + "metadata": {}, + "source": [ + "## Test Memory Persistence\n", + "\n", + "Test that memory persists across invocations using the same thread ID." + ] + }, + { + "cell_type": "markdown", + "id": "p8oc6m0fwzo", + "metadata": {}, + "source": [ + "## Example 3: Triage Functionality\n", + "\n", + "The agent now uses a triage tool to classify emails before taking action. This example shows how the agent triages a marketing email." + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "yeib3g2uxlf", + "metadata": {}, + "outputs": [ { "data": { "text/html": [ - "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm.     \n",
-       "                                                                                                                 \n",
-       " 🔧 Tool Call: check_calendar_availability                                                                       \n",
-       "    Args: {                                                                                                      \n",
-       "   \"day\": \"2025-12-09\"                                                                                           \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_015MCueda45cdMN5XkZyRxou                                                                           \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
╭────────────────────────────────────────────── 📧 Marketing Email ───────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "  **Subject**: Limited Time Offer - 50% Off!                                                                     \n",
+       "  **From**: marketing@newsletter.com                                                                             \n",
+       "  **To**: lance@langchain.dev                                                                                    \n",
+       "                                                                                                                 \n",
+       "  Don't miss out on our amazing sale! Click here to shop now and save big on all your favorite items. This       \n",
+       "  offer expires soon!                                                                                            \n",
+       "                                                                                                                 \n",
+       "  ---                                                                                                            \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ - "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m I'll help you handle this meeting request. Let me check your calendar availability for next Tuesday at 2pm. \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: check_calendar_availability \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"day\": \"2025-12-09\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_015MCueda45cdMN5XkZyRxou \u001b[37m│\u001b[0m\n", - "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + "\u001b[36m╭─\u001b[0m\u001b[36m─────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Marketing Email\u001b[0m\u001b[36m \u001b[0m\u001b[36m──────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **Subject**: Limited Time Offer - 50% Off! \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **From**: marketing@newsletter.com \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **To**: lance@langchain.dev \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Don't miss out on our amazing sale! Click here to shop now and save big on all your favorite items. This \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m offer expires soon! \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m --- \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "============================================================\n", + "Agent Decision\n", + "============================================================\n", + "\n", + "🔍 TRIAGE DECISION:\n", + " Classification: ignore\n", + " Reasoning: This email is from marketing@newsletter.com with the subject \"Limited Time Offer - 50% Off!\" and contains promotional content about a sale. According to Lance's user profile, marketing newsletters and promotional emails should be classified as 'ignore'. This is clearly a marketing/promotional email with no direct questions, no technical work discussion, and no actionable items relevant to Lance's work at LangChain.\n", + "\n", + "✓ Result: Classification Decision: ignore. Reasoning: This email is from marketing@newsletter.com with the subject \"Limited Time Offer - 50% Off!\" and contains promotional content about a sale. According to Lance's user profile, marketing newsletters and promotional emails should be classified as 'ignore'. This is clearly a marketing/promotional email with no direct questions, no technical work discussion, and no actionable items relevant to Lance's work at LangChain.\n", + "\n", + "Since this was classified as 'ignore', the agent calls Done immediately without drafting a response.\n" + ] + } + ], + "source": [ + "# Marketing email as markdown string\n", + "email_markdown_marketing = \"\"\"\n", + "**Subject**: Limited Time Offer - 50% Off!\n", + "**From**: marketing@newsletter.com\n", + "**To**: lance@langchain.dev\n", + "\n", + "Don't miss out on our amazing sale! Click here to shop now and save big on all your favorite items. This offer expires soon!\n", + "\n", + "---\n", + "\"\"\"\n", + "\n", + "# Display email\n", + "show_prompt(email_markdown_marketing, title=\"📧 Marketing Email\", border_style=\"cyan\")\n", + "\n", + "# Invoke agent\n", + "config_marketing = {\"configurable\": {\"thread_id\": \"test-marketing\"}}\n", + "result_marketing = agent.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_marketing}]},\n", + " config=config_marketing,\n", + ")\n", + "\n", + "# Show triage decision\n", + "print(\"\\n\" + \"=\"*60)\n", + "print(\"Agent Decision\")\n", + "print(\"=\"*60 + \"\\n\")\n", + "\n", + "messages = result_marketing.get(\"messages\", [])\n", + "for i, msg in enumerate(messages):\n", + " if hasattr(msg, 'tool_calls') and msg.tool_calls:\n", + " for tool_call in msg.tool_calls:\n", + " if tool_call.get('name') == 'triage_email':\n", + " print(\"🔍 TRIAGE DECISION:\")\n", + " print(f\" Classification: {tool_call.get('args', {}).get('classification', 'N/A')}\")\n", + " print(f\" Reasoning: {tool_call.get('args', {}).get('reasoning', 'N/A')}\")\n", + " print()\n", + " if hasattr(msg, 'name') and msg.name == 'triage_email':\n", + " print(f\"✓ Result: {msg.content}\")\n", + " print()\n", + "\n", + "print(\"Since this was classified as 'ignore', the agent calls Done immediately without drafting a response.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "test_memory_code", + "metadata": {}, + "outputs": [], + "source": [ + "# Follow-up email as markdown string\n", + "email_markdown_3 = \"\"\"\n", + "**Subject**: Follow-up meeting\n", + "**From**: jane@example.com\n", + "**To**: lance@langchain.dev\n", + "\n", + "Hi again,\n", + "\n", + "Can we also schedule a follow-up meeting next week?\n", + "\n", + "Jane\n", + "\n", + "---\n", + "\"\"\"\n", + "\n", + "# Use same config as example 1 (same thread_id) to test memory persistence\n", + "result_3 = agent.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_3}]},\n", + " config=config_1, # Same thread as example 1\n", + ")\n", + "\n", + "# Display result with rich formatting (show last 5 messages)\n", + "print(\"\\n\" + \"=\"*60)\n", + "print(\"Agent Response (with memory from previous conversation)\")\n", + "print(\"=\"*60 + \"\\n\")\n", + "format_messages(result_3[\"messages\"][-5:])" + ] + }, + { + "cell_type": "markdown", + "id": "bb4fc2a3-63ce-4f16-9003-c37294989355", + "metadata": {}, + "source": [ + "## Test Memory Updates on Rejection\n", + "\n", + "Test that rejecting tool calls updates the user profile in the store." + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "kdxq3j0aia", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "✓ Memory testing section - helper functions already loaded above\n" + ] + } + ], + "source": [ + "print(\"✓ Memory testing section - helper functions already loaded above\")" + ] + }, + { + "cell_type": "markdown", + "id": "nbmrr2x9o5", + "metadata": {}, + "source": [ + "### Step 1: Check Initial User Profile\n", + "\n", + "Before any rejections, let's see the default user profile." + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "tu2s0bayt3h", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "📋 Initial User Profile:\n", + "============================================================\n", + "I'm Lance, a software engineer at LangChain. When triaging emails:\n", + "- Respond to: Direct questions about technical work, project inquiries, family/self-care reminders\n", + "- Notify: GitHub notifications, deadline reminders, FYI project updates, team status messages, meeting requests about project roadmap discussions\n", + "- Ignore: Marketing newsletters, promotional emails, CC'd FYI threads\n", + "\n", + "Response style:\n", + "- Professional and concise tone\n", + "- Acknowledge deadlines explicitly in responses\n", + "- For technical questions: State investigation approach and timeline\n", + "- For event invitations: Ask about workshops, discounts, and deadlines; don't commit immediately\n", + "- For collaboration requests: Acknowledge existing materials, mention reviewing them\n", + "\n", + "Meeting scheduling:\n", + "- Prefer 30-minute meetings (15 minutes acceptable)\n", + "- If times proposed: Verify all options, commit to one or decline\n", + "- If no times proposed: Offer multiple options instead of picking one\n", + "- Reference meeting purpose and duration in response\n", + "- Not interested in general project roadmap discussion meetings\n", + "============================================================\n" + ] + } + ], + "source": [ + "# Get initial user profile\n", + "initial_profile = get_user_profile_from_store(agent)\n", + "\n", + "print(\"📋 Initial User Profile:\")\n", + "print(\"=\"*60)\n", + "if initial_profile:\n", + " print(initial_profile)\n", + "else:\n", + " print(\"(No profile in store yet - will use default on first use)\")\n", + "print(\"=\"*60)" + ] + }, + { + "cell_type": "markdown", + "id": "679czgcbyxo", + "metadata": {}, + "source": [ + "### Step 2: Send Newsletter Email and Reject Draft\n", + "\n", + "Send a newsletter email that the agent will try to respond to, then reject it to update memory." + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "nauil4zxm5o", + "metadata": {}, + "outputs": [ { "data": { "text/html": [ - "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM                                                        \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
+       "
╭─────────────────────────────────────────────── 📧 Meeting Email ────────────────────────────────────────────────╮\n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "  **Subject**: Sync on agent architecture                                                                        \n",
+       "  **From**: Alex Rodriguez <alex.rodriguez@company.com>                                                          \n",
+       "  **To**: Lance Martin <lance@langchain.dev>                                                                     \n",
+       "                                                                                                                 \n",
+       "  Hey Lance,                                                                                                     \n",
+       "                                                                                                                 \n",
+       "  Can we schedule 30 minutes next week to discuss the multi-agent architecture for our project? I have some      \n",
+       "  questions about routing between agents and want to get your input.                                             \n",
+       "                                                                                                                 \n",
+       "  I'm free Tuesday afternoon or Thursday morning. Let me know what works!                                        \n",
+       "                                                                                                                 \n",
+       "  Best,                                                                                                          \n",
+       "  Alex                                                                                                           \n",
+       "                                                                                                                 \n",
+       "                                                                                                                 \n",
+       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ - "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Available times on 2025-12-09: 9:00 AM, 2:00 PM, 4:00 PM \u001b[33m│\u001b[0m\n", - "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" + "\u001b[36m╭─\u001b[0m\u001b[36m──────────────────────────────────────────────\u001b[0m\u001b[36m \u001b[0m\u001b[1;32m📧 Meeting Email\u001b[0m\u001b[36m \u001b[0m\u001b[36m───────────────────────────────────────────────\u001b[0m\u001b[36m─╮\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **Subject**: Sync on agent architecture \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **From**: Alex Rodriguez \u001b[1;34m\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m **To**: Lance Martin \u001b[1;34m\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Hey Lance, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Can we schedule 30 minutes next week to discuss the multi-agent architecture for our project? I have some \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m questions about routing between agents and want to get your input. \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m I'm free Tuesday afternoon or Thursday morning. Let me know what works! \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Best, \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m Alex \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m│\u001b[0m \u001b[36m│\u001b[0m\n", + "\u001b[36m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" }, { - "data": { - "text/html": [ - "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting.                        \n",
-       "                                                                                                                 \n",
-       " 🔧 Tool Call: schedule_meeting                                                                                  \n",
-       "    Args: {                                                                                                      \n",
-       "   \"attendees\": [                                                                                                \n",
-       "     \"jane@example.com\"                                                                                          \n",
-       "   ],                                                                                                            \n",
-       "   \"subject\": \"Project Roadmap Discussion\",                                                                      \n",
-       "   \"duration_minutes\": 30,                                                                                       \n",
-       "   \"preferred_day\": \"2025-12-09T14:00:00\",                                                                       \n",
-       "   \"start_time\": 14                                                                                              \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_017JvuuDJKQjTRcNbDrRURoY                                                                           \n",
-       "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
-       "
\n" - ], - "text/plain": [ - "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Perfect! You're available at 2pm on Tuesday, December 9th. Let me schedule this meeting. \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: schedule_meeting \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"attendees\": [ \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"jane@example.com\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ], \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Project Roadmap Discussion\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"duration_minutes\": 30, \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"preferred_day\": \"2025-12-09T14:00:00\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"start_time\": 14 \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_017JvuuDJKQjTRcNbDrRURoY \u001b[37m│\u001b[0m\n", - "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" - ] - }, - "metadata": {}, - "output_type": "display_data" + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "🛑 INTERRUPT: Agent wants to respond to this meeting\n", + "\n", + "Action: write_email\n", + "Args: {'to': 'alex.rodriguez@company.com', 'subject': 'Re: Sync on agent architecture', 'content': \"Hey Alex,\\n\\nHappy to discuss the multi-agent architecture and routing questions with you. I have availability during the times you mentioned:\\n\\n- Tuesday, December 10: 2:00 PM or 4:00 PM\\n- Thursday, December 12: 9:00 AM\\n\\nLet me know which works best for you and I'll send a calendar invite for 30 minutes.\\n\\nBest,\\nLance\"}\n", + "\n", + "------------------------------------------------------------\n" + ] + } + ], + "source": [ + "# Email that agent might try to respond to\n", + "meeting_email = \"\"\"\n", + "**Subject**: Sync on agent architecture\n", + "**From**: Alex Rodriguez \n", + "**To**: Lance Martin \n", + "\n", + "Hey Lance,\n", + "\n", + "Can we schedule 30 minutes next week to discuss the multi-agent architecture for our project? I have some questions about routing between agents and want to get your input.\n", + "\n", + "I'm free Tuesday afternoon or Thursday morning. Let me know what works!\n", + "\n", + "Best,\n", + "Alex\n", + "\"\"\"\n", + "\n", + "show_prompt(meeting_email, title=\"📧 Meeting Email\", border_style=\"cyan\")\n", + "\n", + "# Use a new thread for this test\n", + "config_reject_test = {\"configurable\": {\"thread_id\": \"test-reject-newsletter\"}}\n", + "\n", + "# Invoke agent\n", + "result_newsletter = agent.invoke(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": meeting_email}]},\n", + " config=config_reject_test,\n", + ")\n", + "\n", + "# Check if interrupted\n", + "if \"__interrupt__\" in result_newsletter:\n", + " print(\"\\n🛑 INTERRUPT: Agent wants to respond to this meeting\\n\")\n", + " \n", + " for interrupt in result_newsletter[\"__interrupt__\"]:\n", + " # New interrupt format with action_requests\n", + " action_requests = interrupt.value.get('action_requests', [])\n", + " \n", + " for action_req in action_requests:\n", + " print(\"Action:\", action_req['name'])\n", + " print(\"Args:\", action_req['args'])\n", + " print(\"\\n\" + \"-\"*60)\n", + "else:\n", + " print(\"\\n✓ Agent classified this as ignore/notify (no response needed)\")\n", + " format_messages(result_newsletter[\"messages\"][-3:])" + ] + }, + { + "cell_type": "markdown", + "id": "hqs2c1ngbar", + "metadata": {}, + "source": [ + "### Step 3: Reject with Feedback\n", + "\n", + "Reject the draft and provide feedback about why newsletters shouldn't be responded to." + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "awqwsqrtel5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "🚫 Rejecting with feedback: Let's not respond to emails requesting meetings from any @company.com email addresses.\n", + "\n", + "\n", + "✓ Rejection processed\n" + ] }, { "data": { "text/html": [ "
╭──────────────────────────────────────────────── 🔧 Tool Output ─────────────────────────────────────────────────╮\n",
-       " Meeting 'Project Roadmap Discussion - UPDATED' scheduled on Tuesday, December 09, 2025 at 14 for 45 minutes     \n",
-       " with 1 attendees                                                                                                \n",
+       " done=True                                                                                                       \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[33m╭─\u001b[0m\u001b[33m───────────────────────────────────────────────\u001b[0m\u001b[33m 🔧 Tool Output \u001b[0m\u001b[33m────────────────────────────────────────────────\u001b[0m\u001b[33m─╮\u001b[0m\n", - "\u001b[33m│\u001b[0m Meeting 'Project Roadmap Discussion - UPDATED' scheduled on Tuesday, December 09, 2025 at 14 for 45 minutes \u001b[33m│\u001b[0m\n", - "\u001b[33m│\u001b[0m with 1 attendees \u001b[33m│\u001b[0m\n", + "\u001b[33m│\u001b[0m done=True \u001b[33m│\u001b[0m\n", "\u001b[33m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, @@ -1160,194 +1506,141 @@ "data": { "text/html": [ "
╭───────────────────────────────────────────────────── 📝 AI ─────────────────────────────────────────────────────╮\n",
-       " Now let me send a response email to Jane confirming the meeting.                                                \n",
-       "                                                                                                                 \n",
-       " 🔧 Tool Call: write_email                                                                                       \n",
-       "    Args: {                                                                                                      \n",
-       "   \"to\": \"jane@example.com\",                                                                                     \n",
-       "   \"subject\": \"Re: Quick question about next week\",                                                              \n",
-       "   \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 45-minute  \n",
-       " meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\"                              \n",
-       " }                                                                                                               \n",
-       "    ID: toolu_01Ckz5kut6xXCxU1PrzU9rhS                                                                           \n",
+       " Understood! I've stopped the workflow as requested. No response will be sent to this meeting request from the   \n",
+       " @company.com address.                                                                                           \n",
        "╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n",
        "
\n" ], "text/plain": [ "\u001b[37m╭─\u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m 📝 AI \u001b[0m\u001b[37m────────────────────────────────────────────────────\u001b[0m\u001b[37m─╮\u001b[0m\n", - "\u001b[37m│\u001b[0m Now let me send a response email to Jane confirming the meeting. \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m 🔧 Tool Call: write_email \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m Args: { \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"to\": \"jane@example.com\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"subject\": \"Re: Quick question about next week\", \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m \"content\": \"Hi Jane,\\n\\nYes, Tuesday, December 9th at 2pm works perfectly for me. I've scheduled a 45-minute \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m meeting to discuss the project roadmap.\\n\\nLooking forward to it!\\n\\nBest,\\nLance\" \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m } \u001b[37m│\u001b[0m\n", - "\u001b[37m│\u001b[0m ID: toolu_01Ckz5kut6xXCxU1PrzU9rhS \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m Understood! I've stopped the workflow as requested. No response will be sent to this meeting request from the \u001b[37m│\u001b[0m\n", + "\u001b[37m│\u001b[0m @company.com address. \u001b[37m│\u001b[0m\n", "\u001b[37m╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "🔍 Checking rejection message in tool messages...\n", + " ToolMessage with status='error' found:\n", + " - Name: write_email\n", + " - Content: Let's not respond to emails requesting meetings from any @company.com email addresses.\n", + " ✓ Rejection message captured correctly!\n" + ] } ], "source": [ - "edited_args = {\n", - " \"attendees\": [\"jane@example.com\"],\n", - " \"subject\": \"Project Roadmap Discussion - UPDATED\",\n", - " \"duration_minutes\": 45, # Changed from 30 to 45\n", - " \"preferred_day\": \"2025-12-09T14:00:00\",\n", - " \"start_time\": 14\n", - "}\n", + "# Reject with feedback\n", + "rejection_message = \"Let's not respond to emails requesting meetings from any @company.com email addresses.\"\n", "\n", - "result_1_continued = resume_with_decision(agent, config_1, decision_type=\"edit\", edited_args=edited_args)\n", - "format_messages(result_1_continued[\"messages\"])" + "print(\"🚫 Rejecting with feedback:\", rejection_message)\n", + "print()\n", + "\n", + "result_rejected = resume_with_approve_or_reject(\n", + " agent, \n", + " config_reject_test, \n", + " decision_type=\"reject\",\n", + " message=rejection_message\n", + ")\n", + "\n", + "print(\"\\n✓ Rejection processed\")\n", + "format_messages(result_rejected[\"messages\"][-2:])\n", + "\n", + "# Verify the rejection message was captured\n", + "print(\"\\n🔍 Checking rejection message in tool messages...\")\n", + "for msg in result_rejected[\"messages\"]:\n", + " if hasattr(msg, \"status\") and msg.status == \"error\":\n", + " print(f\" ToolMessage with status='error' found:\")\n", + " print(f\" - Name: {msg.name}\")\n", + " print(f\" - Content: {msg.content}\")\n", + " print(f\" ✓ Rejection message captured correctly!\")" ] }, { "cell_type": "markdown", - "id": "92831797-bf28-4901-9a7d-7568dd5d12fb", + "id": "kmotxafzdbf", "metadata": {}, "source": [ - "### Example: Resume by ignoring" + "### Step 4: Check Updated User Profile\n", + "\n", + "The profile should now include guidance about newsletters based on the rejection." ] }, { "cell_type": "code", - "execution_count": 17, - "id": "9ipquhkakha", + "execution_count": 51, + "id": "yl6y6piav", "metadata": {}, "outputs": [ { - "ename": "BadRequestError", - "evalue": "Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.24: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_015WrAQ82bzbKUbET1uhfixd. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'}, 'request_id': 'req_011CViX58yA9hzBebZGRqkgP'}", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mBadRequestError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[17]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m result_1_continued = \u001b[43mresume_with_decision\u001b[49m\u001b[43m(\u001b[49m\u001b[43magent\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig_1\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdecision_type\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mignore\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 2\u001b[39m format_messages(result_1_continued[\u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m])\n", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 26\u001b[39m, in \u001b[36mresume_with_decision\u001b[39m\u001b[34m(agent, config, decision_type, edited_args, feedback)\u001b[39m\n\u001b[32m 23\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mInvalid decision type: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdecision_type\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 25\u001b[39m \u001b[38;5;66;03m# Resume with the decision\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m26\u001b[39m result = \u001b[43magent\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43mCommand\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresume\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[43mdecision\u001b[49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m result\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py:3068\u001b[39m, in \u001b[36mPregel.invoke\u001b[39m\u001b[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, **kwargs)\u001b[39m\n\u001b[32m 3065\u001b[39m chunks: \u001b[38;5;28mlist\u001b[39m[\u001b[38;5;28mdict\u001b[39m[\u001b[38;5;28mstr\u001b[39m, Any] | Any] = []\n\u001b[32m 3066\u001b[39m interrupts: \u001b[38;5;28mlist\u001b[39m[Interrupt] = []\n\u001b[32m-> \u001b[39m\u001b[32m3068\u001b[39m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 3069\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 3070\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3071\u001b[39m \u001b[43m \u001b[49m\u001b[43mcontext\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcontext\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3072\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m=\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mupdates\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[32m 3073\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m \u001b[49m\u001b[43m==\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\n\u001b[32m 3074\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3075\u001b[39m \u001b[43m \u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m=\u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3076\u001b[39m \u001b[43m \u001b[49m\u001b[43moutput_keys\u001b[49m\u001b[43m=\u001b[49m\u001b[43moutput_keys\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3077\u001b[39m \u001b[43m \u001b[49m\u001b[43minterrupt_before\u001b[49m\u001b[43m=\u001b[49m\u001b[43minterrupt_before\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3078\u001b[39m \u001b[43m \u001b[49m\u001b[43minterrupt_after\u001b[49m\u001b[43m=\u001b[49m\u001b[43minterrupt_after\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3079\u001b[39m \u001b[43m \u001b[49m\u001b[43mdurability\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdurability\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3080\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 3081\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 3082\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m \u001b[49m\u001b[43m==\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mvalues\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\n\u001b[32m 3083\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mlen\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mchunk\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[43m==\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m2\u001b[39;49m\u001b[43m:\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py:2643\u001b[39m, in \u001b[36mPregel.stream\u001b[39m\u001b[34m(self, input, config, context, stream_mode, print_mode, output_keys, interrupt_before, interrupt_after, durability, subgraphs, debug, **kwargs)\u001b[39m\n\u001b[32m 2641\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m task \u001b[38;5;129;01min\u001b[39;00m loop.match_cached_writes():\n\u001b[32m 2642\u001b[39m loop.output_writes(task.id, task.writes, cached=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m-> \u001b[39m\u001b[32m2643\u001b[39m \u001b[43m\u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrunner\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtick\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2644\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43mtasks\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalues\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mnot\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m.\u001b[49m\u001b[43mwrites\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2645\u001b[39m \u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstep_timeout\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2646\u001b[39m \u001b[43m \u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m=\u001b[49m\u001b[43mget_waiter\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2647\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mloop\u001b[49m\u001b[43m.\u001b[49m\u001b[43maccept_push\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 2648\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 2649\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;66;43;03m# emit output\u001b[39;49;00m\n\u001b[32m 2650\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01myield from\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43m_output\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 2651\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mprint_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43msubgraphs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mqueue\u001b[49m\u001b[43m.\u001b[49m\u001b[43mEmpty\u001b[49m\n\u001b[32m 2652\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 2653\u001b[39m loop.after_tick()\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py:167\u001b[39m, in \u001b[36mPregelRunner.tick\u001b[39m\u001b[34m(self, tasks, reraise, timeout, retry_policy, get_waiter, schedule_task)\u001b[39m\n\u001b[32m 165\u001b[39m t = tasks[\u001b[32m0\u001b[39m]\n\u001b[32m 166\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m167\u001b[39m \u001b[43mrun_with_retry\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 168\u001b[39m \u001b[43m \u001b[49m\u001b[43mt\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 169\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 170\u001b[39m \u001b[43m \u001b[49m\u001b[43mconfigurable\u001b[49m\u001b[43m=\u001b[49m\u001b[43m{\u001b[49m\n\u001b[32m 171\u001b[39m \u001b[43m \u001b[49m\u001b[43mCONFIG_KEY_CALL\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mpartial\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 172\u001b[39m \u001b[43m \u001b[49m\u001b[43m_call\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 173\u001b[39m \u001b[43m \u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mt\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 174\u001b[39m \u001b[43m \u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m=\u001b[49m\u001b[43mretry_policy\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 175\u001b[39m \u001b[43m \u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m=\u001b[49m\u001b[43mweakref\u001b[49m\u001b[43m.\u001b[49m\u001b[43mref\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfutures\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 176\u001b[39m \u001b[43m \u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m=\u001b[49m\u001b[43mschedule_task\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 177\u001b[39m \u001b[43m \u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msubmit\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 178\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 179\u001b[39m \u001b[43m \u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 180\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 181\u001b[39m \u001b[38;5;28mself\u001b[39m.commit(t, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m 182\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py:42\u001b[39m, in \u001b[36mrun_with_retry\u001b[39m\u001b[34m(task, retry_policy, configurable)\u001b[39m\n\u001b[32m 40\u001b[39m task.writes.clear()\n\u001b[32m 41\u001b[39m \u001b[38;5;66;03m# run the task\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m42\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43mproc\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtask\u001b[49m\u001b[43m.\u001b[49m\u001b[43minput\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 43\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m ParentCommand \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 44\u001b[39m ns: \u001b[38;5;28mstr\u001b[39m = config[CONF][CONFIG_KEY_CHECKPOINT_NS]\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py:656\u001b[39m, in \u001b[36mRunnableSeq.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 654\u001b[39m \u001b[38;5;66;03m# run in context\u001b[39;00m\n\u001b[32m 655\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m set_config_context(config, run) \u001b[38;5;28;01mas\u001b[39;00m context:\n\u001b[32m--> \u001b[39m\u001b[32m656\u001b[39m \u001b[38;5;28minput\u001b[39m = \u001b[43mcontext\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 657\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 658\u001b[39m \u001b[38;5;28minput\u001b[39m = step.invoke(\u001b[38;5;28minput\u001b[39m, config)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py:400\u001b[39m, in \u001b[36mRunnableCallable.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 398\u001b[39m run_manager.on_chain_end(ret)\n\u001b[32m 399\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m400\u001b[39m ret = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 401\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.recurse \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(ret, Runnable):\n\u001b[32m 402\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ret.invoke(\u001b[38;5;28minput\u001b[39m, config)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:799\u001b[39m, in \u001b[36mToolNode._func\u001b[39m\u001b[34m(self, input, config, runtime)\u001b[39m\n\u001b[32m 797\u001b[39m input_types = [input_type] * \u001b[38;5;28mlen\u001b[39m(tool_calls)\n\u001b[32m 798\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m get_executor_for_config(config) \u001b[38;5;28;01mas\u001b[39;00m executor:\n\u001b[32m--> \u001b[39m\u001b[32m799\u001b[39m outputs = \u001b[38;5;28;43mlist\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[32m 800\u001b[39m \u001b[43m \u001b[49m\u001b[43mexecutor\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmap\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_run_one\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_calls\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minput_types\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_runtimes\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 801\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 803\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._combine_tool_outputs(outputs, input_type)\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:619\u001b[39m, in \u001b[36mExecutor.map..result_iterator\u001b[39m\u001b[34m()\u001b[39m\n\u001b[32m 616\u001b[39m \u001b[38;5;28;01mwhile\u001b[39;00m fs:\n\u001b[32m 617\u001b[39m \u001b[38;5;66;03m# Careful not to keep a reference to the popped future\u001b[39;00m\n\u001b[32m 618\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m timeout \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m619\u001b[39m \u001b[38;5;28;01myield\u001b[39;00m \u001b[43m_result_or_cancel\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfs\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 620\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 621\u001b[39m \u001b[38;5;28;01myield\u001b[39;00m _result_or_cancel(fs.pop(), end_time - time.monotonic())\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:317\u001b[39m, in \u001b[36m_result_or_cancel\u001b[39m\u001b[34m(***failed resolving arguments***)\u001b[39m\n\u001b[32m 315\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 316\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m317\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfut\u001b[49m\u001b[43m.\u001b[49m\u001b[43mresult\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 318\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 319\u001b[39m fut.cancel()\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:456\u001b[39m, in \u001b[36mFuture.result\u001b[39m\u001b[34m(self, timeout)\u001b[39m\n\u001b[32m 454\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m CancelledError()\n\u001b[32m 455\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._state == FINISHED:\n\u001b[32m--> \u001b[39m\u001b[32m456\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m__get_result\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 457\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 458\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTimeoutError\u001b[39;00m()\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/_base.py:401\u001b[39m, in \u001b[36mFuture.__get_result\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 399\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._exception:\n\u001b[32m 400\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m401\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m._exception\n\u001b[32m 402\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 403\u001b[39m \u001b[38;5;66;03m# Break a reference cycle with the exception in self._exception\u001b[39;00m\n\u001b[32m 404\u001b[39m \u001b[38;5;28mself\u001b[39m = \u001b[38;5;28;01mNone\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m/opt/homebrew/Cellar/python@3.13/3.13.1/Frameworks/Python.framework/Versions/3.13/lib/python3.13/concurrent/futures/thread.py:59\u001b[39m, in \u001b[36m_WorkItem.run\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 56\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 58\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m59\u001b[39m result = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mfn\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 60\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m 61\u001b[39m \u001b[38;5;28mself\u001b[39m.future.set_exception(exc)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/runnables/config.py:546\u001b[39m, in \u001b[36mContextThreadPoolExecutor.map.._wrapped_fn\u001b[39m\u001b[34m(*args)\u001b[39m\n\u001b[32m 545\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_wrapped_fn\u001b[39m(*args: Any) -> T:\n\u001b[32m--> \u001b[39m\u001b[32m546\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mcontexts\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfn\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:1025\u001b[39m, in \u001b[36mToolNode._run_one\u001b[39m\u001b[34m(self, call, input_type, tool_runtime)\u001b[39m\n\u001b[32m 1023\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m\n\u001b[32m 1024\u001b[39m \u001b[38;5;66;03m# Convert to error message\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1025\u001b[39m content = \u001b[43m_handle_tool_error\u001b[49m\u001b[43m(\u001b[49m\u001b[43me\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mflag\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_handle_tool_errors\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1026\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ToolMessage(\n\u001b[32m 1027\u001b[39m content=content,\n\u001b[32m 1028\u001b[39m name=tool_request.tool_call[\u001b[33m\"\u001b[39m\u001b[33mname\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 1029\u001b[39m tool_call_id=tool_request.tool_call[\u001b[33m\"\u001b[39m\u001b[33mid\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m 1030\u001b[39m status=\u001b[33m\"\u001b[39m\u001b[33merror\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 1031\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:424\u001b[39m, in \u001b[36m_handle_tool_error\u001b[39m\u001b[34m(e, flag)\u001b[39m\n\u001b[32m 422\u001b[39m content = flag\n\u001b[32m 423\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28mcallable\u001b[39m(flag):\n\u001b[32m--> \u001b[39m\u001b[32m424\u001b[39m content = \u001b[43mflag\u001b[49m\u001b[43m(\u001b[49m\u001b[43me\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# type: ignore [assignment, call-arg]\u001b[39;00m\n\u001b[32m 425\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 426\u001b[39m msg = (\n\u001b[32m 427\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGot unexpected type of `handle_tool_error`. Expected bool, str \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 428\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mor callable. Received: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mflag\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 429\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:381\u001b[39m, in \u001b[36m_default_handle_tool_errors\u001b[39m\u001b[34m(e)\u001b[39m\n\u001b[32m 379\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(e, ToolInvocationError):\n\u001b[32m 380\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m e.message\n\u001b[32m--> \u001b[39m\u001b[32m381\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m e\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langgraph/prebuilt/tool_node.py:1019\u001b[39m, in \u001b[36mToolNode._run_one\u001b[39m\u001b[34m(self, call, input_type, tool_runtime)\u001b[39m\n\u001b[32m 1017\u001b[39m \u001b[38;5;66;03m# Call wrapper with request and execute callable\u001b[39;00m\n\u001b[32m 1018\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1019\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_wrap_tool_call\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_request\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexecute\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1020\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 1021\u001b[39m \u001b[38;5;66;03m# Wrapper threw an exception\u001b[39;00m\n\u001b[32m 1022\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28mself\u001b[39m._handle_tool_errors:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain/agents/factory.py:465\u001b[39m, in \u001b[36m_chain_tool_call_wrappers..compose_two..composed\u001b[39m\u001b[34m(request, execute)\u001b[39m\n\u001b[32m 462\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m inner(req, execute)\n\u001b[32m 464\u001b[39m \u001b[38;5;66;03m# Outer can call call_inner multiple times\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m465\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mouter\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcall_inner\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py:902\u001b[39m, in \u001b[36mFilesystemMiddleware.wrap_tool_call\u001b[39m\u001b[34m(self, request, handler)\u001b[39m\n\u001b[32m 899\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.tool_token_limit_before_evict \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m \u001b[38;5;129;01mor\u001b[39;00m request.tool_call[\u001b[33m\"\u001b[39m\u001b[33mname\u001b[39m\u001b[33m\"\u001b[39m] \u001b[38;5;129;01min\u001b[39;00m TOOL_GENERATORS:\n\u001b[32m 900\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m handler(request)\n\u001b[32m--> \u001b[39m\u001b[32m902\u001b[39m tool_result = \u001b[43mhandler\u001b[49m\u001b[43m(\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 903\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._intercept_large_tool_result(tool_result, request.runtime)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain/agents/factory.py:462\u001b[39m, in \u001b[36m_chain_tool_call_wrappers..compose_two..composed..call_inner\u001b[39m\u001b[34m(req)\u001b[39m\n\u001b[32m 461\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcall_inner\u001b[39m(req: ToolCallRequest) -> ToolMessage | Command:\n\u001b[32m--> \u001b[39m\u001b[32m462\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43minner\u001b[49m\u001b[43m(\u001b[49m\u001b[43mreq\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mexecute\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:167\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware.wrap_tool_call\u001b[39m\u001b[34m(self, request, handler)\u001b[39m\n\u001b[32m 164\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m ToolMessage(content=observation, tool_call_id=tool_call[\u001b[33m\"\u001b[39m\u001b[33mid\u001b[39m\u001b[33m\"\u001b[39m])\n\u001b[32m 166\u001b[39m \u001b[38;5;66;03m# STEP 5: Handle response type\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m167\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_handle_response\u001b[49m\u001b[43m(\u001b[49m\u001b[43mresponse\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_call\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m.\u001b[49m\u001b[43mruntime\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:244\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware._handle_response\u001b[39m\u001b[34m(self, response, tool_call, runtime)\u001b[39m\n\u001b[32m 242\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._handle_edit(response, tool_call, runtime)\n\u001b[32m 243\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m response_type == \u001b[33m\"\u001b[39m\u001b[33mignore\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m244\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_handle_ignore\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_call\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mruntime\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 245\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m response_type == \u001b[33m\"\u001b[39m\u001b[33mresponse\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m 246\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._handle_response_feedback(response, tool_call, runtime)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:332\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware._handle_ignore\u001b[39m\u001b[34m(self, tool_call, runtime)\u001b[39m\n\u001b[32m 329\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mInvalid tool call: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtool_name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 331\u001b[39m \u001b[38;5;66;03m# Update triage preferences to avoid future false positives\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m332\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_update_triage_preferences_ignore\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtool_name\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mruntime\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 334\u001b[39m \u001b[38;5;66;03m# Return Command with goto END\u001b[39;00m\n\u001b[32m 335\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m Command(\n\u001b[32m 336\u001b[39m goto=END,\n\u001b[32m 337\u001b[39m update={\n\u001b[32m 338\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m: [ToolMessage(content=content, tool_call_id=tool_call_id)]\n\u001b[32m 339\u001b[39m },\n\u001b[32m 340\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/middleware/email_assistant_hitl.py:457\u001b[39m, in \u001b[36mEmailAssistantHITLMiddleware._update_triage_preferences_ignore\u001b[39m\u001b[34m(self, tool_name, runtime)\u001b[39m\n\u001b[32m 448\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 450\u001b[39m messages = runtime.state[\u001b[33m\"\u001b[39m\u001b[33mmessages\u001b[39m\u001b[33m\"\u001b[39m] + [\n\u001b[32m 451\u001b[39m {\n\u001b[32m 452\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mrole\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33m\"\u001b[39m\u001b[33muser\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 453\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mcontent\u001b[39m\u001b[33m\"\u001b[39m: \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mfeedback\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m Follow all instructions above, and remember: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mMEMORY_UPDATE_INSTRUCTIONS_REINFORCEMENT\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m.\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 454\u001b[39m }\n\u001b[32m 455\u001b[39m ]\n\u001b[32m--> \u001b[39m\u001b[32m457\u001b[39m \u001b[43mupdate_memory\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mstore\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnamespace\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/src/personal_assistant/utils.py:313\u001b[39m, in \u001b[36mupdate_memory\u001b[39m\u001b[34m(store, namespace, messages)\u001b[39m\n\u001b[32m 311\u001b[39m \u001b[38;5;66;03m# Update the memory\u001b[39;00m\n\u001b[32m 312\u001b[39m llm = init_chat_model(\u001b[33m\"\u001b[39m\u001b[33manthropic:claude-sonnet-4-5-20250929\u001b[39m\u001b[33m\"\u001b[39m, temperature=\u001b[32m0.0\u001b[39m).with_structured_output(UserPreferences)\n\u001b[32m--> \u001b[39m\u001b[32m313\u001b[39m result = \u001b[43mllm\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 314\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\n\u001b[32m 315\u001b[39m \u001b[43m \u001b[49m\u001b[43m{\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrole\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43msystem\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcontent\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mMEMORY_UPDATE_INSTRUCTIONS\u001b[49m\u001b[43m.\u001b[49m\u001b[43mformat\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcurrent_profile\u001b[49m\u001b[43m=\u001b[49m\u001b[43muser_preferences\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalue\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mnamespace\u001b[49m\u001b[43m=\u001b[49m\u001b[43mnamespace\u001b[49m\u001b[43m)\u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 316\u001b[39m \u001b[43m \u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\n\u001b[32m 317\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 318\u001b[39m \u001b[38;5;66;03m# Save the updated memory to the store\u001b[39;00m\n\u001b[32m 319\u001b[39m store.put(namespace, \u001b[33m\"\u001b[39m\u001b[33muser_preferences\u001b[39m\u001b[33m\"\u001b[39m, result.user_preferences)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py:3127\u001b[39m, in \u001b[36mRunnableSequence.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 3125\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m set_config_context(config) \u001b[38;5;28;01mas\u001b[39;00m context:\n\u001b[32m 3126\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m i == \u001b[32m0\u001b[39m:\n\u001b[32m-> \u001b[39m\u001b[32m3127\u001b[39m input_ = \u001b[43mcontext\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43mstep\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minput_\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 3128\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 3129\u001b[39m input_ = context.run(step.invoke, input_, config)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py:5534\u001b[39m, in \u001b[36mRunnableBindingBase.invoke\u001b[39m\u001b[34m(self, input, config, **kwargs)\u001b[39m\n\u001b[32m 5527\u001b[39m \u001b[38;5;129m@override\u001b[39m\n\u001b[32m 5528\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34minvoke\u001b[39m(\n\u001b[32m 5529\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 5532\u001b[39m **kwargs: Any | \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 5533\u001b[39m ) -> Output:\n\u001b[32m-> \u001b[39m\u001b[32m5534\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mbound\u001b[49m\u001b[43m.\u001b[49m\u001b[43minvoke\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 5535\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 5536\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_merge_configs\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 5537\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43m{\u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 5538\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:398\u001b[39m, in \u001b[36mBaseChatModel.invoke\u001b[39m\u001b[34m(self, input, config, stop, **kwargs)\u001b[39m\n\u001b[32m 384\u001b[39m \u001b[38;5;129m@override\u001b[39m\n\u001b[32m 385\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34minvoke\u001b[39m(\n\u001b[32m 386\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 391\u001b[39m **kwargs: Any,\n\u001b[32m 392\u001b[39m ) -> AIMessage:\n\u001b[32m 393\u001b[39m config = ensure_config(config)\n\u001b[32m 394\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m cast(\n\u001b[32m 395\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mAIMessage\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 396\u001b[39m cast(\n\u001b[32m 397\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mChatGeneration\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m--> \u001b[39m\u001b[32m398\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mgenerate_prompt\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 399\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_convert_input\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43minput\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 400\u001b[39m \u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 401\u001b[39m \u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mcallbacks\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 402\u001b[39m \u001b[43m \u001b[49m\u001b[43mtags\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtags\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 403\u001b[39m \u001b[43m \u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmetadata\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 404\u001b[39m \u001b[43m \u001b[49m\u001b[43mrun_name\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrun_name\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 405\u001b[39m \u001b[43m \u001b[49m\u001b[43mrun_id\u001b[49m\u001b[43m=\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m.\u001b[49m\u001b[43mpop\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mrun_id\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 406\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 407\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m.generations[\u001b[32m0\u001b[39m][\u001b[32m0\u001b[39m],\n\u001b[32m 408\u001b[39m ).message,\n\u001b[32m 409\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:1117\u001b[39m, in \u001b[36mBaseChatModel.generate_prompt\u001b[39m\u001b[34m(self, prompts, stop, callbacks, **kwargs)\u001b[39m\n\u001b[32m 1108\u001b[39m \u001b[38;5;129m@override\u001b[39m\n\u001b[32m 1109\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mgenerate_prompt\u001b[39m(\n\u001b[32m 1110\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 1114\u001b[39m **kwargs: Any,\n\u001b[32m 1115\u001b[39m ) -> LLMResult:\n\u001b[32m 1116\u001b[39m prompt_messages = [p.to_messages() \u001b[38;5;28;01mfor\u001b[39;00m p \u001b[38;5;129;01min\u001b[39;00m prompts]\n\u001b[32m-> \u001b[39m\u001b[32m1117\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mgenerate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mprompt_messages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m=\u001b[49m\u001b[43mcallbacks\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:927\u001b[39m, in \u001b[36mBaseChatModel.generate\u001b[39m\u001b[34m(self, messages, stop, callbacks, tags, metadata, run_name, run_id, **kwargs)\u001b[39m\n\u001b[32m 924\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m i, m \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(input_messages):\n\u001b[32m 925\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m 926\u001b[39m results.append(\n\u001b[32m--> \u001b[39m\u001b[32m927\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_generate_with_cache\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 928\u001b[39m \u001b[43m \u001b[49m\u001b[43mm\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 929\u001b[39m \u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 930\u001b[39m \u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrun_managers\u001b[49m\u001b[43m[\u001b[49m\u001b[43mi\u001b[49m\u001b[43m]\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mrun_managers\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mNone\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 931\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 932\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 933\u001b[39m )\n\u001b[32m 934\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mBaseException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 935\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m run_managers:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py:1221\u001b[39m, in \u001b[36mBaseChatModel._generate_with_cache\u001b[39m\u001b[34m(self, messages, stop, run_manager, **kwargs)\u001b[39m\n\u001b[32m 1219\u001b[39m result = generate_from_stream(\u001b[38;5;28miter\u001b[39m(chunks))\n\u001b[32m 1220\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m inspect.signature(\u001b[38;5;28mself\u001b[39m._generate).parameters.get(\u001b[33m\"\u001b[39m\u001b[33mrun_manager\u001b[39m\u001b[33m\"\u001b[39m):\n\u001b[32m-> \u001b[39m\u001b[32m1221\u001b[39m result = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_generate\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 1222\u001b[39m \u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstop\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m=\u001b[49m\u001b[43mrun_manager\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\n\u001b[32m 1223\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1224\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[32m 1225\u001b[39m result = \u001b[38;5;28mself\u001b[39m._generate(messages, stop=stop, **kwargs)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py:1917\u001b[39m, in \u001b[36mChatAnthropic._generate\u001b[39m\u001b[34m(self, messages, stop, run_manager, **kwargs)\u001b[39m\n\u001b[32m 1915\u001b[39m data = \u001b[38;5;28mself\u001b[39m._create(payload)\n\u001b[32m 1916\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m anthropic.BadRequestError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m-> \u001b[39m\u001b[32m1917\u001b[39m \u001b[43m_handle_anthropic_bad_request\u001b[49m\u001b[43m(\u001b[49m\u001b[43me\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1918\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._format_output(data, **kwargs)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py:1915\u001b[39m, in \u001b[36mChatAnthropic._generate\u001b[39m\u001b[34m(self, messages, stop, run_manager, **kwargs)\u001b[39m\n\u001b[32m 1913\u001b[39m payload = \u001b[38;5;28mself\u001b[39m._get_request_payload(messages, stop=stop, **kwargs)\n\u001b[32m 1914\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1915\u001b[39m data = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_create\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpayload\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1916\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m anthropic.BadRequestError \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m 1917\u001b[39m _handle_anthropic_bad_request(e)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py:1777\u001b[39m, in \u001b[36mChatAnthropic._create\u001b[39m\u001b[34m(self, payload)\u001b[39m\n\u001b[32m 1775\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[33m\"\u001b[39m\u001b[33mbetas\u001b[39m\u001b[33m\"\u001b[39m \u001b[38;5;129;01min\u001b[39;00m payload:\n\u001b[32m 1776\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m._client.beta.messages.create(**payload)\n\u001b[32m-> \u001b[39m\u001b[32m1777\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_client\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m.\u001b[49m\u001b[43mcreate\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mpayload\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py:282\u001b[39m, in \u001b[36mrequired_args..inner..wrapper\u001b[39m\u001b[34m(*args, **kwargs)\u001b[39m\n\u001b[32m 280\u001b[39m msg = \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mMissing required argument: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mquote(missing[\u001b[32m0\u001b[39m])\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 281\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(msg)\n\u001b[32m--> \u001b[39m\u001b[32m282\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py:930\u001b[39m, in \u001b[36mMessages.create\u001b[39m\u001b[34m(self, max_tokens, messages, model, metadata, service_tier, stop_sequences, stream, system, temperature, thinking, tool_choice, tools, top_k, top_p, extra_headers, extra_query, extra_body, timeout)\u001b[39m\n\u001b[32m 923\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m model \u001b[38;5;129;01min\u001b[39;00m DEPRECATED_MODELS:\n\u001b[32m 924\u001b[39m warnings.warn(\n\u001b[32m 925\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mThe model \u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mmodel\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m is deprecated and will reach end-of-life on \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mDEPRECATED_MODELS[model]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33mPlease migrate to a newer model. Visit https://docs.anthropic.com/en/docs/resources/model-deprecations for more information.\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 926\u001b[39m \u001b[38;5;167;01mDeprecationWarning\u001b[39;00m,\n\u001b[32m 927\u001b[39m stacklevel=\u001b[32m3\u001b[39m,\n\u001b[32m 928\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m930\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_post\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 931\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43m/v1/messages\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 932\u001b[39m \u001b[43m \u001b[49m\u001b[43mbody\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmaybe_transform\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 933\u001b[39m \u001b[43m \u001b[49m\u001b[43m{\u001b[49m\n\u001b[32m 934\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmax_tokens\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmax_tokens\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 935\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmessages\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmessages\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 936\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmodel\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmodel\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 937\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mmetadata\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mmetadata\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 938\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mservice_tier\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mservice_tier\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 939\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mstop_sequences\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mstop_sequences\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 940\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mstream\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 941\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43msystem\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43msystem\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 942\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtemperature\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtemperature\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 943\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mthinking\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mthinking\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 944\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtool_choice\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtool_choice\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 945\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtools\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtools\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 946\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtop_k\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtop_k\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 947\u001b[39m \u001b[43m \u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mtop_p\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mtop_p\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 948\u001b[39m \u001b[43m \u001b[49m\u001b[43m}\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 949\u001b[39m \u001b[43m \u001b[49m\u001b[43mmessage_create_params\u001b[49m\u001b[43m.\u001b[49m\u001b[43mMessageCreateParamsStreaming\u001b[49m\n\u001b[32m 950\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mif\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\n\u001b[32m 951\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01melse\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mmessage_create_params\u001b[49m\u001b[43m.\u001b[49m\u001b[43mMessageCreateParamsNonStreaming\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 952\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 953\u001b[39m \u001b[43m \u001b[49m\u001b[43moptions\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmake_request_options\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 954\u001b[39m \u001b[43m \u001b[49m\u001b[43mextra_headers\u001b[49m\u001b[43m=\u001b[49m\u001b[43mextra_headers\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mextra_query\u001b[49m\u001b[43m=\u001b[49m\u001b[43mextra_query\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mextra_body\u001b[49m\u001b[43m=\u001b[49m\u001b[43mextra_body\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtimeout\u001b[49m\u001b[43m=\u001b[49m\u001b[43mtimeout\u001b[49m\n\u001b[32m 955\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 956\u001b[39m \u001b[43m \u001b[49m\u001b[43mcast_to\u001b[49m\u001b[43m=\u001b[49m\u001b[43mMessage\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 957\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01mor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[32m 958\u001b[39m \u001b[43m \u001b[49m\u001b[43mstream_cls\u001b[49m\u001b[43m=\u001b[49m\u001b[43mStream\u001b[49m\u001b[43m[\u001b[49m\u001b[43mRawMessageStreamEvent\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 959\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/_base_client.py:1326\u001b[39m, in \u001b[36mSyncAPIClient.post\u001b[39m\u001b[34m(self, path, cast_to, body, options, files, stream, stream_cls)\u001b[39m\n\u001b[32m 1312\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mpost\u001b[39m(\n\u001b[32m 1313\u001b[39m \u001b[38;5;28mself\u001b[39m,\n\u001b[32m 1314\u001b[39m path: \u001b[38;5;28mstr\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 1321\u001b[39m stream_cls: \u001b[38;5;28mtype\u001b[39m[_StreamT] | \u001b[38;5;28;01mNone\u001b[39;00m = \u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[32m 1322\u001b[39m ) -> ResponseT | _StreamT:\n\u001b[32m 1323\u001b[39m opts = FinalRequestOptions.construct(\n\u001b[32m 1324\u001b[39m method=\u001b[33m\"\u001b[39m\u001b[33mpost\u001b[39m\u001b[33m\"\u001b[39m, url=path, json_data=body, files=to_httpx_files(files), **options\n\u001b[32m 1325\u001b[39m )\n\u001b[32m-> \u001b[39m\u001b[32m1326\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m cast(ResponseT, \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mrequest\u001b[49m\u001b[43m(\u001b[49m\u001b[43mcast_to\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mopts\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstream\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mstream_cls\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstream_cls\u001b[49m\u001b[43m)\u001b[49m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/Desktop/Code/deepagents/examples/personal_assistant/.venv/lib/python3.13/site-packages/anthropic/_base_client.py:1114\u001b[39m, in \u001b[36mSyncAPIClient.request\u001b[39m\u001b[34m(self, cast_to, options, stream, stream_cls)\u001b[39m\n\u001b[32m 1111\u001b[39m err.response.read()\n\u001b[32m 1113\u001b[39m log.debug(\u001b[33m\"\u001b[39m\u001b[33mRe-raising status error\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m-> \u001b[39m\u001b[32m1114\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;28mself\u001b[39m._make_status_error_from_response(err.response) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[32m 1116\u001b[39m \u001b[38;5;28;01mbreak\u001b[39;00m\n\u001b[32m 1118\u001b[39m \u001b[38;5;28;01massert\u001b[39;00m response \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[33m\"\u001b[39m\u001b[33mcould not resolve response (should never happen)\u001b[39m\u001b[33m\"\u001b[39m\n", - "\u001b[31mBadRequestError\u001b[39m: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.24: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_015WrAQ82bzbKUbET1uhfixd. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'}, 'request_id': 'req_011CViX58yA9hzBebZGRqkgP'}", - "During task with name 'tools' and id 'd4296337-4ca9-f786-8a1d-b63b4da1765e'" + "name": "stdout", + "output_type": "stream", + "text": [ + "📋 Updated User Profile After Rejection:\n", + "============================================================\n", + "I'm Lance, a software engineer at LangChain. When triaging emails:\n", + "- Respond to: Direct questions about technical work, project inquiries, family/self-care reminders\n", + "- Notify: GitHub notifications, deadline reminders, FYI project updates, team status messages, meeting requests about project roadmap discussions, meeting requests from @company.com addresses\n", + "- Ignore: Marketing newsletters, promotional emails, CC'd FYI threads\n", + "\n", + "Response style:\n", + "- Professional and concise tone\n", + "- Acknowledge deadlines explicitly in responses\n", + "- For technical questions: State investigation approach and timeline\n", + "- For event invitations: Ask about workshops, discounts, and deadlines; don't commit immediately\n", + "- For collaboration requests: Acknowledge existing materials, mention reviewing them\n", + "\n", + "Meeting scheduling:\n", + "- Prefer 30-minute meetings (15 minutes acceptable)\n", + "- If times proposed: Verify all options, commit to one or decline\n", + "- If no times proposed: Offer multiple options instead of picking one\n", + "- Reference meeting purpose and duration in response\n", + "- Not interested in general project roadmap discussion meetings\n", + "\n", + "============================================================\n", + "🔍 Changes from rejection:\n", + " - Should now include guidance about newsletters\n", + " - Likely updated 'Notify' or 'Ignore' section\n", + "============================================================\n" ] } ], "source": [ - "result_1_continued = resume_with_decision(agent, config_1, decision_type=\"ignore\")\n", - "format_messages(result_1_continued[\"messages\"])" - ] - }, - { - "cell_type": "markdown", - "id": "7hodvbagtn5", - "metadata": {}, - "source": [ - "### Resume Example (if interrupted)\n", - "\n", - "If the agent was interrupted, you can resume with a decision:" - ] - }, - { - "cell_type": "markdown", - "id": "example2", - "metadata": {}, - "source": [ - "## Example 2: Simple Question\n", + "# Get updated user profile\n", + "updated_profile = get_user_profile_from_store(agent)\n", "\n", - "Test with a simple question that needs a direct response." + "print(\"📋 Updated User Profile After Rejection:\")\n", + "print(\"=\"*60)\n", + "if updated_profile:\n", + " print(updated_profile)\n", + " \n", + " # Highlight the differences\n", + " print(\"\\n\" + \"=\"*60)\n", + " print(\"🔍 Changes from rejection:\")\n", + " print(\" - Should now include guidance about newsletters\")\n", + " print(\" - Likely updated 'Notify' or 'Ignore' section\")\n", + "else:\n", + " print(\"(No profile found)\")\n", + "print(\"=\"*60)" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "example2_email", - "metadata": {}, - "outputs": [], - "source": "# Example email as markdown string\nemail_markdown_2 = \"\"\"\n**Subject**: LangGraph documentation link\n**From**: bob@example.com\n**To**: lance@langchain.dev\n\nHey Lance,\n\nCould you send me the link to the LangGraph docs?\n\nThanks!\nBob\n\n---\n\"\"\"\n\n# Display email with rich formatting\nshow_prompt(email_markdown_2, title=\"📧 Email to Process\", border_style=\"cyan\")" - }, - { - "cell_type": "code", - "execution_count": null, - "id": "example2_invoke", - "metadata": {}, - "outputs": [], - "source": "config_2 = {\"configurable\": {\"thread_id\": \"test-thread-2\"}}\n\n# Invoke agent with markdown email string\nresult_2 = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_2}]},\n config=config_2,\n)\n\n# Display result with rich formatting\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response\")\nprint(\"=\"*60 + \"\\n\")\n\n# Check for interrupts (HITL)\nif \"__interrupt__\" in result_2:\n print(\"🛑 INTERRUPT: Agent is waiting for your approval\\n\")\n \n for interrupt in result_2[\"__interrupt__\"]:\n for request in interrupt.value:\n print(\"Action:\", request[\"action_request\"][\"action\"])\n print(\"Args:\", request[\"action_request\"][\"args\"])\n print(\"\\nAllowed actions:\", request[\"config\"])\n print(\"\\nDescription:\")\n show_prompt(request[\"description\"], title=\"📋 Action Details\", border_style=\"yellow\")\n print(\"\\n\" + \"-\"*60)\n \n print(\"\\n💡 To continue, you need to resume the agent with a decision.\")\n print(\" Use: agent.invoke(None, config=config_2)\")\nelse:\n format_messages(result_2[\"messages\"])" - }, { "cell_type": "markdown", - "id": "test_memory", + "id": "n55nfsq3hz", "metadata": {}, "source": [ - "## Test Memory Persistence\n", - "\n", - "Test that memory persists across invocations using the same thread ID." + "We now see `meeting requests from @company.com addresses` added to `ignore`." ] }, - { - "cell_type": "markdown", - "id": "p8oc6m0fwzo", - "source": "## Example 3: Triage Functionality\n\nThe agent now uses a triage tool to classify emails before taking action. This example shows how the agent triages a marketing email.", - "metadata": {} - }, - { - "cell_type": "code", - "id": "yeib3g2uxlf", - "source": "# Marketing email as markdown string\nemail_markdown_marketing = \"\"\"\n**Subject**: Limited Time Offer - 50% Off!\n**From**: marketing@newsletter.com\n**To**: lance@langchain.dev\n\nDon't miss out on our amazing sale! Click here to shop now and save big on all your favorite items. This offer expires soon!\n\n---\n\"\"\"\n\n# Display email\nshow_prompt(email_markdown_marketing, title=\"📧 Marketing Email\", border_style=\"cyan\")\n\n# Invoke agent\nconfig_marketing = {\"configurable\": {\"thread_id\": \"test-marketing\"}}\nresult_marketing = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_marketing}]},\n config=config_marketing,\n)\n\n# Show triage decision\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Decision\")\nprint(\"=\"*60 + \"\\n\")\n\nmessages = result_marketing.get(\"messages\", [])\nfor i, msg in enumerate(messages):\n if hasattr(msg, 'tool_calls') and msg.tool_calls:\n for tool_call in msg.tool_calls:\n if tool_call.get('name') == 'triage_email':\n print(\"🔍 TRIAGE DECISION:\")\n print(f\" Classification: {tool_call.get('args', {}).get('classification', 'N/A')}\")\n print(f\" Reasoning: {tool_call.get('args', {}).get('reasoning', 'N/A')}\")\n print()\n if hasattr(msg, 'name') and msg.name == 'triage_email':\n print(f\"✓ Result: {msg.content}\")\n print()\n\nprint(\"Since this was classified as 'ignore', the agent calls Done immediately without drafting a response.\")", - "metadata": {}, - "execution_count": null, - "outputs": [] - }, { "cell_type": "code", "execution_count": null, - "id": "test_memory_code", - "metadata": {}, - "outputs": [], - "source": "# Follow-up email as markdown string\nemail_markdown_3 = \"\"\"\n**Subject**: Follow-up meeting\n**From**: jane@example.com\n**To**: lance@langchain.dev\n\nHi again,\n\nCan we also schedule a follow-up meeting next week?\n\nJane\n\n---\n\"\"\"\n\n# Use same config as example 1 (same thread_id) to test memory persistence\nresult_3 = agent.invoke(\n {\"messages\": [{\"role\": \"user\", \"content\": email_markdown_3}]},\n config=config_1, # Same thread as example 1\n)\n\n# Display result with rich formatting (show last 5 messages)\nprint(\"\\n\" + \"=\"*60)\nprint(\"Agent Response (with memory from previous conversation)\")\nprint(\"=\"*60 + \"\\n\")\nformat_messages(result_3[\"messages\"][-5:])" - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bb4fc2a3-63ce-4f16-9003-c37294989355", + "id": "76fhzej78ra", "metadata": {}, "outputs": [], "source": [] @@ -1374,4 +1667,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} \ No newline at end of file +}