VibecoderMcSwaggins EmployeeNo427 commited on
Commit
20f762e
·
unverified ·
1 Parent(s): b2929fc

feat: add hierarchical orchestration with web search (integrates PR #44) (#46)

Browse files

* feat: add hierarchical orchestration with web search (from PR #44)

Based on contribution by @EmployeeNo427 with fixes:

New features:
- WebSearchTool using DuckDuckGo for web searches
- SubIterationMiddleware for research loop pattern
- HierarchicalOrchestrator with judge-based refinement
- RetrievalAgent for web-based evidence gathering
- CodeExecutorAgent for Modal sandbox execution
- LLMSubIterationJudge for LLM-based assessment
- src/state/ package for state management

Fixes applied:
- Added chroma_db/ to .gitignore (was committed as binaries)
- Added structlog logging per coding guidelines
- Added pytest markers to new tests
- Fixed silent exception swallowing in orchestrator
- Preserved dual-mode architecture from PR #45
- Added "web" to SourceName type
- Fixed mypy type errors

Co-authored-by: EmployeeNo427 <45109567+EmployeeNo427@users.noreply.github.com>

* fix: resolve mypy type errors in middleware and agents

* fix: remove type argument from SubIterationTeam

---------

Co-authored-by: EmployeeNo427 <45109567+EmployeeNo427@users.noreply.github.com>

.gitignore CHANGED
@@ -66,4 +66,9 @@ logs/
66
  .mypy_cache/
67
  .coverage
68
  htmlcov/
 
 
 
 
 
69
  # Trigger rebuild Wed Nov 26 17:51:41 EST 2025
 
66
  .mypy_cache/
67
  .coverage
68
  htmlcov/
69
+
70
+ # Database files
71
+ chroma_db/
72
+ *.sqlite3
73
+
74
  # Trigger rebuild Wed Nov 26 17:51:41 EST 2025
pyproject.toml CHANGED
@@ -25,6 +25,7 @@ dependencies = [
25
  "structlog>=24.1", # Structured logging
26
  "requests>=2.32.5", # ClinicalTrials.gov (httpx blocked by WAF)
27
  "limits>=3.0", # Rate limiting
 
28
  ]
29
 
30
  [project.optional-dependencies]
 
25
  "structlog>=24.1", # Structured logging
26
  "requests>=2.32.5", # ClinicalTrials.gov (httpx blocked by WAF)
27
  "limits>=3.0", # Rate limiting
28
+ "duckduckgo-search>=5.0", # Web search
29
  ]
30
 
31
  [project.optional-dependencies]
src/agents/code_executor_agent.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Code execution agent using Modal."""
2
+
3
+ import asyncio
4
+
5
+ import structlog
6
+ from agent_framework import ChatAgent, ai_function
7
+ from agent_framework.openai import OpenAIChatClient
8
+
9
+ from src.tools.code_execution import get_code_executor
10
+ from src.utils.config import settings
11
+
12
+ logger = structlog.get_logger()
13
+
14
+
15
+ @ai_function # type: ignore[arg-type, misc]
16
+ async def execute_python_code(code: str) -> str:
17
+ """Execute Python code in a secure sandbox.
18
+
19
+ Args:
20
+ code: The Python code to execute.
21
+
22
+ Returns:
23
+ The standard output and standard error of the execution.
24
+ """
25
+ logger.info("Code execution starting", code_length=len(code))
26
+ executor = get_code_executor()
27
+ loop = asyncio.get_running_loop()
28
+
29
+ # Run in executor to avoid blocking
30
+ try:
31
+ result = await loop.run_in_executor(None, lambda: executor.execute(code))
32
+ if result["success"]:
33
+ logger.info("Code execution succeeded")
34
+ return f"Stdout:\n{result['stdout']}"
35
+ else:
36
+ logger.warning("Code execution failed", error=result.get("error"))
37
+ return f"Error:\n{result['error']}\nStderr:\n{result['stderr']}"
38
+ except Exception as e:
39
+ logger.error("Code execution exception", error=str(e))
40
+ return f"Execution failed: {e}"
41
+
42
+
43
+ def create_code_executor_agent(chat_client: OpenAIChatClient | None = None) -> ChatAgent:
44
+ """Create a code executor agent.
45
+
46
+ Args:
47
+ chat_client: Optional custom chat client.
48
+
49
+ Returns:
50
+ ChatAgent configured for code execution.
51
+ """
52
+ client = chat_client or OpenAIChatClient(
53
+ model_id=settings.openai_model,
54
+ api_key=settings.openai_api_key,
55
+ )
56
+
57
+ return ChatAgent(
58
+ name="CodeExecutorAgent",
59
+ description="Executes Python code for data analysis, calculation, and simulation.",
60
+ instructions="""You are a code execution expert.
61
+ When asked to analyze data or perform calculations, write Python code and execute it.
62
+ Use libraries like pandas, numpy, scipy, matplotlib.
63
+
64
+ Always output the code you want to execute using the `execute_python_code` tool.
65
+ Check the output and interpret the results.""",
66
+ chat_client=client,
67
+ tools=[execute_python_code],
68
+ temperature=0.0, # Strict code generation
69
+ )
src/agents/judge_agent_llm.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LLM Judge for sub-iterations."""
2
+
3
+ from typing import Any
4
+
5
+ import structlog
6
+ from pydantic_ai import Agent
7
+
8
+ from src.agent_factory.judges import get_model
9
+ from src.utils.models import JudgeAssessment
10
+
11
+ logger = structlog.get_logger()
12
+
13
+
14
+ class LLMSubIterationJudge:
15
+ """Judge that uses an LLM to assess sub-iteration results."""
16
+
17
+ def __init__(self) -> None:
18
+ self.model = get_model()
19
+ self.agent = Agent(
20
+ model=self.model,
21
+ output_type=JudgeAssessment,
22
+ system_prompt="""You are a strict judge evaluating a research task.
23
+
24
+ Evaluate if the result is sufficient to answer the task.
25
+ Provide scores and detailed reasoning.
26
+ If not sufficient, suggest next steps.""",
27
+ retries=3,
28
+ )
29
+
30
+ async def assess(self, task: str, result: Any, history: list[Any]) -> JudgeAssessment:
31
+ """Assess the result using LLM."""
32
+ logger.info("LLM judge assessing result", task=task[:100], history_len=len(history))
33
+
34
+ prompt = f"""Task: {task}
35
+
36
+ Current Result:
37
+ {str(result)[:4000]}
38
+
39
+ History of previous attempts: {len(history)}
40
+
41
+ Evaluate validity and sufficiency."""
42
+
43
+ run_result = await self.agent.run(prompt)
44
+ logger.info("LLM judge assessment complete", sufficient=run_result.output.sufficient)
45
+ return run_result.output
src/agents/retrieval_agent.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Retrieval agent for web search and context management."""
2
+
3
+ import structlog
4
+ from agent_framework import ChatAgent, ai_function
5
+ from agent_framework.openai import OpenAIChatClient
6
+
7
+ from src.state import get_magentic_state
8
+ from src.tools.web_search import WebSearchTool
9
+ from src.utils.config import settings
10
+
11
+ logger = structlog.get_logger()
12
+
13
+ _web_search = WebSearchTool()
14
+
15
+
16
+ @ai_function # type: ignore[arg-type, misc]
17
+ async def search_web(query: str, max_results: int = 10) -> str:
18
+ """Search the web using DuckDuckGo.
19
+
20
+ Args:
21
+ query: Search keywords.
22
+ max_results: Maximum results to return (default 10).
23
+
24
+ Returns:
25
+ Formatted search results.
26
+ """
27
+ logger.info("Web search starting", query=query, max_results=max_results)
28
+ state = get_magentic_state()
29
+
30
+ results = await _web_search.search(query, max_results)
31
+ if not results.evidence:
32
+ logger.info("Web search returned no results", query=query)
33
+ return f"No web results found for: {query}"
34
+
35
+ # Update state
36
+ # We add *all* found results to state
37
+ new_count = state.add_evidence(results.evidence)
38
+ logger.info(
39
+ "Web search complete",
40
+ query=query,
41
+ results_found=len(results.evidence),
42
+ new_evidence=new_count,
43
+ )
44
+
45
+ # Use embedding service for deduplication/indexing if available
46
+ if state.embedding_service:
47
+ # This method also adds to vector DB as a side effect for unique items
48
+ await state.embedding_service.deduplicate(results.evidence)
49
+
50
+ output = [f"Found {len(results.evidence)} web results ({new_count} new stored):\n"]
51
+ for i, r in enumerate(results.evidence[:max_results], 1):
52
+ output.append(f"{i}. **{r.citation.title}**")
53
+ output.append(f" Source: {r.citation.url}")
54
+ output.append(f" {r.content[:300]}...\n")
55
+
56
+ return "\n".join(output)
57
+
58
+
59
+ def create_retrieval_agent(chat_client: OpenAIChatClient | None = None) -> ChatAgent:
60
+ """Create a retrieval agent.
61
+
62
+ Args:
63
+ chat_client: Optional custom chat client.
64
+
65
+ Returns:
66
+ ChatAgent configured for retrieval.
67
+ """
68
+ client = chat_client or OpenAIChatClient(
69
+ model_id=settings.openai_model,
70
+ api_key=settings.openai_api_key,
71
+ )
72
+
73
+ return ChatAgent(
74
+ name="RetrievalAgent",
75
+ description="Searches the web and manages context/evidence.",
76
+ instructions="""You are a retrieval specialist.
77
+ Use `search_web` to find information on the internet.
78
+ Your goal is to gather relevant evidence for the research task.
79
+ Always summarize what you found.""",
80
+ chat_client=client,
81
+ tools=[search_web],
82
+ )
src/middleware/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Middleware components for orchestration."""
src/middleware/sub_iteration.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Middleware for orchestrating sub-iterations with research teams and judges."""
2
+
3
+ from typing import Any, Protocol
4
+
5
+ import structlog
6
+
7
+ from src.utils.models import AgentEvent, JudgeAssessment
8
+
9
+ logger = structlog.get_logger()
10
+
11
+
12
+ class SubIterationTeam(Protocol):
13
+ """Protocol for a research team that executes a sub-task."""
14
+
15
+ async def execute(self, task: str) -> Any:
16
+ """Execute the sub-task and return a result."""
17
+ ...
18
+
19
+
20
+ class SubIterationJudge(Protocol):
21
+ """Protocol for a judge that evaluates the sub-task result."""
22
+
23
+ async def assess(self, task: str, result: Any, history: list[Any]) -> JudgeAssessment:
24
+ """Assess the quality of the result."""
25
+ ...
26
+
27
+
28
+ class SubIterationMiddleware:
29
+ """
30
+ Middleware that manages a sub-iteration loop:
31
+ 1. Orchestrator delegates to a Research Team.
32
+ 2. Research Team produces a result.
33
+ 3. Judge evaluates the result.
34
+ 4. Loop continues until Judge approves or max iterations reached.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ team: SubIterationTeam,
40
+ judge: SubIterationJudge,
41
+ max_iterations: int = 3,
42
+ ):
43
+ self.team = team
44
+ self.judge = judge
45
+ self.max_iterations = max_iterations
46
+
47
+ async def run(
48
+ self,
49
+ task: str,
50
+ event_callback: Any = None, # Optional callback for streaming events
51
+ ) -> tuple[Any, JudgeAssessment | None]:
52
+ """
53
+ Run the sub-iteration loop.
54
+
55
+ Args:
56
+ task: The research task or question.
57
+ event_callback: Async callable to report events (e.g. to UI).
58
+
59
+ Returns:
60
+ Tuple of (best_result, final_assessment).
61
+ """
62
+ history: list[Any] = []
63
+ best_result: Any = None
64
+ final_assessment: JudgeAssessment | None = None
65
+
66
+ for i in range(1, self.max_iterations + 1):
67
+ logger.info("Sub-iteration starting", iteration=i, task=task)
68
+
69
+ if event_callback:
70
+ await event_callback(
71
+ AgentEvent(
72
+ type="looping",
73
+ message=f"Sub-iteration {i}: Executing task...",
74
+ iteration=i,
75
+ )
76
+ )
77
+
78
+ # 1. Team Execution
79
+ try:
80
+ result = await self.team.execute(task)
81
+ history.append(result)
82
+ best_result = result # Assume latest is best for now
83
+ except Exception as e:
84
+ logger.error("Sub-iteration execution failed", error=str(e))
85
+ if event_callback:
86
+ await event_callback(
87
+ AgentEvent(
88
+ type="error",
89
+ message=f"Sub-iteration execution failed: {e}",
90
+ iteration=i,
91
+ )
92
+ )
93
+ return best_result, final_assessment
94
+
95
+ # 2. Judge Assessment
96
+ try:
97
+ assessment = await self.judge.assess(task, result, history)
98
+ final_assessment = assessment
99
+ except Exception as e:
100
+ logger.error("Sub-iteration judge failed", error=str(e))
101
+ if event_callback:
102
+ await event_callback(
103
+ AgentEvent(
104
+ type="error",
105
+ message=f"Sub-iteration judge failed: {e}",
106
+ iteration=i,
107
+ )
108
+ )
109
+ return best_result, final_assessment
110
+
111
+ # 3. Decision
112
+ if assessment.sufficient:
113
+ logger.info("Sub-iteration sufficient", iteration=i)
114
+ return best_result, assessment
115
+
116
+ # If not sufficient, we might refine the task for the next iteration
117
+ # For this implementation, we assume the team is smart enough or the task stays same
118
+ # but we could append feedback to the task.
119
+
120
+ feedback = assessment.reasoning
121
+ logger.info("Sub-iteration insufficient", feedback=feedback)
122
+
123
+ if event_callback:
124
+ await event_callback(
125
+ AgentEvent(
126
+ type="looping",
127
+ message=(
128
+ f"Sub-iteration {i} result insufficient. "
129
+ f"Feedback: {feedback[:100]}..."
130
+ ),
131
+ iteration=i,
132
+ )
133
+ )
134
+
135
+ logger.warning("Sub-iteration max iterations reached", task=task)
136
+ return best_result, final_assessment
src/orchestrator_hierarchical.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hierarchical orchestrator using middleware and sub-teams."""
2
+
3
+ import asyncio
4
+ from collections.abc import AsyncGenerator
5
+
6
+ import structlog
7
+
8
+ from src.agents.judge_agent_llm import LLMSubIterationJudge
9
+ from src.agents.magentic_agents import create_search_agent
10
+ from src.middleware.sub_iteration import SubIterationMiddleware, SubIterationTeam
11
+ from src.services.embeddings import get_embedding_service
12
+ from src.state import init_magentic_state
13
+ from src.utils.models import AgentEvent
14
+
15
+ logger = structlog.get_logger()
16
+
17
+
18
+ class ResearchTeam(SubIterationTeam):
19
+ """Adapts Magentic ChatAgent to SubIterationTeam protocol."""
20
+
21
+ def __init__(self) -> None:
22
+ self.agent = create_search_agent()
23
+
24
+ async def execute(self, task: str) -> str:
25
+ response = await self.agent.run(task)
26
+ if response.messages:
27
+ for msg in reversed(response.messages):
28
+ if msg.role == "assistant" and msg.text:
29
+ return str(msg.text)
30
+ return "No response from agent."
31
+
32
+
33
+ class HierarchicalOrchestrator:
34
+ """Orchestrator that uses hierarchical teams and sub-iterations."""
35
+
36
+ def __init__(self) -> None:
37
+ self.team = ResearchTeam()
38
+ self.judge = LLMSubIterationJudge()
39
+ self.middleware = SubIterationMiddleware(self.team, self.judge, max_iterations=5)
40
+
41
+ async def run(self, query: str) -> AsyncGenerator[AgentEvent, None]:
42
+ logger.info("Starting hierarchical orchestrator", query=query)
43
+
44
+ try:
45
+ service = get_embedding_service()
46
+ init_magentic_state(service)
47
+ except Exception as e:
48
+ logger.warning(
49
+ "Embedding service initialization failed, using default state",
50
+ error=str(e),
51
+ )
52
+ init_magentic_state()
53
+
54
+ yield AgentEvent(type="started", message=f"Starting research: {query}")
55
+
56
+ queue: asyncio.Queue[AgentEvent | None] = asyncio.Queue()
57
+
58
+ async def event_callback(event: AgentEvent) -> None:
59
+ await queue.put(event)
60
+
61
+ task_future = asyncio.create_task(self.middleware.run(query, event_callback))
62
+
63
+ while not task_future.done():
64
+ get_event = asyncio.create_task(queue.get())
65
+ done, _ = await asyncio.wait(
66
+ {task_future, get_event}, return_when=asyncio.FIRST_COMPLETED
67
+ )
68
+
69
+ if get_event in done:
70
+ event = get_event.result()
71
+ if event:
72
+ yield event
73
+ else:
74
+ get_event.cancel()
75
+
76
+ # Process remaining events
77
+ while not queue.empty():
78
+ ev = queue.get_nowait()
79
+ if ev:
80
+ yield ev
81
+
82
+ try:
83
+ result, assessment = await task_future
84
+
85
+ assessment_text = assessment.reasoning if assessment else "None"
86
+ yield AgentEvent(
87
+ type="complete",
88
+ message=(
89
+ f"Research complete.\n\nResult:\n{result}\n\n" f"Assessment:\n{assessment_text}"
90
+ ),
91
+ data={"assessment": assessment.model_dump() if assessment else None},
92
+ )
93
+ except Exception as e:
94
+ logger.error("Orchestrator failed", error=str(e))
95
+ yield AgentEvent(type="error", message=f"Orchestrator failed: {e}")
src/state/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """State package - re-exports from agents.state for compatibility."""
2
+
3
+ from src.agents.state import (
4
+ MagenticState,
5
+ get_magentic_state,
6
+ init_magentic_state,
7
+ )
8
+
9
+ __all__ = ["MagenticState", "get_magentic_state", "init_magentic_state"]
src/tools/web_search.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Web search tool using DuckDuckGo."""
2
+
3
+ import asyncio
4
+
5
+ import structlog
6
+ from duckduckgo_search import DDGS
7
+
8
+ from src.utils.models import Citation, Evidence, SearchResult
9
+
10
+ logger = structlog.get_logger()
11
+
12
+
13
+ class WebSearchTool:
14
+ """Tool for searching the web using DuckDuckGo."""
15
+
16
+ def __init__(self) -> None:
17
+ self._ddgs = DDGS()
18
+
19
+ async def search(self, query: str, max_results: int = 10) -> SearchResult:
20
+ """Execute a web search."""
21
+ try:
22
+ loop = asyncio.get_running_loop()
23
+
24
+ def _do_search() -> list[dict[str, str]]:
25
+ # text() returns an iterator, need to list() it or iterate
26
+ return list(self._ddgs.text(query, max_results=max_results))
27
+
28
+ raw_results = await loop.run_in_executor(None, _do_search)
29
+
30
+ evidence = []
31
+ for r in raw_results:
32
+ ev = Evidence(
33
+ content=r.get("body", ""),
34
+ citation=Citation(
35
+ title=r.get("title", "No Title"),
36
+ url=r.get("href", ""),
37
+ source="web",
38
+ date="Unknown",
39
+ authors=[],
40
+ ),
41
+ relevance=0.0,
42
+ )
43
+ evidence.append(ev)
44
+
45
+ return SearchResult(
46
+ query=query, evidence=evidence, sources_searched=["web"], total_found=len(evidence)
47
+ )
48
+
49
+ except Exception as e:
50
+ logger.error("Web search failed", error=str(e))
51
+ return SearchResult(
52
+ query=query, evidence=[], sources_searched=["web"], total_found=0, errors=[str(e)]
53
+ )
src/utils/models.py CHANGED
@@ -6,7 +6,7 @@ from typing import Any, ClassVar, Literal
6
  from pydantic import BaseModel, Field
