openai and langmem integrations
This commit is contained in:
parent
5ab7769f0e
commit
aa35223056
75 changed files with 4463 additions and 0 deletions
76
README.md
76
README.md
|
|
@ -381,6 +381,82 @@ memora search <agent_id> "query" -o yaml
|
|||
memora search <agent_id> "query" -v
|
||||
```
|
||||
|
||||
## OpenAI Client Wrapper (`memora-openai`)
|
||||
|
||||
The `memora-openai` package provides a drop-in replacement for the OpenAI Python client that automatically integrates with Memora. It transparently stores conversations and injects relevant memories into prompts.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cd memora-openai
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
from memora_openai import configure, OpenAI
|
||||
|
||||
# Configure Memora integration once
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
store_conversations=True, # Store conversations to Memora
|
||||
inject_memories=True, # Inject relevant memories into prompts
|
||||
)
|
||||
|
||||
# Use OpenAI client as normal - Memora integration happens automatically
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
||||
)
|
||||
```
|
||||
|
||||
### Async Support
|
||||
|
||||
```python
|
||||
from memora_openai import configure, AsyncOpenAI
|
||||
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
)
|
||||
|
||||
client = AsyncOpenAI(api_key="sk-...")
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[{"role": "user", "content": "Tell me about my preferences"}]
|
||||
)
|
||||
```
|
||||
|
||||
### Features
|
||||
|
||||
- **Automatic Memory Injection**: Relevant memories are automatically retrieved and injected as system messages before each API call
|
||||
- **Conversation Storage**: All conversations are automatically stored to Memora for future retrieval
|
||||
- **Zero Code Changes**: Works as a drop-in replacement for `openai.OpenAI` and `openai.AsyncOpenAI`
|
||||
- **Configurable**: Control memory search budget, context window, and enable/disable features
|
||||
- **Transparent**: Original OpenAI API responses are returned unchanged
|
||||
|
||||
### Configuration Options
|
||||
|
||||
```python
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000", # Memora API URL
|
||||
agent_id="my-agent", # Agent identifier (required)
|
||||
store_conversations=True, # Store conversations
|
||||
inject_memories=True, # Inject memories
|
||||
memory_search_budget=10, # Number of memories to retrieve
|
||||
context_window=10, # Conversation history size
|
||||
document_id="session-123", # Optional: Group conversations by document ID
|
||||
enabled=True, # Master switch
|
||||
)
|
||||
```
|
||||
|
||||
See [memora-openai/README.md](memora-openai/README.md) for full documentation and examples.
|
||||
|
||||
## Running Benchmarks
|
||||
|
||||
The system includes two benchmarks for evaluating memory retrieval quality:
|
||||
|
|
|
|||
5
memora-langmem/memora_langmem/__init__.py
Normal file
5
memora-langmem/memora_langmem/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
"""Memora-LangMem: LangGraph langmem drop-in replacement using Memora."""
|
||||
|
||||
from memora_langmem.store import MemoraStore
|
||||
|
||||
__all__ = ["MemoraStore"]
|
||||
337
memora-langmem/memora_langmem/store.py
Normal file
337
memora-langmem/memora_langmem/store.py
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
"""Memora implementation of LangGraph BaseStore interface."""
|
||||
|
||||
import json
|
||||
from typing import Any, Iterable
|
||||
|
||||
from agent_memory_api_client import Client
|
||||
from agent_memory_api_client.api.agent_management import (
|
||||
api_agents_api_v1_agents_get,
|
||||
api_create_or_update_agent_api_v1_agents_agent_id_put,
|
||||
)
|
||||
from agent_memory_api_client.api.memory_operations import (
|
||||
api_batch_put_api_v1_agents_agent_id_memories_post,
|
||||
api_delete_memory_unit_api_v1_agents_agent_id_memories_unit_id_delete,
|
||||
api_list_api_v1_agents_agent_id_memories_list_get,
|
||||
api_search_api_v1_agents_agent_id_memories_search_post,
|
||||
)
|
||||
from agent_memory_api_client.models import BatchPutRequest, CreateAgentRequest, MemoryItem, SearchRequest
|
||||
from langgraph.store.base import (
|
||||
BaseStore,
|
||||
GetOp,
|
||||
Item,
|
||||
ListNamespacesOp,
|
||||
Op,
|
||||
PutOp,
|
||||
Result,
|
||||
SearchItem,
|
||||
SearchOp,
|
||||
)
|
||||
|
||||
|
||||
class MemoraStore(BaseStore):
|
||||
"""
|
||||
Memora implementation of LangGraph BaseStore.
|
||||
|
||||
This store uses Memora's memory system as a backend for LangGraph's memory storage.
|
||||
Each namespace maps to a Memora agent, and items are stored as memory units.
|
||||
|
||||
Args:
|
||||
base_url: The base URL of the Memora API server
|
||||
default_agent_id: Default agent ID to use when namespace is empty (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, base_url: str, default_agent_id: str | None = None):
|
||||
"""Initialize the Memora store.
|
||||
|
||||
Args:
|
||||
base_url: Base URL for the Memora API
|
||||
default_agent_id: Default agent ID when namespace is empty
|
||||
"""
|
||||
super().__init__()
|
||||
self.client = Client(base_url=base_url)
|
||||
self.default_agent_id = default_agent_id or "default"
|
||||
self._ensure_agent_exists(self.default_agent_id)
|
||||
|
||||
def _namespace_to_agent_id(self, namespace: tuple[str, ...]) -> str:
|
||||
"""Convert namespace to agent ID."""
|
||||
if not namespace:
|
||||
return self.default_agent_id
|
||||
return "__".join(namespace)
|
||||
|
||||
def _ensure_agent_exists(self, agent_id: str) -> None:
|
||||
"""Ensure an agent exists, create if it doesn't."""
|
||||
try:
|
||||
response = api_agents_api_v1_agents_get.sync_detailed(client=self.client)
|
||||
if response.parsed and hasattr(response.parsed, "items"):
|
||||
existing_ids = [agent.agent_id for agent in response.parsed.items]
|
||||
if agent_id not in existing_ids:
|
||||
self._create_agent(agent_id)
|
||||
except Exception:
|
||||
self._create_agent(agent_id)
|
||||
|
||||
def _create_agent(self, agent_id: str) -> None:
|
||||
"""Create a new agent."""
|
||||
request = CreateAgentRequest()
|
||||
api_create_or_update_agent_api_v1_agents_agent_id_put.sync_detailed(
|
||||
agent_id=agent_id, client=self.client, body=request
|
||||
)
|
||||
|
||||
def _serialize_value(self, value: dict[str, Any]) -> str:
|
||||
"""Serialize a value to JSON string."""
|
||||
return json.dumps(value, sort_keys=True)
|
||||
|
||||
def _deserialize_value(self, content: str) -> dict[str, Any]:
|
||||
"""Deserialize JSON string back to value."""
|
||||
try:
|
||||
return json.loads(content)
|
||||
except json.JSONDecodeError:
|
||||
return {"content": content}
|
||||
|
||||
def batch(self, ops: Iterable[Op]) -> list[Result]:
|
||||
"""Execute a batch of operations synchronously."""
|
||||
results: list[Result] = []
|
||||
for op in ops:
|
||||
if isinstance(op, PutOp):
|
||||
results.append(self._put(op))
|
||||
elif isinstance(op, GetOp):
|
||||
results.append(self._get(op))
|
||||
elif isinstance(op, SearchOp):
|
||||
results.append(self._search(op))
|
||||
elif isinstance(op, ListNamespacesOp):
|
||||
results.append(self._list_namespaces(op))
|
||||
else:
|
||||
results.append(None)
|
||||
return results
|
||||
|
||||
async def abatch(self, ops: Iterable[Op]) -> list[Result]:
|
||||
"""Execute a batch of operations asynchronously."""
|
||||
return self.batch(ops)
|
||||
|
||||
def _put(self, op: PutOp) -> None:
|
||||
"""Store an item."""
|
||||
agent_id = self._namespace_to_agent_id(op.namespace)
|
||||
self._ensure_agent_exists(agent_id)
|
||||
|
||||
value_with_key = {"__key__": op.key, **op.value}
|
||||
content = self._serialize_value(value_with_key)
|
||||
|
||||
memory_item = MemoryItem(content=content, context=f"key:{op.key}")
|
||||
request = BatchPutRequest(items=[memory_item], document_id=op.key)
|
||||
|
||||
api_batch_put_api_v1_agents_agent_id_memories_post.sync_detailed(
|
||||
agent_id=agent_id, client=self.client, body=request
|
||||
)
|
||||
return None
|
||||
|
||||
def _get(self, op: GetOp) -> Item | None:
|
||||
"""Retrieve an item by namespace and key."""
|
||||
agent_id = self._namespace_to_agent_id(op.namespace)
|
||||
|
||||
try:
|
||||
response = api_list_api_v1_agents_agent_id_memories_list_get.sync_detailed(
|
||||
agent_id=agent_id, client=self.client, limit=1000
|
||||
)
|
||||
|
||||
if not response.parsed or not hasattr(response.parsed, "items"):
|
||||
return None
|
||||
|
||||
for memory_unit in response.parsed.items:
|
||||
if hasattr(memory_unit, "document_id") and memory_unit.document_id == op.key:
|
||||
try:
|
||||
value = self._deserialize_value(memory_unit.content or "{}")
|
||||
stored_key = value.pop("__key__", op.key)
|
||||
if stored_key == op.key:
|
||||
return Item(
|
||||
namespace=op.namespace,
|
||||
key=op.key,
|
||||
value=value,
|
||||
created_at=getattr(memory_unit, "created_at", None),
|
||||
updated_at=getattr(memory_unit, "updated_at", None),
|
||||
)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _search(self, op: SearchOp) -> list[SearchItem]:
|
||||
"""Search for items within a namespace prefix."""
|
||||
agent_id = self._namespace_to_agent_id(op.namespace_prefix)
|
||||
|
||||
try:
|
||||
search_request = SearchRequest(query=op.query or "", max_tokens=op.limit * 100)
|
||||
|
||||
response = api_search_api_v1_agents_agent_id_memories_search_post.sync_detailed(
|
||||
agent_id=agent_id, client=self.client, body=search_request
|
||||
)
|
||||
|
||||
if not response.parsed or not hasattr(response.parsed, "results"):
|
||||
return []
|
||||
|
||||
results: list[SearchItem] = []
|
||||
seen_keys = set()
|
||||
|
||||
for result in response.parsed.results[op.offset : op.offset + op.limit]:
|
||||
try:
|
||||
value = self._deserialize_value(result.fact or "{}")
|
||||
key = value.pop("__key__", result.fact_id)
|
||||
|
||||
if key in seen_keys:
|
||||
continue
|
||||
seen_keys.add(key)
|
||||
|
||||
results.append(
|
||||
SearchItem(
|
||||
namespace=op.namespace_prefix,
|
||||
key=key,
|
||||
value=value,
|
||||
score=getattr(result, "score", 1.0),
|
||||
created_at=getattr(result, "created_at", None),
|
||||
updated_at=getattr(result, "updated_at", None),
|
||||
)
|
||||
)
|
||||
|
||||
if len(results) >= op.limit:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return results
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _list_namespaces(self, op: ListNamespacesOp) -> list[tuple[str, ...]]:
|
||||
"""List all namespaces."""
|
||||
try:
|
||||
response = api_agents_api_v1_agents_get.sync_detailed(client=self.client)
|
||||
|
||||
if not response.parsed or not hasattr(response.parsed, "items"):
|
||||
return []
|
||||
|
||||
namespaces = []
|
||||
for agent in response.parsed.items:
|
||||
if hasattr(agent, "agent_id"):
|
||||
namespace = tuple(agent.agent_id.split("__"))
|
||||
|
||||
if op.prefix and not self._matches_prefix(namespace, op.prefix):
|
||||
continue
|
||||
if op.suffix and not self._matches_suffix(namespace, op.suffix):
|
||||
continue
|
||||
if op.max_depth is not None and len(namespace) > op.max_depth:
|
||||
continue
|
||||
|
||||
namespaces.append(namespace)
|
||||
|
||||
return namespaces[op.offset : op.offset + op.limit]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def _matches_prefix(self, namespace: tuple[str, ...], prefix: tuple[str, ...]) -> bool:
|
||||
"""Check if namespace matches prefix."""
|
||||
if len(namespace) < len(prefix):
|
||||
return False
|
||||
return namespace[: len(prefix)] == prefix
|
||||
|
||||
def _matches_suffix(self, namespace: tuple[str, ...], suffix: tuple[str, ...]) -> bool:
|
||||
"""Check if namespace matches suffix."""
|
||||
if len(namespace) < len(suffix):
|
||||
return False
|
||||
return namespace[-len(suffix) :] == suffix
|
||||
|
||||
def put(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: bool | list[str] | None = None,
|
||||
) -> None:
|
||||
"""Store a single item."""
|
||||
self._put(PutOp(namespace=namespace, key=key, value=value))
|
||||
|
||||
async def aput(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: bool | list[str] | None = None,
|
||||
) -> None:
|
||||
"""Store a single item asynchronously."""
|
||||
self.put(namespace, key, value, index)
|
||||
|
||||
def get(self, namespace: tuple[str, ...], key: str) -> Item | None:
|
||||
"""Retrieve a single item."""
|
||||
return self._get(GetOp(namespace=namespace, key=key))
|
||||
|
||||
async def aget(self, namespace: tuple[str, ...], key: str) -> Item | None:
|
||||
"""Retrieve a single item asynchronously."""
|
||||
return self.get(namespace, key)
|
||||
|
||||
def delete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Delete an item."""
|
||||
agent_id = self._namespace_to_agent_id(namespace)
|
||||
|
||||
try:
|
||||
response = api_list_api_v1_agents_agent_id_memories_list_get.sync_detailed(
|
||||
agent_id=agent_id, client=self.client, limit=1000
|
||||
)
|
||||
|
||||
if response.parsed and hasattr(response.parsed, "items"):
|
||||
for memory_unit in response.parsed.items:
|
||||
if hasattr(memory_unit, "document_id") and memory_unit.document_id == key:
|
||||
if hasattr(memory_unit, "unit_id"):
|
||||
api_delete_memory_unit_api_v1_agents_agent_id_memories_unit_id_delete.sync_detailed(
|
||||
agent_id=agent_id, unit_id=memory_unit.unit_id, client=self.client
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Delete an item asynchronously."""
|
||||
self.delete(namespace, key)
|
||||
|
||||
def search(
|
||||
self,
|
||||
namespace_prefix: tuple[str, ...],
|
||||
query: str | None = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> list[SearchItem]:
|
||||
"""Search for items."""
|
||||
return self._search(SearchOp(namespace_prefix=namespace_prefix, query=query, limit=limit, offset=offset))
|
||||
|
||||
async def asearch(
|
||||
self,
|
||||
namespace_prefix: tuple[str, ...],
|
||||
query: str | None = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
) -> list[SearchItem]:
|
||||
"""Search for items asynchronously."""
|
||||
return self.search(namespace_prefix, query, filter, limit, offset)
|
||||
|
||||
def list_namespaces(
|
||||
self,
|
||||
prefix: tuple[str, ...] | None = None,
|
||||
suffix: tuple[str, ...] | None = None,
|
||||
max_depth: int | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[tuple[str, ...]]:
|
||||
"""List all namespaces."""
|
||||
return self._list_namespaces(
|
||||
ListNamespacesOp(prefix=prefix, suffix=suffix, max_depth=max_depth, limit=limit, offset=offset)
|
||||
)
|
||||
|
||||
async def alist_namespaces(
|
||||
self,
|
||||
prefix: tuple[str, ...] | None = None,
|
||||
suffix: tuple[str, ...] | None = None,
|
||||
max_depth: int | None = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
) -> list[tuple[str, ...]]:
|
||||
"""List all namespaces asynchronously."""
|
||||
return self.list_namespaces(prefix, suffix, max_depth, limit, offset)
|
||||
30
memora-langmem/pyproject.toml
Normal file
30
memora-langmem/pyproject.toml
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
[project]
|
||||
name = "memora-langmem"
|
||||
version = "0.0.1"
|
||||
description = "LangGraph langmem drop-in replacement using Memora memory system"
|
||||
authors = [
|
||||
{name = "Memora Team"}
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"langgraph>=0.2.0",
|
||||
"memora-client",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
memora-client = { path = "../memora-clients/python", editable = true }
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest>=7.0.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["F", "I", "UP"]
|
||||
1
memora-langmem/tests/__init__.py
Normal file
1
memora-langmem/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Tests for memora-langmem package."""
|
||||
119
memora-langmem/tests/test_store.py
Normal file
119
memora-langmem/tests/test_store.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""Tests for MemoraStore implementation."""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from memora_langmem import MemoraStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store():
|
||||
"""Create a MemoraStore instance for testing."""
|
||||
base_url = os.getenv("MEMORA_API_URL", "http://localhost:8000")
|
||||
return MemoraStore(base_url=base_url, default_agent_id=f"test_agent_{int(time.time())}")
|
||||
|
||||
|
||||
def test_put_and_get(store):
|
||||
"""Test storing and retrieving an item."""
|
||||
namespace = ("test", "namespace")
|
||||
key = "test_key"
|
||||
value = {"data": "test_value", "number": 42}
|
||||
|
||||
store.put(namespace, key, value)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
retrieved = store.get(namespace, key)
|
||||
assert retrieved is not None
|
||||
assert retrieved.key == key
|
||||
assert retrieved.namespace == namespace
|
||||
assert retrieved.value == value
|
||||
|
||||
|
||||
def test_search(store):
|
||||
"""Test searching for items."""
|
||||
namespace = ("search", "test")
|
||||
key1 = "item1"
|
||||
value1 = {"content": "This is about machine learning", "type": "note"}
|
||||
|
||||
key2 = "item2"
|
||||
value2 = {"content": "This is about deep learning and neural networks", "type": "article"}
|
||||
|
||||
store.put(namespace, key1, value1)
|
||||
store.put(namespace, key2, value2)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
results = store.search(namespace, query="machine learning", limit=5)
|
||||
|
||||
assert len(results) > 0
|
||||
assert any(r.key in [key1, key2] for r in results)
|
||||
|
||||
|
||||
def test_delete(store):
|
||||
"""Test deleting an item."""
|
||||
namespace = ("delete", "test")
|
||||
key = "to_delete"
|
||||
value = {"data": "temporary"}
|
||||
|
||||
store.put(namespace, key, value)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
retrieved = store.get(namespace, key)
|
||||
assert retrieved is not None
|
||||
|
||||
store.delete(namespace, key)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
retrieved_after_delete = store.get(namespace, key)
|
||||
assert retrieved_after_delete is None
|
||||
|
||||
|
||||
def test_list_namespaces(store):
|
||||
"""Test listing namespaces."""
|
||||
namespace1 = ("list", "test", "one")
|
||||
namespace2 = ("list", "test", "two")
|
||||
|
||||
store.put(namespace1, "key1", {"data": "value1"})
|
||||
store.put(namespace2, "key2", {"data": "value2"})
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
namespaces = store.list_namespaces(prefix=("list",))
|
||||
|
||||
assert len(namespaces) >= 2
|
||||
assert namespace1 in namespaces
|
||||
assert namespace2 in namespaces
|
||||
|
||||
|
||||
def test_batch_operations(store):
|
||||
"""Test batch operations."""
|
||||
from langgraph.store.base import GetOp, PutOp
|
||||
|
||||
namespace = ("batch", "test")
|
||||
|
||||
ops = [
|
||||
PutOp(namespace=namespace, key="key1", value={"data": "value1"}),
|
||||
PutOp(namespace=namespace, key="key2", value={"data": "value2"}),
|
||||
]
|
||||
|
||||
results = store.batch(ops)
|
||||
assert len(results) == 2
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
get_ops = [
|
||||
GetOp(namespace=namespace, key="key1"),
|
||||
GetOp(namespace=namespace, key="key2"),
|
||||
]
|
||||
|
||||
get_results = store.batch(get_ops)
|
||||
assert len(get_results) == 2
|
||||
assert get_results[0] is not None
|
||||
assert get_results[0].value == {"data": "value1"}
|
||||
assert get_results[1] is not None
|
||||
assert get_results[1].value == {"data": "value2"}
|
||||
622
memora-langmem/tutorial.ipynb
Normal file
622
memora-langmem/tutorial.ipynb
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Memora-LangMem: Semantic Memory with Personality-Driven Thinking for LangGraph\n",
|
||||
"\n",
|
||||
"This notebook provides a comprehensive tutorial on using `memora-langmem`, a drop-in replacement for LangGraph's standard memory stores that adds advanced semantic memory capabilities powered by Memora."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## What is Memora-LangMem?\n",
|
||||
"\n",
|
||||
"`memora-langmem` is a Python package that implements LangGraph's `BaseStore` interface using Memora as the backend. It provides:\n",
|
||||
"\n",
|
||||
"### Core Features\n",
|
||||
"\n",
|
||||
"1. **Drop-in Replacement**: Fully compatible with LangGraph's memory system - just swap the store!\n",
|
||||
"2. **Semantic Memory**: Advanced semantic search with spreading activation algorithms\n",
|
||||
"3. **Personality-Driven Thinking**: Memory retrieval influenced by configurable agent personalities\n",
|
||||
"4. **Fact Extraction**: Automatic extraction and structuring of facts from conversations\n",
|
||||
"5. **Temporal Reasoning**: Time-aware memory with event date tracking\n",
|
||||
"6. **Entity Linking**: Automatic recognition and linking of entities across memories\n",
|
||||
"7. **Multi-Agent Support**: Each namespace can represent a different agent with unique personality traits\n",
|
||||
"\n",
|
||||
"### What You Get vs Standard LangGraph Memory\n",
|
||||
"\n",
|
||||
"| Feature | Standard LangGraph Memory | Memora-LangMem |\n",
|
||||
"|---------|---------------------------|----------------|\n",
|
||||
"| Basic Key-Value Storage | ✅ | ✅ |\n",
|
||||
"| Semantic Search | ✅ (with index config) | ✅ Enhanced with spreading activation |\n",
|
||||
"| Namespace Support | ✅ | ✅ |\n",
|
||||
"| Personality-Driven Retrieval | ❌ | ✅ Configurable personality traits |\n",
|
||||
"| Automatic Fact Extraction | ❌ | ✅ NLP-powered extraction |\n",
|
||||
"| Entity Recognition | ❌ | ✅ Automatic entity linking |\n",
|
||||
"| Temporal Reasoning | ❌ | ✅ Time-aware queries |\n",
|
||||
"| Opinion Formation | ❌ | ✅ Agent forms opinions over time |\n",
|
||||
"| Background Knowledge | ❌ | ✅ Agent-specific background context |\n",
|
||||
"| Thinking/Reasoning API | ❌ | ✅ Explicit reasoning with memory |\n",
|
||||
"\n",
|
||||
"### When to Use Memora-LangMem\n",
|
||||
"\n",
|
||||
"- **Conversational Agents**: When you need agents to remember context across long conversations\n",
|
||||
"- **Personalized AI**: When agent responses should be influenced by personality and past interactions\n",
|
||||
"- **Knowledge Management**: When you need to extract and organize facts from unstructured text\n",
|
||||
"- **Multi-Agent Systems**: When different agents need isolated memory with distinct personalities\n",
|
||||
"- **Research & Analysis**: When you need semantic search over large knowledge bases"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Installation\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"# Install from local path (development)\n",
|
||||
"uv pip install -e /path/to/memora-langmem\n",
|
||||
"\n",
|
||||
"# Or with pip\n",
|
||||
"pip install -e /path/to/memora-langmem\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"### Prerequisites\n",
|
||||
"\n",
|
||||
"You need a running Memora API server. Set the URL via environment variable:\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"export MEMORA_API_URL=http://localhost:8000\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Part 1: Basic Usage - Direct Store API\n",
|
||||
"\n",
|
||||
"Let's start with the basic `BaseStore` interface that's compatible with LangGraph."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from memora_langmem import MemoraStore\n",
|
||||
"\n",
|
||||
"# Initialize the store\n",
|
||||
"base_url = os.getenv(\"MEMORA_API_URL\", \"http://localhost:8000\")\n",
|
||||
"store = MemoraStore(base_url=base_url, default_agent_id=\"tutorial_agent\")\n",
|
||||
"\n",
|
||||
"print(\"✅ MemoraStore initialized\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Storing and Retrieving Memories\n",
|
||||
"\n",
|
||||
"The store uses a namespace-key-value structure:\n",
|
||||
"- **Namespace**: A tuple of strings representing a hierarchical path (e.g., `(\"user\", \"alice\")`)\n",
|
||||
"- **Key**: A unique identifier within the namespace\n",
|
||||
"- **Value**: A dictionary containing your data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Store a memory\n",
|
||||
"namespace = (\"user\", \"alice\")\n",
|
||||
"key = \"preferences\"\n",
|
||||
"value = {\n",
|
||||
" \"theme\": \"dark\",\n",
|
||||
" \"language\": \"python\",\n",
|
||||
" \"notifications\": True,\n",
|
||||
" \"interests\": [\"machine learning\", \"data science\", \"artificial intelligence\"]\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"store.put(namespace, key, value)\n",
|
||||
"print(f\"✅ Stored preferences for {namespace}\")\n",
|
||||
"\n",
|
||||
"# Retrieve the memory\n",
|
||||
"import time\n",
|
||||
"time.sleep(1) # Brief pause for processing\n",
|
||||
"\n",
|
||||
"retrieved = store.get(namespace, key)\n",
|
||||
"if retrieved:\n",
|
||||
" print(f\"\\n📦 Retrieved memory:\")\n",
|
||||
" print(f\" Namespace: {retrieved.namespace}\")\n",
|
||||
" print(f\" Key: {retrieved.key}\")\n",
|
||||
" print(f\" Value: {retrieved.value}\")\n",
|
||||
" print(f\" Created: {retrieved.created_at}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Semantic Search - The Power of Memora\n",
|
||||
"\n",
|
||||
"Unlike simple key-value retrieval, Memora provides semantic search with spreading activation. This means:\n",
|
||||
"- Search by natural language queries\n",
|
||||
"- Find semantically related memories\n",
|
||||
"- Memories are ranked by relevance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Add more memories to demonstrate search\n",
|
||||
"memories = [\n",
|
||||
" (\"notes\", \"ml_project\", {\n",
|
||||
" \"title\": \"Machine Learning Project Ideas\",\n",
|
||||
" \"content\": \"Working on a neural network for image classification. Interested in transformers and attention mechanisms.\"\n",
|
||||
" }),\n",
|
||||
" (\"notes\", \"data_tools\", {\n",
|
||||
" \"title\": \"Favorite Data Tools\",\n",
|
||||
" \"content\": \"Love using pandas for data manipulation, scikit-learn for ML, and PyTorch for deep learning.\"\n",
|
||||
" }),\n",
|
||||
" (\"notes\", \"meeting_summary\", {\n",
|
||||
" \"title\": \"Team Meeting Notes\",\n",
|
||||
" \"content\": \"Discussed the new AI assistant project. Team decided to use LangGraph for orchestration.\"\n",
|
||||
" })\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for ns, k, v in memories:\n",
|
||||
" store.put((\"user\", \"alice\", ns), k, v)\n",
|
||||
"\n",
|
||||
"time.sleep(2) # Allow time for indexing\n",
|
||||
"\n",
|
||||
"# Now search semantically\n",
|
||||
"print(\"🔍 Searching for 'machine learning projects'...\\n\")\n",
|
||||
"results = store.search(\n",
|
||||
" namespace_prefix=(\"user\", \"alice\"),\n",
|
||||
" query=\"machine learning projects\",\n",
|
||||
" limit=5\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"for i, result in enumerate(results, 1):\n",
|
||||
" print(f\"{i}. Score: {result.score:.3f}\")\n",
|
||||
" print(f\" Key: {result.key}\")\n",
|
||||
" print(f\" Value: {result.value}\")\n",
|
||||
" print()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Namespace Management\n",
|
||||
"\n",
|
||||
"Namespaces allow you to organize memories hierarchically. In Memora, each unique namespace combination maps to a separate agent."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create memories in different namespaces\n",
|
||||
"store.put((\"user\", \"bob\", \"preferences\"), \"theme\", {\"theme\": \"light\", \"language\": \"javascript\"})\n",
|
||||
"store.put((\"user\", \"charlie\", \"preferences\"), \"theme\", {\"theme\": \"auto\", \"language\": \"rust\"})\n",
|
||||
"\n",
|
||||
"time.sleep(1)\n",
|
||||
"\n",
|
||||
"# List all namespaces\n",
|
||||
"print(\"📁 All namespaces:\")\n",
|
||||
"namespaces = store.list_namespaces(prefix=(\"user\",), limit=10)\n",
|
||||
"for ns in namespaces:\n",
|
||||
" print(f\" - {ns}\")\n",
|
||||
"\n",
|
||||
"# List namespaces with specific prefix\n",
|
||||
"print(\"\\n📁 Namespaces for alice:\")\n",
|
||||
"alice_namespaces = store.list_namespaces(prefix=(\"user\", \"alice\"), limit=10)\n",
|
||||
"for ns in alice_namespaces:\n",
|
||||
" print(f\" - {ns}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Part 2: Integration with LangGraph Memory Tools\n",
|
||||
"\n",
|
||||
"The real power comes from using MemoraStore with LangGraph's memory tools and agents."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langmem import create_manage_memory_tool, create_search_memory_tool\n",
|
||||
"from langgraph.prebuilt import create_react_agent\n",
|
||||
"\n",
|
||||
"# Create a fresh store for the agent\n",
|
||||
"agent_store = MemoraStore(\n",
|
||||
" base_url=base_url,\n",
|
||||
" default_agent_id=\"intelligent_assistant\"\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Create memory tools\n",
|
||||
"manage_tool = create_manage_memory_tool(namespace=(\"conversations\",))\n",
|
||||
"search_tool = create_search_memory_tool(namespace=(\"conversations\",))\n",
|
||||
"\n",
|
||||
"# Create a LangGraph agent with Memora-backed memory\n",
|
||||
"agent = create_react_agent(\n",
|
||||
" \"anthropic:claude-3-5-sonnet-latest\",\n",
|
||||
" tools=[manage_tool, search_tool],\n",
|
||||
" store=agent_store # This is where MemoraStore plugs in!\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"✅ Agent created with Memora-backed memory\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Conversational Memory in Action\n",
|
||||
"\n",
|
||||
"Let's see how the agent uses Memora to remember information across conversations."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# First conversation: Share information\n",
|
||||
"print(\"💬 Conversation 1: Sharing preferences\\n\")\n",
|
||||
"result1 = agent.invoke({\n",
|
||||
" \"messages\": [{\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"\"\"Hi! I want you to remember some things about me:\n",
|
||||
" - My name is David\n",
|
||||
" - I'm a software engineer working on AI projects\n",
|
||||
" - I love Python and machine learning\n",
|
||||
" - I'm currently building a chatbot using LangGraph\n",
|
||||
" Please remember these details for future conversations.\"\"\"\n",
|
||||
" }]\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(\"Agent response:\")\n",
|
||||
"print(result1[\"messages\"][-1].content)\n",
|
||||
"print(\"\\n\" + \"=\"*80 + \"\\n\")\n",
|
||||
"\n",
|
||||
"# Second conversation: Recall information\n",
|
||||
"time.sleep(2) # Brief pause\n",
|
||||
"\n",
|
||||
"print(\"💬 Conversation 2: Testing recall\\n\")\n",
|
||||
"result2 = agent.invoke({\n",
|
||||
" \"messages\": [{\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"What do you remember about me and my work?\"\n",
|
||||
" }]\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(\"Agent response:\")\n",
|
||||
"print(result2[\"messages\"][-1].content)\n",
|
||||
"print(\"\\n\" + \"=\"*80 + \"\\n\")\n",
|
||||
"\n",
|
||||
"# Third conversation: Contextual recommendations\n",
|
||||
"time.sleep(2)\n",
|
||||
"\n",
|
||||
"print(\"💬 Conversation 3: Using memory for personalization\\n\")\n",
|
||||
"result3 = agent.invoke({\n",
|
||||
" \"messages\": [{\n",
|
||||
" \"role\": \"user\",\n",
|
||||
" \"content\": \"Can you suggest some relevant learning resources based on what you know about me?\"\n",
|
||||
" }]\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(\"Agent response:\")\n",
|
||||
"print(result3[\"messages\"][-1].content)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Part 3: Advanced Features - Beyond Standard LangGraph\n",
|
||||
"\n",
|
||||
"Memora provides capabilities beyond the standard BaseStore interface."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Automatic Fact Extraction\n",
|
||||
"\n",
|
||||
"When you store natural language content, Memora automatically:\n",
|
||||
"- Extracts structured facts\n",
|
||||
"- Identifies entities (people, places, concepts)\n",
|
||||
"- Links related facts together\n",
|
||||
"- Categorizes facts by type (world knowledge, agent actions, opinions)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Store rich natural language content\n",
|
||||
"conversation_store = MemoraStore(base_url=base_url, default_agent_id=\"fact_extractor\")\n",
|
||||
"\n",
|
||||
"conversation_content = {\n",
|
||||
" \"text\": \"\"\"Yesterday I met with Sarah from the marketing team. She mentioned that our new \n",
|
||||
" product launch is scheduled for next month. The team is really excited about the AI features \n",
|
||||
" we've built. Sarah thinks it will revolutionize how customers interact with our platform. \n",
|
||||
" I personally believe we should focus more on user experience rather than just adding features.\"\"\",\n",
|
||||
" \"context\": \"team meeting\",\n",
|
||||
" \"participants\": [\"self\", \"Sarah\"]\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"conversation_store.put(\n",
|
||||
" namespace=(\"meetings\", \"2024\"),\n",
|
||||
" key=\"marketing_sync\",\n",
|
||||
" value=conversation_content\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"✅ Stored conversation - Memora is now extracting facts...\")\n",
|
||||
"print(\" Behind the scenes, Memora identifies:\")\n",
|
||||
"print(\" • Entities: Sarah, marketing team, new product, AI features\")\n",
|
||||
"print(\" • Events: product launch next month, meeting with Sarah\")\n",
|
||||
"print(\" • Opinions: belief about UX focus vs features\")\n",
|
||||
"print(\" • Relationships: Sarah works in marketing\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Temporal Reasoning\n",
|
||||
"\n",
|
||||
"Memora is time-aware. You can query memories with temporal context."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from datetime import datetime, timedelta\n",
|
||||
"\n",
|
||||
"# Store time-sensitive information\n",
|
||||
"today = datetime.now()\n",
|
||||
"yesterday = today - timedelta(days=1)\n",
|
||||
"last_week = today - timedelta(days=7)\n",
|
||||
"\n",
|
||||
"events = [\n",
|
||||
" (\"event_today\", {\"description\": \"Team standup meeting\", \"date\": today.isoformat()}),\n",
|
||||
" (\"event_yesterday\", {\"description\": \"Product demo\", \"date\": yesterday.isoformat()}),\n",
|
||||
" (\"event_last_week\", {\"description\": \"Sprint planning\", \"date\": last_week.isoformat()})\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for key, value in events:\n",
|
||||
" conversation_store.put((\"events\",), key, value)\n",
|
||||
"\n",
|
||||
"time.sleep(2)\n",
|
||||
"\n",
|
||||
"# Search with temporal context\n",
|
||||
"print(\"🗓️ Searching for recent events...\\n\")\n",
|
||||
"recent_events = conversation_store.search(\n",
|
||||
" namespace_prefix=(\"events\",),\n",
|
||||
" query=\"meetings this week\",\n",
|
||||
" limit=5\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"for event in recent_events:\n",
|
||||
" print(f\"• {event.value.get('description')} - {event.value.get('date')}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Multi-Agent Scenarios\n",
|
||||
"\n",
|
||||
"Each namespace combination creates a separate agent in Memora, allowing for isolated memories and distinct personalities."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create stores for different agent personas\n",
|
||||
"store_creative = MemoraStore(base_url=base_url, default_agent_id=\"creative_writer\")\n",
|
||||
"store_analyst = MemoraStore(base_url=base_url, default_agent_id=\"data_analyst\")\n",
|
||||
"store_engineer = MemoraStore(base_url=base_url, default_agent_id=\"software_engineer\")\n",
|
||||
"\n",
|
||||
"# Each agent has its own perspective on the same information\n",
|
||||
"shared_info = {\n",
|
||||
" \"topic\": \"New AI Feature Launch\",\n",
|
||||
" \"description\": \"We're launching an AI-powered recommendation system\"\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"# Creative writer stores with focus on narrative\n",
|
||||
"store_creative.put((\"projects\",), \"ai_launch\", {\n",
|
||||
" **shared_info,\n",
|
||||
" \"note\": \"This is a revolutionary moment - we're changing how people discover content!\"\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"# Data analyst stores with focus on metrics\n",
|
||||
"store_analyst.put((\"projects\",), \"ai_launch\", {\n",
|
||||
" **shared_info,\n",
|
||||
" \"note\": \"Need to track engagement metrics, conversion rates, and user retention\"\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"# Software engineer stores with focus on implementation\n",
|
||||
"store_engineer.put((\"projects\",), \"ai_launch\", {\n",
|
||||
" **shared_info,\n",
|
||||
" \"note\": \"Built using transformer models, need to optimize inference latency\"\n",
|
||||
"})\n",
|
||||
"\n",
|
||||
"print(\"✅ Created three agents with different perspectives on the same project\")\n",
|
||||
"print(\" Each agent's memory is isolated and can develop unique personality traits\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Part 4: Batch Operations for Performance\n",
|
||||
"\n",
|
||||
"When working with multiple memories, batch operations are more efficient."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langgraph.store.base import PutOp, GetOp, SearchOp\n",
|
||||
"\n",
|
||||
"batch_store = MemoraStore(base_url=base_url, default_agent_id=\"batch_demo\")\n",
|
||||
"\n",
|
||||
"# Batch put operations\n",
|
||||
"put_ops = [\n",
|
||||
" PutOp(namespace=(\"docs\",), key=\"intro\", value={\"title\": \"Introduction\", \"content\": \"Welcome to the tutorial\"}),\n",
|
||||
" PutOp(namespace=(\"docs\",), key=\"setup\", value={\"title\": \"Setup\", \"content\": \"Installation instructions\"}),\n",
|
||||
" PutOp(namespace=(\"docs\",), key=\"usage\", value={\"title\": \"Usage\", \"content\": \"How to use the API\"}),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"print(\"📦 Performing batch PUT operations...\")\n",
|
||||
"put_results = batch_store.batch(put_ops)\n",
|
||||
"print(f\"✅ Stored {len(put_results)} documents\\n\")\n",
|
||||
"\n",
|
||||
"time.sleep(1)\n",
|
||||
"\n",
|
||||
"# Batch get operations\n",
|
||||
"get_ops = [\n",
|
||||
" GetOp(namespace=(\"docs\",), key=\"intro\"),\n",
|
||||
" GetOp(namespace=(\"docs\",), key=\"setup\"),\n",
|
||||
" GetOp(namespace=(\"docs\",), key=\"usage\"),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"print(\"📦 Performing batch GET operations...\")\n",
|
||||
"get_results = batch_store.batch(get_ops)\n",
|
||||
"\n",
|
||||
"for item in get_results:\n",
|
||||
" if item:\n",
|
||||
" print(f\" • {item.value['title']}: {item.value['content']}\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Part 5: Cleanup\n",
|
||||
"\n",
|
||||
"Clean up memories when they're no longer needed."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Delete specific memories\n",
|
||||
"print(\"🗑️ Cleaning up...\\n\")\n",
|
||||
"\n",
|
||||
"# Delete from the docs namespace\n",
|
||||
"batch_store.delete((\"docs\",), \"intro\")\n",
|
||||
"print(\"✅ Deleted 'intro' document\")\n",
|
||||
"\n",
|
||||
"# Verify deletion\n",
|
||||
"time.sleep(1)\n",
|
||||
"deleted_item = batch_store.get((\"docs\",), \"intro\")\n",
|
||||
"if deleted_item is None:\n",
|
||||
" print(\"✅ Confirmed: document is deleted\")\n",
|
||||
"else:\n",
|
||||
" print(\"⚠️ Document still exists\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Summary: Why Choose Memora-LangMem?\n",
|
||||
"\n",
|
||||
"### Key Advantages\n",
|
||||
"\n",
|
||||
"1. **Zero Code Changes**: Drop-in replacement for existing LangGraph memory stores\n",
|
||||
"2. **Enhanced Intelligence**: Automatic fact extraction, entity linking, and semantic understanding\n",
|
||||
"3. **Personality System**: Agents can develop unique personalities that influence memory retrieval\n",
|
||||
"4. **Production Ready**: Built on robust Memora backend with proper persistence\n",
|
||||
"5. **Rich Context**: Beyond simple key-value, stores temporal, relational, and semantic information\n",
|
||||
"6. **Research-Backed**: Implements spreading activation and advanced memory retrieval algorithms\n",
|
||||
"\n",
|
||||
"### Use Cases\n",
|
||||
"\n",
|
||||
"- **Customer Support Bots**: Remember customer preferences, history, and context\n",
|
||||
"- **Personal Assistants**: Build agents that truly understand and remember user preferences\n",
|
||||
"- **Knowledge Workers**: Agents that accumulate domain expertise over time\n",
|
||||
"- **Research Assistants**: Semantic search over large knowledge bases\n",
|
||||
"- **Team Collaboration**: Multiple agents with distinct roles and memories\n",
|
||||
"\n",
|
||||
"### Getting Started\n",
|
||||
"\n",
|
||||
"1. Install: `uv pip install -e /path/to/memora-langmem`\n",
|
||||
"2. Start Memora API: Ensure server is running at `http://localhost:8000`\n",
|
||||
"3. Replace store: `store = MemoraStore(base_url=base_url)`\n",
|
||||
"4. Use normally: All LangGraph memory APIs work as expected\n",
|
||||
"5. Enjoy enhanced memory capabilities automatically!\n",
|
||||
"\n",
|
||||
"### Next Steps\n",
|
||||
"\n",
|
||||
"- Explore the Memora API documentation for advanced features\n",
|
||||
"- Configure agent personalities for different use cases\n",
|
||||
"- Experiment with the thinking/reasoning API\n",
|
||||
"- Build multi-agent systems with isolated memories\n",
|
||||
"- Integrate with your existing LangGraph applications"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
1080
memora-langmem/uv.lock
Normal file
1080
memora-langmem/uv.lock
Normal file
File diff suppressed because it is too large
Load diff
218
memora-openai/README.md
Normal file
218
memora-openai/README.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
# Memora-OpenAI
|
||||
|
||||
Drop-in replacement for OpenAI Python client with automatic Memora integration.
|
||||
|
||||
## Overview
|
||||
|
||||
`memora-openai` is a transparent wrapper around the official OpenAI Python client that automatically:
|
||||
- 🧠 **Injects relevant memories** from your Memora system into conversations
|
||||
- 💾 **Stores conversation history** to Memora for future retrieval
|
||||
- 🔄 **Works seamlessly** with existing OpenAI code (just change the import)
|
||||
- ⚡ **Supports both sync and async** clients
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
cd memora-openai
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```python
|
||||
from memora_openai import configure, OpenAI
|
||||
|
||||
# Configure Memora integration once
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
store_conversations=True,
|
||||
inject_memories=True,
|
||||
)
|
||||
|
||||
# Use OpenAI client as normal - Memora integration happens automatically
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[
|
||||
{"role": "user", "content": "What did we discuss about AI last week?"}
|
||||
]
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
```
|
||||
|
||||
### Async Usage
|
||||
|
||||
```python
|
||||
from memora_openai import configure, AsyncOpenAI
|
||||
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
)
|
||||
|
||||
client = AsyncOpenAI(api_key="sk-...")
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="gpt-4",
|
||||
messages=[
|
||||
{"role": "user", "content": "Remind me about my preferences"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `configure()` function accepts the following parameters:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `memora_api_url` | str | `"http://localhost:8000"` | URL of your Memora API server |
|
||||
| `agent_id` | str | `None` | **Required.** Agent identifier for memory operations |
|
||||
| `api_key` | str | `None` | Optional API key for Memora authentication |
|
||||
| `store_conversations` | bool | `True` | Store conversations to Memora |
|
||||
| `inject_memories` | bool | `True` | Inject relevant memories into prompts |
|
||||
| `memory_search_budget` | int | `10` | Number of memories to retrieve for context |
|
||||
| `auto_extract_facts` | bool | `False` | Automatically extract facts from responses |
|
||||
| `event_timestamp` | str | `None` | Custom timestamp for memory events (ISO format) |
|
||||
| `context_window` | int | `10` | Number of recent conversation turns to consider |
|
||||
| `document_id` | str | `None` | Optional document ID for stored conversations |
|
||||
| `enabled` | bool | `True` | Master switch to enable/disable Memora integration |
|
||||
|
||||
## How It Works
|
||||
|
||||
### Memory Injection
|
||||
|
||||
When `inject_memories=True`, the wrapper:
|
||||
|
||||
1. Extracts the user's query from the last message
|
||||
2. Searches Memora for relevant memories using the query
|
||||
3. Injects the top memories as a system message before the conversation
|
||||
4. Sends the enhanced conversation to OpenAI
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
# Your code:
|
||||
messages = [
|
||||
{"role": "user", "content": "What's my favorite programming language?"}
|
||||
]
|
||||
|
||||
# What gets sent to OpenAI (automatically):
|
||||
messages = [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Relevant context from your memory:\n\n1. User prefers Python for its simplicity\n (Date: 2024-01-15)\n (Type: opinion)"
|
||||
},
|
||||
{"role": "user", "content": "What's my favorite programming language?"}
|
||||
]
|
||||
```
|
||||
|
||||
### Conversation Storage
|
||||
|
||||
When `store_conversations=True`, the wrapper:
|
||||
|
||||
1. Captures the conversation context (recent messages)
|
||||
2. Captures the assistant's response
|
||||
3. Stores the complete exchange to Memora asynchronously
|
||||
4. Tags it with context `"openai_conversation"` for filtering
|
||||
|
||||
This creates a searchable memory of all your AI conversations.
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Disable for Specific Requests
|
||||
|
||||
```python
|
||||
from memora_openai import configure, OpenAI, reset_config
|
||||
|
||||
# Configure globally
|
||||
configure(memora_api_url="http://localhost:8000", agent_id="agent-1")
|
||||
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
# Normal request with Memora
|
||||
response1 = client.chat.completions.create(...)
|
||||
|
||||
# Temporarily disable
|
||||
reset_config()
|
||||
response2 = client.chat.completions.create(...) # No Memora integration
|
||||
|
||||
# Re-enable
|
||||
configure(memora_api_url="http://localhost:8000", agent_id="agent-1")
|
||||
```
|
||||
|
||||
### Custom Memory Search Budget
|
||||
|
||||
```python
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
memory_search_budget=20, # Retrieve more memories for richer context
|
||||
)
|
||||
```
|
||||
|
||||
### Using Document ID
|
||||
|
||||
Group related conversations together using a document ID:
|
||||
|
||||
```python
|
||||
configure(
|
||||
memora_api_url="http://localhost:8000",
|
||||
agent_id="my-agent",
|
||||
document_id="meeting-2024-01-15", # All conversations tagged with this ID
|
||||
)
|
||||
|
||||
client = OpenAI(api_key="sk-...")
|
||||
|
||||
# All these calls will be stored under the same document
|
||||
response1 = client.chat.completions.create(...)
|
||||
response2 = client.chat.completions.create(...)
|
||||
```
|
||||
|
||||
### Cleanup
|
||||
|
||||
```python
|
||||
from memora_openai import cleanup_interceptor
|
||||
|
||||
# Clean up resources when done
|
||||
await cleanup_interceptor()
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python >= 3.10
|
||||
- openai >= 1.0.0
|
||||
- httpx >= 0.23.0
|
||||
- A running Memora API server
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
uv run pytest tests
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
memora-openai/
|
||||
├── src/memora_openai/
|
||||
│ ├── __init__.py # Main exports
|
||||
│ ├── client.py # OpenAI client wrappers
|
||||
│ ├── config.py # Global configuration
|
||||
│ └── interceptor.py # Request/response interception logic
|
||||
├── tests/
|
||||
│ └── test_client.py # Test suite
|
||||
├── pyproject.toml # Package configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Part of the Memora project.
|
||||
31
memora-openai/pyproject.toml
Normal file
31
memora-openai/pyproject.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
[project]
|
||||
name = "memora-openai"
|
||||
version = "0.1.0"
|
||||
description = "Drop-in replacement for OpenAI client with automatic Memora integration"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"openai>=1.0.0",
|
||||
"memora-client",
|
||||
]
|
||||
|
||||
[tool.uv.sources]
|
||||
memora-client = { path = "../memora-clients/python", editable = true }
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-mock>=3.10.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/memora_openai"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
71
memora-openai/src/memora_openai/__init__.py
Normal file
71
memora-openai/src/memora_openai/__init__.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""Memora-OpenAI: Drop-in replacement for OpenAI client with automatic Memora integration.
|
||||
|
||||
This package provides a transparent wrapper around the OpenAI Python client that
|
||||
automatically stores conversations and injects relevant memories from your Memora
|
||||
memory system.
|
||||
|
||||
Basic usage:
|
||||
>>> from memora_openai import configure, OpenAI
|
||||
>>>
|
||||
>>> # Configure Memora integration
|
||||
>>> configure(
|
||||
... memora_api_url="http://localhost:8000",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... )
|
||||
>>>
|
||||
>>> # Use OpenAI client as normal - Memora integration is automatic
|
||||
>>> client = OpenAI(api_key="sk-...")
|
||||
>>> response = client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "What did we discuss about AI?"}]
|
||||
... )
|
||||
|
||||
Async usage:
|
||||
>>> from memora_openai import configure, AsyncOpenAI
|
||||
>>>
|
||||
>>> configure(
|
||||
... memora_api_url="http://localhost:8000",
|
||||
... agent_id="my-agent",
|
||||
... )
|
||||
>>>
|
||||
>>> client = AsyncOpenAI(api_key="sk-...")
|
||||
>>> response = await client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "Tell me about quantum computing"}]
|
||||
... )
|
||||
|
||||
Configuration options:
|
||||
- memora_api_url: URL of your Memora API server
|
||||
- agent_id: Agent identifier for memory operations
|
||||
- api_key: Optional API key for Memora authentication
|
||||
- store_conversations: Whether to store conversations to Memora (default: True)
|
||||
- inject_memories: Whether to inject relevant memories (default: True)
|
||||
- memory_search_budget: Number of memories to retrieve (default: 10)
|
||||
- context_window: Number of conversation turns to store (default: 10)
|
||||
- enabled: Master switch to disable Memora integration (default: True)
|
||||
"""
|
||||
|
||||
from .client import OpenAI, AsyncOpenAI
|
||||
from .config import (
|
||||
configure,
|
||||
get_config,
|
||||
is_configured,
|
||||
reset_config,
|
||||
MemoraConfig,
|
||||
)
|
||||
from .interceptor import cleanup_interceptor
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
__all__ = [
|
||||
"OpenAI",
|
||||
"AsyncOpenAI",
|
||||
"configure",
|
||||
"get_config",
|
||||
"is_configured",
|
||||
"reset_config",
|
||||
"cleanup_interceptor",
|
||||
"MemoraConfig",
|
||||
]
|
||||
155
memora-openai/src/memora_openai/client.py
Normal file
155
memora-openai/src/memora_openai/client.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
"""Drop-in replacement for OpenAI client with Memora integration."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Optional, List, Dict
|
||||
|
||||
from openai import OpenAI as _OpenAI, AsyncOpenAI as _AsyncOpenAI
|
||||
|
||||
from .config import get_config, is_configured
|
||||
from .interceptor import get_interceptor
|
||||
|
||||
|
||||
class _CompletionsWrapper:
|
||||
"""Wrapper for chat completions with Memora integration (sync)."""
|
||||
|
||||
def __init__(self, original_completions):
|
||||
"""Initialize wrapper with original completions object."""
|
||||
self._original = original_completions
|
||||
|
||||
def create(self, *args, **kwargs):
|
||||
"""Create a chat completion with Memora integration."""
|
||||
if not is_configured():
|
||||
return self._original.create(*args, **kwargs)
|
||||
|
||||
config = get_config()
|
||||
if not config.enabled:
|
||||
return self._original.create(*args, **kwargs)
|
||||
|
||||
messages = kwargs.get("messages", [])
|
||||
if not messages:
|
||||
return self._original.create(*args, **kwargs)
|
||||
|
||||
# Run async operations in a new event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
# Inject memories if configured
|
||||
if config.inject_memories:
|
||||
interceptor = get_interceptor()
|
||||
modified_messages = loop.run_until_complete(
|
||||
interceptor.inject_memories(messages, config)
|
||||
)
|
||||
kwargs["messages"] = modified_messages
|
||||
|
||||
# Call original OpenAI API
|
||||
response = self._original.create(*args, **kwargs)
|
||||
|
||||
# Store conversation if configured
|
||||
if config.store_conversations:
|
||||
interceptor = get_interceptor()
|
||||
loop.run_until_complete(
|
||||
interceptor.store_conversation(
|
||||
kwargs["messages"], response, config
|
||||
)
|
||||
)
|
||||
|
||||
return response
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate all other attributes to the original completions."""
|
||||
return getattr(self._original, name)
|
||||
|
||||
|
||||
class _AsyncCompletionsWrapper:
|
||||
"""Wrapper for chat completions with Memora integration (async)."""
|
||||
|
||||
def __init__(self, original_completions):
|
||||
"""Initialize wrapper with original completions object."""
|
||||
self._original = original_completions
|
||||
|
||||
async def create(self, *args, **kwargs):
|
||||
"""Create a chat completion with Memora integration."""
|
||||
if not is_configured():
|
||||
return await self._original.create(*args, **kwargs)
|
||||
|
||||
config = get_config()
|
||||
if not config.enabled:
|
||||
return await self._original.create(*args, **kwargs)
|
||||
|
||||
messages = kwargs.get("messages", [])
|
||||
if not messages:
|
||||
return await self._original.create(*args, **kwargs)
|
||||
|
||||
# Inject memories if configured
|
||||
if config.inject_memories:
|
||||
interceptor = get_interceptor()
|
||||
modified_messages = await interceptor.inject_memories(messages, config)
|
||||
kwargs["messages"] = modified_messages
|
||||
|
||||
# Call original OpenAI API
|
||||
response = await self._original.create(*args, **kwargs)
|
||||
|
||||
# Store conversation if configured
|
||||
if config.store_conversations:
|
||||
interceptor = get_interceptor()
|
||||
await interceptor.store_conversation(
|
||||
kwargs["messages"], response, config
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""Delegate all other attributes to the original completions."""
|
||||
return getattr(self._original, name)
|
||||
|
||||
|
||||
class OpenAI(_OpenAI):
|
||||
"""Drop-in replacement for OpenAI client with Memora integration.
|
||||
|
||||
Usage:
|
||||
>>> from memora_openai import configure, OpenAI
|
||||
>>> configure(
|
||||
... memora_api_url="http://localhost:8000",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... )
|
||||
>>> client = OpenAI(api_key="sk-...")
|
||||
>>> response = client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "Hello!"}]
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize OpenAI client with Memora integration."""
|
||||
super().__init__(*args, **kwargs)
|
||||
# Wrap chat completions with our interceptor
|
||||
self.chat.completions = _CompletionsWrapper(self.chat.completions)
|
||||
|
||||
|
||||
class AsyncOpenAI(_AsyncOpenAI):
|
||||
"""Drop-in replacement for AsyncOpenAI client with Memora integration.
|
||||
|
||||
Usage:
|
||||
>>> from memora_openai import configure, AsyncOpenAI
|
||||
>>> configure(
|
||||
... memora_api_url="http://localhost:8000",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... )
|
||||
>>> client = AsyncOpenAI(api_key="sk-...")
|
||||
>>> response = await client.chat.completions.create(
|
||||
... model="gpt-4",
|
||||
... messages=[{"role": "user", "content": "Hello!"}]
|
||||
... )
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize AsyncOpenAI client with Memora integration."""
|
||||
super().__init__(*args, **kwargs)
|
||||
# Wrap chat completions with our interceptor
|
||||
self.chat.completions = _AsyncCompletionsWrapper(self.chat.completions)
|
||||
124
memora-openai/src/memora_openai/config.py
Normal file
124
memora-openai/src/memora_openai/config.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"""Global configuration for Memora-OpenAI integration."""
|
||||
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoraConfig:
|
||||
"""Configuration for Memora integration.
|
||||
|
||||
Attributes:
|
||||
memora_api_url: URL of the Memora API server
|
||||
agent_id: Agent ID for memory operations
|
||||
api_key: Optional API key for Memora authentication
|
||||
store_conversations: Whether to store conversations to Memora
|
||||
inject_memories: Whether to inject relevant memories into prompts
|
||||
memory_search_budget: Number of memories to retrieve for context
|
||||
auto_extract_facts: Whether to automatically extract facts from responses
|
||||
event_timestamp: Optional timestamp for memory events (defaults to current time)
|
||||
context_window: Number of recent conversation turns to consider
|
||||
document_id: Optional document ID for stored conversations
|
||||
enabled: Master switch to enable/disable Memora integration
|
||||
"""
|
||||
|
||||
memora_api_url: str = "http://localhost:8000"
|
||||
agent_id: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
store_conversations: bool = True
|
||||
inject_memories: bool = True
|
||||
memory_search_budget: int = 10
|
||||
auto_extract_facts: bool = False
|
||||
event_timestamp: Optional[str] = None
|
||||
context_window: int = 10
|
||||
document_id: Optional[str] = None
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
# Global configuration instance
|
||||
_global_config: Optional[MemoraConfig] = None
|
||||
|
||||
|
||||
def configure(
|
||||
memora_api_url: str = "http://localhost:8000",
|
||||
agent_id: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
store_conversations: bool = True,
|
||||
inject_memories: bool = True,
|
||||
memory_search_budget: int = 10,
|
||||
auto_extract_facts: bool = False,
|
||||
event_timestamp: Optional[str] = None,
|
||||
context_window: int = 10,
|
||||
document_id: Optional[str] = None,
|
||||
enabled: bool = True,
|
||||
) -> MemoraConfig:
|
||||
"""Configure global Memora integration settings.
|
||||
|
||||
Args:
|
||||
memora_api_url: URL of the Memora API server
|
||||
agent_id: Agent ID for memory operations (required)
|
||||
api_key: Optional API key for Memora authentication
|
||||
store_conversations: Whether to store conversations to Memora
|
||||
inject_memories: Whether to inject relevant memories into prompts
|
||||
memory_search_budget: Number of memories to retrieve for context
|
||||
auto_extract_facts: Whether to automatically extract facts from responses
|
||||
event_timestamp: Optional timestamp for memory events
|
||||
context_window: Number of recent conversation turns to consider
|
||||
document_id: Optional document ID for stored conversations
|
||||
enabled: Master switch to enable/disable Memora integration
|
||||
|
||||
Returns:
|
||||
The configured MemoraConfig instance
|
||||
|
||||
Example:
|
||||
>>> from memora_openai import configure, OpenAI
|
||||
>>> configure(
|
||||
... memora_api_url="http://localhost:8000",
|
||||
... agent_id="my-agent",
|
||||
... store_conversations=True,
|
||||
... inject_memories=True,
|
||||
... document_id="conversation-123",
|
||||
... )
|
||||
>>> client = OpenAI(api_key="...")
|
||||
"""
|
||||
global _global_config
|
||||
|
||||
_global_config = MemoraConfig(
|
||||
memora_api_url=memora_api_url,
|
||||
agent_id=agent_id,
|
||||
api_key=api_key,
|
||||
store_conversations=store_conversations,
|
||||
inject_memories=inject_memories,
|
||||
memory_search_budget=memory_search_budget,
|
||||
auto_extract_facts=auto_extract_facts,
|
||||
event_timestamp=event_timestamp,
|
||||
context_window=context_window,
|
||||
document_id=document_id,
|
||||
enabled=enabled,
|
||||
)
|
||||
|
||||
return _global_config
|
||||
|
||||
|
||||
def get_config() -> Optional[MemoraConfig]:
|
||||
"""Get the current global configuration.
|
||||
|
||||
Returns:
|
||||
The current MemoraConfig instance, or None if not configured
|
||||
"""
|
||||
return _global_config
|
||||
|
||||
|
||||
def is_configured() -> bool:
|
||||
"""Check if Memora has been configured.
|
||||
|
||||
Returns:
|
||||
True if configure() has been called, False otherwise
|
||||
"""
|
||||
return _global_config is not None and _global_config.enabled
|
||||
|
||||
|
||||
def reset_config() -> None:
|
||||
"""Reset the global configuration to None."""
|
||||
global _global_config
|
||||
_global_config = None
|
||||
268
memora-openai/src/memora_openai/interceptor.py
Normal file
268
memora-openai/src/memora_openai/interceptor.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
"""Request/response interceptor for OpenAI API calls."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from agent_memory_api_client.client import Client
|
||||
from agent_memory_api_client.models.search_request import SearchRequest
|
||||
from agent_memory_api_client.models.batch_put_request import BatchPutRequest
|
||||
from agent_memory_api_client.models.memory_item import MemoryItem
|
||||
from agent_memory_api_client.api.memory_operations import (
|
||||
api_search_api_v1_agents_agent_id_memories_search_post as search_api,
|
||||
api_batch_put_async_api_v1_agents_agent_id_memories_async_post as batch_put_async_api,
|
||||
)
|
||||
|
||||
from .config import get_config, MemoraConfig
|
||||
|
||||
|
||||
class MemoraInterceptor:
|
||||
"""Intercepts OpenAI API calls to integrate with Memora."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the interceptor with a Memora client."""
|
||||
self._client: Optional[Client] = None
|
||||
|
||||
def get_client(self, config: MemoraConfig) -> Client:
|
||||
"""Get or create the Memora client."""
|
||||
if self._client is None:
|
||||
self._client = Client(base_url=config.memora_api_url, timeout=30.0)
|
||||
return self._client
|
||||
|
||||
async def close(self):
|
||||
"""Close the client."""
|
||||
if self._client is not None:
|
||||
await self._client.__aexit__(None, None, None)
|
||||
self._client = None
|
||||
|
||||
def _extract_user_query(self, messages: List[Dict[str, Any]]) -> Optional[str]:
|
||||
"""Extract the most recent user message as a query.
|
||||
|
||||
Args:
|
||||
messages: List of chat messages
|
||||
|
||||
Returns:
|
||||
The last user message content, or None if not found
|
||||
"""
|
||||
for message in reversed(messages):
|
||||
if message.get("role") == "user":
|
||||
content = message.get("content")
|
||||
if isinstance(content, str):
|
||||
return content
|
||||
elif isinstance(content, list):
|
||||
# Handle structured content (e.g., with images)
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
return item.get("text")
|
||||
return None
|
||||
|
||||
def _extract_conversation_context(
|
||||
self, messages: List[Dict[str, Any]], window: int = 10
|
||||
) -> str:
|
||||
"""Extract recent conversation context.
|
||||
|
||||
Args:
|
||||
messages: List of chat messages
|
||||
window: Number of recent messages to include
|
||||
|
||||
Returns:
|
||||
Formatted conversation context
|
||||
"""
|
||||
recent_messages = messages[-window:] if len(messages) > window else messages
|
||||
context_parts = []
|
||||
|
||||
for msg in recent_messages:
|
||||
role = msg.get("role", "unknown")
|
||||
content = msg.get("content", "")
|
||||
|
||||
if isinstance(content, str):
|
||||
context_parts.append(f"{role}: {content}")
|
||||
elif isinstance(content, list):
|
||||
# Handle structured content
|
||||
text_parts = []
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
text_parts.append(item.get("text", ""))
|
||||
if text_parts:
|
||||
context_parts.append(f"{role}: {' '.join(text_parts)}")
|
||||
|
||||
return "\n".join(context_parts)
|
||||
|
||||
async def inject_memories(
|
||||
self, messages: List[Dict[str, Any]], config: MemoraConfig
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Inject relevant memories into the conversation.
|
||||
|
||||
Args:
|
||||
messages: Original chat messages
|
||||
config: Memora configuration
|
||||
|
||||
Returns:
|
||||
Modified messages with injected memories
|
||||
"""
|
||||
if not config.agent_id:
|
||||
return messages
|
||||
|
||||
# Extract query from last user message
|
||||
query = self._extract_user_query(messages)
|
||||
if not query:
|
||||
return messages
|
||||
|
||||
# Search for relevant memories
|
||||
try:
|
||||
client = self.get_client(config)
|
||||
|
||||
# Create search request
|
||||
search_request = SearchRequest(
|
||||
query=query,
|
||||
thinking_budget=config.memory_search_budget,
|
||||
max_tokens=2048,
|
||||
trace=False,
|
||||
reranker="heuristic",
|
||||
)
|
||||
|
||||
# Perform search
|
||||
search_response = await search_api.asyncio(
|
||||
agent_id=config.agent_id,
|
||||
client=client,
|
||||
body=search_request,
|
||||
)
|
||||
|
||||
if not search_response or not search_response.results:
|
||||
return messages
|
||||
|
||||
# Format memories as context
|
||||
memory_context = self._format_memories(
|
||||
[result.to_dict() for result in search_response.results]
|
||||
)
|
||||
|
||||
# Inject memories into the conversation
|
||||
# We'll add it as a system message before the conversation
|
||||
memory_message = {
|
||||
"role": "system",
|
||||
"content": f"Relevant context from your memory:\n\n{memory_context}",
|
||||
}
|
||||
|
||||
# Insert after any existing system messages but before the conversation
|
||||
insert_index = 0
|
||||
for i, msg in enumerate(messages):
|
||||
if msg.get("role") != "system":
|
||||
insert_index = i
|
||||
break
|
||||
|
||||
modified_messages = (
|
||||
messages[:insert_index] + [memory_message] + messages[insert_index:]
|
||||
)
|
||||
return modified_messages
|
||||
|
||||
except Exception as e:
|
||||
# Don't fail the request if memory injection fails
|
||||
print(f"Warning: Failed to inject memories: {e}")
|
||||
return messages
|
||||
|
||||
def _format_memories(self, memories: List[Dict[str, Any]]) -> str:
|
||||
"""Format memory search results as context.
|
||||
|
||||
Args:
|
||||
memories: List of memory results from Memora API
|
||||
|
||||
Returns:
|
||||
Formatted context string
|
||||
"""
|
||||
parts = []
|
||||
for i, memory in enumerate(memories, 1):
|
||||
text = memory.get("text", "")
|
||||
event_date = memory.get("event_date")
|
||||
fact_type = memory.get("fact_type", "")
|
||||
|
||||
parts.append(f"{i}. {text}")
|
||||
if event_date:
|
||||
parts.append(f" (Date: {event_date})")
|
||||
if fact_type:
|
||||
parts.append(f" (Type: {fact_type})")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
async def store_conversation(
|
||||
self,
|
||||
messages: List[Dict[str, Any]],
|
||||
response: Any,
|
||||
config: MemoraConfig,
|
||||
) -> None:
|
||||
"""Store conversation to Memora.
|
||||
|
||||
Args:
|
||||
messages: Chat messages sent to OpenAI
|
||||
response: Response from OpenAI
|
||||
config: Memora configuration
|
||||
"""
|
||||
if not config.agent_id:
|
||||
return
|
||||
|
||||
try:
|
||||
# Extract conversation context
|
||||
conversation_text = self._extract_conversation_context(
|
||||
messages, config.context_window
|
||||
)
|
||||
|
||||
# Extract assistant response
|
||||
assistant_response = ""
|
||||
if hasattr(response, "choices") and response.choices:
|
||||
choice = response.choices[0]
|
||||
if hasattr(choice, "message") and hasattr(choice.message, "content"):
|
||||
assistant_response = choice.message.content or ""
|
||||
|
||||
# Combine into a single memory item
|
||||
if conversation_text and assistant_response:
|
||||
full_conversation = (
|
||||
f"{conversation_text}\nassistant: {assistant_response}"
|
||||
)
|
||||
|
||||
# Determine event timestamp
|
||||
event_date = config.event_timestamp or datetime.utcnow().isoformat()
|
||||
|
||||
# Create memory item
|
||||
memory_item = MemoryItem(
|
||||
content=full_conversation,
|
||||
event_date=event_date,
|
||||
context="openai_conversation",
|
||||
)
|
||||
|
||||
# Create batch put request
|
||||
batch_request = BatchPutRequest(
|
||||
items=[memory_item],
|
||||
document_id=config.document_id,
|
||||
)
|
||||
|
||||
# Get client
|
||||
client = self.get_client(config)
|
||||
|
||||
# Store to Memora
|
||||
await batch_put_async_api.asyncio(
|
||||
agent_id=config.agent_id,
|
||||
client=client,
|
||||
body=batch_request,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
# Don't fail the request if storage fails
|
||||
print(f"Warning: Failed to store conversation: {e}")
|
||||
|
||||
|
||||
# Global interceptor instance
|
||||
_interceptor: Optional[MemoraInterceptor] = None
|
||||
|
||||
|
||||
def get_interceptor() -> MemoraInterceptor:
|
||||
"""Get or create the global interceptor instance."""
|
||||
global _interceptor
|
||||
if _interceptor is None:
|
||||
_interceptor = MemoraInterceptor()
|
||||
return _interceptor
|
||||
|
||||
|
||||
async def cleanup_interceptor():
|
||||
"""Cleanup the global interceptor instance."""
|
||||
global _interceptor
|
||||
if _interceptor is not None:
|
||||
await _interceptor.close()
|
||||
_interceptor = None
|
||||
260
memora-openai/tests/test_client.py
Normal file
260
memora-openai/tests/test_client.py
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
"""Tests for Memora-OpenAI client wrapper."""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from memora_openai import (
|
||||
configure,
|
||||
reset_config,
|
||||
OpenAI,
|
||||
AsyncOpenAI,
|
||||
is_configured,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup():
|
||||
"""Reset configuration after each test."""
|
||||
yield
|
||||
reset_config()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def groq_api_key():
|
||||
"""Get Groq API key from environment."""
|
||||
api_key = os.getenv("GROQ_API_KEY")
|
||||
if not api_key:
|
||||
pytest.skip("GROQ_API_KEY environment variable not set")
|
||||
return api_key
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memora_api_url():
|
||||
"""Get Memora API URL from environment."""
|
||||
return os.getenv("MEMORA_API_URL", "http://localhost:8000")
|
||||
|
||||
|
||||
class TestConfiguration:
|
||||
"""Test configuration management."""
|
||||
|
||||
def test_configure_basic(self):
|
||||
"""Test basic configuration."""
|
||||
config = configure(
|
||||
memora_api_url="http://test:8000",
|
||||
agent_id="test-agent",
|
||||
)
|
||||
|
||||
assert config.memora_api_url == "http://test:8000"
|
||||
assert config.agent_id == "test-agent"
|
||||
assert config.store_conversations is True
|
||||
assert config.inject_memories is True
|
||||
assert is_configured()
|
||||
|
||||
def test_configure_custom_options(self):
|
||||
"""Test configuration with custom options."""
|
||||
config = configure(
|
||||
memora_api_url="http://test:8000",
|
||||
agent_id="test-agent",
|
||||
store_conversations=False,
|
||||
inject_memories=False,
|
||||
memory_search_budget=20,
|
||||
context_window=5,
|
||||
document_id="test-doc",
|
||||
)
|
||||
|
||||
assert config.store_conversations is False
|
||||
assert config.inject_memories is False
|
||||
assert config.memory_search_budget == 20
|
||||
assert config.context_window == 5
|
||||
assert config.document_id == "test-doc"
|
||||
|
||||
def test_reset_config(self):
|
||||
"""Test resetting configuration."""
|
||||
configure(memora_api_url="http://test:8000", agent_id="test-agent")
|
||||
assert is_configured()
|
||||
|
||||
reset_config()
|
||||
assert not is_configured()
|
||||
|
||||
|
||||
class TestSyncClient:
|
||||
"""Test synchronous OpenAI client wrapper."""
|
||||
|
||||
def test_client_creation(self, groq_api_key):
|
||||
"""Test that client can be created."""
|
||||
configure(memora_api_url="http://test:8000", agent_id="test-agent")
|
||||
|
||||
client = OpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
assert client is not None
|
||||
assert hasattr(client.chat.completions, "_original")
|
||||
|
||||
def test_chat_completion_without_config(self, groq_api_key):
|
||||
"""Test that chat completion works without Memora configuration."""
|
||||
reset_config()
|
||||
|
||||
client = OpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'test' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
assert response.choices[0].message.content is not None
|
||||
|
||||
def test_wrapper_passthrough(self, groq_api_key, memora_api_url):
|
||||
"""Test that wrapper passes through when features disabled."""
|
||||
configure(
|
||||
memora_api_url=memora_api_url,
|
||||
agent_id="test-sync-passthrough",
|
||||
inject_memories=False,
|
||||
store_conversations=False,
|
||||
)
|
||||
|
||||
client = OpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'hello' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
|
||||
|
||||
class TestAsyncClient:
|
||||
"""Test asynchronous OpenAI client wrapper."""
|
||||
|
||||
def test_client_creation(self, groq_api_key):
|
||||
"""Test that async client can be created."""
|
||||
configure(memora_api_url="http://test:8000", agent_id="test-agent")
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
assert client is not None
|
||||
assert hasattr(client.chat.completions, "_original")
|
||||
|
||||
async def test_chat_completion_without_config(self, groq_api_key):
|
||||
"""Test that async chat completion works without Memora configuration."""
|
||||
reset_config()
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'test' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
assert response.choices[0].message.content is not None
|
||||
|
||||
async def test_wrapper_passthrough(self, groq_api_key, memora_api_url):
|
||||
"""Test that async wrapper passes through when features disabled."""
|
||||
configure(
|
||||
memora_api_url=memora_api_url,
|
||||
agent_id="test-async-passthrough",
|
||||
inject_memories=False,
|
||||
store_conversations=False,
|
||||
)
|
||||
|
||||
client = AsyncOpenAI(
|
||||
api_key=groq_api_key,
|
||||
base_url="https://api.groq.com/openai/v1",
|
||||
)
|
||||
|
||||
response = await client.chat.completions.create(
|
||||
model="llama-3.1-8b-instant",
|
||||
messages=[{"role": "user", "content": "Say 'hello' and nothing else"}],
|
||||
max_tokens=10,
|
||||
)
|
||||
|
||||
assert response is not None
|
||||
assert len(response.choices) > 0
|
||||
|
||||
|
||||
class TestInterceptor:
|
||||
"""Test interceptor functionality."""
|
||||
|
||||
def test_extract_user_query_simple(self):
|
||||
"""Test extracting user query from simple messages."""
|
||||
from memora_openai.interceptor import MemoraInterceptor
|
||||
|
||||
interceptor = MemoraInterceptor()
|
||||
messages = [
|
||||
{"role": "system", "content": "You are a helpful assistant."},
|
||||
{"role": "user", "content": "What is Python?"},
|
||||
]
|
||||
|
||||
query = interceptor._extract_user_query(messages)
|
||||
assert query == "What is Python?"
|
||||
|
||||
def test_extract_user_query_structured(self):
|
||||
"""Test extracting user query from structured content."""
|
||||
from memora_openai.interceptor import MemoraInterceptor
|
||||
|
||||
interceptor = MemoraInterceptor()
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{"type": "text", "text": "What's in this image?"},
|
||||
{"type": "image_url", "image_url": {"url": "https://..."}},
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
query = interceptor._extract_user_query(messages)
|
||||
assert query == "What's in this image?"
|
||||
|
||||
def test_extract_conversation_context(self):
|
||||
"""Test extracting conversation context."""
|
||||
from memora_openai.interceptor import MemoraInterceptor
|
||||
|
||||
interceptor = MemoraInterceptor()
|
||||
messages = [
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi! How can I help?"},
|
||||
{"role": "user", "content": "Tell me about AI"},
|
||||
]
|
||||
|
||||
context = interceptor._extract_conversation_context(messages)
|
||||
assert "user: Hello" in context
|
||||
assert "assistant: Hi! How can I help?" in context
|
||||
assert "user: Tell me about AI" in context
|
||||
|
||||
def test_format_memories(self):
|
||||
"""Test formatting memories."""
|
||||
from memora_openai.interceptor import MemoraInterceptor
|
||||
|
||||
interceptor = MemoraInterceptor()
|
||||
memories = [
|
||||
{
|
||||
"text": "User likes Python",
|
||||
"event_date": "2024-01-01",
|
||||
"fact_type": "opinion",
|
||||
},
|
||||
{"text": "Working on AI project", "event_date": None, "fact_type": "world"},
|
||||
]
|
||||
|
||||
formatted = interceptor._format_memories(memories)
|
||||
assert "1. User likes Python" in formatted
|
||||
assert "2. Working on AI project" in formatted
|
||||
assert "Date: 2024-01-01" in formatted
|
||||
452
memora-openai/tutorial.ipynb
Normal file
452
memora-openai/tutorial.ipynb
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Memora-OpenAI Tutorial\n",
|
||||
"\n",
|
||||
"**A drop-in replacement for the OpenAI Python client with automatic memory integration**\n",
|
||||
"\n",
|
||||
"## What is Memora-OpenAI?\n",
|
||||
"\n",
|
||||
"`memora-openai` is a transparent wrapper around the official OpenAI Python client that automatically:\n",
|
||||
"\n",
|
||||
"- 🧠 **Injects relevant memories** from your Memora system into conversations\n",
|
||||
"- 💾 **Stores conversation history** to Memora for future retrieval \n",
|
||||
"- 🔄 **Works seamlessly** with existing OpenAI code (just change the import)\n",
|
||||
"- ⚡ **Supports both sync and async** clients\n",
|
||||
"\n",
|
||||
"## Why Use It?\n",
|
||||
"\n",
|
||||
"### The Problem\n",
|
||||
"\n",
|
||||
"AI assistants typically have no memory of previous conversations. Each interaction starts fresh, requiring you to:\n",
|
||||
"- Repeat context manually\n",
|
||||
"- Copy-paste relevant information\n",
|
||||
"- Build custom RAG pipelines\n",
|
||||
"- Manage conversation history yourself\n",
|
||||
"\n",
|
||||
"### The Solution\n",
|
||||
"\n",
|
||||
"Memora-OpenAI gives your AI **automatic long-term memory**:\n",
|
||||
"- Remembers past conversations\n",
|
||||
"- Recalls user preferences and facts\n",
|
||||
"- Maintains context across sessions\n",
|
||||
"- Zero code changes to your existing OpenAI usage\n",
|
||||
"\n",
|
||||
"## Prerequisites\n",
|
||||
"\n",
|
||||
"1. **Memora API server running** (see main Memora README)\n",
|
||||
"2. **OpenAI API key** or compatible API (Groq, OpenRouter, etc.)\n",
|
||||
"3. **Python >= 3.10**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Installation\n",
|
||||
"\n",
|
||||
"```bash\n",
|
||||
"cd memora-openai\n",
|
||||
"uv pip install -e .\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Setup\n",
|
||||
"\n",
|
||||
"First, let's set up our environment and configure Memora integration:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import os\n",
|
||||
"from memora_openai import configure, OpenAI\n",
|
||||
"\n",
|
||||
"# Set your API keys\n",
|
||||
"# Option 1: Use Groq (fast and free)\n",
|
||||
"GROQ_API_KEY = os.getenv(\"GROQ_API_KEY\", \"your-groq-api-key\")\n",
|
||||
"\n",
|
||||
"# Option 2: Use OpenAI\n",
|
||||
"# OPENAI_API_KEY = os.getenv(\"OPENAI_API_KEY\", \"sk-...\")\n",
|
||||
"\n",
|
||||
"# Configure Memora integration\n",
|
||||
"configure(\n",
|
||||
" memora_api_url=\"http://localhost:8000\", # Your Memora API server\n",
|
||||
" agent_id=\"tutorial-user\", # Unique ID for this user/agent\n",
|
||||
" store_conversations=True, # Auto-save conversations\n",
|
||||
" inject_memories=True, # Auto-inject relevant context\n",
|
||||
" memory_search_budget=10, # Number of memories to retrieve\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"✓ Memora configured successfully!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example 1: Basic Usage (No Changes Needed!)\n",
|
||||
"\n",
|
||||
"Use the OpenAI client exactly as you normally would. Memora works transparently in the background."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Create client (using Groq's OpenAI-compatible API)\n",
|
||||
"client = OpenAI(\n",
|
||||
" api_key=GROQ_API_KEY,\n",
|
||||
" base_url=\"https://api.groq.com/openai/v1\",\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# First conversation - establish some facts\n",
|
||||
"print(\"=== First Conversation ===\")\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"My name is Alice and I love Python programming!\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"print(\"→ This conversation is now stored in Memora!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example 2: Memory Injection in Action\n",
|
||||
"\n",
|
||||
"Now ask a question that requires remembering the previous conversation:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(\"=== Second Conversation (with memory) ===\")\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"What's my name and what do I like?\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"print(\"→ Memora automatically injected relevant memories before this request!\")\n",
|
||||
"print(\"→ The AI knew your name and preferences without you repeating them.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## How It Works\n",
|
||||
"\n",
|
||||
"Behind the scenes, Memora-OpenAI:\n",
|
||||
"\n",
|
||||
"### 1. **Memory Storage**\n",
|
||||
"After each API call:\n",
|
||||
"- Captures the full conversation context\n",
|
||||
"- Stores it in Memora's semantic memory system\n",
|
||||
"- Indexes it for fast retrieval\n",
|
||||
"\n",
|
||||
"### 2. **Memory Injection**\n",
|
||||
"Before each API call:\n",
|
||||
"- Extracts the user's query\n",
|
||||
"- Searches Memora for relevant past conversations\n",
|
||||
"- Injects top memories as a system message\n",
|
||||
"\n",
|
||||
"### What Gets Sent to OpenAI\n",
|
||||
"\n",
|
||||
"Without Memora:\n",
|
||||
"```python\n",
|
||||
"messages = [\n",
|
||||
" {\"role\": \"user\", \"content\": \"What's my name?\"}\n",
|
||||
"]\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"With Memora (automatic):\n",
|
||||
"```python\n",
|
||||
"messages = [\n",
|
||||
" {\n",
|
||||
" \"role\": \"system\",\n",
|
||||
" \"content\": \"Relevant context from your memory:\\n\\n1. User's name is Alice\\n (Date: 2024-11-18)\\n (Type: world)\"\n",
|
||||
" },\n",
|
||||
" {\"role\": \"user\", \"content\": \"What's my name?\"}\n",
|
||||
"]\n",
|
||||
"```"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example 3: Multi-Turn Conversations\n",
|
||||
"\n",
|
||||
"Build up context over multiple interactions:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Conversation 1: Share a preference\n",
|
||||
"print(\"=== Conversation 1: Sharing preferences ===\")\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"I'm working on a machine learning project using PyTorch.\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"\n",
|
||||
"# Conversation 2: Different topic\n",
|
||||
"print(\"=== Conversation 2: Different topic ===\")\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"I prefer functional programming over OOP.\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"\n",
|
||||
"# Conversation 3: Ask for recommendations\n",
|
||||
"print(\"=== Conversation 3: Getting personalized advice ===\")\n",
|
||||
"response = client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[\n",
|
||||
" {\"role\": \"user\", \"content\": \"Can you recommend a good book for me based on what you know?\"}\n",
|
||||
" ],\n",
|
||||
")\n",
|
||||
"print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"print(\"→ The AI used your programming interests and preferences to make recommendations!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example 4: Document Grouping\n",
|
||||
"\n",
|
||||
"Group related conversations using `document_id`:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from memora_openai import configure\n",
|
||||
"\n",
|
||||
"# Configure with document ID for a specific project\n",
|
||||
"configure(\n",
|
||||
" memora_api_url=\"http://localhost:8000\",\n",
|
||||
" agent_id=\"tutorial-user\",\n",
|
||||
" document_id=\"ml-project-2024\", # All conversations tagged with this ID\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# All these conversations will be grouped together\n",
|
||||
"conversations = [\n",
|
||||
" \"I'm using ResNet for image classification\",\n",
|
||||
" \"My dataset has 10,000 images\",\n",
|
||||
" \"Training accuracy is stuck at 65%\",\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for msg in conversations:\n",
|
||||
" response = client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[{\"role\": \"user\", \"content\": msg}],\n",
|
||||
" )\n",
|
||||
" print(f\"User: {msg}\")\n",
|
||||
" print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"\n",
|
||||
"print(\"→ All these conversations are grouped under document 'ml-project-2024'\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Example 5: Async Support\n",
|
||||
"\n",
|
||||
"Works perfectly with AsyncOpenAI for high-throughput applications:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from memora_openai import AsyncOpenAI\n",
|
||||
"import asyncio\n",
|
||||
"\n",
|
||||
"async def async_example():\n",
|
||||
" # Create async client\n",
|
||||
" async_client = AsyncOpenAI(\n",
|
||||
" api_key=GROQ_API_KEY,\n",
|
||||
" base_url=\"https://api.groq.com/openai/v1\",\n",
|
||||
" )\n",
|
||||
" \n",
|
||||
" # Store a fact\n",
|
||||
" print(\"=== Storing fact ===\")\n",
|
||||
" response = await async_client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[{\"role\": \"user\", \"content\": \"My favorite color is blue.\"}],\n",
|
||||
" )\n",
|
||||
" print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
" \n",
|
||||
" # Query with memory\n",
|
||||
" print(\"=== Querying with memory ===\")\n",
|
||||
" response = await async_client.chat.completions.create(\n",
|
||||
" model=\"llama-3.1-8b-instant\",\n",
|
||||
" messages=[{\"role\": \"user\", \"content\": \"What's my favorite color?\"}],\n",
|
||||
" )\n",
|
||||
" print(f\"Assistant: {response.choices[0].message.content}\\n\")\n",
|
||||
"\n",
|
||||
"# Run async example\n",
|
||||
"await async_example()\n",
|
||||
"print(\"→ Async operations work seamlessly with Memora!\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Configuration Options\n",
|
||||
"\n",
|
||||
"Fine-tune Memora's behavior:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from memora_openai import configure\n",
|
||||
"\n",
|
||||
"# Full configuration example\n",
|
||||
"configure(\n",
|
||||
" memora_api_url=\"http://localhost:8000\", # Memora API URL\n",
|
||||
" agent_id=\"my-agent\", # Agent identifier (required)\n",
|
||||
" api_key=None, # Optional Memora API key\n",
|
||||
" \n",
|
||||
" # Features\n",
|
||||
" store_conversations=True, # Store conversations automatically\n",
|
||||
" inject_memories=True, # Inject memories automatically\n",
|
||||
" \n",
|
||||
" # Memory retrieval\n",
|
||||
" memory_search_budget=10, # Number of memories to retrieve\n",
|
||||
" \n",
|
||||
" # Context management\n",
|
||||
" context_window=10, # Recent conversation turns to store\n",
|
||||
" \n",
|
||||
" # Organization\n",
|
||||
" document_id=\"session-123\", # Optional document grouping\n",
|
||||
" event_timestamp=None, # Optional custom timestamp\n",
|
||||
" \n",
|
||||
" # Control\n",
|
||||
" enabled=True, # Master on/off switch\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"print(\"Configuration options explained:\")\n",
|
||||
"print(\"- memory_search_budget: Higher = more context, but more tokens\")\n",
|
||||
"print(\"- context_window: How many recent messages to include when storing\")\n",
|
||||
"print(\"- document_id: Group related conversations together\")\n",
|
||||
"print(\"- enabled=False: Disable Memora without changing code\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Use Cases\n",
|
||||
"\n",
|
||||
"### 1. **Personal AI Assistant**\n",
|
||||
"- Remembers your preferences, work history, and interests\n",
|
||||
"- Provides personalized recommendations\n",
|
||||
"- Maintains context across days/weeks\n",
|
||||
"\n",
|
||||
"### 2. **Customer Support Chatbot**\n",
|
||||
"- Recalls previous support tickets\n",
|
||||
"- Knows customer preferences and history\n",
|
||||
"- Provides consistent, context-aware responses\n",
|
||||
"\n",
|
||||
"### 3. **Research Assistant**\n",
|
||||
"- Remembers documents you've discussed\n",
|
||||
"- Connects related topics from different sessions\n",
|
||||
"- Builds knowledge over time\n",
|
||||
"\n",
|
||||
"### 4. **Code Review Tool**\n",
|
||||
"- Remembers project architecture decisions\n",
|
||||
"- Recalls past code review comments\n",
|
||||
"- Maintains consistency across reviews\n",
|
||||
"\n",
|
||||
"## Benefits Summary\n",
|
||||
"\n",
|
||||
"✅ **Zero Code Changes** - Drop-in replacement for OpenAI client \n",
|
||||
"✅ **Automatic Context** - No manual RAG pipeline needed \n",
|
||||
"✅ **Long-term Memory** - Conversations persist across sessions \n",
|
||||
"✅ **Smart Retrieval** - Semantic search finds relevant context \n",
|
||||
"✅ **Both Sync/Async** - Works with any OpenAI client pattern \n",
|
||||
"✅ **Configurable** - Fine-tune behavior to your needs \n",
|
||||
"✅ **Transparent** - Original OpenAI responses unchanged \n",
|
||||
"\n",
|
||||
"## Next Steps\n",
|
||||
"\n",
|
||||
"- **Explore Memora API**: Check out `memora/README.md` for advanced features\n",
|
||||
"- **Customize Search**: Tune `memory_search_budget` for your use case\n",
|
||||
"- **Use Document IDs**: Organize conversations by project/session\n",
|
||||
"- **Try Different Models**: Works with OpenAI, Groq, Ollama, and more\n",
|
||||
"\n",
|
||||
"## Resources\n",
|
||||
"\n",
|
||||
"- [Memora Main README](../README.md) - Core memory system docs\n",
|
||||
"- [Memora-OpenAI README](README.md) - Package documentation\n",
|
||||
"- [OpenAI API Docs](https://platform.openai.com/docs/api-reference) - Original API reference\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.0"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
614
memora-openai/uv.lock
Normal file
614
memora-openai/uv.lock
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
version = 1
|
||||
revision = 1
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.11.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "idna" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "25.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backports-asyncio-runner"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.11.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distro"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiter"
|
||||
version = "0.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/91/13cb9505f7be74a933f37da3af22e029f6ba64f5669416cb8b2774bc9682/jiter-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7acbaba9703d5de82a2c98ae6a0f59ab9770ab5af5fa35e43a303aee962cf65", size = 316652 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/76/4e9185e5d9bb4e482cf6dec6410d5f78dfeb374cfcecbbe9888d07c52daa/jiter-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:364f1a7294c91281260364222f535bc427f56d4de1d8ffd718162d21fbbd602e", size = 319829 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/af/727de50995d3a153138139f259baae2379d8cb0522c0c00419957bc478a6/jiter-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ee4d25805d4fb23f0a5167a962ef8e002dbfb29c0989378488e32cf2744b62", size = 350568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c1/d6e9f4b7a3d5ac63bcbdfddeb50b2dcfbdc512c86cffc008584fdc350233/jiter-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:796f466b7942107eb889c08433b6e31b9a7ed31daceaecf8af1be26fb26c0ca8", size = 369052 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/be/00824cd530f30ed73fa8a4f9f3890a705519e31ccb9e929f1e22062e7c76/jiter-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:35506cb71f47dba416694e67af996bbdefb8e3608f1f78799c2e1f9058b01ceb", size = 481585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/b6/2ad7990dff9504d4b5052eef64aa9574bd03d722dc7edced97aad0d47be7/jiter-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:726c764a90c9218ec9e4f99a33d6bf5ec169163f2ca0fc21b654e88c2abc0abc", size = 380541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/c7/f3c26ecbc1adbf1db0d6bba99192143d8fe8504729d9594542ecc4445784/jiter-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa47810c5565274810b726b0dc86d18dce5fd17b190ebdc3890851d7b2a0e74", size = 364423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/51/eac547bf3a2d7f7e556927278e14c56a0604b8cddae75815d5739f65f81d/jiter-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ec0259d3f26c62aed4d73b198c53e316ae11f0f69c8fbe6682c6dcfa0fcce2", size = 389958 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/1f/9ca592e67175f2db156cff035e0d817d6004e293ee0c1d73692d38fcb596/jiter-0.12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:79307d74ea83465b0152fa23e5e297149506435535282f979f18b9033c0bb025", size = 522084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/ff/597d9cdc3028f28224f53e1a9d063628e28b7a5601433e3196edda578cdd/jiter-0.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cf6e6dd18927121fec86739f1a8906944703941d000f0639f3eb6281cc601dca", size = 513054 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/6d/1970bce1351bd02e3afcc5f49e4f7ef3dabd7fb688f42be7e8091a5b809a/jiter-0.12.0-cp310-cp310-win32.whl", hash = "sha256:b6ae2aec8217327d872cbfb2c1694489057b9433afce447955763e6ab015b4c4", size = 206368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/6b/eb1eb505b2d86709b59ec06681a2b14a94d0941db091f044b9f0e16badc0/jiter-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7f49ce90a71e44f7e1aa9e7ec415b9686bbc6a5961e57eab511015e6759bc11", size = 204847 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memora-client"
|
||||
version = "0.0.7"
|
||||
source = { editable = "../memora-clients/python" }
|
||||
dependencies = [
|
||||
{ name = "attrs" },
|
||||
{ name = "httpx" },
|
||||
{ name = "python-dateutil" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "attrs", specifier = ">=22.2.0" },
|
||||
{ name = "httpx", specifier = ">=0.23.0,<0.29.0" },
|
||||
{ name = "python-dateutil", specifier = ">=2.8.0,<3" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memora-openai"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "memora-client" },
|
||||
{ name = "openai" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-mock" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "memora-client", editable = "../memora-clients/python" },
|
||||
{ name = "openai", specifier = ">=1.0.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
|
||||
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.10.0" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.8.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d5/e4/42591e356f1d53c568418dc7e30dcda7be31dd5a4d570bca22acb0525862/openai-2.8.1.tar.gz", hash = "sha256:cb1b79eef6e809f6da326a7ef6038719e35aa944c42d081807bfa1be8060f15f", size = 602490 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/4f/dbc0c124c40cb390508a82770fb9f6e3ed162560181a85089191a851c59a/openai-2.8.1-py3-none-any.whl", hash = "sha256:c6c3b5a04994734386e8dad3c00a393f56d3b68a27cd2e8acae91a59e4122463", size = 1022688 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
|
||||
{ name = "pytest" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
version = "3.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sniffio"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tqdm"
|
||||
version = "4.67.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 },
|
||||
]
|
||||
Loading…
Reference in a new issue