refactor: replace mirascope with handrolled client (#202)

* feat: add optional JWT and webhook secrets to honcho instance creation

* chore: ignore spurious warnings

* feat: add response format if using gpt-5 model family

* feat: add response models to all apis except anthropic

* fix: raise NotImplementedError for response models in AsyncAnthropic client

* chore: address review

* chore: add tests, cleanup

* fix: use tenacity instead of custom fns

* fix: pass all params to streaming, nonblocking streaming

* chore: fix test mock
This commit is contained in:
doria 2025-09-24 11:53:30 -04:00 committed by GitHub
parent 3f47866ae0
commit 5d88c459b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 2006 additions and 818 deletions

View File

@ -9,6 +9,7 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fastapi[standard]>=0.111.0",
"groq>=0.31.0",
"python-dotenv>=1.0.0",
"sqlalchemy>=2.0.30",
"fastapi-pagination>=0.12.24",
@ -21,12 +22,13 @@ dependencies = [
"nanoid>=2.0.0",
"alembic>=1.14.0",
"pyjwt>=2.10.0",
"tenacity>=9.1.2",
"tiktoken>=0.9.0",
"mirascope[anthropic,google,groq,langfuse]>=1.25.5",
"langfuse>=3.3.2",
"openai>=1.99.7",
"pydantic>=2.11.7",
"pydantic-settings>=2.10.1",
"google-generativeai>=0.8.5",
"google-genai>=1.32.0",
"pdfplumber>=0.11.7",
"typing-extensions>=4.11.0",
]
@ -93,6 +95,6 @@ reportUnusedCallResult = false
reportCallInDefaultInitializer = false
reportAny = false
reportExplicitAny = false
allowedUntypedLibraries = ["langfuse", "langfuse.decorators", "mirascope"]
allowedUntypedLibraries = ["langfuse"]
reportImplicitOverride = false
reportImportCycles = false

View File

@ -14,7 +14,7 @@ from pydantic_settings import (
SettingsConfigDict,
)
from src.utils.types import Providers
from src.utils.types import SupportedProviders
# Load .env file for local development.
# Make sure this is called before AppSettings is instantiated if you rely on .env for AppSettings construction.
@ -187,14 +187,14 @@ class DeriverSettings(HonchoSettings):
] = 1.0
STALE_SESSION_TIMEOUT_MINUTES: Annotated[int, Field(default=5, gt=0, le=1440)] = 5
PROVIDER: Providers = "google"
PROVIDER: SupportedProviders = "google"
MODEL: str = "gemini-2.5-flash"
MAX_OUTPUT_TOKENS: Annotated[int, Field(default=2500, gt=0, le=100_000)] = 2500
# Thinking budget tokens are only applied when using Anthropic as provider
THINKING_BUDGET_TOKENS: Annotated[int, Field(default=1024, gt=0, le=5000)] = 1024
PEER_CARD_PROVIDER: Providers = "openai"
PEER_CARD_PROVIDER: SupportedProviders = "openai"
PEER_CARD_MODEL: str = "gpt-5-nano-2025-08-07"
# Note: peer cards should be very short, but GPT-5 models need output tokens for thinking which cannot be turned off...
PEER_CARD_MAX_OUTPUT_TOKENS: Annotated[
@ -216,11 +216,11 @@ class DeriverSettings(HonchoSettings):
class DialecticSettings(HonchoSettings):
model_config = SettingsConfigDict(env_prefix="DIALECTIC_", extra="ignore") # pyright: ignore
PROVIDER: Providers = "anthropic"
PROVIDER: SupportedProviders = "anthropic"
MODEL: str = "claude-sonnet-4-20250514"
PERFORM_QUERY_GENERATION: bool = False
QUERY_GENERATION_PROVIDER: Providers = "groq"
QUERY_GENERATION_PROVIDER: SupportedProviders = "groq"
QUERY_GENERATION_MODEL: str = "llama-3.1-8b-instant"
MAX_OUTPUT_TOKENS: Annotated[int, Field(default=2500, gt=0, le=100_000)] = 2500
@ -243,7 +243,7 @@ class SummarySettings(HonchoSettings):
MESSAGES_PER_SHORT_SUMMARY: Annotated[int, Field(default=20, gt=0, le=100)] = 20
MESSAGES_PER_LONG_SUMMARY: Annotated[int, Field(default=60, gt=0, le=500)] = 60
PROVIDER: Providers = "openai"
PROVIDER: SupportedProviders = "openai"
MODEL: str = "gpt-4o-mini-2024-07-18"
MAX_TOKENS_SHORT: Annotated[int, Field(default=1000, gt=0, le=10_000)] = 1000
MAX_TOKENS_LONG: Annotated[int, Field(default=4000, gt=0, le=20_000)] = 4000

View File

@ -1,5 +1,5 @@
from logging import getLogger
from typing import Any, cast
from typing import Any, Final, cast
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
@ -12,7 +12,7 @@ from src.utils.shared_models import ObservationDict
logger = getLogger(__name__)
# The collection name for documents that make up a peer's global representation
GLOBAL_REPRESENTATION_COLLECTION_NAME = "global_representation"
GLOBAL_REPRESENTATION_COLLECTION_NAME: Final[str] = "global_representation"
# The key for the working representation in the session peer's internal_metadata
WORKING_REPRESENTATION_METADATA_KEY = "working_representation"

View File

@ -2,7 +2,7 @@ import logging
from typing import Any
import sentry_sdk
from langfuse.decorators import langfuse_context
from langfuse import get_client
from pydantic import ValidationError
from rich.console import Console
@ -24,6 +24,8 @@ logging.getLogger("sqlalchemy.engine.Engine").disabled = True
console = Console(markup=True)
lf = get_client()
async def process_item(task_type: str, payload: dict[str, Any]) -> None:
"""Validate an incoming queue payload and dispatch it to the appropriate handler.
@ -46,7 +48,7 @@ async def process_item(task_type: str, payload: dict[str, Any]) -> None:
logger.debug("Finished processing webhook %s", validated.event_type)
elif task_type == "summary":
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_trace( # type: ignore
lf.update_current_trace( # type: ignore
metadata={
"critical_analysis_model": settings.DERIVER.MODEL,
}
@ -61,7 +63,7 @@ async def process_item(task_type: str, payload: dict[str, Any]) -> None:
await process_summary_task(validated)
elif task_type == "representation":
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_trace(
lf.update_current_trace(
metadata={
"critical_analysis_model": settings.DERIVER.MODEL,
}

View File

@ -5,7 +5,7 @@ import time
from typing import Any
import sentry_sdk
from langfuse.decorators import langfuse_context
from langfuse import get_client
from src import crud, exceptions
from src.config import settings
@ -48,20 +48,9 @@ from .queue_payload import (
logger = logging.getLogger(__name__)
logging.getLogger("sqlalchemy.engine.Engine").disabled = True
lf = get_client()
@honcho_llm_call(
provider=settings.DERIVER.PROVIDER,
model=settings.DERIVER.MODEL,
track_name="Critical Analysis Call",
response_model=ReasoningResponse,
json_mode=True,
max_tokens=settings.DERIVER.MAX_OUTPUT_TOKENS or settings.LLM.DEFAULT_MAX_TOKENS,
thinking_budget_tokens=settings.DERIVER.THINKING_BUDGET_TOKENS
if settings.DERIVER.PROVIDER == "anthropic"
else None,
enable_retry=True,
retry_attempts=3,
)
async def critical_analysis_call(
peer_id: str,
peer_card: list[str] | None,
@ -69,8 +58,8 @@ async def critical_analysis_call(
working_representation: str | None,
history: str,
new_turn: str,
):
return critical_analysis_prompt(
) -> ReasoningResponse:
prompt = critical_analysis_prompt(
peer_id=peer_id,
peer_card=peer_card,
message_created_at=message_created_at,
@ -79,28 +68,51 @@ async def critical_analysis_call(
new_turn=new_turn,
)
response = await honcho_llm_call(
provider=settings.DERIVER.PROVIDER,
model=settings.DERIVER.MODEL,
prompt=prompt,
max_tokens=settings.DERIVER.MAX_OUTPUT_TOKENS
or settings.LLM.DEFAULT_MAX_TOKENS,
track_name="Critical Analysis Call",
response_model=ReasoningResponse,
json_mode=True,
thinking_budget_tokens=settings.DERIVER.THINKING_BUDGET_TOKENS,
enable_retry=True,
retry_attempts=3,
)
return response.content
@honcho_llm_call(
provider=settings.DERIVER.PEER_CARD_PROVIDER,
model=settings.DERIVER.PEER_CARD_MODEL,
track_name="Peer Card Call",
response_model=PeerCardQuery,
json_mode=True,
max_tokens=settings.DERIVER.PEER_CARD_MAX_OUTPUT_TOKENS
or settings.LLM.DEFAULT_MAX_TOKENS,
reasoning_effort="minimal",
enable_retry=True,
retry_attempts=1, # unstructured output means we shouldn't need to retry, 1 just in case
)
async def peer_card_call(
old_peer_card: list[str] | None,
new_observations: list[str],
):
return peer_card_prompt(
) -> PeerCardQuery:
"""
Generate peer card prompt, call LLM with response model.
"""
prompt = peer_card_prompt(
old_peer_card=old_peer_card,
new_observations=new_observations,
)
response = await honcho_llm_call(
provider=settings.DERIVER.PEER_CARD_PROVIDER,
model=settings.DERIVER.PEER_CARD_MODEL,
prompt=prompt,
max_tokens=settings.DERIVER.PEER_CARD_MAX_OUTPUT_TOKENS
or settings.LLM.DEFAULT_MAX_TOKENS,
track_name="Peer Card Call",
response_model=PeerCardQuery,
json_mode=True,
reasoning_effort="minimal",
enable_retry=True,
retry_attempts=3,
)
return response.content
@conditional_observe
@sentry_sdk.trace
@ -272,7 +284,7 @@ async def process_representation_task(
)
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_trace(
lf.update_current_trace(
output=format_reasoning_response_as_markdown(final_observations)
)
@ -302,7 +314,7 @@ class CertaintyReasoner:
"""
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_observation(
lf.update_current_generation(
input=format_reasoning_inputs_as_markdown(
working_representation,
history,
@ -392,7 +404,7 @@ class CertaintyReasoner:
)
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_observation(
lf.update_current_generation(
output=format_reasoning_response_as_markdown(response),
)

View File

@ -8,10 +8,7 @@ and reasoning tasks.
import datetime
from inspect import cleandoc as c
from mirascope import prompt_template
@prompt_template()
def critical_analysis_prompt(
peer_id: str,
peer_card: list[str] | None,
@ -102,7 +99,6 @@ New conversation turn to analyze:
)
@prompt_template()
def peer_card_prompt(
old_peer_card: list[str] | None,
new_observations: list[str],

View File

@ -9,18 +9,18 @@ historical observations.
import asyncio
import logging
import uuid
from collections.abc import AsyncIterator
import tiktoken
from dotenv import load_dotenv
from langfuse.decorators import langfuse_context
from mirascope.llm import Stream
from langfuse import get_client
from src import crud
from src.config import settings
from src.crud.representation import GLOBAL_REPRESENTATION_COLLECTION_NAME
from src.dependencies import tracked_db
from src.utils import summarizer
from src.utils.clients import honcho_llm_call
from src.utils.clients import HonchoLLMCallStreamChunk, honcho_llm_call
from src.utils.embedding_store import EmbeddingStore
from src.utils.logging import (
accumulate_metric,
@ -36,18 +36,10 @@ logger = logging.getLogger(__name__)
# Load environment variables
load_dotenv()
# Create langfuse client
lf = get_client()
@honcho_llm_call(
provider=settings.DIALECTIC.PROVIDER,
model=settings.DIALECTIC.MODEL,
track_name="Dialectic Call",
max_tokens=settings.DIALECTIC.MAX_OUTPUT_TOKENS,
thinking_budget_tokens=settings.DIALECTIC.THINKING_BUDGET_TOKENS
if settings.DIALECTIC.PROVIDER == "anthropic"
else None,
enable_retry=True,
retry_attempts=3,
)
async def dialectic_call(
query: str,
working_representation: str | None,
@ -71,7 +63,7 @@ async def dialectic_call(
Model response
"""
# Generate the prompt and log it
prompt_result = dialectic_prompt(
prompt = dialectic_prompt(
query,
working_representation,
recent_conversation_history,
@ -82,32 +74,26 @@ async def dialectic_call(
target_peer_card,
)
# Pretty print the prompt content
if len(prompt_result) > 0:
# Extract content from the first BaseMessageParam
prompt_content = prompt_result[0].content
else:
prompt_content = str(prompt_result)
response = await honcho_llm_call(
provider=settings.DIALECTIC.PROVIDER,
model=settings.DIALECTIC.MODEL,
prompt=prompt,
max_tokens=settings.DIALECTIC.MAX_OUTPUT_TOKENS,
track_name="Dialectic Call",
thinking_budget_tokens=settings.DIALECTIC.THINKING_BUDGET_TOKENS
if settings.DIALECTIC.PROVIDER == "anthropic"
else None,
enable_retry=True,
retry_attempts=3,
)
logger.debug("=== DIALECTIC PROMPT ===")
logger.debug(prompt_content)
logger.debug(prompt)
logger.debug("=== END DIALECTIC PROMPT ===")
return prompt_result
return response.content
@honcho_llm_call(
provider=settings.DIALECTIC.PROVIDER,
model=settings.DIALECTIC.MODEL,
track_name="Dialectic Stream",
max_tokens=settings.DIALECTIC.MAX_OUTPUT_TOKENS,
thinking_budget_tokens=settings.DIALECTIC.THINKING_BUDGET_TOKENS
if settings.DIALECTIC.PROVIDER == "anthropic"
else None,
enable_retry=True,
retry_attempts=3,
stream=True,
)
async def dialectic_stream(
query: str,
working_representation: str | None,
@ -131,7 +117,7 @@ async def dialectic_stream(
Streaming model response
"""
# Generate the prompt and log it
prompt_result = dialectic_prompt(
prompt = dialectic_prompt(
query,
working_representation,
recent_conversation_history,
@ -142,18 +128,25 @@ async def dialectic_stream(
target_peer_card,
)
# Pretty print the prompt content
if len(prompt_result) > 0:
# Extract content from the first BaseMessageParam
prompt_content = prompt_result[0].content
else:
prompt_content = str(prompt_result)
response = await honcho_llm_call(
provider=settings.DIALECTIC.PROVIDER,
model=settings.DIALECTIC.MODEL,
prompt=prompt,
max_tokens=settings.DIALECTIC.MAX_OUTPUT_TOKENS,
track_name="Dialectic Stream",
thinking_budget_tokens=settings.DIALECTIC.THINKING_BUDGET_TOKENS
if settings.DIALECTIC.PROVIDER == "anthropic"
else None,
enable_retry=True,
retry_attempts=3,
stream=True,
)
logger.debug("=== DIALECTIC PROMPT (STREAM) ===")
logger.debug(prompt_content)
logger.debug(prompt)
logger.debug("=== END DIALECTIC PROMPT ===")
return prompt_result
return response
async def chat(
@ -164,7 +157,7 @@ async def chat(
query: str,
*,
stream: bool = False,
) -> Stream | str:
) -> str | AsyncIterator[HonchoLLMCallStreamChunk]:
"""
Chat with the Dialectic API that builds on-demand user representations.
@ -197,7 +190,7 @@ async def chat(
context_window_size -= len(tokenizer.encode(query))
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_trace(
lf.update_current_trace(
metadata={
"query_generation_model": settings.DIALECTIC.QUERY_GENERATION_MODEL,
"query_generation_provider": settings.DIALECTIC.QUERY_GENERATION_PROVIDER,

View File

@ -1,9 +1,6 @@
from inspect import cleandoc as c
from mirascope import prompt_template
@prompt_template()
def dialectic_prompt(
query: str,
working_representation: str | None,
@ -86,7 +83,6 @@ Provide a natural language response that:
)
@prompt_template()
def query_generation_prompt(query: str, target_peer_name: str) -> str:
"""
Generate the prompt for semantic query expansion.
@ -101,7 +97,7 @@ def query_generation_prompt(query: str, target_peer_name: str) -> str:
"""
return c(
f"""
You are a query expansion agent helping AI applications understand their users. The user's name is {target_peer_name}. Your job is to take application queries about this user and generate targeted search queries that will retrieve the most relevant observations using semantic search over an embedding store containing observations about the user.
You are a query expansion agent helping AI applications understand their users. The user's name is {target_peer_name}. Your job is to take application queries about this user and generate targeted search queries that will retrieve the most relevant observations using semantic search over an embedding store containing observations about the user.
## QUERY EXPANSION STRATEGY FOR SEMANTIC SIMILARITY

View File

@ -3,7 +3,7 @@ import json
import logging
from typing import Any
from langfuse.decorators import langfuse_context
from langfuse import get_client
from src.config import settings
from src.models import Document
@ -21,6 +21,8 @@ from .prompts import query_generation_prompt
# Configure logging
logger = logging.getLogger(__name__)
lf = get_client()
@conditional_observe
async def get_observations(
@ -75,7 +77,7 @@ async def get_observations(
unique_observations = _deduplicate_observations(all_results)
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_observation(
lf.update_current_generation(
input={
"query": query,
"include_premises": include_premises,
@ -87,7 +89,7 @@ async def get_observations(
},
)
langfuse_context.update_current_trace(
lf.update_current_trace(
metadata={
"search_queries": search_queries,
"observations_retrieved": unique_observations,
@ -227,13 +229,18 @@ def _format_observations(
return "\n".join(parts).strip()
@honcho_llm_call(
provider=settings.DIALECTIC.QUERY_GENERATION_PROVIDER,
model=settings.DIALECTIC.QUERY_GENERATION_MODEL,
response_model=SemanticQueries,
enable_retry=True,
retry_attempts=3,
)
async def generate_semantic_queries(query: str, target_peer_name: str):
async def generate_semantic_queries(
query: str, target_peer_name: str
) -> SemanticQueries:
"""Generate semantic search queries for observation retrieval."""
return query_generation_prompt(query, target_peer_name)
prompt = query_generation_prompt(query, target_peer_name)
response = await honcho_llm_call(
provider=settings.DIALECTIC.QUERY_GENERATION_PROVIDER,
model=settings.DIALECTIC.QUERY_GENERATION_MODEL,
prompt=prompt,
max_tokens=settings.LLM.DEFAULT_MAX_TOKENS,
response_model=SemanticQueries,
enable_retry=True,
retry_attempts=3,
)
return response.content

View File

@ -1,5 +1,5 @@
import logging
from collections.abc import AsyncGenerator
from collections.abc import AsyncGenerator, AsyncIterator
from fastapi import (
APIRouter,
@ -11,7 +11,6 @@ from fastapi.exceptions import HTTPException
from fastapi.responses import StreamingResponse
from fastapi_pagination import Page
from fastapi_pagination.ext.sqlalchemy import apaginate
from mirascope.llm import Stream
from sqlalchemy.ext.asyncio import AsyncSession
from src import crud, schemas
@ -188,8 +187,8 @@ async def chat(
query=options.query,
stream=options.stream,
)
if isinstance(stream, Stream):
async for chunk, _ in stream:
if isinstance(stream, AsyncIterator):
async for chunk in stream:
yield chunk.content
else:
raise HTTPException(status_code=500, detail="Invalid stream type")

View File

@ -1,392 +1,657 @@
from collections.abc import Awaitable, Callable
from typing import (
Any,
Literal,
ParamSpec,
Protocol,
TypeVar,
overload,
runtime_checkable,
)
# --- OpenAI compatibility shim must run BEFORE importing mirascope ---
# Some versions of the OpenAI SDK do not expose ChatCompletionMessageToolCall
# at openai.types.chat, but some integrations import it from there at runtime.
# We defensively define it if missing to avoid import-time failures.
#
# We can get rid of this by getting rid of mirascope...
from openai.types import chat as _openai_chat_types # type: ignore
if not hasattr(_openai_chat_types, "ChatCompletionMessageToolCall"):
_openai_chat_types.ChatCompletionMessageToolCall = object # pyright: ignore
from collections.abc import AsyncIterator, Callable
from functools import wraps
from typing import Any, Generic, Literal, TypeVar, cast, overload
from anthropic import AsyncAnthropic
from anthropic.types import TextBlock
from anthropic.types.message import Message as AnthropicMessage
from google import genai
from google.genai.types import GenerateContentResponse
from groq import AsyncGroq
from mirascope import llm
from mirascope.core import ResponseModelConfigDict
from mirascope.integrations.langfuse import with_langfuse
from mirascope.llm import Stream
from langfuse import get_client
from openai import AsyncOpenAI
from pydantic import BaseModel
from openai.types.chat import ChatCompletion, ChatCompletionChunk
from pydantic import BaseModel, Field
from sentry_sdk.ai.monitoring import ai_track
from tenacity import retry, stop_after_attempt, wait_exponential
from src.config import settings
from src.utils.types import Providers
from src.utils.types import SupportedProviders
clients: dict[Providers, AsyncAnthropic | AsyncOpenAI | genai.Client | AsyncGroq] = {}
T = TypeVar("T")
M = TypeVar("M", bound=BaseModel)
lf = get_client()
CLIENTS: dict[
SupportedProviders,
AsyncAnthropic | AsyncOpenAI | genai.Client | AsyncGroq,
] = {}
if settings.LLM.ANTHROPIC_API_KEY:
anthropic = AsyncAnthropic(api_key=settings.LLM.ANTHROPIC_API_KEY)
clients["anthropic"] = anthropic
CLIENTS["anthropic"] = anthropic
if settings.LLM.OPENAI_API_KEY:
openai_client = AsyncOpenAI(
api_key=settings.LLM.OPENAI_API_KEY,
)
clients["openai"] = openai_client
CLIENTS["openai"] = openai_client
if settings.LLM.OPENAI_COMPATIBLE_BASE_URL:
clients["custom"] = AsyncOpenAI(
CLIENTS["custom"] = AsyncOpenAI(
api_key=settings.LLM.OPENAI_COMPATIBLE_API_KEY,
base_url=settings.LLM.OPENAI_COMPATIBLE_BASE_URL,
)
if settings.LLM.GEMINI_API_KEY:
google = genai.Client(api_key=settings.LLM.GEMINI_API_KEY)
clients["google"] = google
google = genai.client.Client(api_key=settings.LLM.GEMINI_API_KEY)
CLIENTS["google"] = google
if settings.LLM.GROQ_API_KEY:
groq = AsyncGroq(api_key=settings.LLM.GROQ_API_KEY)
clients["groq"] = groq
CLIENTS["groq"] = groq
providers = [
SELECTED_PROVIDERS = [
("Dialectic", settings.DIALECTIC.PROVIDER),
("Summary", settings.SUMMARY.PROVIDER),
("Deriver", settings.DERIVER.PROVIDER),
("Query Generation Provider", settings.DIALECTIC.QUERY_GENERATION_PROVIDER),
]
for provider_name, provider_value in providers:
if provider_value not in clients:
for provider_name, provider_value in SELECTED_PROVIDERS:
if provider_value not in CLIENTS:
raise ValueError(f"Missing client for {provider_name}: {provider_value}")
P = ParamSpec("P")
T = TypeVar("T", bound=BaseModel)
T_co = TypeVar("T_co", bound=BaseModel, covariant=True)
F = TypeVar("F", bound=Callable[..., Any])
# Define protocols for different return types
@runtime_checkable
class AsyncResponseModelCallable(Protocol[P, T_co]):
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T_co: ...
@runtime_checkable
class SyncResponseModelCallable(Protocol[P, T_co]):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T_co: ...
@runtime_checkable
class AsyncStreamCallable(Protocol[P]):
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Stream: ...
@runtime_checkable
class SyncStreamCallable(Protocol[P]):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> Stream: ...
@runtime_checkable
class AsyncStringCallable(Protocol[P]):
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> str: ...
@runtime_checkable
class SyncStringCallable(Protocol[P]):
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> str: ...
@runtime_checkable
class AsyncCallResponseCallable(Protocol[P]):
async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> llm.CallResponse: ...
# Overload for stream=True with async function
@overload
def honcho_llm_call(
*,
provider: Providers | None = None,
model: str | None = None,
track_name: str | None = None,
response_model: type[BaseModel] | None = None,
json_mode: bool = False,
max_tokens: int | None = None,
reasoning_effort: Literal["low", "medium", "high", "minimal"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: Literal[True],
**extra_call_params: Any,
) -> Callable[[Callable[P, Awaitable[Any]]], AsyncStreamCallable[P]]: ...
# Overload for response_model with async function
@overload
def honcho_llm_call(
*,
provider: Providers | None = None,
model: str | None = None,
track_name: str | None = None,
response_model: type[T],
json_mode: bool = False,
max_tokens: int | None = None,
reasoning_effort: Literal["low", "medium", "high", "minimal"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: Literal[False] = False,
**extra_call_params: Any,
) -> Callable[[Callable[P, Awaitable[Any]]], AsyncResponseModelCallable[P, T]]: ...
# Overload for return_call_response=True with async function
@overload
def honcho_llm_call(
*,
provider: Providers | None = None,
model: str | None = None,
track_name: str | None = None,
response_model: None = None,
json_mode: bool = False,
max_tokens: int | None = None,
reasoning_effort: Literal["low", "medium", "high", "minimal"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: Literal[False] = False,
return_call_response: Literal[True],
**extra_call_params: Any,
) -> Callable[[Callable[P, Awaitable[Any]]], AsyncCallResponseCallable[P]]: ...
# Overload for no response_model with async function (string return)
@overload
def honcho_llm_call(
*,
provider: Providers | None = None,
model: str | None = None,
track_name: str | None = None,
response_model: None = None,
json_mode: bool = False,
max_tokens: int | None = None,
reasoning_effort: Literal["low", "medium", "high", "minimal"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: Literal[False] = False,
return_call_response: Literal[False],
**extra_call_params: Any,
) -> Callable[[Callable[P, Awaitable[Any]]], AsyncStringCallable[P]]: ...
# Generic overload for sync functions (fallback)
@overload
def honcho_llm_call(
*,
provider: Providers | None = None,
model: str | None = None,
track_name: str | None = None,
response_model: type[BaseModel] | None = None,
json_mode: bool = False,
max_tokens: int | None = None,
reasoning_effort: Literal["low", "medium", "high", "minimal"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: bool = False,
**extra_call_params: Any,
) -> Callable[[Callable[P, Any]], Callable[P, Any]]: ...
def honcho_llm_call(
provider: Providers | None = None,
model: str | None = None,
track_name: str | None = None,
response_model: type[BaseModel] | None = None,
json_mode: bool = False,
max_tokens: int | None = None,
reasoning_effort: Literal["low", "medium", "high", "minimal"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: bool = False,
return_call_response: bool = False, # pyright: ignore
**extra_call_params: Any,
) -> Any:
class HonchoLLMCallResponse(BaseModel, Generic[T]):
"""
Consolidated decorator for LLM calls that handles provider-specific configurations.
This decorator automatically:
- Handles both sync and async functions seamlessly
- Applies retry logic with exponential backoff
- Adds AI tracking for Sentry
- Integrates with Langfuse for observability
- Builds provider-specific call parameters
- Handles client selection from the global clients dict
Response object for LLM calls.
Args:
provider: The LLM provider to use (e.g., "anthropic", "google", "openai")
model: The model to use
track_name: Name for AI tracking (e.g., "Critical Analysis Call")
response_model: Optional Pydantic model for structured responses
json_mode: Whether to enable JSON mode (for providers that support it)
max_tokens: Maximum tokens for the response
reasoning_effort: Optional reasoning effort hint passed to OpenAI GPT-5 models only
verbosity: Optional verbosity hint passed to OpenAI GPT-5 models only
thinking_budget_tokens: Budget for thinking tokens (Anthropic only)
enable_retry: Whether to enable retry logic (default: True)
retry_attempts: Number of retry attempts (default: 3)
stream: Whether to enable streaming responses (default: False)
_return_call_response: Whether to return the full CallResponse object (default: False)
**extra_call_params: Additional provider-specific parameters
Returns:
A decorator that returns:
- For async functions: Callable[P, Awaitable[T]] where T is Stream, response_model, CallResponse, or str
- For sync functions: Callable[P, T] where T is Stream, response_model, CallResponse, or str
Note: Type annotations may be needed at the call site for proper type checking.
Example (async function):
@honcho_llm_call(
provider=settings.DERIVER.PROVIDER,
model=settings.DERIVER.MODEL,
track_name="Critical Analysis Call",
response_model=ReasoningResponse,
json_mode=True,
max_tokens=settings.DERIVER.MAX_OUTPUT_TOKENS,
)
async def analyze(context: str, query: str):
return prompt_template(context, query)
Example (sync function):
@honcho_llm_call(
provider="openai",
model="gpt-4",
max_tokens=1000,
)
def generate_summary(text: str) -> str:
return f"Summarize: {text}"
# Call synchronously
result = generate_summary("Long text here...")
content: The response content. When a response_model is provided, this will be
the parsed object of that type. Otherwise, it will be a string.
output_tokens: Number of tokens generated in the response.
finish_reasons: List of finish reasons for the response.
"""
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
# Handle special case for custom provider
# Custom providers use OpenAI-compatible endpoints, so we resolve to "openai" for the provider name
# but keep the original "custom" for client lookup
resolved_provider = "openai" if provider == "custom" else provider
content: T
output_tokens: int
finish_reasons: list[str]
# Build provider-specific call params
call_params: dict[str, Any] = {}
if resolved_provider == "google":
# Google uses 'config' parameter
config: dict[str, Any] = {}
if max_tokens:
config["max_output_tokens"] = max_tokens
class HonchoLLMCallStreamChunk(BaseModel):
"""
A single chunk in a streaming LLM response.
Args:
content: The text content for this chunk. Empty for chunks that only contain metadata.
is_done: Whether this is the final chunk in the stream.
finish_reasons: List of finish reasons if the stream is complete.
"""
content: str
is_done: bool = False
finish_reasons: list[str] = Field(default_factory=list)
@overload
async def honcho_llm_call(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
track_name: str | None = None,
*,
response_model: type[M],
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: Literal[False] = False,
) -> HonchoLLMCallResponse[M]: ...
@overload
async def honcho_llm_call(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
track_name: str | None = None,
response_model: None = None,
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: Literal[False] = False,
) -> HonchoLLMCallResponse[str]: ...
@overload
async def honcho_llm_call(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
track_name: str | None = None,
response_model: type[BaseModel] | None = None,
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: Literal[True] = ...,
) -> AsyncIterator[HonchoLLMCallStreamChunk]: ...
async def honcho_llm_call(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
track_name: str | None = None,
response_model: type[BaseModel] | None = None,
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None,
enable_retry: bool = True,
retry_attempts: int = 3,
stream: bool = False,
) -> HonchoLLMCallResponse[Any] | AsyncIterator[HonchoLLMCallStreamChunk]:
client = CLIENTS.get(provider)
if not client:
raise ValueError(f"Missing client for {provider}")
decorated = honcho_llm_call_inner
# apply langfuse if enabled
if settings.LANGFUSE_PUBLIC_KEY:
decorated = with_langfuse(decorated)
# apply tracking
if track_name:
decorated = ai_track(track_name)(decorated)
# apply retry logic
if enable_retry:
decorated = retry(
stop=stop_after_attempt(retry_attempts),
wait=wait_exponential(multiplier=1, min=4, max=10),
)(decorated)
if stream:
return await decorated(
provider,
model,
prompt,
max_tokens,
response_model,
json_mode,
reasoning_effort,
verbosity,
thinking_budget_tokens,
True,
)
else:
return await decorated(
provider,
model,
prompt,
max_tokens,
response_model,
json_mode,
reasoning_effort,
verbosity,
thinking_budget_tokens,
False,
)
@overload
async def honcho_llm_call_inner(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
response_model: type[M],
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None, # Anthropic only
stream: Literal[False] = False,
) -> HonchoLLMCallResponse[M]: ...
@overload
async def honcho_llm_call_inner(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
response_model: None = None,
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None, # Anthropic only
stream: Literal[False] = False,
) -> HonchoLLMCallResponse[str]: ...
@overload
async def honcho_llm_call_inner(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
response_model: type[BaseModel] | None = None,
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None, # Anthropic only
stream: Literal[True] = ...,
) -> AsyncIterator[HonchoLLMCallStreamChunk]: ...
async def honcho_llm_call_inner(
provider: SupportedProviders,
model: str,
prompt: str,
max_tokens: int,
response_model: type[BaseModel] | None = None,
json_mode: bool = False,
reasoning_effort: Literal["low", "medium", "high", "minimal"]
| None = None, # OpenAI only
verbosity: Literal["low", "medium", "high"] | None = None, # OpenAI only
thinking_budget_tokens: int | None = None, # Anthropic only
stream: bool = False,
) -> HonchoLLMCallResponse[Any] | AsyncIterator[HonchoLLMCallStreamChunk]:
# has already been validated by honcho_llm_call
client = CLIENTS[provider]
params: dict[str, Any] = {
"model": model,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt}],
"stream": stream,
}
if stream:
# Return async generator for streaming responses
return handle_streaming_response(
client,
params,
json_mode,
thinking_budget_tokens,
response_model,
reasoning_effort,
verbosity,
)
# Remove stream parameter for non-streaming calls as some providers don't accept it
params.pop("stream", None)
match client:
case AsyncAnthropic():
if response_model:
config["response_schema"] = response_model
raise NotImplementedError(
"Response model is not supported for Anthropic"
)
anthropic_params: dict[str, Any] = {
"model": params["model"],
"max_tokens": params["max_tokens"],
"messages": list(params["messages"]),
}
if json_mode:
config["response_mime_type"] = "application/json"
if config:
call_params["config"] = config
elif resolved_provider == "anthropic":
# Anthropic uses thinking params and max_tokens
anthropic_params["messages"].append(
{"role": "assistant", "content": "{"}
)
if thinking_budget_tokens:
call_params["thinking"] = {
anthropic_params["thinking"] = {
"type": "enabled",
"budget_tokens": thinking_budget_tokens,
}
if max_tokens:
call_params["max_tokens"] = max_tokens
elif resolved_provider == "openai" and model and "gpt-5" in model:
call_params["max_completion_tokens"] = max_tokens
if reasoning_effort is not None:
call_params["reasoning_effort"] = reasoning_effort
if verbosity is not None:
call_params["verbosity"] = verbosity
else:
# Other providers just use max_tokens
if max_tokens:
call_params["max_tokens"] = max_tokens
anthropic_response: AnthropicMessage = await client.messages.create( # pyright: ignore
**anthropic_params
)
# Extract text content from content blocks
text_blocks: list[str] = []
for block in anthropic_response.content: # pyright: ignore
if isinstance(block, TextBlock):
text_blocks.append(block.text)
# Merge with any user-supplied provider call params
# Accept an explicit "extra_call_params" dict kwarg and merge its contents
# Do NOT forward the key itself into provider params.
# Also drop any wrapper-only flags.
user_extra_call_params: dict[str, Any] | None = extra_call_params.pop(
"extra_call_params", None
)
extra_call_params.pop("return_call_response", None)
if isinstance(user_extra_call_params, dict):
call_params.update(user_extra_call_params)
# Safely extract usage and stop_reason
usage = anthropic_response.usage # pyright: ignore
stop_reason = anthropic_response.stop_reason # pyright: ignore
# Build kwargs for llm.call
llm_kwargs: dict[str, Any] = {}
if resolved_provider and provider:
llm_kwargs["provider"] = resolved_provider
llm_kwargs["client"] = clients[
provider
] # Use original provider for client lookup
if model:
llm_kwargs["model"] = model
if response_model:
# https://mirascope.com/docs/mirascope/learn/provider-specific/openai#response-models
if resolved_provider == "openai":
response_model.model_config = ResponseModelConfigDict(strict=True)
llm_kwargs["response_model"] = response_model
if json_mode:
llm_kwargs["json_mode"] = json_mode
if stream:
llm_kwargs["stream"] = stream
if call_params:
llm_kwargs["call_params"] = call_params
return HonchoLLMCallResponse(
content="\n".join(text_blocks),
output_tokens=usage.output_tokens if usage else 0, # pyright: ignore
finish_reasons=[stop_reason] if stop_reason else [],
)
case AsyncOpenAI():
openai_params: dict[str, Any] = {
"model": params["model"],
"messages": params["messages"],
}
if "gpt-5" in model:
openai_params["max_completion_tokens"] = params["max_tokens"]
if reasoning_effort:
openai_params["reasoning_effort"] = reasoning_effort
if verbosity:
openai_params["verbosity"] = verbosity
else:
openai_params["max_tokens"] = params["max_tokens"]
if json_mode:
openai_params["response_format"] = {"type": "json_object"}
if response_model:
openai_params["response_format"] = response_model
response: ChatCompletion = await client.chat.completions.parse( # pyright: ignore
**openai_params
)
# Extract the parsed object for structured output
parsed_content = response.choices[0].message.parsed
if parsed_content is None:
raise ValueError("No parsed content in structured response")
# Apply decorators in order
decorated: Any = func
# Safely extract usage and finish_reason
usage = response.usage
finish_reason = response.choices[0].finish_reason
# Apply llm.call
decorated = llm.call(**llm_kwargs)(decorated) # pyright: ignore
return HonchoLLMCallResponse(
content=parsed_content,
output_tokens=usage.completion_tokens if usage else 0,
finish_reasons=[finish_reason] if finish_reason else [],
)
else:
response: ChatCompletion = await client.chat.completions.create( # pyright: ignore
**openai_params
)
# Apply langfuse if enabled
if settings.LANGFUSE_PUBLIC_KEY:
decorated = with_langfuse()(decorated) # pyright: ignore
# Safely extract usage and finish_reason
usage = response.usage # pyright: ignore
finish_reason = response.choices[0].finish_reason # pyright: ignore
# Apply AI tracking if name provided
if track_name:
decorated = ai_track(track_name)(decorated)
return HonchoLLMCallResponse(
content=response.choices[0].message.content or "", # pyright: ignore
output_tokens=usage.completion_tokens if usage else 0, # pyright: ignore
finish_reasons=[finish_reason] if finish_reason else [],
)
case genai.Client():
if response_model is None:
gemini_response: GenerateContentResponse = (
client.models.generate_content(
model=model,
contents=prompt,
config={
"response_mime_type": "application/json"
if json_mode
else None,
},
)
)
# Apply retry logic if enabled
if enable_retry:
decorated = retry( # pyright: ignore
stop=stop_after_attempt(retry_attempts),
wait=wait_exponential(multiplier=1, min=4, max=10),
)(decorated) # pyright: ignore
# Safely extract response data
text_content = gemini_response.text if gemini_response.text else ""
token_count = (
gemini_response.candidates[0].token_count or 0
if gemini_response.candidates
else 0
)
finish_reason = (
gemini_response.candidates[0].finish_reason.name
if gemini_response.candidates
and gemini_response.candidates[0].finish_reason
else "stop"
)
return decorated # pyright: ignore
return HonchoLLMCallResponse(
content=text_content,
output_tokens=token_count,
finish_reasons=[finish_reason],
)
return decorator
else:
gemini_response = client.models.generate_content(
model=model,
contents=prompt,
config={
"response_mime_type": "application/json",
"response_schema": response_model,
},
)
token_count = (
gemini_response.candidates[0].token_count or 0
if gemini_response.candidates
else 0
)
finish_reason = (
gemini_response.candidates[0].finish_reason.name
if gemini_response.candidates
and gemini_response.candidates[0].finish_reason
else "stop"
)
return HonchoLLMCallResponse(
content=gemini_response.parsed,
output_tokens=token_count,
finish_reasons=[finish_reason],
)
case AsyncGroq():
groq_params: dict[str, Any] = {
"model": params["model"],
"max_tokens": params["max_tokens"],
"messages": params["messages"],
}
if response_model:
groq_params["response_format"] = response_model
elif json_mode:
groq_params["response_format"] = {"type": "json_object"}
response: ChatCompletion = await client.chat.completions.create( # pyright: ignore
**groq_params
)
if response.choices[0].message.content is None: # pyright: ignore
raise ValueError("No content in response")
# Safely extract usage and finish_reason
usage = response.usage # pyright: ignore
finish_reason = response.choices[0].finish_reason # pyright: ignore
return HonchoLLMCallResponse(
content=response.choices[0].message.content, # pyright: ignore
output_tokens=usage.completion_tokens if usage else 0, # pyright: ignore
finish_reasons=[finish_reason] if finish_reason else [],
)
async def handle_streaming_response(
client: AsyncAnthropic | AsyncOpenAI | genai.Client | AsyncGroq,
params: dict[str, Any],
json_mode: bool,
thinking_budget_tokens: int | None,
response_model: type[BaseModel] | None = None,
reasoning_effort: Literal["low", "medium", "high", "minimal"] | None = None,
verbosity: Literal["low", "medium", "high"] | None = None,
) -> AsyncIterator[HonchoLLMCallStreamChunk]:
"""
Handle streaming responses for all supported providers.
Args:
client: The LLM client instance
params: Request parameters including stream=True
json_mode: Whether to use JSON mode
thinking_budget_tokens: Anthropic thinking budget tokens
response_model: Pydantic model for structured output
reasoning_effort: OpenAI reasoning effort level (GPT-5 only)
verbosity: OpenAI verbosity level (GPT-5 only)
Yields:
HonchoLLMCallStreamChunk: Individual chunks of the streaming response
"""
match client:
case AsyncAnthropic():
if response_model:
raise NotImplementedError(
"Response model is not supported for Anthropic"
)
anthropic_params: dict[str, Any] = {
"model": params["model"],
"max_tokens": params["max_tokens"],
"messages": list(params["messages"]),
}
if json_mode:
anthropic_params["messages"].append(
{"role": "assistant", "content": "{"}
)
if thinking_budget_tokens:
anthropic_params["thinking"] = {
"type": "enabled",
"budget_tokens": thinking_budget_tokens,
}
async with client.messages.stream(**anthropic_params) as anthropic_stream:
async for chunk in anthropic_stream:
if (
chunk.type == "content_block_delta"
and hasattr(chunk, "delta")
and hasattr(chunk.delta, "text")
):
text_content = getattr(chunk.delta, "text", "")
yield HonchoLLMCallStreamChunk(content=text_content)
final_message = await anthropic_stream.get_final_message()
yield HonchoLLMCallStreamChunk(
content="",
is_done=True,
finish_reasons=[final_message.stop_reason]
if final_message.stop_reason
else [],
)
case AsyncOpenAI():
openai_params: dict[str, Any] = {
"model": params["model"],
"messages": params["messages"],
"stream": True,
}
model_name = params["model"]
if "gpt-5" in model_name:
openai_params["max_completion_tokens"] = params["max_tokens"]
if reasoning_effort:
openai_params["reasoning_effort"] = reasoning_effort
if verbosity:
openai_params["verbosity"] = verbosity
else:
openai_params["max_tokens"] = params["max_tokens"]
if response_model:
openai_params["response_format"] = response_model
elif json_mode:
openai_params["response_format"] = {"type": "json_object"}
openai_stream = await client.chat.completions.create(**openai_params) # pyright: ignore
async for chunk in openai_stream: # pyright: ignore
chunk = cast(ChatCompletionChunk, chunk)
if chunk.choices and chunk.choices[0].delta.content:
yield HonchoLLMCallStreamChunk(
content=chunk.choices[0].delta.content
)
if chunk.choices and chunk.choices[0].finish_reason:
yield HonchoLLMCallStreamChunk(
content="",
is_done=True,
finish_reasons=[chunk.choices[0].finish_reason],
)
case genai.Client():
prompt_text = params["messages"][0]["content"] if params["messages"] else ""
if response_model is not None:
response_stream = await client.aio.models.generate_content_stream(
model=params["model"],
contents=prompt_text,
config={
"response_mime_type": "application/json",
"response_schema": response_model,
},
)
else:
response_stream = await client.aio.models.generate_content_stream(
model=params["model"],
contents=prompt_text,
config={
"response_mime_type": "application/json" if json_mode else None,
},
)
final_chunk = None
async for chunk in response_stream:
if chunk.text:
yield HonchoLLMCallStreamChunk(content=chunk.text)
final_chunk = chunk
finish_reason = "stop" # Default fallback
if (
final_chunk
and hasattr(final_chunk, "candidates")
and final_chunk.candidates
and hasattr(final_chunk.candidates[0], "finish_reason")
and final_chunk.candidates[0].finish_reason
):
finish_reason = final_chunk.candidates[0].finish_reason.name
yield HonchoLLMCallStreamChunk(
content="", is_done=True, finish_reasons=[finish_reason]
)
case AsyncGroq():
groq_params: dict[str, Any] = {
"model": params["model"],
"max_tokens": params["max_tokens"],
"messages": params["messages"],
"stream": True,
}
if response_model:
groq_params["response_format"] = response_model
elif json_mode:
groq_params["response_format"] = {"type": "json_object"}
groq_stream = await client.chat.completions.create(**groq_params) # pyright: ignore
async for chunk in groq_stream: # pyright: ignore
chunk = cast(ChatCompletionChunk, chunk)
if chunk.choices and chunk.choices[0].delta.content:
yield HonchoLLMCallStreamChunk(
content=chunk.choices[0].delta.content
)
if chunk.choices and chunk.choices[0].finish_reason:
yield HonchoLLMCallStreamChunk(
content="",
is_done=True,
finish_reasons=[chunk.choices[0].finish_reason],
)
def with_langfuse(func: Callable[..., Any]) -> Callable[..., Any]:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
lf.start_as_current_generation(name="LLM Call")
return await func(*args, **kwargs)
return wrapper

View File

@ -4,7 +4,7 @@ import datetime
import logging
from typing import Any, Literal, overload
from langfuse.decorators import langfuse_context
from langfuse import get_client
from openai.types import CreateEmbeddingResponse
from sqlalchemy.ext.asyncio import AsyncSession
@ -24,6 +24,8 @@ from src.utils.shared_models import (
logger = logging.getLogger(__name__)
lf = get_client()
class EmbeddingStore:
"""Embedding store specialized for observation-based reasoning with structured metadata."""
@ -68,7 +70,7 @@ class EmbeddingStore:
conclusions, similarity_threshold=similarity_threshold
)
if settings.LANGFUSE_PUBLIC_KEY:
langfuse_context.update_current_observation(
lf.update_current_generation(
input={"observations": [obs.model_dump() for obs in observations]},
output={"unique_conclusions": unique_conclusions},
)

View File

@ -33,7 +33,7 @@ def conditional_observe(func: Callable[..., Any]) -> Callable[..., Any]:
"""
if settings.LANGFUSE_PUBLIC_KEY:
# Import here to avoid circular imports and only import when needed
from langfuse.decorators import observe # pyright: ignore
from langfuse import observe # pyright: ignore
return observe()(func)
else:

View File

@ -2,8 +2,8 @@ import asyncio
import logging
import time
from enum import Enum
from inspect import cleandoc as c
from mirascope import llm
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from typing_extensions import TypedDict
@ -12,7 +12,7 @@ from src import schemas
from src.config import settings
from src.dependencies import tracked_db
from src.exceptions import ResourceNotFoundException
from src.utils.clients import honcho_llm_call
from src.utils.clients import HonchoLLMCallResponse, honcho_llm_call
from src.utils.formatting import utc_now_iso
from src.utils.logging import accumulate_metric
@ -77,17 +77,11 @@ class SummaryType(Enum):
LONG = "honcho_chat_summary_long"
@honcho_llm_call(
provider=settings.SUMMARY.PROVIDER,
model=settings.SUMMARY.MODEL,
max_tokens=settings.SUMMARY.MAX_TOKENS_SHORT,
return_call_response=True,
)
async def create_short_summary(
messages: list[models.Message],
input_tokens: int,
previous_summary: str | None = None,
):
) -> HonchoLLMCallResponse[str]:
# input_tokens indicates how many tokens the message list + previous summary take up
# we want to optimize short summaries to be smaller than the actual content being summarized
# so we ask the agent to produce a word count roughly equal to either the input, or the max
@ -100,7 +94,7 @@ async def create_short_summary(
else:
previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
return f"""
prompt = c(f"""
You are a system that summarizes parts of a conversation to create a concise and accurate summary. Focus on capturing:
1. Key facts and information shared (**Capture as many explicit facts as possible**)
@ -123,19 +117,20 @@ Return only the summary without any explanation or meta-commentary.
</conversation>
Produce as thorough a summary as possible in {output_words} words or less.
"""
""")
return await honcho_llm_call(
provider=settings.SUMMARY.PROVIDER,
model=settings.SUMMARY.MODEL,
prompt=prompt,
max_tokens=settings.SUMMARY.MAX_TOKENS_SHORT,
)
@honcho_llm_call(
provider=settings.SUMMARY.PROVIDER,
model=settings.SUMMARY.MODEL,
max_tokens=settings.SUMMARY.MAX_TOKENS_LONG,
return_call_response=True,
)
async def create_long_summary(
messages: list[models.Message],
previous_summary: str | None = None,
):
) -> HonchoLLMCallResponse[str]:
# the word/token ratio is roughly 4:3 so we multiply by 0.75.
# LLMs *seem* to respond better to getting asked for a word count but should workshop this.
output_words = int(settings.SUMMARY.MAX_TOKENS_LONG * 0.75)
@ -145,7 +140,7 @@ async def create_long_summary(
else:
previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
return f"""
prompt = c(f"""
You are a system that creates thorough, comprehensive summaries of conversations. Focus on capturing:
1. Key facts and information shared (**Capture as many explicit facts as possible**)
@ -170,7 +165,14 @@ Return only the summary without any explanation or meta-commentary.
</conversation>
Produce as thorough a summary as possible in {output_words} words or less.
"""
""")
return await honcho_llm_call(
provider=settings.SUMMARY.PROVIDER,
model=settings.SUMMARY.MODEL,
prompt=prompt,
max_tokens=settings.SUMMARY.MAX_TOKENS_LONG,
)
async def summarize_if_needed(
@ -345,7 +347,7 @@ async def _create_summary(
A full summary of the conversation up to the last message
"""
response: llm.CallResponse | None = None
response: HonchoLLMCallResponse[str] | None = None
try:
if summary_type == SummaryType.SHORT:
response = await create_short_summary(
@ -355,11 +357,7 @@ async def _create_summary(
response = await create_long_summary(messages, previous_summary_text)
summary_text = response.content
summary_tokens = (
response.usage.output_tokens
if response.usage
else len(response.content) // 4
)
summary_tokens = response.output_tokens
# Detect potential issues with the summary
if not summary_text.strip():
@ -379,19 +377,10 @@ async def _create_summary(
)
summary_tokens = 50
accumulate_metric(
f"summary_{messages[-1].workspace_name}_{messages[-1].id}",
f"{summary_type.name}_summary_input",
response.usage.input_tokens if response and response.usage else "unknown",
"tokens",
)
accumulate_metric(
f"summary_{messages[-1].workspace_name}_{messages[-1].id}",
f"{summary_type.name}_summary_size",
response.usage.output_tokens
if response and response.usage
else f"{summary_tokens} (est.)",
response.output_tokens if response else f"{summary_tokens} (est.)",
"tokens",
)

View File

@ -1,5 +1,3 @@
from typing import Literal
from mirascope import Provider
Providers = Provider | Literal["custom"]
SupportedProviders = Literal["anthropic", "openai", "google", "groq", "custom"]

View File

@ -328,6 +328,7 @@ class HonchoHarness:
# "src.routers.",
# "src.crud.",
"google_genai.models",
"google.genai.models",
]
try:

View File

@ -141,24 +141,25 @@ def build_peer_card_caller(
"openai" if candidate.provider == "custom" else candidate.provider
)
@honcho_llm_call(
provider=cast(Any, resolved_provider),
model=candidate.model,
track_name="Peer Card Call",
response_model=PeerCardQuery,
json_mode=True,
max_tokens=settings.DERIVER.PEER_CARD_MAX_OUTPUT_TOKENS,
reasoning_effort="minimal",
enable_retry=True,
retry_attempts=1, # unstructured output means we shouldn't need to retry, 1 just in case
)
async def call(old_peer_card: list[str] | None, new_observations: list[str]) -> Any:
"""Return the prompt content for Mirascope to execute as a model call."""
return peer_card_prompt(
prompt = peer_card_prompt(
old_peer_card=old_peer_card, new_observations=new_observations
)
response = await honcho_llm_call(
provider=cast(Any, resolved_provider),
model=candidate.model,
prompt=prompt,
max_tokens=settings.DERIVER.PEER_CARD_MAX_OUTPUT_TOKENS,
response_model=PeerCardQuery,
json_mode=True,
reasoning_effort="minimal",
enable_retry=True,
retry_attempts=3,
)
return response.content
return call
@ -323,7 +324,7 @@ async def run_benchmark(candidates: list[Candidate], cases: list[Case]) -> int:
case.old_peer_card, case.new_observations
)
new_card = card.card
if new_card is None:
if new_card is None or new_card == []:
new_card = case.old_peer_card or []
judgment = await judge_response(anthropic, case, new_card)
return case, {"card": card, "judgment": judgment}

View File

@ -257,8 +257,7 @@ async def sample_data(
def mock_langfuse():
"""Mock Langfuse decorator and context during tests"""
with (
patch("langfuse.decorators.observe") as mock_observe,
patch("langfuse.decorators.langfuse_context") as mock_context,
patch("langfuse.observe") as mock_observe,
):
# Mock the decorator to just return the function
def return_value(func: Callable[..., Any]):
@ -266,12 +265,6 @@ def mock_langfuse():
mock_observe.return_value = return_value
# Mock the context object
mock_context_obj = MagicMock()
mock_context_obj.update_current_observation = MagicMock()
mock_context_obj.update_current_trace = MagicMock()
mock_context.return_value = mock_context_obj
# Disable httpx logging during tests
logging.getLogger("httpx").setLevel(logging.WARNING)
@ -309,8 +302,8 @@ def mock_openai_embeddings():
@pytest.fixture(autouse=True)
def mock_mirascope_functions():
"""Mock Mirascope LLM functions to avoid needing API keys during tests"""
def mock_llm_call_functions():
"""Mock LLM functions to avoid needing API keys during tests"""
# Create mock responses for different function types
with (

1
tests/utils/__init__.py Normal file
View File

@ -0,0 +1 @@
# Test utilities package

1046
tests/utils/test_clients.py Normal file

File diff suppressed because it is too large Load Diff

489
uv.lock
View File

@ -1,5 +1,5 @@
version = 1
revision = 3
revision = 2
requires-python = ">=3.10"
resolution-markers = [
"python_full_version >= '3.13'",
@ -439,15 +439,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" },
]
[[package]]
name = "docstring-parser"
version = "0.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
]
[[package]]
name = "email-validator"
version = "2.2.0"
@ -558,59 +549,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" },
]
[[package]]
name = "google-ai-generativelanguage"
version = "0.6.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", extra = ["grpc"] },
{ name = "google-auth" },
{ name = "proto-plus" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/11/d1/48fe5d7a43d278e9f6b5ada810b0a3530bbeac7ed7fcbcd366f932f05316/google_ai_generativelanguage-0.6.15.tar.gz", hash = "sha256:8f6d9dc4c12b065fe2d0289026171acea5183ebf2d0b11cefe12f3821e159ec3", size = 1375443, upload-time = "2025-01-13T21:50:47.459Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/a3/67b8a6ff5001a1d8864922f2d6488dc2a14367ceb651bc3f09a947f2f306/google_ai_generativelanguage-0.6.15-py3-none-any.whl", hash = "sha256:5a03ef86377aa184ffef3662ca28f19eeee158733e45d7947982eb953c6ebb6c", size = 1327356, upload-time = "2025-01-13T21:50:44.174Z" },
]
[[package]]
name = "google-api-core"
version = "2.25.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "googleapis-common-protos" },
{ name = "proto-plus" },
{ name = "protobuf" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/21/e9d043e88222317afdbdb567165fdbc3b0aad90064c7e0c9eb0ad9955ad8/google_api_core-2.25.1.tar.gz", hash = "sha256:d2aaa0b13c78c61cb3f4282c464c046e45fbd75755683c9c525e6e8f7ed0a5e8", size = 165443, upload-time = "2025-06-12T20:52:20.439Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/4b/ead00905132820b623732b175d66354e9d3e69fcf2a5dcdab780664e7896/google_api_core-2.25.1-py3-none-any.whl", hash = "sha256:8a2a56c1fef82987a524371f99f3bd0143702fecc670c72e600c1cda6bf8dbb7", size = 160807, upload-time = "2025-06-12T20:52:19.334Z" },
]
[package.optional-dependencies]
grpc = [
{ name = "grpcio" },
{ name = "grpcio-status" },
]
[[package]]
name = "google-api-python-client"
version = "2.178.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
{ name = "google-auth" },
{ name = "google-auth-httplib2" },
{ name = "httplib2" },
{ name = "uritemplate" },
]
sdist = { url = "https://files.pythonhosted.org/packages/99/98/916385a87d145a27661b630c480fadf9db32bb1ad9fb1b13e8dbcbe2af70/google_api_python_client-2.178.0.tar.gz", hash = "sha256:99cba921eb471bb5973b780c653ac54d96eef8a42f1b7375b7ab98f257a4414c", size = 13282628, upload-time = "2025-08-06T14:04:51.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/34/8ae31410a2d3f28b16b7135931133caf759d3aa0653f8397e344acec5a88/google_api_python_client-2.178.0-py3-none-any.whl", hash = "sha256:f420adcd050150ff1baefa817e96e1ffa16872744f53471cd34096612e580c34", size = 13809959, upload-time = "2025-08-06T14:04:47.94Z" },
]
[[package]]
name = "google-auth"
version = "2.40.3"
@ -625,22 +563,9 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" },
]
[[package]]
name = "google-auth-httplib2"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "httplib2" },
]
sdist = { url = "https://files.pythonhosted.org/packages/56/be/217a598a818567b28e859ff087f347475c807a5649296fb5a817c58dacef/google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05", size = 10842, upload-time = "2023-12-12T17:40:30.722Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/8a/fe34d2f3f9470a27b01c9e76226965863f153d5fbe276f83608562e49c04/google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d", size = 9253, upload-time = "2023-12-12T17:40:13.055Z" },
]
[[package]]
name = "google-genai"
version = "1.28.0"
version = "1.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@ -652,27 +577,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/23/f1/039bb08df4670e204c55b5da0b2fa5228dff3346bda01389a86b300f6f58/google_genai-1.28.0.tar.gz", hash = "sha256:e93053c02e616842679ba5ecce5b99db8c0ca6310623c55ff6245b5b1d293138", size = 221029, upload-time = "2025-07-30T21:39:57.002Z" }
sdist = { url = "https://files.pythonhosted.org/packages/03/ab/e6cdd8fa957c647ef00c4da7c59d0e734354bd49ed8d98c860732d8e1944/google_genai-1.32.0.tar.gz", hash = "sha256:349da3f5ff0e981066bd508585fcdd308d28fc4646f318c8f6d1aa6041f4c7e3", size = 240802, upload-time = "2025-08-27T22:16:32.781Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/ea/b704df3b348d3ae3572b0db5b52438fa426900b0830cff664107abfdba69/google_genai-1.28.0-py3-none-any.whl", hash = "sha256:7fd506799005cc87d3c5704a2eb5a2cb020d45b4d216a802e606700308f7f2f3", size = 219384, upload-time = "2025-07-30T21:39:55.652Z" },
]
[[package]]
name = "google-generativeai"
version = "0.8.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-ai-generativelanguage" },
{ name = "google-api-core" },
{ name = "google-api-python-client" },
{ name = "google-auth" },
{ name = "protobuf" },
{ name = "pydantic" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/40/c42ff9ded9f09ec9392879a8e6538a00b2dc185e834a3392917626255419/google_generativeai-0.8.5-py3-none-any.whl", hash = "sha256:22b420817fb263f8ed520b33285f45976d5b21e904da32b80d4fd20c055123a2", size = 155427, upload-time = "2025-04-17T00:40:00.67Z" },
{ url = "https://files.pythonhosted.org/packages/59/55/be09472f7a656af1208196d2ef9a3d2710f3cbcf695f51acbcbe28b9472b/google_genai-1.32.0-py3-none-any.whl", hash = "sha256:c0c4b1d45adf3aa99501050dd73da2f0dea09374002231052d81a6765d15e7f6", size = 241680, upload-time = "2025-08-27T22:16:31.409Z" },
]
[[package]]
@ -755,68 +662,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/f8/14672d69a91495f43462c5490067eeafc30346e81bda1a62848e897f9bc3/groq-0.31.0-py3-none-any.whl", hash = "sha256:5e3c7ec9728b7cccf913da982a9b5ebb46dc18a070b35e12a3d6a1e12d6b0f7f", size = 131365, upload-time = "2025-08-05T23:13:59.768Z" },
]
[[package]]
name = "grpcio"
version = "1.74.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/66/54/68e51a90797ad7afc5b0a7881426c337f6a9168ebab73c3210b76aa7c90d/grpcio-1.74.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:85bd5cdf4ed7b2d6438871adf6afff9af7096486fcf51818a81b77ef4dd30907", size = 5481935, upload-time = "2025-07-24T18:52:43.756Z" },
{ url = "https://files.pythonhosted.org/packages/32/2a/af817c7e9843929e93e54d09c9aee2555c2e8d81b93102a9426b36e91833/grpcio-1.74.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:68c8ebcca945efff9d86d8d6d7bfb0841cf0071024417e2d7f45c5e46b5b08eb", size = 10986796, upload-time = "2025-07-24T18:52:47.219Z" },
{ url = "https://files.pythonhosted.org/packages/d5/94/d67756638d7bb07750b07d0826c68e414124574b53840ba1ff777abcd388/grpcio-1.74.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:e154d230dc1bbbd78ad2fdc3039fa50ad7ffcf438e4eb2fa30bce223a70c7486", size = 5983663, upload-time = "2025-07-24T18:52:49.463Z" },
{ url = "https://files.pythonhosted.org/packages/35/f5/c5e4853bf42148fea8532d49e919426585b73eafcf379a712934652a8de9/grpcio-1.74.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8978003816c7b9eabe217f88c78bc26adc8f9304bf6a594b02e5a49b2ef9c11", size = 6653765, upload-time = "2025-07-24T18:52:51.094Z" },
{ url = "https://files.pythonhosted.org/packages/fd/75/a1991dd64b331d199935e096cc9daa3415ee5ccbe9f909aa48eded7bba34/grpcio-1.74.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3d7bd6e3929fd2ea7fbc3f562e4987229ead70c9ae5f01501a46701e08f1ad9", size = 6215172, upload-time = "2025-07-24T18:52:53.282Z" },
{ url = "https://files.pythonhosted.org/packages/01/a4/7cef3dbb3b073d0ce34fd507efc44ac4c9442a0ef9fba4fb3f5c551efef5/grpcio-1.74.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:136b53c91ac1d02c8c24201bfdeb56f8b3ac3278668cbb8e0ba49c88069e1bdc", size = 6329142, upload-time = "2025-07-24T18:52:54.927Z" },
{ url = "https://files.pythonhosted.org/packages/bf/d3/587920f882b46e835ad96014087054655312400e2f1f1446419e5179a383/grpcio-1.74.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fe0f540750a13fd8e5da4b3eaba91a785eea8dca5ccd2bc2ffe978caa403090e", size = 7018632, upload-time = "2025-07-24T18:52:56.523Z" },
{ url = "https://files.pythonhosted.org/packages/1f/95/c70a3b15a0bc83334b507e3d2ae20ee8fa38d419b8758a4d838f5c2a7d32/grpcio-1.74.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4e4181bfc24413d1e3a37a0b7889bea68d973d4b45dd2bc68bb766c140718f82", size = 6509641, upload-time = "2025-07-24T18:52:58.495Z" },
{ url = "https://files.pythonhosted.org/packages/4b/06/2e7042d06247d668ae69ea6998eca33f475fd4e2855f94dcb2aa5daef334/grpcio-1.74.0-cp310-cp310-win32.whl", hash = "sha256:1733969040989f7acc3d94c22f55b4a9501a30f6aaacdbccfaba0a3ffb255ab7", size = 3817478, upload-time = "2025-07-24T18:53:00.128Z" },
{ url = "https://files.pythonhosted.org/packages/93/20/e02b9dcca3ee91124060b65bbf5b8e1af80b3b76a30f694b44b964ab4d71/grpcio-1.74.0-cp310-cp310-win_amd64.whl", hash = "sha256:9e912d3c993a29df6c627459af58975b2e5c897d93287939b9d5065f000249b5", size = 4493971, upload-time = "2025-07-24T18:53:02.068Z" },
{ url = "https://files.pythonhosted.org/packages/e7/77/b2f06db9f240a5abeddd23a0e49eae2b6ac54d85f0e5267784ce02269c3b/grpcio-1.74.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:69e1a8180868a2576f02356565f16635b99088da7df3d45aaa7e24e73a054e31", size = 5487368, upload-time = "2025-07-24T18:53:03.548Z" },
{ url = "https://files.pythonhosted.org/packages/48/99/0ac8678a819c28d9a370a663007581744a9f2a844e32f0fa95e1ddda5b9e/grpcio-1.74.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8efe72fde5500f47aca1ef59495cb59c885afe04ac89dd11d810f2de87d935d4", size = 10999804, upload-time = "2025-07-24T18:53:05.095Z" },
{ url = "https://files.pythonhosted.org/packages/45/c6/a2d586300d9e14ad72e8dc211c7aecb45fe9846a51e558c5bca0c9102c7f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:a8f0302f9ac4e9923f98d8e243939a6fb627cd048f5cd38595c97e38020dffce", size = 5987667, upload-time = "2025-07-24T18:53:07.157Z" },
{ url = "https://files.pythonhosted.org/packages/c9/57/5f338bf56a7f22584e68d669632e521f0de460bb3749d54533fc3d0fca4f/grpcio-1.74.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f609a39f62a6f6f05c7512746798282546358a37ea93c1fcbadf8b2fed162e3", size = 6655612, upload-time = "2025-07-24T18:53:09.244Z" },
{ url = "https://files.pythonhosted.org/packages/82/ea/a4820c4c44c8b35b1903a6c72a5bdccec92d0840cf5c858c498c66786ba5/grpcio-1.74.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c98e0b7434a7fa4e3e63f250456eaef52499fba5ae661c58cc5b5477d11e7182", size = 6219544, upload-time = "2025-07-24T18:53:11.221Z" },
{ url = "https://files.pythonhosted.org/packages/a4/17/0537630a921365928f5abb6d14c79ba4dcb3e662e0dbeede8af4138d9dcf/grpcio-1.74.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:662456c4513e298db6d7bd9c3b8df6f75f8752f0ba01fb653e252ed4a59b5a5d", size = 6334863, upload-time = "2025-07-24T18:53:12.925Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a6/85ca6cb9af3f13e1320d0a806658dca432ff88149d5972df1f7b51e87127/grpcio-1.74.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3d14e3c4d65e19d8430a4e28ceb71ace4728776fd6c3ce34016947474479683f", size = 7019320, upload-time = "2025-07-24T18:53:15.002Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a7/fe2beab970a1e25d2eff108b3cf4f7d9a53c185106377a3d1989216eba45/grpcio-1.74.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bf949792cee20d2078323a9b02bacbbae002b9e3b9e2433f2741c15bdeba1c4", size = 6514228, upload-time = "2025-07-24T18:53:16.999Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c2/2f9c945c8a248cebc3ccda1b7a1bf1775b9d7d59e444dbb18c0014e23da6/grpcio-1.74.0-cp311-cp311-win32.whl", hash = "sha256:55b453812fa7c7ce2f5c88be3018fb4a490519b6ce80788d5913f3f9d7da8c7b", size = 3817216, upload-time = "2025-07-24T18:53:20.564Z" },
{ url = "https://files.pythonhosted.org/packages/ff/d1/a9cf9c94b55becda2199299a12b9feef0c79946b0d9d34c989de6d12d05d/grpcio-1.74.0-cp311-cp311-win_amd64.whl", hash = "sha256:86ad489db097141a907c559988c29718719aa3e13370d40e20506f11b4de0d11", size = 4495380, upload-time = "2025-07-24T18:53:22.058Z" },
{ url = "https://files.pythonhosted.org/packages/4c/5d/e504d5d5c4469823504f65687d6c8fb97b7f7bf0b34873b7598f1df24630/grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8", size = 5445551, upload-time = "2025-07-24T18:53:23.641Z" },
{ url = "https://files.pythonhosted.org/packages/43/01/730e37056f96f2f6ce9f17999af1556df62ee8dab7fa48bceeaab5fd3008/grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6", size = 10979810, upload-time = "2025-07-24T18:53:25.349Z" },
{ url = "https://files.pythonhosted.org/packages/79/3d/09fd100473ea5c47083889ca47ffd356576173ec134312f6aa0e13111dee/grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5", size = 5941946, upload-time = "2025-07-24T18:53:27.387Z" },
{ url = "https://files.pythonhosted.org/packages/8a/99/12d2cca0a63c874c6d3d195629dcd85cdf5d6f98a30d8db44271f8a97b93/grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49", size = 6621763, upload-time = "2025-07-24T18:53:29.193Z" },
{ url = "https://files.pythonhosted.org/packages/9d/2c/930b0e7a2f1029bbc193443c7bc4dc2a46fedb0203c8793dcd97081f1520/grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7", size = 6180664, upload-time = "2025-07-24T18:53:30.823Z" },
{ url = "https://files.pythonhosted.org/packages/db/d5/ff8a2442180ad0867717e670f5ec42bfd8d38b92158ad6bcd864e6d4b1ed/grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3", size = 6301083, upload-time = "2025-07-24T18:53:32.454Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ba/b361d390451a37ca118e4ec7dccec690422e05bc85fba2ec72b06cefec9f/grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707", size = 6994132, upload-time = "2025-07-24T18:53:34.506Z" },
{ url = "https://files.pythonhosted.org/packages/3b/0c/3a5fa47d2437a44ced74141795ac0251bbddeae74bf81df3447edd767d27/grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b", size = 6489616, upload-time = "2025-07-24T18:53:36.217Z" },
{ url = "https://files.pythonhosted.org/packages/ae/95/ab64703b436d99dc5217228babc76047d60e9ad14df129e307b5fec81fd0/grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c", size = 3807083, upload-time = "2025-07-24T18:53:37.911Z" },
{ url = "https://files.pythonhosted.org/packages/84/59/900aa2445891fc47a33f7d2f76e00ca5d6ae6584b20d19af9c06fa09bf9a/grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc", size = 4490123, upload-time = "2025-07-24T18:53:39.528Z" },
{ url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488, upload-time = "2025-07-24T18:53:41.174Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059, upload-time = "2025-07-24T18:53:43.066Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647, upload-time = "2025-07-24T18:53:45.269Z" },
{ url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101, upload-time = "2025-07-24T18:53:47.015Z" },
{ url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562, upload-time = "2025-07-24T18:53:48.967Z" },
{ url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425, upload-time = "2025-07-24T18:53:50.847Z" },
{ url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533, upload-time = "2025-07-24T18:53:52.747Z" },
{ url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489, upload-time = "2025-07-24T18:53:55.06Z" },
{ url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811, upload-time = "2025-07-24T18:53:56.798Z" },
{ url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214, upload-time = "2025-07-24T18:53:59.771Z" },
]
[[package]]
name = "grpcio-status"
version = "1.71.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/d1/b6e9877fedae3add1afdeae1f89d1927d296da9cf977eca0eb08fb8a460e/grpcio_status-1.71.2.tar.gz", hash = "sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50", size = 13677, upload-time = "2025-06-28T04:24:05.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/58/317b0134129b556a93a3b0afe00ee675b5657f0155509e22fcb853bafe2d/grpcio_status-1.71.2-py3-none-any.whl", hash = "sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3", size = 14424, upload-time = "2025-06-28T04:23:42.136Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@ -834,10 +679,11 @@ dependencies = [
{ name = "alembic" },
{ name = "fastapi", extra = ["standard"] },
{ name = "fastapi-pagination" },
{ name = "google-generativeai" },
{ name = "google-genai" },
{ name = "greenlet" },
{ name = "groq" },
{ name = "httpx" },
{ name = "mirascope", extra = ["anthropic", "google", "groq", "langfuse"] },
{ name = "langfuse" },
{ name = "nanoid" },
{ name = "openai" },
{ name = "pdfplumber" },
@ -850,6 +696,7 @@ dependencies = [
{ name = "rich" },
{ name = "sentry-sdk", extra = ["anthropic", "fastapi", "sqlalchemy"] },
{ name = "sqlalchemy" },
{ name = "tenacity" },
{ name = "tiktoken" },
{ name = "typing-extensions" },
]
@ -874,10 +721,11 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.14.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.111.0" },
{ name = "fastapi-pagination", specifier = ">=0.12.24" },
{ name = "google-generativeai", specifier = ">=0.8.5" },
{ name = "google-genai", specifier = ">=1.32.0" },
{ name = "greenlet", specifier = ">=3.0.3" },
{ name = "groq", specifier = ">=0.31.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "mirascope", extras = ["anthropic", "google", "groq", "langfuse"], specifier = ">=1.25.5" },
{ name = "langfuse", specifier = ">=3.3.2" },
{ name = "nanoid", specifier = ">=2.0.0" },
{ name = "openai", specifier = ">=1.99.7" },
{ name = "pdfplumber", specifier = ">=0.11.7" },
@ -890,6 +738,7 @@ requires-dist = [
{ name = "rich", specifier = ">=13.7.1" },
{ name = "sentry-sdk", extras = ["anthropic", "fastapi", "sqlalchemy"], specifier = ">=2.3.1" },
{ name = "sqlalchemy", specifier = ">=2.0.30" },
{ name = "tenacity", specifier = ">=9.1.2" },
{ name = "tiktoken", specifier = ">=0.9.0" },
{ name = "typing-extensions", specifier = ">=4.11.0" },
]
@ -964,18 +813,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httplib2"
version = "0.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116, upload-time = "2023-03-21T22:29:37.214Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854, upload-time = "2023-03-21T22:29:35.683Z" },
]
[[package]]
name = "httptools"
version = "0.6.4"
@ -1045,6 +882,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "importlib-metadata"
version = "8.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
@ -1157,21 +1006,22 @@ wheels = [
[[package]]
name = "langfuse"
version = "2.60.9"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "backoff" },
{ name = "httpx" },
{ name = "idna" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "opentelemetry-sdk" },
{ name = "packaging" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "wrapt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/1a/2443e3715767f1bf9d8cf32d74ac59cfb60e1d9b84e99df13fd656639eb3/langfuse-2.60.9.tar.gz", hash = "sha256:040753346d7df4a0be6967dfc7efe3de313fee362524fe2f801867fcbbca3c98", size = 152684, upload-time = "2025-06-29T09:39:27.628Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6d/91/b92566f4fd73b607136bcd3ed011af98a571dc4c0f60b95fffb768d3cb5c/langfuse-3.3.2.tar.gz", hash = "sha256:4b029fed675b2b631b96da157459c285930420139667f25a6ee71ed3b3c5a71c", size = 164001, upload-time = "2025-08-27T15:55:11.716Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/50/3aa93fc284ba5f81dcdd00b6414caee338fd45d77fa4959c3e4f838cebc6/langfuse-2.60.9-py3-none-any.whl", hash = "sha256:e4291a66bc579c66d7652da5603ca7f0409536700d7b812e396780b5d9a0685d", size = 275543, upload-time = "2025-06-29T09:39:26.234Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ed/c4e914c5362740a40b12eaa8f90205cc05af81bacf983f3e89bf26b1c3cf/langfuse-3.3.2-py3-none-any.whl", hash = "sha256:0a5871501720c362183fbf2970c895fb0d46f4ed121a5f95a1874f9af081cfdd", size = 317629, upload-time = "2025-08-27T15:55:09.435Z" },
]
[[package]]
@ -1265,37 +1115,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mirascope"
version = "1.25.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "docstring-parser" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/bf/7b2441d8740fe82fdd201bcdefacff300ac9f61245169d74d2ebfa607a25/mirascope-1.25.5.tar.gz", hash = "sha256:bcc2734e4f83d6a8d66c7fb1660a12285418ea8f4d52e8f341b8e47ee54c6667", size = 627330, upload-time = "2025-08-05T22:06:41.809Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/2a/b5d57fcdaa7253cbfb6a2b3100d1e08a2133cf9f44964f8774541d09c36e/mirascope-1.25.5-py3-none-any.whl", hash = "sha256:b7f383ff6f4eeecd80f172fae51718103a473f8213859249014a65d30959226a", size = 373303, upload-time = "2025-08-05T22:06:40.077Z" },
]
[package.optional-dependencies]
anthropic = [
{ name = "anthropic" },
]
google = [
{ name = "google-genai" },
{ name = "pillow" },
{ name = "proto-plus" },
]
groq = [
{ name = "groq" },
]
langfuse = [
{ name = "langfuse" },
]
[[package]]
name = "nanoid"
version = "2.0.0"
@ -1497,6 +1316,88 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/2d/a41c49550a69374111647be22b587e3311a6fc31fc7370e14448e78018bc/openai-1.99.7-py3-none-any.whl", hash = "sha256:ef4165cc4f8872dd4a967d109f12b0b9c98a1e20ae05940c28701729c2883891", size = 786809, upload-time = "2025-08-11T15:13:08.537Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/27/d2/c782c88b8afbf961d6972428821c302bd1e9e7bc361352172f0ca31296e2/opentelemetry_api-1.36.0.tar.gz", hash = "sha256:9a72572b9c416d004d492cbc6e61962c0501eaf945ece9b5a0f56597d8348aa0", size = 64780, upload-time = "2025-07-29T15:12:06.02Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/ee/6b08dde0a022c463b88f55ae81149584b125a42183407dc1045c486cc870/opentelemetry_api-1.36.0-py3-none-any.whl", hash = "sha256:02f20bcacf666e1333b6b1f04e647dc1d5111f86b8e510238fcc56d7762cda8c", size = 65564, upload-time = "2025-07-29T15:11:47.998Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/34/da/7747e57eb341c59886052d733072bc878424bf20f1d8cf203d508bbece5b/opentelemetry_exporter_otlp_proto_common-1.36.0.tar.gz", hash = "sha256:6c496ccbcbe26b04653cecadd92f73659b814c6e3579af157d8716e5f9f25cbf", size = 20302, upload-time = "2025-07-29T15:12:07.71Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/ed/22290dca7db78eb32e0101738366b5bbda00d0407f00feffb9bf8c3fdf87/opentelemetry_exporter_otlp_proto_common-1.36.0-py3-none-any.whl", hash = "sha256:0fc002a6ed63eac235ada9aa7056e5492e9a71728214a61745f6ad04b923f840", size = 18349, upload-time = "2025-07-29T15:11:51.327Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/25/85/6632e7e5700ba1ce5b8a065315f92c1e6d787ccc4fb2bdab15139eaefc82/opentelemetry_exporter_otlp_proto_http-1.36.0.tar.gz", hash = "sha256:dd3637f72f774b9fc9608ab1ac479f8b44d09b6fb5b2f3df68a24ad1da7d356e", size = 16213, upload-time = "2025-07-29T15:12:08.932Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/41/a680d38b34f8f5ddbd78ed9f0042e1cc712d58ec7531924d71cb1e6c629d/opentelemetry_exporter_otlp_proto_http-1.36.0-py3-none-any.whl", hash = "sha256:3d769f68e2267e7abe4527f70deb6f598f40be3ea34c6adc35789bea94a32902", size = 18752, upload-time = "2025-07-29T15:11:53.164Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/02/f6556142301d136e3b7e95ab8ea6a5d9dc28d879a99f3dd673b5f97dca06/opentelemetry_proto-1.36.0.tar.gz", hash = "sha256:0f10b3c72f74c91e0764a5ec88fd8f1c368ea5d9c64639fb455e2854ef87dd2f", size = 46152, upload-time = "2025-07-29T15:12:15.717Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/57/3361e06136225be8180e879199caea520f38026f8071366241ac458beb8d/opentelemetry_proto-1.36.0-py3-none-any.whl", hash = "sha256:151b3bf73a09f94afc658497cf77d45a565606f62ce0c17acb08cd9937ca206e", size = 72537, upload-time = "2025-07-29T15:12:02.243Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.36.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4c/85/8567a966b85a2d3f971c4d42f781c305b2b91c043724fa08fd37d158e9dc/opentelemetry_sdk-1.36.0.tar.gz", hash = "sha256:19c8c81599f51b71670661ff7495c905d8fdf6976e41622d5245b791b06fa581", size = 162557, upload-time = "2025-07-29T15:12:16.76Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/59/7bed362ad1137ba5886dac8439e84cd2df6d087be7c09574ece47ae9b22c/opentelemetry_sdk-1.36.0-py3-none-any.whl", hash = "sha256:19fe048b42e98c5c1ffe85b569b7073576ad4ce0bcb6e9b4c6a39e890a6c45fb", size = 119995, upload-time = "2025-07-29T15:12:03.181Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.57b0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7e/31/67dfa252ee88476a29200b0255bda8dfc2cf07b56ad66dc9a6221f7dc787/opentelemetry_semantic_conventions-0.57b0.tar.gz", hash = "sha256:609a4a79c7891b4620d64c7aac6898f872d790d75f22019913a660756f27ff32", size = 124225, upload-time = "2025-07-29T15:12:17.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/75/7d591371c6c39c73de5ce5da5a2cc7b72d1d1cd3f8f4638f553c01c37b11/opentelemetry_semantic_conventions-0.57b0-py3-none-any.whl", hash = "sha256:757f7e76293294f124c827e514c2a3144f191ef175b069ce8d1211e1e38e9e78", size = 201627, upload-time = "2025-07-29T15:12:04.174Z" },
]
[[package]]
name = "packaging"
version = "24.2"
@ -1639,18 +1540,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" },
]
[[package]]
name = "proto-plus"
version = "1.26.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" },
]
[[package]]
name = "protobuf"
version = "5.29.5"
@ -1927,15 +1816,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
[[package]]
name = "pyparsing"
version = "3.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
]
[[package]]
name = "pypdfium2"
version = "4.30.0"
@ -2438,11 +2318,11 @@ wheels = [
[[package]]
name = "tenacity"
version = "8.5.0"
version = "9.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/4d/6a19536c50b849338fcbe9290d562b52cbdcf30d8963d3588a68a4107df1/tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78", size = 47309, upload-time = "2024-07-05T07:25:31.836Z" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/3f/8ba87d9e287b9d385a02a7114ddcef61b26f86411e121c9003eb509a1773/tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687", size = 28165, upload-time = "2024-07-05T07:25:29.591Z" },
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
]
[[package]]
@ -2577,15 +2457,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]]
name = "uritemplate"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
@ -2827,64 +2698,78 @@ wheels = [
[[package]]
name = "wrapt"
version = "1.17.2"
version = "1.17.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/d1/1daec934997e8b160040c78d7b31789f19b122110a75eca3d4e8da0049e1/wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984", size = 53307, upload-time = "2025-01-14T10:33:13.616Z" },
{ url = "https://files.pythonhosted.org/packages/1b/7b/13369d42651b809389c1a7153baa01d9700430576c81a2f5c5e460df0ed9/wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22", size = 38486, upload-time = "2025-01-14T10:33:15.947Z" },
{ url = "https://files.pythonhosted.org/packages/62/bf/e0105016f907c30b4bd9e377867c48c34dc9c6c0c104556c9c9126bd89ed/wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7", size = 38777, upload-time = "2025-01-14T10:33:17.462Z" },
{ url = "https://files.pythonhosted.org/packages/27/70/0f6e0679845cbf8b165e027d43402a55494779295c4b08414097b258ac87/wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c", size = 83314, upload-time = "2025-01-14T10:33:21.282Z" },
{ url = "https://files.pythonhosted.org/packages/0f/77/0576d841bf84af8579124a93d216f55d6f74374e4445264cb378a6ed33eb/wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72", size = 74947, upload-time = "2025-01-14T10:33:24.414Z" },
{ url = "https://files.pythonhosted.org/packages/90/ec/00759565518f268ed707dcc40f7eeec38637d46b098a1f5143bff488fe97/wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061", size = 82778, upload-time = "2025-01-14T10:33:26.152Z" },
{ url = "https://files.pythonhosted.org/packages/f8/5a/7cffd26b1c607b0b0c8a9ca9d75757ad7620c9c0a9b4a25d3f8a1480fafc/wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2", size = 81716, upload-time = "2025-01-14T10:33:27.372Z" },
{ url = "https://files.pythonhosted.org/packages/7e/09/dccf68fa98e862df7e6a60a61d43d644b7d095a5fc36dbb591bbd4a1c7b2/wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c", size = 74548, upload-time = "2025-01-14T10:33:28.52Z" },
{ url = "https://files.pythonhosted.org/packages/b7/8e/067021fa3c8814952c5e228d916963c1115b983e21393289de15128e867e/wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62", size = 81334, upload-time = "2025-01-14T10:33:29.643Z" },
{ url = "https://files.pythonhosted.org/packages/4b/0d/9d4b5219ae4393f718699ca1c05f5ebc0c40d076f7e65fd48f5f693294fb/wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563", size = 36427, upload-time = "2025-01-14T10:33:30.832Z" },
{ url = "https://files.pythonhosted.org/packages/72/6a/c5a83e8f61aec1e1aeef939807602fb880e5872371e95df2137142f5c58e/wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f", size = 38774, upload-time = "2025-01-14T10:33:32.897Z" },
{ url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" },
{ url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" },
{ url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" },
{ url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" },
{ url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" },
{ url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" },
{ url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" },
{ url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" },
{ url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" },
{ url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
{ url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
{ url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
{ url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
{ url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
{ url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
{ url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
{ url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
{ url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
{ url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
{ url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
{ url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },
{ url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" },
{ url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" },
{ url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" },
{ url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" },
{ url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" },
{ url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" },
{ url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" },
{ url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" },
{ url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" },
{ url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" },
{ url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" },
{ url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" },
{ url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" },
{ url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" },
{ url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" },
{ url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" },
{ url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" },
{ url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" },
{ url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" },
{ url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" },
{ url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" },
{ url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" },
{ url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" },
{ url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" },
{ url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" },
{ url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" },
{ url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" },
{ url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" },
{ url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" },
{ url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" },
{ url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" },
{ url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" },
{ url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" },
{ url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" },
{ url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" },
{ url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" },
{ url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" },
{ url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" },
{ url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" },
{ url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" },
{ url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" },
{ url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" },
{ url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" },
{ url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" },
{ url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" },
{ url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" },
{ url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" },
{ url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" },
{ url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
{ url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
{ url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
{ url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
{ url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
{ url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
{ url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
{ url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
{ url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
{ url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
{ url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
{ url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
{ url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
{ url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
{ url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
{ url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
{ url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
{ url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
{ url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
{ url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
{ url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
{ url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
{ url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
{ url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
{ url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
{ url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
{ url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]