diff --git a/backend/src/analytics_agent/agent/graph.py b/backend/src/analytics_agent/agent/graph.py index 4e44833..da0de89 100644 --- a/backend/src/analytics_agent/agent/graph.py +++ b/backend/src/analytics_agent/agent/graph.py @@ -3,14 +3,16 @@ from typing import Literal import orjson -from langchain.agents import create_agent +from deepagents import create_deep_agent from langchain_core.messages import SystemMessage, ToolMessage +from langgraph.checkpoint.memory import InMemorySaver from langgraph.graph import END, START, StateGraph from analytics_agent.agent.llm import get_llm from analytics_agent.agent.state import AgentState from analytics_agent.config import settings from analytics_agent.prompts.system import build_system_prompt +from analytics_agent.skills.loader import build_skill_sources # Write-back skills are opt-in; only included when explicitly enabled by the user _SKILL_TOOL_NAMES: frozenset[str] = frozenset({"publish_analysis", "save_correction"}) @@ -45,14 +47,21 @@ def build_graph( context_tools: list | None = None, # pre-built from DB context platforms at request time engine_tools: list | None = None, # pre-built for MCP data sources (bypasses QueryEngine) ): + """Build the agent graph backed by `deepagents.create_deep_agent`. + + The inner agent gains a planning tool (`write_todos`) and a virtual + filesystem (`ls`/`read_file`/`write_file`/`edit_file`) — keeping + high-volume turns out of the parent's context window. + + The outer `StateGraph` keeps the conditional `chart_node` post-step. + """ from analytics_agent.agent.chart_generator import chart_node + from analytics_agent.agent.chart_tool import create_chart from analytics_agent.engines.factory import get_registry disabled = disabled_tools or set() llm = get_llm(streaming=True) - from analytics_agent.agent.chart_tool import create_chart - # Context platform tools — built dynamically from DB at request time. # Falls back to env-var based build only when caller doesn't provide them. if context_tools is not None: @@ -77,23 +86,14 @@ def build_graph( if not engine: raise ValueError(f"Engine '{engine_name}' not found.") engine_tools = [t for t in engine.get_tools() if t.name not in disabled] + chart_tools = [] if "create_chart" in disabled else [create_chart] all_tools = datahub_tools + skill_tools + engine_tools + chart_tools if system_prompt_override: - from analytics_agent.skills.loader import ( - get_improve_context_prompt_section, - get_search_business_context_section, - get_skill_system_prompt_section, - ) - system_prompt = system_prompt_override.format(engine_name=engine_name) - system_prompt += get_search_business_context_section() - system_prompt += get_improve_context_prompt_section() - if enabled_mutations: - system_prompt += get_skill_system_prompt_section(enabled_mutations) else: - system_prompt = build_system_prompt(engine_name, enabled_skills=enabled_mutations) + system_prompt = build_system_prompt(engine_name) # Enable per-tool error handling so validation errors (e.g. hallucinated # arguments like filter= on get_entities) are returned as tool messages @@ -127,15 +127,15 @@ def build_graph( ] ) - react_agent = create_agent( + deep_agent = create_deep_agent( model=llm, tools=all_tools, - state_schema=AgentState, system_prompt=system_for_agent, + skills=build_skill_sources(enabled_mutations), ) graph = StateGraph(AgentState) - graph.add_node("agent", react_agent) + graph.add_node("agent", deep_agent) graph.add_node("chart", chart_node) graph.add_edge(START, "agent") graph.add_conditional_edges( @@ -145,4 +145,9 @@ def build_graph( ) graph.add_edge("chart", END) - return graph.compile() + # An InMemorySaver lets the streaming layer call `aget_state(subgraphs=True)` + # to snapshot the deepagents virtual filesystem (the `files` DeltaChannel + # doesn't reliably propagate to the outer graph mid-step). `build_graph` is + # called fresh per request, so this checkpointer is per-turn — no state + # leaks across turns and history is still replayed from the DB each time. + return graph.compile(checkpointer=InMemorySaver()) diff --git a/backend/src/analytics_agent/agent/streaming.py b/backend/src/analytics_agent/agent/streaming.py index c994648..bcc7468 100644 --- a/backend/src/analytics_agent/agent/streaming.py +++ b/backend/src/analytics_agent/agent/streaming.py @@ -12,6 +12,59 @@ re.DOTALL, ) +# Per-conversation snapshot of the deepagents virtual filesystem. Populated +# during streaming whenever write_file/edit_file fires (and at end-of-turn). +# The `/files` HTTP endpoint reads this so the Files panel can survive a reload +# without depending on outer-graph checkpoint propagation, which is unreliable +# for the `files` DeltaChannel. +FILES_SNAPSHOTS: dict[str, dict[str, str]] = {} + + +def _normalize_files(raw: Any) -> dict[str, str]: + """Coerce a deepagents `files` channel value into {path: content_str}. + + deepagents stores entries as `FileData` dicts (`{content, encoding, ...}`) + in v2 format and plain strings in v1. We surface only the content for the + UI panel — encoding/mtime aren't displayed. + """ + if not isinstance(raw, dict): + return {} + out: dict[str, str] = {} + for path, data in raw.items(): + if isinstance(data, str): + out[path] = data + elif isinstance(data, dict): + content = data.get("content") + if isinstance(content, str): + out[path] = content + elif isinstance(content, list): + # v1 stored content as list[str] (lines) + out[path] = "\n".join(str(line) for line in content) + return out + + +async def _snapshot_virtual_files(graph: Any, cfg: dict) -> dict[str, str]: + """Read the current virtual-filesystem snapshot from the live graph. + + `aget_state(subgraphs=True)` walks into the deepagents subgraph so we pick + up files even when the outer state's `files` channel hasn't received the + subgraph's delta yet (DeltaChannel propagation across StateGraph nodes is + not guaranteed to be synchronous within a single agent step). + """ + try: + snap = await graph.aget_state(cfg, subgraphs=True) + except Exception: + return {} + files = _normalize_files((snap.values or {}).get("files")) + # Walk pending subgraph tasks; deepagents writes files inside the inner + # agent's state and we want those even mid-step. + for task in getattr(snap, "tasks", ()) or (): + sub = getattr(task, "state", None) + sub_values = getattr(sub, "values", None) if sub is not None else None + if isinstance(sub_values, dict): + files.update(_normalize_files(sub_values.get("files"))) + return files + def _extract_chart_from_text(text: str) -> dict | None: """Extract a Vega-Lite chart spec if the model output it as a JSON code block.""" @@ -68,12 +121,28 @@ async def stream_graph_events( final_state: dict[str, Any] = {} chart_emitted = False # guard against double-emitting CHART + # A per-turn thread id so the (per-request) InMemorySaver checkpoints this + # run's state, letting us snapshot the deepagents virtual filesystem via + # `aget_state`. The graph is rebuilt per request, so this never leaks state. + cfg: dict[str, Any] = {"configurable": {"thread_id": conversation_id}} + + def _emit_files(files: dict[str, str]) -> dict | None: + """Cache + build a FILES_UPDATE event, or None if nothing changed.""" + if FILES_SNAPSHOTS.get(conversation_id) == files: + return None + FILES_SNAPSHOTS[conversation_id] = files + return { + "event": "FILES_UPDATE", + "conversation_id": conversation_id, + "message_id": str(uuid.uuid4()), + "payload": {"files": files}, + } + try: from analytics_agent.config import settings as _settings - async for event in graph.astream_events( - inputs, version="v2", config={"recursion_limit": _settings.agent_recursion_limit} - ): + cfg["recursion_limit"] = _settings.agent_recursion_limit + async for event in graph.astream_events(inputs, version="v2", config=cfg): event_type: str = event.get("event", "") data: dict[str, Any] = event.get("data", {}) name: str = event.get("name", "") @@ -117,6 +186,16 @@ async def stream_graph_events( # create_chart renders as a CHART event — don't show a tool call bubble if name == "create_chart": continue + # `write_todos` is the deepagents planning tool — surface as a + # dedicated TODOS event so the UI plan panel can subscribe. + if name == "write_todos": + yield { + "event": "TODOS", + "conversation_id": conversation_id, + "message_id": str(uuid.uuid4()), + "payload": {"todos": tool_input.get("todos", [])}, + } + continue yield { "event": "TOOL_CALL", "conversation_id": conversation_id, @@ -141,6 +220,18 @@ async def stream_graph_events( # ── SQL / TOOL_RESULT / CHART ── elif event_type == "on_tool_end": + # File-mutating deepagents tools — re-snapshot the virtual FS so + # the Files panel updates live as the agent writes scratch files. + if name in ("write_file", "edit_file"): + files_evt = _emit_files(await _snapshot_virtual_files(graph, cfg)) + if files_evt is not None: + yield files_evt + + if name == "write_todos": + # The TODOS event was already emitted at tool_start with the + # full planned list; the tool_end output is just an ack. + continue + output = data.get("output", "") if hasattr(output, "content"): output = output.content @@ -311,6 +402,13 @@ async def stream_graph_events( if clean_text != full_text: final_text_parts[:] = [clean_text] + # End-of-turn filesystem snapshot — catches files the FilesystemMiddleware + # offloaded large tool results into (e.g. /large_tool_results/) that no + # write_file/edit_file event covered. + files_evt = _emit_files(await _snapshot_virtual_files(graph, cfg)) + if files_evt is not None: + yield files_evt + yield { "event": "COMPLETE", "conversation_id": conversation_id, diff --git a/backend/src/analytics_agent/api/chat.py b/backend/src/analytics_agent/api/chat.py index d9c63fa..c6f094a 100644 --- a/backend/src/analytics_agent/api/chat.py +++ b/backend/src/analytics_agent/api/chat.py @@ -318,7 +318,11 @@ def _broadcast(evt: dict) -> None: keepalive_interval=keepalive_interval, history=history, ): - if evt.get("event") not in (None, "KEEPALIVE"): + # FILES_UPDATE carries the full virtual-FS snapshot (potentially + # large, re-sent on every write); don't persist it — the Files + # panel re-fetches via GET /files on reload. Everything else, + # including the small TODOS plan, is persisted for history. + if evt.get("event") not in (None, "KEEPALIVE", "FILES_UPDATE"): with contextlib.suppress(Exception): await _persist_message( session, @@ -466,6 +470,19 @@ async def send_message( ) +@router.get("/{conversation_id}/files") +async def get_virtual_files(conversation_id: str) -> dict[str, Any]: + """Return the deepagents virtual filesystem snapshot for this conversation. + + The streaming layer maintains an in-memory snapshot (`FILES_SNAPSHOTS`) as + write_file / edit_file fire and at end-of-turn, so the UI's Files panel can + survive a reload without re-reading the (per-turn, ephemeral) checkpoint. + """ + from analytics_agent.agent.streaming import FILES_SNAPSHOTS + + return {"files": FILES_SNAPSHOTS.get(conversation_id, {})} + + @router.get("/{conversation_id}/stream") async def reattach_stream(conversation_id: str): """Reattach to an in-progress agent stream after switching conversations.""" diff --git a/backend/src/analytics_agent/prompts/system.py b/backend/src/analytics_agent/prompts/system.py index c925791..8f9457b 100644 --- a/backend/src/analytics_agent/prompts/system.py +++ b/backend/src/analytics_agent/prompts/system.py @@ -10,24 +10,13 @@ def get_prompt_template() -> str: def build_system_prompt( engine_name: str, - enabled_skills: set[str] | None = None, + enabled_skills: set[str] | None = None, # accepted for caller compat; unused ) -> str: - from analytics_agent.skills.loader import ( - get_improve_context_prompt_section, - get_search_business_context_section, - get_skill_system_prompt_section, - ) + """Render the base system prompt. + Skill bodies are no longer injected here — `SkillsMiddleware` handles + progressive disclosure. The `enabled_skills` parameter is kept so callers + don't break, but is not consumed. + """ today = date.today().strftime("%B %d, %Y") - base = get_prompt_template().format(engine_name=engine_name, today=today) - - # Always inject always-on meta-skills - base = base + get_search_business_context_section() - base = base + get_improve_context_prompt_section() - - if enabled_skills: - skills_section = get_skill_system_prompt_section(enabled_skills) - if skills_section: - base = base + skills_section - - return base + return get_prompt_template().format(engine_name=engine_name, today=today) diff --git a/backend/src/analytics_agent/prompts/system_prompt.md b/backend/src/analytics_agent/prompts/system_prompt.md index 02e6af7..070d897 100644 --- a/backend/src/analytics_agent/prompts/system_prompt.md +++ b/backend/src/analytics_agent/prompts/system_prompt.md @@ -30,6 +30,39 @@ Your goal is to answer the user's data questions by: Use this whenever the user asks for a chart, graph, plot, or visualization. You can call it with data from DataHub search results, SQL results, or any table you've assembled. +## Planning with write_todos + +For any non-trivial question — anything that will take more than one or two +tool calls — start by calling `write_todos` to lay out a short plan, and update +it as steps complete; skip planning only for simple lookups that resolve in a +single call. + +## Working filesystem & auto-evicted tool results + +You have a per-turn scratch filesystem (`ls`, `read_file`, `write_file`, +`edit_file`, `glob`, `grep`). Use it to stash intermediate results +between tool calls instead of dumping them back into your reply — +e.g. a long entity list before filtering, a SQL draft, notes between +sub-agent calls. The filesystem is **not persistent**: it lives for the +duration of the turn, not across conversations. + +**Auto-evicted large tool results.** When a tool returns a payload +larger than the inline threshold (≈ 20K tokens — e.g. a wide +`get_lineage`, a `get_entities` with 50 URNs, a 5K-row SQL result), +the harness automatically writes the full content to +`/large_tool_results/` and replaces the inline tool +message with a head/tail preview plus that path. **When you see a path +like `/large_tool_results/tooluse_…` in a tool result, it's real** — +`read_file` it, or `grep` across `/large_tool_results/` to pull only +the bits you need. Don't `read_file` the whole offloaded blob just to +extract one field; `grep` for the field name first, then `read_file` +the matching slice. + +This is also a useful pattern even when a result isn't auto-evicted: +if you're about to do multi-step filtering on a big tool response, +`write_file` it once and `grep`/`read_file` for what you need rather +than restating the whole blob in your reasoning. + ## Core principles ### Documentation is authoritative about *intent*; the catalog is authoritative about *existence* diff --git a/backend/src/analytics_agent/skills/improve-context/SKILL.md b/backend/src/analytics_agent/skills/library/improve-context/SKILL.md similarity index 99% rename from backend/src/analytics_agent/skills/improve-context/SKILL.md rename to backend/src/analytics_agent/skills/library/improve-context/SKILL.md index 0ebe659..460bddf 100644 --- a/backend/src/analytics_agent/skills/improve-context/SKILL.md +++ b/backend/src/analytics_agent/skills/library/improve-context/SKILL.md @@ -1,5 +1,5 @@ --- -name: improve_context +name: improve-context description: Use this skill when the user types /improve-context or asks to capture learnings, improve documentation, or enrich the knowledge base based on this conversation. metadata: author: analytics-agent diff --git a/backend/src/analytics_agent/skills/publish-analysis/SKILL.md b/backend/src/analytics_agent/skills/library/publish-analysis/SKILL.md similarity index 99% rename from backend/src/analytics_agent/skills/publish-analysis/SKILL.md rename to backend/src/analytics_agent/skills/library/publish-analysis/SKILL.md index c96e083..dec2994 100644 --- a/backend/src/analytics_agent/skills/publish-analysis/SKILL.md +++ b/backend/src/analytics_agent/skills/library/publish-analysis/SKILL.md @@ -1,5 +1,5 @@ --- -name: publish_analysis +name: publish-analysis description: > Use this skill when the user wants to publish, share, save, or preserve a completed data analysis so others in the org can find it. Saves as a versioned diff --git a/backend/src/analytics_agent/skills/save-correction/SKILL.md b/backend/src/analytics_agent/skills/library/save-correction/SKILL.md similarity index 99% rename from backend/src/analytics_agent/skills/save-correction/SKILL.md rename to backend/src/analytics_agent/skills/library/save-correction/SKILL.md index 200d6e6..8dd8723 100644 --- a/backend/src/analytics_agent/skills/save-correction/SKILL.md +++ b/backend/src/analytics_agent/skills/library/save-correction/SKILL.md @@ -1,5 +1,5 @@ --- -name: save_correction +name: save-correction description: > Use this skill when the user identifies that knowledge in DataHub is wrong, incomplete, or missing — whether that's a glossary term definition, a domain diff --git a/backend/src/analytics_agent/skills/search-business-context/SKILL.md b/backend/src/analytics_agent/skills/library/search-business-context/SKILL.md similarity index 99% rename from backend/src/analytics_agent/skills/search-business-context/SKILL.md rename to backend/src/analytics_agent/skills/library/search-business-context/SKILL.md index cd29e0b..e634344 100644 --- a/backend/src/analytics_agent/skills/search-business-context/SKILL.md +++ b/backend/src/analytics_agent/skills/library/search-business-context/SKILL.md @@ -1,5 +1,5 @@ --- -name: search_business_context +name: search-business-context description: > Call this FIRST whenever the user's question names a business concept, metric, or domain (e.g. revenue, churn, active seller, delivery SLA, marketing, diff --git a/backend/src/analytics_agent/skills/loader.py b/backend/src/analytics_agent/skills/loader.py index 78dee91..4ff86d4 100644 --- a/backend/src/analytics_agent/skills/loader.py +++ b/backend/src/analytics_agent/skills/loader.py @@ -1,14 +1,10 @@ -""" -SKILL.md loader for Analytics Agent skills. - -Each skill lives in skills//SKILL.md following the Agent Skills -Specification: YAML frontmatter (name, description, allowed-tools, …) + -markdown body with step-by-step instructions. +"""SKILL.md → tool wrappers + skill source paths for SkillsMiddleware. -At graph-build time: - - Frontmatter description → LangChain tool .description (routing) - - Markdown body → injected into system prompt (instructions) - - Python impl in datahub_skills.py → tool execution layer +Skills live under `library//SKILL.md`. The agent discovers them +through `deepagents.middleware.SkillsMiddleware` (progressive disclosure: the +descriptions appear in the system prompt, bodies are loaded on demand). A +subset of skills also have Python implementations that we expose as +LangChain tools — see `build_skill_tools` below. """ from __future__ import annotations @@ -23,6 +19,7 @@ logger = logging.getLogger(__name__) _SKILLS_DIR = Path(__file__).parent +SKILLS_LIBRARY_DIR = _SKILLS_DIR / "library" def _parse_skill_md(text: str) -> tuple[dict[str, Any], str]: @@ -43,120 +40,83 @@ def _parse_skill_md(text: str) -> tuple[dict[str, Any], str]: return fm, body -def _load_skill_md(skill_name: str) -> tuple[dict[str, Any], str] | None: - """Read and parse a skill's SKILL.md. Returns None if not found.""" - skill_dir = _SKILLS_DIR / skill_name - skill_file = skill_dir / "SKILL.md" +def _load_skill_md(skill_dir_name: str) -> tuple[dict[str, Any], str] | None: + """Read and parse a skill's SKILL.md from library//SKILL.md.""" + skill_file = SKILLS_LIBRARY_DIR / skill_dir_name / "SKILL.md" if not skill_file.exists(): - logger.warning("Skill '%s' has no SKILL.md at %s", skill_name, skill_file) + logger.warning("Skill '%s' has no SKILL.md at %s", skill_dir_name, skill_file) return None return _parse_skill_md(skill_file.read_text()) -def _build_tool_from_skill(folder_name: str, impl_fn: Any) -> BaseTool | None: - """Load a SKILL.md and wrap its impl as a StructuredTool. Returns None on failure.""" - parsed = _load_skill_md(folder_name) +def _build_tool(skill_dir_name: str, impl_fn: Any, *, tool_name: str) -> BaseTool | None: + """Wrap a Python impl as a StructuredTool with description from SKILL.md.""" + parsed = _load_skill_md(skill_dir_name) if parsed is None: return None fm, _body = parsed - tool_name = fm.get("name", folder_name) description = str(fm.get("description", "")).strip().replace("\n", " ") - tool = StructuredTool.from_function( - func=impl_fn, - name=tool_name, - description=description, - ) - logger.info("Loaded skill tool '%s' from SKILL.md", tool_name) + tool = StructuredTool.from_function(func=impl_fn, name=tool_name, description=description) + logger.info("Loaded skill tool '%s' from %s/SKILL.md", tool_name, skill_dir_name) return tool def build_always_on_skill_tools() -> list[BaseTool]: - """Return skills that are always active, regardless of user-enabled mutations.""" + """Tools backed by skills that are always active (read-only).""" from analytics_agent.skills import datahub_skills as _impl tools: list[BaseTool] = [] - tool = _build_tool_from_skill("search-business-context", _impl._search_business_context_impl) + tool = _build_tool( + "search-business-context", + _impl._search_business_context_impl, + tool_name="search_business_context", + ) if tool is not None: tools.append(tool) return tools +# Map opt-in skill IDs (as stored in `enabled_mutation_tools` setting) to +# (skill_dir_name, impl_attribute, tool_name) triples. Tool names use snake_case +# to match prior agent-facing identifiers; skill dir names are hyphenated to +# satisfy the Agent Skills specification. +_OPT_IN_TOOL_SPECS: dict[str, tuple[str, str, str]] = { + "publish_analysis": ("publish-analysis", "_publish_analysis_impl", "publish_analysis"), + "save_correction": ("save-correction", "_save_correction_impl", "save_correction"), +} + + def build_skill_tools(enabled_skills: set[str]) -> list[BaseTool]: - """ - For each enabled skill, load its SKILL.md and return a LangChain tool - whose description comes from the frontmatter and whose implementation - comes from datahub_skills.py. - """ + """For each enabled opt-in skill, expose its Python impl as a LangChain tool.""" from analytics_agent.skills import datahub_skills as _impl - _implementations: dict[str, Any] = { - "publish-analysis": _impl._publish_analysis_impl, - "save-correction": _impl._save_correction_impl, - } - _name_map: dict[str, str] = { - "publish_analysis": "publish-analysis", - "save_correction": "save-correction", - } - tools: list[BaseTool] = [] for skill_id in enabled_skills: - folder_name = _name_map.get(skill_id, skill_id) - impl = _implementations.get(folder_name) + spec = _OPT_IN_TOOL_SPECS.get(skill_id) + if spec is None: + logger.warning("No tool wrapping defined for skill '%s'", skill_id) + continue + skill_dir, impl_attr, tool_name = spec + impl = getattr(_impl, impl_attr, None) if impl is None: - logger.warning("No implementation found for skill '%s'", skill_id) + logger.warning("Missing impl '%s' for skill '%s'", impl_attr, skill_id) continue - tool = _build_tool_from_skill(folder_name, impl) + tool = _build_tool(skill_dir, impl, tool_name=tool_name) if tool is not None: tools.append(tool) - return tools -def get_improve_context_prompt_section() -> str: - """Return the always-on /improve-context meta-skill section for the system prompt.""" - parsed = _load_skill_md("improve-context") - if parsed is None: - return "" - _fm, body = parsed - return f"\n\n## Meta-Skill: /improve-context\n\n{body}" - - -def get_search_business_context_section() -> str: - """Return the always-on search-business-context skill section for the system prompt.""" - parsed = _load_skill_md("search-business-context") - if parsed is None: - return "" - _fm, body = parsed - return f"\n\n## Skill: search_business_context\n\n{body}" - +def build_skill_sources(enabled_mutations: set[str] | None = None) -> list[str]: + """Return source paths for SkillsMiddleware. -def get_skill_system_prompt_section(enabled_skills: set[str]) -> str: - """ - Return a markdown section to inject into the system prompt containing - the full SKILL.md body for each enabled skill. + Always includes the full library so descriptions are discoverable. Body + contents are loaded on demand by the middleware (progressive disclosure), + so unused skills cost only their description in the system prompt. - Empty string if no skills are enabled. + The `enabled_mutations` argument is accepted for parity with the tool + builders but does not currently filter sources — mutation skills become + actually executable only because their corresponding tools are absent + when the user has not enabled them. """ - _name_map: dict[str, str] = { - "publish_analysis": "publish-analysis", - "save_correction": "save-correction", - } - - sections: list[str] = [] - for skill_id in sorted(enabled_skills): - folder_name = _name_map.get(skill_id, skill_id) - parsed = _load_skill_md(folder_name) - if parsed is None: - continue - fm, body = parsed - tool_name = fm.get("name", skill_id) - sections.append(f"### Skill: {tool_name}\n\n{body}") - - if not sections: - return "" - - return ( - "\n\n## Write-Back Skills\n\n" - "The following skills are enabled. Follow their instructions exactly " - "when the relevant situation arises.\n\n" + "\n\n---\n\n".join(sections) - ) + return [str(SKILLS_LIBRARY_DIR)] diff --git a/frontend/src/api/conversations.ts b/frontend/src/api/conversations.ts index 6c60251..b1e0ab0 100644 --- a/frontend/src/api/conversations.ts +++ b/frontend/src/api/conversations.ts @@ -42,6 +42,13 @@ export async function generateTitle( return res.json(); } +export async function getVirtualFiles(id: string): Promise> { + const res = await fetch(`${BASE}/conversations/${id}/files`); + if (!res.ok) throw new Error("Failed to fetch virtual files"); + const data = await res.json(); + return data.files ?? {}; +} + export async function listEngines(): Promise { const res = await fetch(`${BASE}/engines`); if (!res.ok) throw new Error("Failed to fetch engines"); diff --git a/frontend/src/components/Chat/ChatView.tsx b/frontend/src/components/Chat/ChatView.tsx index 7e21ca8..43a49ea 100644 --- a/frontend/src/components/Chat/ChatView.tsx +++ b/frontend/src/components/Chat/ChatView.tsx @@ -1,8 +1,8 @@ import { useEffect, useCallback, useState, useRef } from "react"; -import type { MessageRecord, UsagePayload, TurnUsage } from "@/types"; +import type { MessageRecord, UsagePayload, TurnUsage, TodosPayload } from "@/types"; import { streamMessage, reattachStream } from "@/api/stream"; -import { generateTitle, getConversation, createConversation } from "@/api/conversations"; -import { Download, X } from "lucide-react"; +import { generateTitle, getConversation, createConversation, getVirtualFiles } from "@/api/conversations"; +import { Download, PanelRight, X } from "lucide-react"; import { useConversationsStore } from "@/store/conversations"; import { useDisplayStore } from "@/store/display"; import { MessageList } from "./MessageList"; @@ -10,6 +10,7 @@ import { MessageInput } from "./MessageInput"; import { EngineSelector } from "./EngineSelector"; import { WelcomeView } from "./WelcomeView"; import { ContextStatusBar } from "./ContextStatusBar"; +import { DeepAgentsPanel } from "./DeepAgentsPanel"; import type { UIMessage } from "@/types"; import { buildUiMessages } from "@/lib/buildUiMessages"; import { v4 as uuidv4 } from "uuid"; @@ -36,8 +37,13 @@ export function ChatView() { attachUsageToMessage, setFinalMsgTurnUsage, finalizeStreaming, + todos, + setTodos, + setFiles, } = useConversationsStore(); + const [panelOpen, setPanelOpen] = useState(false); + const activeConv = conversations.find((c) => c.id === activeId); const pendingFirstMessage = useRef(null); const chartErrorRetried = useRef(false); @@ -68,9 +74,11 @@ export function ChatView() { if (activeId !== snapId) return; if (!detail.is_streaming) { - const { messages: uiMsgs, totals } = buildUiMessages(detail.messages); + const { messages: uiMsgs, totals, lastTodos } = buildUiMessages(detail.messages); setMessages(uiMsgs); setUsageTotals(totals); + setTodos(lastTodos); + getVirtualFiles(snapId).then(setFiles).catch(() => {}); return; } @@ -84,9 +92,11 @@ export function ChatView() { const previousMessages = lastUserIdx >= 0 ? detail.messages.slice(0, lastUserIdx + 1) : detail.messages; - const { messages: prevUiMsgs, totals: prevTotals } = buildUiMessages(previousMessages); + const { messages: prevUiMsgs, totals: prevTotals, lastTodos } = buildUiMessages(previousMessages); setMessages(prevUiMsgs); setUsageTotals(prevTotals); + setTodos(lastTodos); + getVirtualFiles(snapId).then(setFiles).catch(() => {}); setStreaming(true); resetStreamingText(); @@ -132,6 +142,11 @@ export function ChatView() { [...state.messages].reverse().find((m) => m.role === "assistant" && m.event_type === "TOOL_CALL")?.id ?? [...state.messages].reverse().find((m) => m.role === "assistant")?.id; if (targetId) attachUsageToMessage(targetId, usage); + } else if (event.event === "TODOS") { + setTodos(((event.payload as unknown) as TodosPayload).todos || []); + setPanelOpen(true); + } else if (event.event === "FILES_UPDATE") { + setFiles((event.payload as { files?: Record }).files ?? {}); } else if (event.event === "COMPLETE") { if (reattachTurnUsage.calls > 0) setFinalMsgTurnUsage({ ...reattachTurnUsage }); } else { @@ -258,6 +273,11 @@ export function ChatView() { [...state.messages].reverse().find((m) => m.role === "assistant" && m.event_type === "TOOL_CALL")?.id ?? [...state.messages].reverse().find((m) => m.role === "assistant")?.id; if (targetId) attachUsageToMessage(targetId, usage); + } else if (event.event === "TODOS") { + setTodos(((event.payload as unknown) as TodosPayload).todos || []); + setPanelOpen(true); + } else if (event.event === "FILES_UPDATE") { + setFiles((event.payload as { files?: Record }).files ?? {}); } else if (event.event === "COMPLETE") { if (sendTurnUsage.calls > 0) setFinalMsgTurnUsage({ ...sendTurnUsage }); } else { @@ -306,7 +326,8 @@ export function ChatView() { } return ( -
+
+
{/* Hidden print header — visible only when printing */} + setPanelOpen(false)} /> +
); } diff --git a/frontend/src/components/Chat/DeepAgentsPanel.tsx b/frontend/src/components/Chat/DeepAgentsPanel.tsx new file mode 100644 index 0000000..d3423d9 --- /dev/null +++ b/frontend/src/components/Chat/DeepAgentsPanel.tsx @@ -0,0 +1,166 @@ +import { useState } from "react"; +import { Check, Circle, CircleDot, FileText, ListTodo, X } from "lucide-react"; +import { useConversationsStore } from "@/store/conversations"; +import type { TodoItem } from "@/types"; + +type Tab = "plan" | "files"; + +interface Props { + open: boolean; + onClose: () => void; +} + +export function DeepAgentsPanel({ open, onClose }: Props) { + const todos = useConversationsStore((s) => s.todos); + const files = useConversationsStore((s) => s.files); + const [tab, setTab] = useState("plan"); + const [selectedFile, setSelectedFile] = useState(null); + + if (!open) return null; + + const fileEntries = Object.entries(files); + + return ( + + ); +} + +function PlanTab({ todos }: { todos: TodoItem[] }) { + if (todos.length === 0) { + return ( +
+ The agent's plan will appear here when it calls write_todos. +
+ ); + } + return ( +
    + {todos.map((t, i) => ( +
  • + + {t.status === "completed" ? ( + + ) : t.status === "in_progress" ? ( + + ) : ( + + )} + + + {t.status === "in_progress" && t.activeForm ? t.activeForm : t.content} + +
  • + ))} +
+ ); +} + +function FilesTab({ + files, + selected, + onSelect, +}: { + files: Record; + selected: string | null; + onSelect: (path: string | null) => void; +}) { + const entries = Object.entries(files); + if (entries.length === 0) { + return ( +
+ The agent's virtual filesystem will appear here when it calls{" "} + write_file or edit_file. +
+ ); + } + if (selected && files[selected] !== undefined) { + return ( +
+
+ {selected} + +
+
+          {files[selected]}
+        
+
+ ); + } + return ( +
    + {entries.map(([path, content]) => ( +
  • + +
  • + ))} +
+ ); +} diff --git a/frontend/src/lib/buildUiMessages.ts b/frontend/src/lib/buildUiMessages.ts index cdeee92..968d519 100644 --- a/frontend/src/lib/buildUiMessages.ts +++ b/frontend/src/lib/buildUiMessages.ts @@ -1,4 +1,4 @@ -import type { MessageRecord, UIMessage, UsagePayload, TurnUsage } from "@/types"; +import type { MessageRecord, UIMessage, UsagePayload, TurnUsage, TodoItem } from "@/types"; export function buildUiMessages(records: MessageRecord[]): { messages: UIMessage[]; @@ -12,6 +12,8 @@ export function buildUiMessages(records: MessageRecord[]): { model?: string; provider?: string; }; + // Latest `write_todos` snapshot in this history — used to restore the Plan panel. + lastTodos: TodoItem[]; } { const result: UIMessage[] = []; const totals: { @@ -40,6 +42,7 @@ export function buildUiMessages(records: MessageRecord[]): { // flushes the pending text. Stash it here and attach to the message pushed by // the next flushText() call (the thinking/response from the same LLM call). let pendingUsage: UsagePayload | null = null; + let lastTodos: TodoItem[] = []; const flushText = (asThinking: boolean) => { if (pendingTextChunks.length === 0) return; @@ -110,6 +113,13 @@ export function buildUiMessages(records: MessageRecord[]): { result.push({ id: m.id, event_type: "TOOL_CALL", role: "assistant", payload: m.payload, created_at: m.created_at }); break; + case "TODOS": { + // Not rendered inline — feeds the Plan side-panel. Keep the latest. + const todos = (m.payload as { todos?: TodoItem[] }).todos; + if (Array.isArray(todos)) lastTodos = todos; + break; + } + case "TOOL_RESULT": case "SQL": case "CHART": @@ -163,5 +173,5 @@ export function buildUiMessages(records: MessageRecord[]): { } flushText(seenToolCallAfterText); - return { messages: result, totals }; + return { messages: result, totals, lastTodos }; } diff --git a/frontend/src/store/conversations.ts b/frontend/src/store/conversations.ts index e3f713e..0345beb 100644 --- a/frontend/src/store/conversations.ts +++ b/frontend/src/store/conversations.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { v4 as uuidv4 } from "uuid"; -import type { ConversationSummary, Engine, UIMessage, UsagePayload, TurnUsage } from "@/types"; +import type { ConversationSummary, Engine, UIMessage, UsagePayload, TurnUsage, TodoItem } from "@/types"; export interface UsageTotals { input_tokens: number; @@ -31,6 +31,11 @@ interface ConversationsState { // ID of the current turn's streaming TEXT message (reset each turn) streamingTextId: string | null; usageTotals: UsageTotals; + // deepagents side-panel state, reset per conversation: + // - todos: latest snapshot from the `write_todos` planning tool + // - files: virtual filesystem snapshot (path → content) + todos: TodoItem[]; + files: Record; setConversations: (list: ConversationSummary[]) => void; addConversation: (conv: ConversationSummary) => void; @@ -49,6 +54,8 @@ interface ConversationsState { attachUsageToMessage: (messageId: string, usage: UsagePayload) => void; setFinalMsgTurnUsage: (tu: TurnUsage) => void; finalizeStreaming: () => void; + setTodos: (todos: TodoItem[]) => void; + setFiles: (files: Record) => void; } export const useConversationsStore = create((set) => ({ @@ -59,6 +66,8 @@ export const useConversationsStore = create((set) => ({ isStreaming: false, streamingTextId: null, usageTotals: { ...EMPTY_USAGE }, + todos: [], + files: {}, setConversations: (list) => set({ conversations: list }), addConversation: (conv) => @@ -69,7 +78,14 @@ export const useConversationsStore = create((set) => ({ activeId: s.activeId === id ? null : s.activeId, })), setActiveId: (id) => - set({ activeId: id, messages: [], streamingTextId: null, usageTotals: { ...EMPTY_USAGE } }), + set({ + activeId: id, + messages: [], + streamingTextId: null, + usageTotals: { ...EMPTY_USAGE }, + todos: [], + files: {}, + }), setMessages: (msgs) => set({ messages: msgs }), appendMessage: (msg) => set((s) => ({ messages: [...s.messages, msg] })), @@ -161,4 +177,6 @@ export const useConversationsStore = create((set) => ({ provider: usage.provider || s.usageTotals.provider, }, })), + setTodos: (todos) => set({ todos }), + setFiles: (files) => set({ files }), })); diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 8bbd4ae..276d9da 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -6,6 +6,8 @@ export type SSEEventType = | "SQL" | "CHART" | "USAGE" + | "TODOS" + | "FILES_UPDATE" | "COMPLETE" | "ERROR"; @@ -55,6 +57,24 @@ export interface UsagePayload { provider?: string; } +// deepagents planning tool (`write_todos`) — surfaced in the Plan panel. +export type TodoStatus = "pending" | "in_progress" | "completed"; + +export interface TodoItem { + content: string; + status: TodoStatus; + activeForm?: string; +} + +export interface TodosPayload { + todos: TodoItem[]; +} + +// deepagents virtual filesystem snapshot (path → content). +export interface FilesPayload { + files: Record; +} + export interface Engine { name: string; type: string; diff --git a/pyproject.toml b/pyproject.toml index 28cd7e7..7a436c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ dependencies = [ "langchain-google-genai>=4.2.2", # Telemetry "mixpanel>=4.10.0", + "deepagents>=0.6.8", ] [project.urls] diff --git a/uv.lock b/uv.lock index 56f32b8..a87a52f 100644 --- a/uv.lock +++ b/uv.lock @@ -388,6 +388,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/1e/44afcdc3b526b6e1569dd142083c6ed1cb8b92b4141de1c78ded883b449a/botocore-1.42.90-py3-none-any.whl", hash = "sha256:5c95504720346990adc8e3ae1023eb46f9409084b79688e4773ba7099c5fd3db", size = 14892274, upload-time = "2026-04-16T20:27:24.057Z" }, ] +[[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 = "cached-property" version = "2.0.1" @@ -714,6 +723,7 @@ dependencies = [ { name = "click" }, { name = "cryptography" }, { name = "datahub-agent-context", extra = ["langchain"] }, + { name = "deepagents" }, { name = "duckdb-engine" }, { name = "fastapi" }, { name = "httpx" }, @@ -771,6 +781,7 @@ requires-dist = [ { name = "click", specifier = ">=8.1.0" }, { name = "cryptography", specifier = ">=42.0.0" }, { name = "datahub-agent-context", extras = ["langchain"], specifier = ">=1.5.0" }, + { name = "deepagents", specifier = ">=0.6.8" }, { name = "duckdb-engine", specifier = ">=0.13.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "httpx", specifier = ">=0.27.0" }, @@ -815,6 +826,23 @@ dev = [ { name = "ruff", specifier = ">=0.7.0" }, ] +[[package]] +name = "deepagents" +version = "0.6.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core" }, + { name = "langchain-google-genai" }, + { name = "langsmith" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/77/e3b7efd9bff9cd101c085a5a3bf74180c13ab6c41a96f725cd1cb1bf53e8/deepagents-0.6.8.tar.gz", hash = "sha256:70cdd4da920cc420a8a0f729792ec559688bbbff39f7ab1508110cce9f901c06", size = 196927, upload-time = "2026-06-03T17:08:36.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/19/1b7b76e958ac7f4e40886edc70f67aff4d7188770ab68105c9c48cbeb769/deepagents-0.6.8-py3-none-any.whl", hash = "sha256:087bdc1458202a3436854cf180f7ec059d07d2114a6c232819e9ad6533a5174a", size = 221469, upload-time = "2026-06-03T17:08:35.133Z" }, +] + [[package]] name = "deprecated" version = "1.3.1" @@ -1651,30 +1679,30 @@ wheels = [ [[package]] name = "langchain" -version = "1.2.15" +version = "1.3.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/3f/888a7099d2bd2917f8b0c3ffc7e347f1e664cf64267820b0b923c4f339fc/langchain-1.2.15.tar.gz", hash = "sha256:1717b6719daefae90b2728314a5e2a117ff916291e2862595b6c3d6fba33d652", size = 574732, upload-time = "2026-04-03T14:26:03.994Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/3f/034eb6cbef90bfccc89b7f8ed0c1d853dc9cb0bea17c7a269534c647ba3a/langchain-1.3.4.tar.gz", hash = "sha256:d6e0654c22848925534f5c0a706f9be481bb09a619ec60a738fbd1e5502e457a", size = 606617, upload-time = "2026-06-02T20:04:49.411Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/e8/a3b8cb0005553f6a876865073c81ef93bd7c5b18381bcb9ba4013af96ebc/langchain-1.2.15-py3-none-any.whl", hash = "sha256:e349db349cb3e9550c4044077cf90a1717691756cc236438404b23500e615874", size = 112714, upload-time = "2026-04-03T14:26:02.557Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/9ffe99c7dc4891a0215ec59c423bea320f943c08a231bc5bae392a438a83/langchain-1.3.4-py3-none-any.whl", hash = "sha256:e51b05ab23d056bc6bf2d97d8c694fb92d6d5765126fef74565d007c27581672", size = 125286, upload-time = "2026-06-02T20:04:48.13Z" }, ] [[package]] name = "langchain-anthropic" -version = "1.4.0" +version = "1.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anthropic" }, { name = "langchain-core" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/c7/259d4d805c6ac90c8695714fc15498a4557bb515eb24f692fd611966e383/langchain_anthropic-1.4.0.tar.gz", hash = "sha256:bbf64e99f9149a34ba67813e9582b2160a0968de9e9f54f7ba8d1658f253c2e5", size = 674360, upload-time = "2026-03-17T18:42:20.751Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/e3/d2f9dec95602524b1cfb4be2747ba5bc38d32501b2a56cb4bcb76e80bb45/langchain_anthropic-1.4.3.tar.gz", hash = "sha256:f8a2442463c0629b1b3110eaeaa56fdbdc87df2a802f8c7f5ecf611eb4874ec8", size = 685219, upload-time = "2026-05-03T17:33:27.118Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/c0/77f99373276d4f06c38a887ef6023f101cfc7ba3b2bf9af37064cdbadde5/langchain_anthropic-1.4.0-py3-none-any.whl", hash = "sha256:c84f55722336935f7574d5771598e674f3959fdca0b51de14c9788dbf52761be", size = 48463, upload-time = "2026-03-17T18:42:19.742Z" }, + { url = "https://files.pythonhosted.org/packages/d3/55/482a1968c95275e8be6d8c1e53b54f0f7be0b8b155ce1608c947a95cf543/langchain_anthropic-1.4.3-py3-none-any.whl", hash = "sha256:65466e0f2f95909a009708f2958e917dfdbfab79c612b4484a30866a85e1f291", size = 50389, upload-time = "2026-05-03T17:33:25.671Z" }, ] [[package]] @@ -1694,7 +1722,7 @@ wheels = [ [[package]] name = "langchain-core" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonpatch" }, @@ -1707,9 +1735,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/03/7219502e8ca728d65eb44d7a3eb60239230742a70dbfc9241b9bfd61c4ab/langchain_core-1.3.2.tar.gz", hash = "sha256:fd7a50b2f28ba561fd9d7f5d2760bc9e06cf00cdf820a3ccafe88a94ffa8d5b7", size = 911813, upload-time = "2026-04-24T15:49:23.699Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/de/679a53472c25860837e32c0442c962fa86e95317a36460e2c9d5c91b17c2/langchain_core-1.4.0.tar.gz", hash = "sha256:1dc341eed802ed9c117c0df3923c991e5e9e226571e5725c194eeb5bd93d1a7f", size = 920260, upload-time = "2026-05-11T18:42:35.919Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/d5/8fa4431007cbb7cfed7590f4d6a5dea3ad724f4174d248f6642ef5ce7d05/langchain_core-1.3.2-py3-none-any.whl", hash = "sha256:d44a66127f9f8db735bdfd0ab9661bccb47a97113cfd3f2d89c74864422b7274", size = 542390, upload-time = "2026-04-24T15:49:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1a/86c38c27b81913a1c6c12448cab55defb5a1097c7dc9a4cea83f55477a2d/langchain_core-1.4.0-py3-none-any.whl", hash = "sha256:23cbbdb46e38ddd1dd5247e6167e96013eae74bea4c5949c550809970a9e565c", size = 548120, upload-time = "2026-05-11T18:42:33.992Z" }, ] [[package]] @@ -1757,19 +1785,19 @@ wheels = [ [[package]] name = "langchain-protocol" -version = "0.0.11" +version = "0.0.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/bb/38b5eaefa41c67735eedd9f9a2568b11c9eb376fa129a5edd7cc3dcde071/langchain_protocol-0.0.11.tar.gz", hash = "sha256:c276e2373b5ac691fc7ac9a72019d55182444ce8e89385c3f7e9f0185d0aace7", size = 6622, upload-time = "2026-04-23T22:13:16.771Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/fa/6a8ecad8472b182f2caf9d83fd89f40fc1590cb96546d90089b7869b7f5e/langchain_protocol-0.0.11-py3-none-any.whl", hash = "sha256:364da1faf6f5d3001413bede792c1a822c0f23ae55d1ce1266ca7d8e80e79011", size = 6778, upload-time = "2026-04-23T22:13:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" }, ] [[package]] name = "langgraph" -version = "1.1.6" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -1779,53 +1807,56 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/e5/d3f72ead3c7f15769d5a9c07e373628f1fbaf6cbe7735694d7085859acf6/langgraph-1.1.6.tar.gz", hash = "sha256:1783f764b08a607e9f288dbcf6da61caeb0dd40b337e5c9fb8b412341fbc0b60", size = 549634, upload-time = "2026-04-03T19:01:32.561Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/43/dac5a2621c1e57f8eb7f0703f6f6fe34a5caf62f8f0fb4d2bb395bb454ea/langgraph-1.2.4.tar.gz", hash = "sha256:5df076973a2d23efb13eceb279d1e5b46feebcbbeded0a86a2ef669abd9e4399", size = 720374, upload-time = "2026-06-02T17:07:37.347Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/e6/b36ecdb3ff4ba9a290708d514bae89ebbe2f554b6abbe4642acf3fddbe51/langgraph-1.1.6-py3-none-any.whl", hash = "sha256:fdbf5f54fa5a5a4c4b09b7b5e537f1b2fa283d2f0f610d3457ddeecb479458b9", size = 169755, upload-time = "2026-04-03T19:01:30.686Z" }, + { url = "https://files.pythonhosted.org/packages/48/9e/31ca236104966d7bb14ea9e93cfd73350aea8c41008ddf057b65794ed10d/langgraph-1.2.4-py3-none-any.whl", hash = "sha256:ffe3e1e31dce28907640f82525858470f293506d2b272d07ea3b3ce97974b067", size = 245402, upload-time = "2026-06-02T17:07:35.977Z" }, ] [[package]] name = "langgraph-checkpoint" -version = "4.0.2" +version = "4.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "ormsgpack" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/f2/cf8086e1f1a3358d9228805614e72602c281b18307f3fae64a5b854aad2d/langgraph_checkpoint-4.0.2.tar.gz", hash = "sha256:4f6f99cba8e272deabf81b2d8cdc96582af07a57a6ad591cdf216bb310497039", size = 160810, upload-time = "2026-04-15T21:03:00.062Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/b4/6005c5dd88ad484fe6235d4c43a0d2cee7e91b08ad85a180985c2662df87/langgraph_checkpoint-4.1.0.tar.gz", hash = "sha256:e5bb304e30fc1363ac8fcb5f7dee5ca2185d77fe475b0d01de2c5f91324c2c21", size = 181942, upload-time = "2026-05-12T03:33:49.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/5a/6dba29dd89b0a46ae21c707da0f9d17e94f27d3e481ed15bc99d6bd20aa6/langgraph_checkpoint-4.0.2-py3-none-any.whl", hash = "sha256:59b0f29216128a629c58dd07c98aa004f82f51805d5573126ffb419b753ff253", size = 51000, upload-time = "2026-04-15T21:02:59.096Z" }, + { url = "https://files.pythonhosted.org/packages/93/74/d3be2b41955e20ccd624dba5f6fe9d38dcee385ba470a6e13ed86732fc86/langgraph_checkpoint-4.1.0-py3-none-any.whl", hash = "sha256:8bc2a0466a20c38b865ce6671b42093fd5c041133f32351cae4222e0eeaf7fb5", size = 56047, upload-time = "2026-05-12T03:33:48.548Z" }, ] [[package]] name = "langgraph-prebuilt" -version = "1.0.9" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/99/4c/06dac899f4945bedb0c3a1583c19484c2cc894114ea30d9a538dd270086e/langgraph_prebuilt-1.0.9.tar.gz", hash = "sha256:93de7512e9caade4b77ead92428f6215c521fdb71b8ffda8cd55f0ad814e64de", size = 165850, upload-time = "2026-04-03T14:06:37.721Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/a2/8368ac187b75e7f9d938ca075d34f116683f5cfc48d924029ee79aea147b/langgraph_prebuilt-1.0.9-py3-none-any.whl", hash = "sha256:776c8e3154a5aef5ad0e5bf3f263f2dcaab3983786cc20014b7f955d99d2d1b2", size = 35958, upload-time = "2026-04-03T14:06:36.58Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.3.13" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, { name = "orjson" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/db/77a45127dddcfea5e4256ba916182903e4c31dc4cfca305b8c386f0a9e53/langgraph_sdk-0.3.13.tar.gz", hash = "sha256:419ca5663eec3cec192ad194ac0647c0c826866b446073eb40f384f950986cd5", size = 196360, upload-time = "2026-04-07T20:34:18.766Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/ef/64d64e9f8eea47ce7b939aa6da6863b674c8d418647813c20111645fcc62/langgraph_sdk-0.3.13-py3-none-any.whl", hash = "sha256:aee09e345c90775f6de9d6f4c7b847cfc652e49055c27a2aed0d981af2af3bd0", size = 96668, upload-time = "2026-04-07T20:34:17.866Z" }, + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, ] [[package]] name = "langsmith" -version = "0.7.32" +version = "0.8.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -1838,9 +1869,9 @@ dependencies = [ { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/b4/a0b4a501bee6b8a741ce29f8c48155b132118483cddc6f9247735ddb38fa/langsmith-0.7.32.tar.gz", hash = "sha256:b59b8e106d0e4c4842e158229296086e2aa7c561e3f602acda73d3ad0062e915", size = 1184518, upload-time = "2026-04-15T23:42:41.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/17/eb/8883d1158c743d0aac350f09df7880714d27283497e8c80bb9fe3480f165/langsmith-0.8.5.tar.gz", hash = "sha256:3615243d99c12f4047f13042bdc05a373dce232d106a6511b3ca7b48c5af1c2c", size = 4462348, upload-time = "2026-05-15T21:31:41.093Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/bc/148f98ac7dad73ac5e1b1c985290079cfeeb9ba13d760a24f25002beb2c9/langsmith-0.7.32-py3-none-any.whl", hash = "sha256:e1fde928990c4c52f47dc5132708cec674355d9101723d564183e965f383bf5f", size = 378272, upload-time = "2026-04-15T23:42:39.905Z" }, + { url = "https://files.pythonhosted.org/packages/23/85/968c88a63e32a59b3e5c68afd2fe114ce0708a125db0be1a85efc25fb2ea/langsmith-0.8.5-py3-none-any.whl", hash = "sha256:efc779f9d450dcaf9d97bc8894f4926276509d6e730e05289af9a64debce06ae", size = 399564, upload-time = "2026-05-15T21:31:39.046Z" }, ] [[package]] @@ -4036,63 +4067,58 @@ wheels = [ { 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 = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]]