7
 
8
  # Centralized source type - add new sources here (e.g., new databases)
9
- SourceName = Literal["pubmed", "clinicaltrials", "europepmc", "preprint", "openalex"]
10
 
11
 
12
  class Citation(BaseModel):
 
6
  from pydantic import BaseModel, Field
7
 
8
  # Centralized source type - add new sources here (e.g., new databases)
9
+ SourceName = Literal["pubmed", "clinicaltrials", "europepmc", "preprint", "openalex", "web"]
10
 
11
 
12
  class Citation(BaseModel):
tests/unit/test_hierarchical.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unit tests for hierarchical orchestration middleware."""
2
+
3
+ from unittest.mock import AsyncMock
4
+
5
+ import pytest
6
+
7
+ from src.middleware.sub_iteration import SubIterationMiddleware
8
+ from src.utils.models import AssessmentDetails, JudgeAssessment
9
+
10
+ pytestmark = pytest.mark.unit
11
+
12
+
13
+ @pytest.mark.asyncio
14
+ async def test_sub_iteration_middleware():
15
+ team = AsyncMock()
16
+ team.execute.return_value = "Result"
17
+
18
+ judge = AsyncMock()
19
+ judge.assess.return_value = JudgeAssessment(
20
+ details=AssessmentDetails(
21
+ mechanism_score=10,
22
+ mechanism_reasoning="Good reasoning text here",
23
+ clinical_evidence_score=10,
24
+ clinical_reasoning="Good reasoning text here",
25
+ drug_candidates=[],
26
+ key_findings=[],
27
+ ),
28
+ sufficient=True,
29
+ confidence=1.0,
30
+ recommendation="synthesize",
31
+ next_search_queries=[],
32
+ reasoning="Good reasoning text here for the overall assessment which must be long enough.",
33
+ )
34
+
35
+ middleware = SubIterationMiddleware(team, judge)
36
+ result, assessment = await middleware.run("task")
37
+
38
+ assert result == "Result"
39
+ assert assessment.sufficient
40
+ assert team.execute.call_count == 1
uv.lock CHANGED
@@ -1063,6 +1063,7 @@ source = { editable = "." }
1063
  dependencies = [
1064
  { name = "anthropic" },
1065
  { name = "beautifulsoup4" },
 
1066
  { name = "gradio", extra = ["mcp"] },
1067
  { name = "httpx" },
1068
  { name = "huggingface-hub" },
@@ -1114,6 +1115,7 @@ requires-dist = [
1114
  { name = "beautifulsoup4", specifier = ">=4.12" },
1115
  { name = "chromadb", marker = "extra == 'embeddings'", specifier = ">=0.4.0" },
1116
  { name = "chromadb", marker = "extra == 'modal'", specifier = ">=0.4.0" },
 
1117
  { name = "gradio", extras = ["mcp"], specifier = ">=6.0.0" },
1118
  { name = "httpx", specifier = ">=0.27" },
1119
  { name = "huggingface-hub", specifier = ">=0.20.0" },
@@ -1230,6 +1232,20 @@ wheels = [
1230
  { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032 },
1231
  ]
1232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1233
  [[package]]
1234
  name = "durationpy"
1235
  version = "0.10"
@@ -2532,6 +2548,108 @@ wheels = [
2532
  { url = "https://files.pythonhosted.org/packages/f3/2b/851e78a60b85e8e8e8c6ebb9928f8e883df0340a93e34960ed9f0a41fa82/logfire_api-4.15.1-py3-none-any.whl", hash = "sha256:a88b5c4b6e4acbf6f35a3e992a63f271cf2797aefd21e1cfc93d52b21ade65f6", size = 95031 },
2533
  ]
2534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2535
  [[package]]
2536
  name = "markdown-it-py"
2537
  version = "4.0.0"
@@ -3794,6 +3912,22 @@ wheels = [
3794
  { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429 },
3795
  ]
3796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3797
  [[package]]
3798
  name = "prompt-toolkit"
3799
  version = "3.0.52"
 
1063
  dependencies = [
1064
  { name = "anthropic" },
1065
  { name = "beautifulsoup4" },
1066
+ { name = "duckduckgo-search" },
1067
  { name = "gradio", extra = ["mcp"] },
1068
  { name = "httpx" },
1069
  { name = "huggingface-hub" },
 
1115
  { name = "beautifulsoup4", specifier = ">=4.12" },
1116
  { name = "chromadb", marker = "extra == 'embeddings'", specifier = ">=0.4.0" },
1117
  { name = "chromadb", marker = "extra == 'modal'", specifier = ">=0.4.0" },
1118
+ { name = "duckduckgo-search", specifier = ">=5.0" },
1119
  { name = "gradio", extras = ["mcp"], specifier = ">=6.0.0" },
1120
  { name = "httpx", specifier = ">=0.27" },
1121
  { name = "huggingface-hub", specifier = ">=0.20.0" },
 
1232
  { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032 },
1233
  ]
1234
 
1235
+ [[package]]
1236
+ name = "duckduckgo-search"
1237
+ version = "8.1.1"
1238
+ source = { registry = "https://pypi.org/simple" }
1239
+ dependencies = [
1240
+ { name = "click" },
1241
+ { name = "lxml" },
1242
+ { name = "primp" },
1243
+ ]
1244
+ sdist = { url = "https://files.pythonhosted.org/packages/10/ef/07791a05751e6cc9de1dd49fb12730259ee109b18e6d097e25e6c32d5617/duckduckgo_search-8.1.1.tar.gz", hash = "sha256:9da91c9eb26a17e016ea1da26235d40404b46b0565ea86d75a9f78cc9441f935", size = 22868 }
1245
+ wheels = [
1246
+ { url = "https://files.pythonhosted.org/packages/db/72/c027b3b488b1010cf71670032fcf7e681d44b81829d484bb04e31a949a8d/duckduckgo_search-8.1.1-py3-none-any.whl", hash = "sha256:f48adbb06626ee05918f7e0cef3a45639e9939805c4fc179e68c48a12f1b5062", size = 18932 },
1247
+ ]
1248
+
1249
  [[package]]
1250
  name = "durationpy"
1251
  version = "0.10"
 
2548
  { url = "https://files.pythonhosted.org/packages/f3/2b/851e78a60b85e8e8e8c6ebb9928f8e883df0340a93e34960ed9f0a41fa82/logfire_api-4.15.1-py3-none-any.whl", hash = "sha256:a88b5c4b6e4acbf6f35a3e992a63f271cf2797aefd21e1cfc93d52b21ade65f6", size = 95031 },
2549
  ]
2550
 
2551
+ [[package]]
2552
+ name = "lxml"
2553
+ version = "6.0.2"
2554
+ source = { registry = "https://pypi.org/simple" }
2555
+ sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426 }
2556
+ wheels = [
2557
+ { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365 },
2558
+ { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793 },
2559
+ { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362 },
2560
+ { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152 },
2561
+ { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539 },
2562
+ { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853 },
2563
+ { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133 },
2564
+ { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944 },
2565
+ { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535 },
2566
+ { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343 },
2567
+ { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419 },
2568
+ { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008 },
2569
+ { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906 },
2570
+ { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357 },
2571
+ { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583 },
2572
+ { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591 },
2573
+ { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887 },
2574
+ { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818 },
2575
+ { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807 },
2576
+ { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179 },
2577
+ { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044 },
2578
+ { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685 },
2579
+ { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127 },
2580
+ { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958 },
2581
+ { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541 },
2582
+ { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426 },
2583
+ { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917 },
2584
+ { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795 },
2585
+ { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759 },
2586
+ { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666 },
2587
+ { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989 },
2588
+ { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456 },
2589
+ { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793 },
2590
+ { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836 },
2591
+ { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494 },
2592
+ { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146 },
2593
+ { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932 },
2594
+ { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060 },
2595
+ { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000 },
2596
+ { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496 },
2597
+ { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779 },
2598
+ { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072 },
2599
+ { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675 },
2600
+ { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171 },
2601
+ { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175 },
2602
+ { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688 },
2603
+ { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655 },
2604
+ { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695 },
2605
+ { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841 },
2606
+ { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700 },
2607
+ { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347 },
2608
+ { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248 },
2609
+ { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801 },
2610
+ { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403 },
2611
+ { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974 },
2612
+ { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953 },
2613
+ { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054 },
2614
+ { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421 },
2615
+ { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684 },
2616
+ { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463 },
2617
+ { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437 },
2618
+ { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890 },
2619
+ { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185 },
2620
+ { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895 },
2621
+ { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246 },
2622
+ { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797 },
2623
+ { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404 },
2624
+ { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072 },
2625
+ { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617 },
2626
+ { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930 },
2627
+ { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380 },
2628
+ { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632 },
2629
+ { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171 },
2630
+ { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109 },
2631
+ { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061 },
2632
+ { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233 },
2633
+ { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739 },
2634
+ { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119 },
2635
+ { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665 },
2636
+ { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997 },
2637
+ { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957 },
2638
+ { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372 },
2639
+ { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653 },
2640
+ { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795 },
2641
+ { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023 },
2642
+ { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420 },
2643
+ { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837 },
2644
+ { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205 },
2645
+ { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829 },
2646
+ { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277 },
2647
+ { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433 },
2648
+ { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119 },
2649
+ { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314 },
2650
+ { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768 },
2651
+ ]
2652
+
2653
  [[package]]
2654
  name = "markdown-it-py"
2655
  version = "4.0.0"
 
3912
  { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429 },
3913
  ]
3914
 
3915
+ [[package]]
3916
+ name = "primp"
3917
+ version = "0.15.0"
3918
+ source = { registry = "https://pypi.org/simple" }
3919
+ sdist = { url = "https://files.pythonhosted.org/packages/56/0b/a87556189da4de1fc6360ca1aa05e8335509633f836cdd06dd17f0743300/primp-0.15.0.tar.gz", hash = "sha256:1af8ea4b15f57571ff7fc5e282a82c5eb69bc695e19b8ddeeda324397965b30a", size = 113022 }
3920
+ wheels = [
3921
+ { url = "https://files.pythonhosted.org/packages/f5/5a/146ac964b99ea7657ad67eb66f770be6577dfe9200cb28f9a95baffd6c3f/primp-0.15.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:1b281f4ca41a0c6612d4c6e68b96e28acfe786d226a427cd944baa8d7acd644f", size = 3178914 },
3922
+ { url = "https://files.pythonhosted.org/packages/bc/8a/cc2321e32db3ce64d6e32950d5bcbea01861db97bfb20b5394affc45b387/primp-0.15.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:489cbab55cd793ceb8f90bb7423c6ea64ebb53208ffcf7a044138e3c66d77299", size = 2955079 },
3923
+ { url = "https://files.pythonhosted.org/packages/c3/7b/cbd5d999a07ff2a21465975d4eb477ae6f69765e8fe8c9087dab250180d8/primp-0.15.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b45c23f94016215f62d2334552224236217aaeb716871ce0e4dcfa08eb161", size = 3281018 },
3924
+ { url = "https://files.pythonhosted.org/packages/1b/6e/a6221c612e61303aec2bcac3f0a02e8b67aee8c0db7bdc174aeb8010f975/primp-0.15.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e985a9cba2e3f96a323722e5440aa9eccaac3178e74b884778e926b5249df080", size = 3255229 },
3925
+ { url = "https://files.pythonhosted.org/packages/3b/54/bfeef5aca613dc660a69d0760a26c6b8747d8fdb5a7f20cb2cee53c9862f/primp-0.15.0-cp38-abi3-manylinux_2_34_armv7l.whl", hash = "sha256:6b84a6ffa083e34668ff0037221d399c24d939b5629cd38223af860de9e17a83", size = 3014522 },
3926
+ { url = "https://files.pythonhosted.org/packages/ac/96/84078e09f16a1dad208f2fe0f8a81be2cf36e024675b0f9eec0c2f6e2182/primp-0.15.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:592f6079646bdf5abbbfc3b0a28dac8de943f8907a250ce09398cda5eaebd260", size = 3418567 },
3927
+ { url = "https://files.pythonhosted.org/packages/6c/80/8a7a9587d3eb85be3d0b64319f2f690c90eb7953e3f73a9ddd9e46c8dc42/primp-0.15.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a728e5a05f37db6189eb413d22c78bd143fa59dd6a8a26dacd43332b3971fe8", size = 3606279 },
3928
+ { url = "https://files.pythonhosted.org/packages/0c/dd/f0183ed0145e58cf9d286c1b2c14f63ccee987a4ff79ac85acc31b5d86bd/primp-0.15.0-cp38-abi3-win_amd64.whl", hash = "sha256:aeb6bd20b06dfc92cfe4436939c18de88a58c640752cf7f30d9e4ae893cdec32", size = 3149967 },
3929
+ ]
3930
+
3931
  [[package]]
3932
  name = "prompt-toolkit"
3933
  version = "3.0.52"