Typing (#137)
* type stuff * add action * bump python * Refactor type annotations and update tracking decorators in agent and dependencies modules. Replace ai_track with track from src.utils.types, and enhance type hints for better clarity. Update pyproject.toml to allow untyped libraries. * type everything basically * fix migration typing * type like crazy * remove usless tests * Update mocks in tests to use AsyncMock for dialectic_call and dialectic_stream, ensuring proper async behavior in test cases. Adjust mock return values for consistency and clarity. * Update src/deriver/tom/single_prompt.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update src/deriver/tom/long_term.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Enhance CLAUDE.md documentation with additional details on core concepts, API structure, and development commands. Update command syntax for running server and tests to use 'uv run' for consistency. Improve clarity in configuration and architectural decisions sections. * Refactor type annotations in CRUD functions to accept more flexible filter types, changing from dict[str, str] to dict[str, Any]. Clean up logging in agent.py by removing unnecessary timing logs for user representation generation and query execution. * Remove unused import of ai_track from long_term.py and single_prompt.py to clean up the codebase. * pass tests * update some stuff * fix unused * ruff * make stuff work again * Add LLM_GROQ_API_KEY to GitHub Actions and format tom_inference parameters * test * test * Refactor LLM settings to use 'gemini' provider and update related model parameters; remove unused API keys from GitHub Actions workflow. * Update LLM settings to use 'anthropic' provider and change model to 'claude-3-5-haiku-20241022'; maintain existing summarization provider. * test * llm provider stuff * update * revert * Integrate client management for LLM providers across various modules; remove deprecated environment variable setup for API keys. * only if key avaialble * Refactor type hints and improve schema definitions for queue processing; remove unused imports and enhance function signatures for clarity. * fix test * model * test * Update LLM provider type annotations and enhance client management; replace Provider with Providers for better type handling in config and clients modules. * Refactor LLM provider handling to default to "openai" for custom providers across multiple modules; update type annotations and improve client management for consistency. --------- Co-authored-by: Dani Balcells <18307962+danibalcells@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
parent
80652d4395
commit
24bf8eeeb4
|
|
@ -74,7 +74,7 @@ LLM_ANTHROPIC_API_KEY=your-anthropic-api-key-here
|
|||
# LLM_QUERY_GENERATION_MODEL=llama-3.1-8b-instant
|
||||
|
||||
# Summarization LLM settings
|
||||
# LLM_SUMMARY_PROVIDER=gemini
|
||||
# LLM_SUMMARY_PROVIDER=google
|
||||
# LLM_SUMMARY_MODEL=gemini-2.0-flash-lite
|
||||
# LLM_SUMMARY_MAX_TOKENS_SHORT=1000
|
||||
# LLM_SUMMARY_MAX_TOKENS_LONG=2000
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
name: Static Analysis
|
||||
on: [push]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
basedpyright:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: "pyproject.toml"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v2
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
- name: Install the project
|
||||
run: uv sync --all-extras --dev
|
||||
- name: run basedpyright
|
||||
run: uv run basedpyright
|
||||
|
|
@ -51,3 +51,11 @@ jobs:
|
|||
SENTRY_ENABLED: false
|
||||
LLM_OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
LLM_ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
LLM_DIALECTIC_PROVIDER: openai
|
||||
LLM_DIALECTIC_MODEL: test
|
||||
LLM_QUERY_GENERATION_PROVIDER: openai
|
||||
LLM_QUERY_GENERATION_MODEL: test
|
||||
LLM_TOM_INFERENCE_PROVIDER: openai
|
||||
LLM_TOM_INFERENCE_MODEL: test
|
||||
LLM_SUMMARY_PROVIDER: openai
|
||||
LLM_SUMMARY_MODEL: test
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "start",
|
||||
"label": "sync",
|
||||
"type": "shell",
|
||||
"command": "uv sync && uv run fastapi dev src/main.py",
|
||||
"command": "uv sync",
|
||||
"group": "none",
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
|
|
|
|||
11
CLAUDE.md
11
CLAUDE.md
|
|
@ -80,11 +80,12 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
|
|||
### Commands
|
||||
|
||||
- Setup: `uv sync`
|
||||
- Run server: `fastapi dev src/main.py`
|
||||
- Run tests: `pytest tests/`
|
||||
- Run single test: `pytest tests/path/to/test_file.py::test_function`
|
||||
- Linting: `ruff check src/`
|
||||
- Format code: `ruff format src/`
|
||||
- Run server: `uv run fastapi dev src/main.py`
|
||||
- Run tests: `uv run pytest tests/`
|
||||
- Run single test: `uv run pytest tests/path/to/test_file.py::test_function`
|
||||
- Linting: `uv run ruff check src/`
|
||||
- Typechecking: `uv run basedpyright`
|
||||
- Format code: `uv run ruff format src/`
|
||||
|
||||
### Code Style
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ QUERY_GENERATION_PROVIDER = "groq"
|
|||
QUERY_GENERATION_MODEL = "llama-3.1-8b-instant"
|
||||
|
||||
# Summarization specific
|
||||
SUMMARY_PROVIDER = "gemini"
|
||||
SUMMARY_PROVIDER = "google"
|
||||
SUMMARY_MODEL = "gemini-2.0-flash-lite"
|
||||
SUMMARY_MAX_TOKENS_SHORT = 1000
|
||||
SUMMARY_MAX_TOKENS_LONG = 2000
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ FastAPI routes should be simple, letting global exception handlers do most of th
|
|||
|
||||
```python
|
||||
@router.get("/{user_id}")
|
||||
async def get_user(app_id: str, user_id: str, db=db):
|
||||
async def get_user(app_id: str, user_id: str, db: AsyncSession = db):
|
||||
# ResourceNotFoundException will be caught by global handler
|
||||
return await crud.get_user(db, app_id, user_id)
|
||||
```
|
||||
|
|
@ -88,7 +88,7 @@ For specific handling, use try/except only when necessary:
|
|||
|
||||
```python
|
||||
@router.post("")
|
||||
async def create_user(app_id: str, user: schemas.UserCreate, db=db):
|
||||
async def create_user(app_id: str, user: schemas.UserCreate, db: AsyncSession = db):
|
||||
try:
|
||||
return await crud.create_user(db, app_id, user)
|
||||
except ConflictException:
|
||||
|
|
@ -197,7 +197,7 @@ async def get_document(
|
|||
user_id: str,
|
||||
collection_id: str,
|
||||
document_id: str,
|
||||
db=db
|
||||
db: AsyncSession = db
|
||||
):
|
||||
"""Get a document by ID"""
|
||||
# ResourceNotFoundException will be caught by global handler
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
from os import getenv
|
||||
from typing import Optional
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
|
@ -11,7 +9,7 @@ def get_schema() -> str:
|
|||
return settings.DB.SCHEMA
|
||||
|
||||
|
||||
def table_exists(table_name: str, inspector: Optional[sa.Inspector] = None) -> bool:
|
||||
def table_exists(table_name: str, inspector: sa.Inspector | None = None) -> bool:
|
||||
"""Check if a table exists in the database."""
|
||||
if inspector is None:
|
||||
inspector = sa.inspect(op.get_bind())
|
||||
|
|
@ -20,7 +18,7 @@ def table_exists(table_name: str, inspector: Optional[sa.Inspector] = None) -> b
|
|||
|
||||
|
||||
def column_exists(
|
||||
table_name: str, column_name: str, inspector: Optional[sa.Inspector] = None
|
||||
table_name: str, column_name: str, inspector: sa.Inspector | None = None
|
||||
) -> bool:
|
||||
"""Check if a column exists in a table."""
|
||||
if inspector is None:
|
||||
|
|
@ -33,7 +31,7 @@ def column_exists(
|
|||
|
||||
|
||||
def fk_exists(
|
||||
table_name: str, fk_name: str, inspector: Optional[sa.Inspector] = None
|
||||
table_name: str, fk_name: str, inspector: sa.Inspector | None = None
|
||||
) -> bool:
|
||||
"""Check if a foreign key exists in a table."""
|
||||
if inspector is None:
|
||||
|
|
@ -44,7 +42,7 @@ def fk_exists(
|
|||
|
||||
|
||||
def index_exists(
|
||||
table_name: str, index_name: str, inspector: Optional[sa.Inspector] = None
|
||||
table_name: str, index_name: str, inspector: sa.Inspector | None = None
|
||||
) -> bool:
|
||||
"""Check if an index exists in a table."""
|
||||
if inspector is None:
|
||||
|
|
@ -58,7 +56,7 @@ def constraint_exists(
|
|||
table_name: str,
|
||||
constraint_name: str,
|
||||
type: str,
|
||||
inspector: Optional[sa.Inspector] = None,
|
||||
inspector: sa.Inspector | None = None,
|
||||
) -> bool:
|
||||
"""Check if a constraint exists in a table."""
|
||||
if inspector is None:
|
||||
|
|
|
|||
|
|
@ -7,16 +7,15 @@ Create Date: 2025-05-13 16:11:54.859842
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "20f89a421aff"
|
||||
down_revision: Union[str, None] = "556a16564f50"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "556a16564f50"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ Create Date: 2025-05-13 17:10:33.805495
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
|
@ -22,9 +21,9 @@ from migrations.utils import (
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "556a16564f50"
|
||||
down_revision: Union[str, None] = "b765d82110bd"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "b765d82110bd"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
|
|
|||
|
|
@ -7,19 +7,17 @@ Create Date: 2025-05-19 17:00:18.151735
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
from os import getenv
|
||||
|
||||
from alembic import op
|
||||
from sqlalchemy import text
|
||||
|
||||
from src.config import settings
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "66e63cf2cf77"
|
||||
down_revision: Union[str, None] = "20f89a421aff"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
schema = settings.DB.SCHEMA
|
||||
down_revision: str | None = "20f89a421aff"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
schema = getenv("DATABASE_SCHEMA", "public")
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ Create Date: 2024-01-01 00:00:00.000000
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
|
@ -18,9 +17,9 @@ from src.config import settings
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "a1b2c3d4e5f6"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
|
@ -360,7 +359,7 @@ def upgrade() -> None:
|
|||
server_default="{}",
|
||||
),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("embedding", Vector(1536), nullable=True),
|
||||
sa.Column("embedding", Vector(1536), nullable=True), # pyright: ignore
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
|
|
@ -464,4 +463,3 @@ def downgrade() -> None:
|
|||
op.drop_table("sessions", schema=schema)
|
||||
op.drop_table("users", schema=schema)
|
||||
op.drop_table("apps", schema=schema)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ Create Date: 2025-04-03 15:32:16.733312
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
|
@ -18,9 +17,9 @@ from src.config import settings
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "b765d82110bd"
|
||||
down_revision: Union[str, None] = "c3828084f472"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "c3828084f472"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
|
@ -99,13 +98,15 @@ def upgrade() -> None:
|
|||
|
||||
# 4. Data migration: Fill session_id from messages
|
||||
try:
|
||||
op.execute("""
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE metamessages m
|
||||
SET session_id = msg.session_id
|
||||
FROM messages msg
|
||||
WHERE m.message_id = msg.public_id
|
||||
AND m.session_id IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
print("Updated session_id values from messages")
|
||||
except Exception as e:
|
||||
print(f"Error updating session_id values: {e}")
|
||||
|
|
@ -122,23 +123,27 @@ def upgrade() -> None:
|
|||
print(
|
||||
f"Found {orphaned_count} orphaned records. Setting message_id to NULL."
|
||||
)
|
||||
op.execute("""
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE metamessages
|
||||
SET message_id = NULL
|
||||
WHERE message_id IS NOT NULL AND session_id IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Error handling orphaned records: {e}")
|
||||
|
||||
# 6. Data migration: Fill user_id from session's user
|
||||
try:
|
||||
op.execute("""
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE metamessages m
|
||||
SET user_id = s.user_id
|
||||
FROM sessions s
|
||||
WHERE m.session_id = s.public_id
|
||||
AND m.user_id IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
print("Updated user_id values from sessions")
|
||||
except Exception as e:
|
||||
print(f"Error updating user_id values from sessions: {e}")
|
||||
|
|
@ -152,10 +157,15 @@ def upgrade() -> None:
|
|||
}
|
||||
|
||||
# Helper function to create index if not exists
|
||||
def create_index_if_not_exists(index_name, table_name, columns, **kwargs):
|
||||
def create_index_if_not_exists(
|
||||
index_name: str,
|
||||
table_name: str,
|
||||
columns: list[str | sa.TextClause],
|
||||
schema: str,
|
||||
):
|
||||
if index_name not in existing_indices:
|
||||
try:
|
||||
op.create_index(index_name, table_name, columns, **kwargs)
|
||||
op.create_index(index_name, table_name, columns, schema=schema)
|
||||
print(f"Created index {index_name}")
|
||||
except Exception as e:
|
||||
print(f"Error creating index {index_name}: {e}")
|
||||
|
|
@ -191,11 +201,13 @@ def upgrade() -> None:
|
|||
if constraint_violations and constraint_violations > 0:
|
||||
print(f"WARNING: {constraint_violations} records would violate the constraint!")
|
||||
print("Setting these message_ids to NULL to prevent constraint violation")
|
||||
op.execute("""
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE metamessages
|
||||
SET message_id = NULL
|
||||
WHERE message_id IS NOT NULL AND session_id IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# 9. Add the check constraint for message_id and session_id relationship
|
||||
try:
|
||||
|
|
@ -230,7 +242,8 @@ def upgrade() -> None:
|
|||
if null_user_count and null_user_count > 0:
|
||||
print(f"WARNING: {null_user_count} records have NULL user_id!")
|
||||
# Try to derive user_id from message for these orphaned records
|
||||
op.execute("""
|
||||
op.execute(
|
||||
"""
|
||||
UPDATE metamessages m
|
||||
SET user_id = u.public_id
|
||||
FROM messages msg
|
||||
|
|
@ -238,7 +251,8 @@ def upgrade() -> None:
|
|||
JOIN users u ON s.user_id = u.public_id
|
||||
WHERE m.message_id = msg.public_id
|
||||
AND m.user_id IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Check again after fixes
|
||||
null_user_count = conn.execute(
|
||||
|
|
@ -321,9 +335,11 @@ def downgrade() -> None:
|
|||
|
||||
# 4. Make message_id required again and clean up data if needed
|
||||
try:
|
||||
op.execute("""
|
||||
op.execute(
|
||||
"""
|
||||
DELETE FROM metamessages WHERE message_id IS NULL
|
||||
""")
|
||||
"""
|
||||
)
|
||||
op.alter_column(
|
||||
"metamessages",
|
||||
"message_id",
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ Create Date: 2024-12-12 13:41:40.156095
|
|||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from os import getenv
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
|
@ -17,9 +15,9 @@ from src.config import settings
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "c3828084f472"
|
||||
down_revision: Union[str, None] = "a1b2c3d4e5f6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "a1b2c3d4e5f6"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ Create Date: 2025-06-09 15:16:38.164067
|
|||
from collections.abc import Sequence
|
||||
from contextlib import suppress
|
||||
from os import getenv
|
||||
from typing import Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
import tiktoken
|
||||
|
|
@ -28,9 +27,9 @@ from migrations.utils import (
|
|||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "d429de0e5338"
|
||||
down_revision: Union[str, None] = "66e63cf2cf77"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
down_revision: str | None = "66e63cf2cf77"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ authors = [
|
|||
{name = "Plastic Labs", email = "hello@plasticlabs.ai"},
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.111.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
|
|
@ -18,16 +18,14 @@ dependencies = [
|
|||
"psycopg[binary]>=3.1.19",
|
||||
"httpx>=0.27.0",
|
||||
"rich>=13.7.1",
|
||||
"openai>=1.43.0",
|
||||
"anthropic>=0.36.0",
|
||||
"nanoid>=2.0.0",
|
||||
"alembic>=1.14.0",
|
||||
"langfuse>=2.57.1,<3.0.0",
|
||||
"pyjwt>=2.10.0",
|
||||
"google-genai>=1.10.0",
|
||||
"pydantic-settings>=2.9.1",
|
||||
"tomli>=2.0.0; python_version < '3.11'",
|
||||
"tiktoken>=0.9.0",
|
||||
"mirascope[anthropic,google,groq,langfuse]>=1.25.1",
|
||||
"openai>=1.91.0",
|
||||
"pydantic>=2.11.7",
|
||||
"pydantic-settings>=2.10.1",
|
||||
]
|
||||
[tool.uv]
|
||||
dev-dependencies = [
|
||||
|
|
@ -38,6 +36,7 @@ dev-dependencies = [
|
|||
"interrogate>=1.7.0",
|
||||
"py-spy>=0.3.14",
|
||||
"ruff>=0.11.2",
|
||||
"basedpyright>=1.29.4",
|
||||
]
|
||||
|
||||
[tool.ruff.lint]
|
||||
|
|
@ -64,3 +63,37 @@ extend-immutable-calls = ["fastapi.Depends"]
|
|||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "session"
|
||||
|
||||
|
||||
[tool.basedpyright]
|
||||
# BasedPyright currently seems like the best type checker option, much faster
|
||||
# than mypy and with a good extension for VSCode/Cursor.
|
||||
# https://marketplace.visualstudio.com/items?itemName=detachhead.basedpyright
|
||||
# https://docs.basedpyright.com/latest/configuration/config-files/#sample-pyprojecttoml-file
|
||||
include = ["src", "tests"]
|
||||
exclude = ["tests/**/disabled*.py"]
|
||||
# By default BasedPyright is very strict, so you almost certainly want to disable
|
||||
# some of the rules.
|
||||
# First, these turn off warnings about (yes) how you ignore warnings:
|
||||
reportIgnoreCommentWithoutRule = false
|
||||
# reportUnnecessaryTypeIgnoreComment = false
|
||||
# A few typically noisy warnings are next.
|
||||
# How many you enable is up to you. The first few are off by default, but you can
|
||||
# comment/uncomment these as desired:
|
||||
reportMissingTypeStubs = false
|
||||
reportUnusedCallResult = false
|
||||
reportCallInDefaultInitializer = false
|
||||
reportAny = false
|
||||
reportExplicitAny = false
|
||||
allowedUntypedLibraries = ["langfuse", "langfuse.decorators", "mirascope"]
|
||||
reportImplicitOverride = false
|
||||
# reportImplicitStringConcatenation = false
|
||||
# reportUnreachable = false
|
||||
# reportPrivateImportUsage = false
|
||||
# reportPrivateLocalImportUsage = false
|
||||
# reportMissingImports = false
|
||||
# reportUnnecessaryIsInstance = false
|
||||
# reportUnknownVariableType = false
|
||||
# reportUnknownArgumentType = false
|
||||
# reportUnknownParameterType = false
|
||||
# reportUnknownMemberType = false
|
||||
300
src/agent.py
300
src/agent.py
|
|
@ -1,127 +1,103 @@
|
|||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
|
||||
import sentry_sdk
|
||||
from anthropic import MessageStreamManager
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
from sentry_sdk.ai.monitoring import ai_track
|
||||
from langfuse.decorators import langfuse_context, observe # pyright: ignore
|
||||
from mirascope import llm
|
||||
from mirascope.integrations.langfuse import with_langfuse
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, models, schemas
|
||||
from src import crud, models
|
||||
from src.config import settings
|
||||
from src.dependencies import tracked_db
|
||||
from src.deriver.tom import get_tom_inference
|
||||
from src.deriver.tom.embeddings import CollectionEmbeddingStore
|
||||
from src.deriver.tom.long_term import get_user_representation_long_term
|
||||
from src.deriver.tom.single_prompt import UserRepresentationOutput
|
||||
from src.utils import history, parse_xml_content
|
||||
from src.utils.model_client import ModelClient, ModelProvider
|
||||
from src.utils.clients import clients
|
||||
from src.utils.types import track
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
QUERY_GENERATION_SYSTEM = """Given this query about a user, generate 3 focused search queries that would help retrieve relevant facts about the user.
|
||||
Each query should focus on a specific aspect related to the original query, rephrased to maximize semantic search effectiveness.
|
||||
For example, if the original query asks "what does the user like to eat?", generated queries might include "user's food preferences", "user's favorite cuisine", etc.
|
||||
|
||||
Format your response as a JSON array of strings, with each string being a search query.
|
||||
Respond only in valid JSON, without markdown formatting or quotes, and nothing else.
|
||||
Example:
|
||||
["query about interests", "query about personality", "query about experiences"]"""
|
||||
|
||||
@track("Dialectic Call")
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.DIALECTIC_PROVIDER
|
||||
if settings.LLM.DIALECTIC_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.DIALECTIC_MODEL,
|
||||
client=clients[settings.LLM.DIALECTIC_PROVIDER],
|
||||
)
|
||||
async def dialectic_call(
|
||||
query: str, working_representation: str, additional_context: str
|
||||
):
|
||||
return f"""
|
||||
You are operating as a context service that helps maintain psychological understanding of users across applications. Alongside a query, you'll receive: 1) previously collected psychological context about the user that I've maintained, 2) a series of long-term facts about the user, and 3) their current conversation/interaction from the requesting application. Your goal is to analyze this information and provide theory-of-mind insights that help applications personalize their responses. Please respond in a brief, matter-of-fact, and appropriate manner to convey as much relevant information to the application based on its query and the user's most recent message. You are encouraged to provide any context from the provided resources that helps provide a more complete or nuanced understanding of the user, as long as it is somewhat relevant to the query. If the context provided doesn't help address the query, write absolutely NOTHING but "None".
|
||||
|
||||
<query>{query}</query>
|
||||
<context>{working_representation}</context>
|
||||
<conversation_history>{additional_context}</conversation_history>
|
||||
"""
|
||||
|
||||
|
||||
class Dialectic:
|
||||
def __init__(self, agent_input: str, user_representation: str, chat_history: str):
|
||||
self.agent_input = agent_input
|
||||
self.user_representation = user_representation
|
||||
self.chat_history = chat_history
|
||||
self.client = ModelClient(
|
||||
provider=ModelProvider(settings.LLM.DIALECTIC_PROVIDER),
|
||||
model=settings.LLM.DIALECTIC_MODEL,
|
||||
)
|
||||
self.system_prompt = """You are operating as a context service that helps maintain psychological understanding of users across applications. Alongside a query, you'll receive: 1) previously collected psychological context about the user that I've maintained, 2) a series of long-term facts about the user, and 3) their current conversation/interaction from the requesting application. Your goal is to analyze this information and provide theory-of-mind insights that help applications personalize their responses. Please respond in a brief, matter-of-fact, and appropriate manner to convey as much relevant information to the application based on its query and the user's most recent message. You are encouraged to provide any context from the provided resources that helps provide a more complete or nuanced understanding of the user, as long as it is somewhat relevant to the query. If the context provided doesn't help address the query, write absolutely NOTHING but "None"."""
|
||||
@track("Dialectic Stream")
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.DIALECTIC_PROVIDER
|
||||
if settings.LLM.DIALECTIC_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.DIALECTIC_MODEL,
|
||||
stream=True,
|
||||
client=clients[settings.LLM.DIALECTIC_PROVIDER],
|
||||
)
|
||||
async def dialectic_stream(
|
||||
query: str, working_representation: str, additional_context: str
|
||||
):
|
||||
return f"""
|
||||
You are operating as a context service that helps maintain psychological understanding of users across applications. Alongside a query, you'll receive: 1) previously collected psychological context about the user that I've maintained, 2) a series of long-term facts about the user, and 3) their current conversation/interaction from the requesting application. Your goal is to analyze this information and provide theory-of-mind insights that help applications personalize their responses. Please respond in a brief, matter-of-fact, and appropriate manner to convey as much relevant information to the application based on its query and the user's most recent message. You are encouraged to provide any context from the provided resources that helps provide a more complete or nuanced understanding of the user, as long as it is somewhat relevant to the query. If the context provided doesn't help address the query, write absolutely NOTHING but "None".
|
||||
|
||||
@ai_track("Dialectic Call")
|
||||
@observe()
|
||||
async def call(self):
|
||||
with sentry_sdk.start_transaction(
|
||||
op="dialectic-inference", name="Dialectic API Response"
|
||||
):
|
||||
logger.debug(
|
||||
f"Starting call() method with query length: {len(self.agent_input)}"
|
||||
)
|
||||
call_start = asyncio.get_event_loop().time()
|
||||
<query>{query}</query>
|
||||
<context>{working_representation}</context>
|
||||
<conversation_history>{additional_context}</conversation_history>
|
||||
"""
|
||||
|
||||
prompt = f"""
|
||||
<query>{self.agent_input}</query>
|
||||
<context>{self.user_representation}</context>
|
||||
<conversation_history>{self.chat_history}</conversation_history>
|
||||
"""
|
||||
logger.debug(
|
||||
f"Prompt constructed with context length: {len(self.user_representation)} chars"
|
||||
)
|
||||
|
||||
# Create a properly formatted message
|
||||
message: dict[str, Any] = {"role": "user", "content": prompt}
|
||||
class SemanticQueries(BaseModel):
|
||||
queries: list[str]
|
||||
|
||||
# Generate the response
|
||||
logger.debug("Calling model for generation")
|
||||
model_start = asyncio.get_event_loop().time()
|
||||
response = await self.client.generate(
|
||||
messages=[message],
|
||||
system=self.system_prompt,
|
||||
max_tokens=settings.LLM.DEFAULT_MAX_TOKENS,
|
||||
)
|
||||
model_time = asyncio.get_event_loop().time() - model_start
|
||||
logger.debug(
|
||||
f"Model response received in {model_time:.2f}s: {len(response)} chars"
|
||||
)
|
||||
|
||||
total_time = asyncio.get_event_loop().time() - call_start
|
||||
logger.debug(f"call() completed in {total_time:.2f}s")
|
||||
return [{"text": response}]
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.QUERY_GENERATION_PROVIDER
|
||||
if settings.LLM.QUERY_GENERATION_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.QUERY_GENERATION_MODEL,
|
||||
response_model=SemanticQueries,
|
||||
client=clients[settings.LLM.QUERY_GENERATION_PROVIDER],
|
||||
)
|
||||
async def generate_semantic_queries_llm(query: str):
|
||||
return f"""
|
||||
Given this query about a user, generate 3 focused search queries that would help retrieve relevant facts about the user. Each query should focus on a specific aspect related to the original query, rephrased to maximize semantic search effectiveness.
|
||||
For example, if the original query asks "what does the user like to eat?", generated queries might include "user's food preferences", "user's favorite cuisine", etc.
|
||||
|
||||
@ai_track("Dialectic Call")
|
||||
@observe()
|
||||
async def stream(self):
|
||||
with sentry_sdk.start_transaction(
|
||||
op="dialectic-inference", name="Dialectic API Response"
|
||||
):
|
||||
logger.debug(
|
||||
f"Starting stream() method with query length: {len(self.agent_input)}"
|
||||
)
|
||||
stream_start = asyncio.get_event_loop().time()
|
||||
Format your response as a JSON array of strings, with each string being a search query.
|
||||
Respond only in valid JSON, without markdown formatting or quotes, and nothing else.
|
||||
Example:
|
||||
["query about interests", "query about personality", "query about experiences"]
|
||||
|
||||
prompt = f"""
|
||||
<query>{self.agent_input}</query>
|
||||
<context>{self.user_representation}</context>
|
||||
<conversation_history>{self.chat_history}</conversation_history>
|
||||
"""
|
||||
logger.debug(
|
||||
f"Prompt constructed with context length: {len(self.user_representation)} chars"
|
||||
)
|
||||
|
||||
# Create a properly formatted message
|
||||
message: dict[str, Any] = {"role": "user", "content": prompt}
|
||||
|
||||
# Stream the response
|
||||
logger.debug("Calling model for streaming")
|
||||
model_start = asyncio.get_event_loop().time()
|
||||
stream = await self.client.stream(
|
||||
messages=[message],
|
||||
system=self.system_prompt,
|
||||
max_tokens=settings.LLM.DEFAULT_MAX_TOKENS,
|
||||
)
|
||||
|
||||
stream_setup_time = asyncio.get_event_loop().time() - model_start
|
||||
logger.debug(f"Stream started in {stream_setup_time:.2f}s")
|
||||
|
||||
total_time = asyncio.get_event_loop().time() - stream_start
|
||||
logger.debug(f"stream() setup completed in {total_time:.2f}s")
|
||||
return stream
|
||||
<query>{query}</query>
|
||||
"""
|
||||
|
||||
|
||||
@observe()
|
||||
|
|
@ -131,7 +107,7 @@ async def chat(
|
|||
session_name: str | None,
|
||||
queries: str | list[str],
|
||||
stream: bool = False,
|
||||
) -> schemas.DialecticResponse | MessageStreamManager:
|
||||
) -> llm.Stream | llm.CallResponse:
|
||||
"""
|
||||
Chat with the Dialectic API using on-demand user representation generation.
|
||||
|
||||
|
|
@ -161,8 +137,6 @@ async def chat(
|
|||
logger.debug(f"Received query: {final_query} for session {session_name}")
|
||||
logger.debug("Starting on-demand user representation generation")
|
||||
|
||||
start_time = asyncio.get_event_loop().time()
|
||||
|
||||
# Setup phase - create resources we'll need for all operations
|
||||
|
||||
# 1. Fetch latest peer message & chat history
|
||||
|
|
@ -237,15 +211,7 @@ async def chat(
|
|||
f"User representation generated: {len(user_representation)} characters"
|
||||
)
|
||||
|
||||
# Create a Dialectic chain with the fresh user representation
|
||||
chain = Dialectic(
|
||||
agent_input=final_query,
|
||||
user_representation=user_representation,
|
||||
chat_history=chat_history,
|
||||
)
|
||||
|
||||
generation_time = asyncio.get_event_loop().time() - start_time
|
||||
logger.debug(f"User representation generation completed in {generation_time:.2f}s")
|
||||
# Call dialectic with enhanced context
|
||||
|
||||
langfuse_context.update_current_trace(
|
||||
session_id=session_name,
|
||||
|
|
@ -256,21 +222,16 @@ async def chat(
|
|||
|
||||
# Use streaming or non-streaming response based on the request
|
||||
logger.debug(f"Calling Dialectic with streaming={stream}")
|
||||
query_start_time = asyncio.get_event_loop().time()
|
||||
if stream:
|
||||
response_stream = await chain.stream()
|
||||
logger.debug(
|
||||
f"Dialectic stream started after {asyncio.get_event_loop().time() - query_start_time:.2f}s"
|
||||
logger.debug("Calling Dialectic with streaming")
|
||||
response = await dialectic_stream(
|
||||
final_query, user_representation, chat_history
|
||||
)
|
||||
return response_stream
|
||||
|
||||
response = await chain.call()
|
||||
query_time = asyncio.get_event_loop().time() - query_start_time
|
||||
total_time = asyncio.get_event_loop().time() - start_time
|
||||
logger.debug(
|
||||
f"Dialectic response received in {query_time:.2f}s (total: {total_time:.2f}s)"
|
||||
)
|
||||
return schemas.DialecticResponse(content=response[0]["text"])
|
||||
return response
|
||||
else:
|
||||
logger.debug("Calling Dialectic with non-streaming")
|
||||
response = await dialectic_call(final_query, user_representation, chat_history)
|
||||
return response
|
||||
|
||||
|
||||
async def get_long_term_facts(
|
||||
|
|
@ -324,7 +285,7 @@ async def get_long_term_facts(
|
|||
all_facts_lists = await asyncio.gather(*query_tasks)
|
||||
|
||||
# Combine all facts into a single set to remove duplicates
|
||||
retrieved_facts = set()
|
||||
retrieved_facts: set[str] = set()
|
||||
for facts in all_facts_lists:
|
||||
retrieved_facts.update(facts)
|
||||
|
||||
|
|
@ -360,7 +321,14 @@ async def run_tom_inference(chat_history: str) -> str:
|
|||
tom_time = asyncio.get_event_loop().time() - tom_start_time
|
||||
|
||||
logger.debug(f"ToM inference completed in {tom_time:.2f}s")
|
||||
prediction = parse_xml_content(tom_inference_response, "prediction")
|
||||
|
||||
# Create a prediction summary from the structured Pydantic object
|
||||
prediction = (
|
||||
f"Current context: {tom_inference_response.current_state.immediate_context}"
|
||||
)
|
||||
if tom_inference_response.tentative_inferences:
|
||||
prediction += f"\nKey inferences: {', '.join([inf.interpretation for inf in tom_inference_response.tentative_inferences[:3]])}"
|
||||
|
||||
logger.debug(f"Prediction length: {len(prediction)} characters")
|
||||
|
||||
return prediction
|
||||
|
|
@ -381,47 +349,9 @@ async def generate_semantic_queries(query: str) -> list[str]:
|
|||
query_start = asyncio.get_event_loop().time()
|
||||
|
||||
logger.debug("Calling LLM for query generation")
|
||||
llm_start = asyncio.get_event_loop().time()
|
||||
|
||||
try:
|
||||
provider = ModelProvider(settings.LLM.QUERY_GENERATION_PROVIDER)
|
||||
except ValueError as e:
|
||||
logger.error(
|
||||
f"Invalid query-generation provider '{settings.LLM.QUERY_GENERATION_PROVIDER}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
# Create a new model client
|
||||
client = ModelClient(
|
||||
provider=provider,
|
||||
model=settings.LLM.QUERY_GENERATION_MODEL,
|
||||
)
|
||||
|
||||
# Prepare the messages for Anthropic
|
||||
messages: list[dict[str, Any]] = [{"role": "user", "content": query}]
|
||||
|
||||
# Generate the response
|
||||
try:
|
||||
result = await client.generate(
|
||||
messages=messages,
|
||||
system=QUERY_GENERATION_SYSTEM,
|
||||
max_tokens=settings.LLM.DEFAULT_MAX_TOKENS,
|
||||
use_caching=True, # Likely not caching because the system prompt is under 1000 tokens
|
||||
)
|
||||
llm_time = asyncio.get_event_loop().time() - llm_start
|
||||
logger.debug(f"LLM response received in {llm_time:.2f}s: {result[:100]}...")
|
||||
|
||||
# Parse the JSON response to get a list of queries
|
||||
try:
|
||||
queries = json.loads(result)
|
||||
if not isinstance(queries, list):
|
||||
# Fallback if response is not a valid list
|
||||
logger.debug("LLM response not a list, using as single query")
|
||||
queries = [result]
|
||||
except json.JSONDecodeError:
|
||||
# Fallback if response is not valid JSON
|
||||
logger.debug("Failed to parse JSON response, using raw response as query")
|
||||
queries = [query] # Fall back to the original query
|
||||
queries_result = await generate_semantic_queries_llm(query)
|
||||
queries = queries_result.queries
|
||||
|
||||
# Ensure we always include the original query
|
||||
if query not in queries:
|
||||
|
|
@ -433,8 +363,8 @@ async def generate_semantic_queries(query: str) -> list[str]:
|
|||
|
||||
return queries
|
||||
except Exception as e:
|
||||
logger.error(f"Error during API call: {str(e)}")
|
||||
raise
|
||||
logger.error(f"Error during query generation: {str(e)}")
|
||||
return [query] # Fallback to original query
|
||||
|
||||
|
||||
async def generate_user_representation(
|
||||
|
|
@ -445,7 +375,7 @@ async def generate_user_representation(
|
|||
tom_inference: str,
|
||||
facts: list[str],
|
||||
db: AsyncSession,
|
||||
message_id: Optional[str] = None,
|
||||
message_id: str | None = None,
|
||||
with_inference: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
|
|
@ -473,19 +403,37 @@ async def generate_user_representation(
|
|||
# Generate the new user representation
|
||||
logger.debug("Calling get_user_representation")
|
||||
gen_start_time = asyncio.get_event_loop().time()
|
||||
user_representation_response = await get_user_representation_long_term(
|
||||
chat_history=chat_history,
|
||||
facts=facts,
|
||||
user_representation=latest_representation,
|
||||
tom_inference=tom_inference,
|
||||
user_representation_response: UserRepresentationOutput = (
|
||||
await get_user_representation_long_term(
|
||||
chat_history=chat_history,
|
||||
facts=facts,
|
||||
user_representation=latest_representation,
|
||||
tom_inference=tom_inference,
|
||||
)
|
||||
)
|
||||
gen_time = asyncio.get_event_loop().time() - gen_start_time
|
||||
logger.debug(f"get_user_representation completed in {gen_time:.2f}s")
|
||||
|
||||
# Extract the representation from the response
|
||||
representation = parse_xml_content(
|
||||
user_representation_response, "representation"
|
||||
)
|
||||
if hasattr(user_representation_response, "current_state"):
|
||||
# New Mirascope response model
|
||||
representation = f"""
|
||||
CURRENT STATE: {user_representation_response.current_state}
|
||||
|
||||
TENTATIVE PATTERNS:
|
||||
{chr(10).join([pattern.pattern for pattern in user_representation_response.tentative_patterns])}
|
||||
|
||||
KNOWLEDGE GAPS:
|
||||
{chr(10).join([gap.missing_info for gap in user_representation_response.knowledge_gaps])}
|
||||
|
||||
RECENT UPDATES:
|
||||
{chr(10).join([update.detail for update in user_representation_response.updates.new_information])}
|
||||
"""
|
||||
else:
|
||||
# Fallback to XML parsing for backwards compatibility
|
||||
representation = parse_xml_content(
|
||||
str(user_representation_response), "representation"
|
||||
)
|
||||
logger.debug(f"Extracted representation: {len(representation)} characters")
|
||||
else:
|
||||
representation = f"""
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, ClassVar, Optional
|
||||
from typing import Annotated, Any, ClassVar
|
||||
|
||||
import tomllib
|
||||
from dotenv import load_dotenv
|
||||
|
|
@ -14,6 +14,8 @@ from pydantic_settings import (
|
|||
SettingsConfigDict,
|
||||
)
|
||||
|
||||
from src.utils.types import Providers
|
||||
|
||||
# Load .env file for local development.
|
||||
# Make sure this is called before AppSettings is instantiated if you rely on .env for AppSettings construction.
|
||||
load_dotenv(override=True)
|
||||
|
|
@ -96,7 +98,7 @@ class HonchoSettings(BaseSettings):
|
|||
"""
|
||||
|
||||
@classmethod
|
||||
def settings_customise_sources(
|
||||
def settings_customise_sources( # pyright: ignore
|
||||
cls,
|
||||
settings_cls: type[BaseSettings],
|
||||
init_settings: PydanticBaseSettingsSource,
|
||||
|
|
@ -115,7 +117,7 @@ class HonchoSettings(BaseSettings):
|
|||
|
||||
|
||||
class DBSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="DB_")
|
||||
model_config = SettingsConfigDict(env_prefix="DB_") # pyright: ignore
|
||||
|
||||
CONNECTION_URI: str = (
|
||||
"postgresql+psycopg://postgres:postgres@localhost:5432/postgres"
|
||||
|
|
@ -137,12 +139,12 @@ class DBSettings(HonchoSettings):
|
|||
|
||||
|
||||
class AuthSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="AUTH_")
|
||||
model_config = SettingsConfigDict(env_prefix="AUTH_") # pyright: ignore
|
||||
|
||||
USE_AUTH: bool = False
|
||||
JWT_SECRET: Optional[str] = None # Must be set if USE_AUTH is true
|
||||
JWT_SECRET: str | None = None # Must be set if USE_AUTH is true
|
||||
|
||||
@model_validator(mode="after")
|
||||
@model_validator(mode="after") # type: ignore
|
||||
def _require_jwt_secret(self) -> "AuthSettings":
|
||||
if self.USE_AUTH and not self.JWT_SECRET:
|
||||
raise ValueError("JWT_SECRET must be set if USE_AUTH is true")
|
||||
|
|
@ -150,41 +152,47 @@ class AuthSettings(HonchoSettings):
|
|||
|
||||
|
||||
class SentrySettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="SENTRY_")
|
||||
model_config = SettingsConfigDict(env_prefix="SENTRY_") # pyright: ignore
|
||||
|
||||
ENABLED: bool = False
|
||||
DSN: Optional[str] = None
|
||||
DSN: str | None = None
|
||||
TRACES_SAMPLE_RATE: Annotated[float, Field(default=0.1, ge=0.0, le=1.0)] = 0.1
|
||||
PROFILES_SAMPLE_RATE: Annotated[float, Field(default=0.1, ge=0.0, le=1.0)] = 0.1
|
||||
|
||||
|
||||
class LLMSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="LLM_")
|
||||
model_config = SettingsConfigDict(env_prefix="LLM_") # pyright: ignore
|
||||
|
||||
# API Keys for LLM providers
|
||||
ANTHROPIC_API_KEY: Optional[str] = None
|
||||
OPENAI_API_KEY: Optional[str] = None
|
||||
OPENAI_COMPATIBLE_API_KEY: Optional[str] = None
|
||||
GEMINI_API_KEY: Optional[str] = None
|
||||
GROQ_API_KEY: Optional[str] = None # Added missing GROQ API key
|
||||
OPENAI_COMPATIBLE_BASE_URL: Optional[str] = None
|
||||
ANTHROPIC_API_KEY: str | None = None
|
||||
OPENAI_API_KEY: str | None = None
|
||||
OPENAI_COMPATIBLE_API_KEY: str | None = None
|
||||
GEMINI_API_KEY: str | None = None
|
||||
GROQ_API_KEY: str | None = None
|
||||
OPENAI_COMPATIBLE_BASE_URL: str | None = None
|
||||
|
||||
# General LLM settings
|
||||
DEFAULT_MAX_TOKENS: Annotated[int, Field(default=1000, gt=0, le=100000)] = 1000
|
||||
DEFAULT_TEMPERATURE: Annotated[float, Field(default=0.0, ge=0.0, le=2.0)] = 0.0
|
||||
|
||||
# Dialectic specific
|
||||
DIALECTIC_PROVIDER: str = "anthropic"
|
||||
DIALECTIC_MODEL: str = "claude-3-haiku-20240307"
|
||||
DIALECTIC_PROVIDER: Providers = "anthropic"
|
||||
DIALECTIC_MODEL: str = "claude-3-5-haiku-20241022"
|
||||
# DIALECTIC_SYSTEM_PROMPT_FILE: Optional[str] = "prompts/dialectic_system.txt" # Example for file-based
|
||||
|
||||
# Query Generation specific
|
||||
QUERY_GENERATION_PROVIDER: str = "groq"
|
||||
QUERY_GENERATION_MODEL: str = "llama3-8b-8192"
|
||||
QUERY_GENERATION_PROVIDER: Providers = "google"
|
||||
QUERY_GENERATION_MODEL: str = "gemini-2.0-flash-lite"
|
||||
# QUERY_GENERATION_SYSTEM_PROMPT_FILE: Optional[str] = "prompts/query_generation_system.txt"
|
||||
|
||||
# Tom Inference specific
|
||||
# TOM_INFERENCE_PROVIDER: Providers = "groq"
|
||||
# TOM_INFERENCE_MODEL: str = "llama-3.3-70b-versatile"
|
||||
TOM_INFERENCE_PROVIDER: Providers = "anthropic"
|
||||
TOM_INFERENCE_MODEL: str = "claude-3-5-haiku-20241022"
|
||||
|
||||
# Summarization specific
|
||||
SUMMARY_PROVIDER: str = "gemini"
|
||||
SUMMARY_PROVIDER: Providers = "google"
|
||||
SUMMARY_MODEL: str = (
|
||||
"gemini-1.5-flash-latest" # Consider specific model version if needed
|
||||
)
|
||||
|
|
@ -195,7 +203,7 @@ class LLMSettings(HonchoSettings):
|
|||
|
||||
|
||||
class AgentSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="AGENT_")
|
||||
model_config = SettingsConfigDict(env_prefix="AGENT_") # pyright: ignore
|
||||
|
||||
SEMANTIC_SEARCH_TOP_K: Annotated[int, Field(default=10, gt=0, le=100)] = 10
|
||||
SEMANTIC_SEARCH_MAX_DISTANCE: Annotated[
|
||||
|
|
@ -205,7 +213,7 @@ class AgentSettings(HonchoSettings):
|
|||
|
||||
|
||||
class DeriverSettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="DERIVER_")
|
||||
model_config = SettingsConfigDict(env_prefix="DERIVER_") # pyright: ignore
|
||||
|
||||
WORKERS: Annotated[int, Field(default=1, gt=0, le=100)] = 1
|
||||
STALE_SESSION_TIMEOUT_MINUTES: Annotated[int, Field(default=5, gt=0, le=1440)] = (
|
||||
|
|
@ -219,7 +227,7 @@ class DeriverSettings(HonchoSettings):
|
|||
|
||||
|
||||
class HistorySettings(HonchoSettings):
|
||||
model_config = SettingsConfigDict(env_prefix="HISTORY_")
|
||||
model_config = SettingsConfigDict(env_prefix="HISTORY_") # pyright: ignore
|
||||
|
||||
MESSAGES_PER_SHORT_SUMMARY: Annotated[int, Field(default=20, gt=0, le=100)] = 20
|
||||
MESSAGES_PER_LONG_SUMMARY: Annotated[int, Field(default=60, gt=0, le=500)] = 60
|
||||
|
|
@ -227,7 +235,9 @@ class HistorySettings(HonchoSettings):
|
|||
|
||||
class AppSettings(HonchoSettings):
|
||||
# No env_prefix for app-level settings
|
||||
model_config = SettingsConfigDict(env_prefix="", env_nested_delimiter="__")
|
||||
model_config = SettingsConfigDict( # pyright: ignore
|
||||
env_prefix="", env_nested_delimiter="__"
|
||||
)
|
||||
|
||||
# Application-wide settings
|
||||
LOG_LEVEL: str = "INFO"
|
||||
|
|
|
|||
182
src/crud.py
182
src/crud.py
|
|
@ -1,13 +1,14 @@
|
|||
import os
|
||||
from collections.abc import Sequence
|
||||
from logging import getLogger
|
||||
from typing import Optional
|
||||
from typing import Any, final
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from nanoid import generate as generate_nanoid
|
||||
from openai import AsyncOpenAI
|
||||
from sqlalchemy import Select, cast, func, insert, select, update
|
||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.types import BigInteger
|
||||
|
||||
|
|
@ -17,11 +18,25 @@ from . import models, schemas
|
|||
from .exceptions import (
|
||||
ResourceNotFoundException,
|
||||
)
|
||||
from .utils.model_client import ModelClient, ModelProvider
|
||||
|
||||
load_dotenv(override=True)
|
||||
|
||||
openai_client = AsyncOpenAI(api_key=settings.LLM.OPENAI_API_KEY)
|
||||
|
||||
@final
|
||||
class EmbeddingClient:
|
||||
def __init__(self, api_key: str | None):
|
||||
if api_key is None:
|
||||
raise ValueError("API key is required")
|
||||
self.client = AsyncOpenAI(api_key=api_key)
|
||||
|
||||
async def embed(self, query: str) -> list[float]:
|
||||
response = await self.client.embeddings.create(
|
||||
input=query, model="text-embedding-3-small"
|
||||
)
|
||||
return response.data[0].embedding
|
||||
|
||||
|
||||
embedding_client = EmbeddingClient(settings.LLM.OPENAI_API_KEY)
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
|
@ -29,10 +44,6 @@ USER_REPRESENTATION_METADATA_KEY = "user_representation"
|
|||
|
||||
SESSION_PEERS_LIMIT = int(os.getenv("SESSION_PEERS_LIMIT", 10))
|
||||
|
||||
# Create a ModelClient instance for embeddings
|
||||
# Using OpenAI provider for embeddings as it's the most common
|
||||
embedding_client = ModelClient(provider=ModelProvider.OPENAI)
|
||||
|
||||
########################################################
|
||||
# workspace methods
|
||||
########################################################
|
||||
|
|
@ -77,8 +88,8 @@ async def get_or_create_workspace(
|
|||
|
||||
|
||||
async def get_all_workspaces(
|
||||
filter: Optional[dict] = None,
|
||||
) -> Select:
|
||||
filter: dict[str, Any] | None = None,
|
||||
) -> Select[tuple[models.Workspace]]:
|
||||
"""
|
||||
Get all workspaces.
|
||||
|
||||
|
|
@ -89,7 +100,7 @@ async def get_all_workspaces(
|
|||
stmt = select(models.Workspace)
|
||||
if filter is not None:
|
||||
stmt = stmt.where(models.Workspace.h_metadata.contains(filter))
|
||||
stmt = stmt.order_by(models.Workspace.created_at)
|
||||
stmt: Select[tuple[models.Workspace]] = stmt.order_by(models.Workspace.created_at)
|
||||
return stmt
|
||||
|
||||
|
||||
|
|
@ -231,8 +242,8 @@ async def get_peer(
|
|||
|
||||
async def get_peers(
|
||||
workspace_name: str,
|
||||
filter: Optional[dict] = None,
|
||||
) -> Select:
|
||||
filter: dict[str, str] | None = None,
|
||||
) -> Select[tuple[models.Peer]]:
|
||||
stmt = select(models.Peer).where(models.Peer.workspace_name == workspace_name)
|
||||
|
||||
if filter is not None:
|
||||
|
|
@ -283,9 +294,9 @@ async def update_peer(
|
|||
async def get_sessions_for_peer(
|
||||
workspace_name: str,
|
||||
peer_name: str,
|
||||
is_active: Optional[bool] = None,
|
||||
filter: Optional[dict] = None,
|
||||
) -> Select:
|
||||
is_active: bool | None = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
) -> Select[tuple[models.Session]]:
|
||||
"""
|
||||
Get all sessions for a peer through the session_peers relationship.
|
||||
|
||||
|
|
@ -315,7 +326,7 @@ async def get_sessions_for_peer(
|
|||
if filter is not None:
|
||||
stmt = stmt.where(models.Session.h_metadata.contains(filter))
|
||||
|
||||
stmt = stmt.order_by(models.Session.created_at)
|
||||
stmt: Select[tuple[models.Session]] = stmt.order_by(models.Session.created_at)
|
||||
|
||||
return stmt
|
||||
|
||||
|
|
@ -327,9 +338,9 @@ async def get_sessions_for_peer(
|
|||
|
||||
async def get_sessions(
|
||||
workspace_name: str,
|
||||
is_active: Optional[bool] = False,
|
||||
filter: Optional[dict] = None,
|
||||
) -> Select:
|
||||
is_active: bool | None = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
) -> Select[tuple[models.Session]]:
|
||||
"""
|
||||
Get all sessions in a workspace.
|
||||
"""
|
||||
|
|
@ -540,7 +551,7 @@ async def clone_session(
|
|||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
original_session_name: str,
|
||||
cutoff_message_id: Optional[str] = None,
|
||||
cutoff_message_id: str | None = None,
|
||||
) -> models.Session:
|
||||
"""
|
||||
Clone a session and its messages. If cutoff_message_id is provided,
|
||||
|
|
@ -692,7 +703,7 @@ async def remove_peers_from_session(
|
|||
async def get_peers_from_session(
|
||||
workspace_name: str,
|
||||
session_name: str,
|
||||
) -> Select:
|
||||
) -> Select[tuple[models.Peer]]:
|
||||
"""
|
||||
Get all peers from a session.
|
||||
|
||||
|
|
@ -719,7 +730,7 @@ async def get_peers_from_session(
|
|||
async def get_session_peer_configuration(
|
||||
workspace_name: str,
|
||||
session_name: str,
|
||||
) -> Select:
|
||||
) -> Select[tuple[str, dict[str, Any], dict[str, Any]]]:
|
||||
"""
|
||||
Get configuration from both SessionPeer and Peer tables for active peers in a session.
|
||||
|
||||
|
|
@ -730,7 +741,7 @@ async def get_session_peer_configuration(
|
|||
Returns:
|
||||
Select statement returning peer_name, peer_configuration, and session_peer_configuration
|
||||
"""
|
||||
stmt = (
|
||||
stmt: Select[tuple[str, dict[str, Any], dict[str, Any]]] = (
|
||||
select(
|
||||
models.Peer.name.label("peer_name"),
|
||||
models.Peer.configuration.label("peer_configuration"),
|
||||
|
|
@ -988,9 +999,9 @@ async def search(
|
|||
query: str,
|
||||
*,
|
||||
workspace_name: str,
|
||||
session_name: Optional[str] = None,
|
||||
peer_name: Optional[str] = None,
|
||||
) -> Select:
|
||||
session_name: str | None = None,
|
||||
peer_name: str | None = None,
|
||||
) -> Select[tuple[models.Message]]:
|
||||
"""
|
||||
Search across message content using a hybrid approach:
|
||||
- Uses PostgreSQL full text search for natural language queries
|
||||
|
|
@ -1249,11 +1260,11 @@ async def create_messages_for_peer(
|
|||
async def get_messages(
|
||||
workspace_name: str,
|
||||
session_name: str,
|
||||
reverse: Optional[bool] = False,
|
||||
filter: Optional[dict] = None,
|
||||
token_limit: Optional[int] = None,
|
||||
message_count_limit: Optional[int] = None,
|
||||
) -> Select:
|
||||
reverse: bool | None = False,
|
||||
filter: dict[str, Any] | None = None,
|
||||
token_limit: int | None = None,
|
||||
message_count_limit: int | None = None,
|
||||
) -> Select[tuple[models.Message]]:
|
||||
"""
|
||||
Get messages from a session. If token_limit is provided, the n most recent messages
|
||||
with token count adding up to the limit will be returned. If message_count_limit is provided,
|
||||
|
|
@ -1338,7 +1349,7 @@ async def get_messages_id_range(
|
|||
session_name: str | None,
|
||||
peer_name: str | None,
|
||||
start_id: int = 0,
|
||||
end_id: Optional[int] = None,
|
||||
end_id: int | None = None,
|
||||
) -> list[models.Message]:
|
||||
"""
|
||||
Get messages from a session or peer by primary key ID range.
|
||||
|
|
@ -1386,9 +1397,9 @@ async def get_messages_id_range(
|
|||
async def get_messages_for_peer(
|
||||
workspace_name: str,
|
||||
peer_name: str,
|
||||
reverse: Optional[bool] = False,
|
||||
filter: Optional[dict] = None,
|
||||
) -> Select:
|
||||
reverse: bool | None = False,
|
||||
filter: dict[str, Any] | None = None,
|
||||
) -> Select[tuple[models.Message]]:
|
||||
stmt = (
|
||||
select(models.Message)
|
||||
.where(models.Message.workspace_name == workspace_name)
|
||||
|
|
@ -1412,7 +1423,7 @@ async def get_message(
|
|||
workspace_name: str,
|
||||
session_name: str,
|
||||
message_id: str,
|
||||
) -> Optional[models.Message]:
|
||||
) -> models.Message | None:
|
||||
stmt = (
|
||||
select(models.Message)
|
||||
.where(models.Message.workspace_name == workspace_name)
|
||||
|
|
@ -1520,8 +1531,8 @@ async def query_documents(
|
|||
peer_name: str,
|
||||
collection_name: str,
|
||||
query: str,
|
||||
filter: Optional[dict] = None,
|
||||
max_distance: Optional[float] = None,
|
||||
filter: dict[str, Any] | None = None,
|
||||
max_distance: float | None = None,
|
||||
top_k: int = 5,
|
||||
) -> Sequence[models.Document]:
|
||||
# Using ModelClient for embeddings
|
||||
|
|
@ -1552,7 +1563,7 @@ async def create_document(
|
|||
workspace_name: str,
|
||||
peer_name: str,
|
||||
collection_name: str,
|
||||
duplicate_threshold: Optional[float] = None,
|
||||
duplicate_threshold: float | None = None,
|
||||
) -> models.Document:
|
||||
"""
|
||||
Embed text as a vector and create a document.
|
||||
|
|
@ -1665,8 +1676,8 @@ async def get_duplicate_documents(
|
|||
async def get_deriver_status(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
peer_name: Optional[str] = None,
|
||||
session_name: Optional[str] = None,
|
||||
peer_name: str | None = None,
|
||||
session_name: str | None = None,
|
||||
include_sender: bool = False,
|
||||
) -> schemas.DeriverStatus:
|
||||
"""
|
||||
|
|
@ -1706,10 +1717,10 @@ async def get_deriver_status(
|
|||
|
||||
def _build_queue_status_query(
|
||||
workspace_name: str,
|
||||
peer_name: Optional[str],
|
||||
session_name: Optional[str],
|
||||
peer_name: str | None,
|
||||
session_name: str | None,
|
||||
include_sender: bool,
|
||||
):
|
||||
) -> Select[Any]:
|
||||
"""Build SQL query for queue status with validation and aggregation."""
|
||||
from sqlalchemy import case, func
|
||||
|
||||
|
|
@ -1788,72 +1799,77 @@ def _build_queue_status_query(
|
|||
return stmt
|
||||
|
||||
|
||||
def _process_queue_rows(rows):
|
||||
def _process_queue_rows(rows: Sequence[Row[Any]]) -> schemas.QueueCounts:
|
||||
"""Process query results that already contain aggregated counts."""
|
||||
if not rows:
|
||||
return {
|
||||
"total": 0,
|
||||
"completed": 0,
|
||||
"in_progress": 0,
|
||||
"pending": 0,
|
||||
"sessions": {},
|
||||
}
|
||||
return schemas.QueueCounts(
|
||||
total=0,
|
||||
completed=0,
|
||||
in_progress=0,
|
||||
pending=0,
|
||||
sessions={},
|
||||
)
|
||||
|
||||
# Since we're using window functions, all rows have the same overall totals
|
||||
# We just need the first row for overall counts
|
||||
first_row = rows[0]
|
||||
|
||||
# Build sessions dictionary from unique session_ids
|
||||
sessions = {}
|
||||
seen_sessions = set()
|
||||
sessions: dict[str, schemas.SessionCounts] = {}
|
||||
seen_sessions: set[str] = set()
|
||||
|
||||
for row in rows:
|
||||
if row.session_id and row.session_id not in seen_sessions:
|
||||
sessions[row.session_id] = {
|
||||
"completed": row.session_completed,
|
||||
"in_progress": row.session_in_progress,
|
||||
"pending": row.session_pending,
|
||||
}
|
||||
sessions[row.session_id] = schemas.SessionCounts(
|
||||
completed=row.session_completed,
|
||||
in_progress=row.session_in_progress,
|
||||
pending=row.session_pending,
|
||||
)
|
||||
seen_sessions.add(row.session_id)
|
||||
|
||||
return {
|
||||
"total": first_row.total,
|
||||
"completed": first_row.completed,
|
||||
"in_progress": first_row.in_progress,
|
||||
"pending": first_row.pending,
|
||||
"sessions": sessions,
|
||||
}
|
||||
return schemas.QueueCounts(
|
||||
total=first_row.total,
|
||||
completed=first_row.completed,
|
||||
in_progress=first_row.in_progress,
|
||||
pending=first_row.pending,
|
||||
sessions=sessions,
|
||||
)
|
||||
|
||||
|
||||
def _build_status_response(
|
||||
peer_name: Optional[str], session_name: Optional[str], counts: dict
|
||||
):
|
||||
peer_name: str | None, session_name: str | None, counts: schemas.QueueCounts
|
||||
) -> schemas.DeriverStatus:
|
||||
"""Build the final response object."""
|
||||
base_response = {
|
||||
"peer_id": peer_name,
|
||||
"total_work_units": counts["total"],
|
||||
"completed_work_units": counts["completed"],
|
||||
"in_progress_work_units": counts["in_progress"],
|
||||
"pending_work_units": counts["pending"],
|
||||
}
|
||||
|
||||
if session_name:
|
||||
return schemas.DeriverStatus(session_id=session_name, **base_response)
|
||||
return schemas.DeriverStatus(
|
||||
session_id=session_name,
|
||||
peer_id=peer_name,
|
||||
total_work_units=counts.total,
|
||||
completed_work_units=counts.completed,
|
||||
in_progress_work_units=counts.in_progress,
|
||||
pending_work_units=counts.pending,
|
||||
)
|
||||
|
||||
sessions = {}
|
||||
for session_id, data in counts["sessions"].items():
|
||||
total = data["completed"] + data["in_progress"] + data["pending"]
|
||||
sessions: dict[str, schemas.DeriverStatus] = {}
|
||||
for session_id, data in counts.sessions.items():
|
||||
total = data.completed + data.in_progress + data.pending
|
||||
sessions[session_id] = schemas.DeriverStatus(
|
||||
peer_id=peer_name,
|
||||
session_id=session_id,
|
||||
total_work_units=total,
|
||||
completed_work_units=data["completed"],
|
||||
in_progress_work_units=data["in_progress"],
|
||||
pending_work_units=data["pending"],
|
||||
completed_work_units=data.completed,
|
||||
in_progress_work_units=data.in_progress,
|
||||
pending_work_units=data.pending,
|
||||
)
|
||||
|
||||
return schemas.DeriverStatus(
|
||||
sessions=sessions if sessions else None, **base_response
|
||||
sessions=sessions if sessions else None,
|
||||
peer_id=peer_name,
|
||||
total_work_units=counts.total,
|
||||
completed_work_units=counts.completed,
|
||||
in_progress_work_units=counts.in_progress,
|
||||
pending_work_units=counts.pending,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import contextvars
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import MetaData, create_engine, text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
|
@ -11,7 +10,7 @@ from src.config import settings
|
|||
connect_args = {"prepare_threshold": None}
|
||||
|
||||
# Context variable to store request context
|
||||
request_context: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
|
||||
request_context: contextvars.ContextVar[str | None] = contextvars.ContextVar(
|
||||
"request_context", default=None
|
||||
)
|
||||
|
||||
|
|
@ -21,7 +20,7 @@ if settings.DB.POOL_CLASS == "null":
|
|||
engine_kwargs["poolclass"] = NullPool
|
||||
else:
|
||||
# Only add pool-related kwargs for pooled connections
|
||||
engine_kwargs.update(
|
||||
engine_kwargs.update( # pyright: ignore
|
||||
{
|
||||
"pool_pre_ping": settings.DB.POOL_PRE_PING,
|
||||
"pool_size": settings.DB.POOL_SIZE,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ async def get_db():
|
|||
|
||||
|
||||
@asynccontextmanager
|
||||
async def tracked_db(operation_name=None):
|
||||
async def tracked_db(operation_name: str | None = None):
|
||||
"""Context manager for tracked database sessions"""
|
||||
# Get request ID if available, or create operation-specific one
|
||||
context = request_context.get()
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import logging
|
||||
import os
|
||||
from typing import Literal
|
||||
from typing import Any, Literal
|
||||
|
||||
import sentry_sdk
|
||||
from langfuse.decorators import observe
|
||||
from langfuse.decorators import observe # pyright: ignore
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from rich.console import Console
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -40,10 +40,10 @@ class PayloadSchema(BaseModel):
|
|||
|
||||
class Config:
|
||||
# Forbid extra fields to prevent injection of unexpected data
|
||||
extra = "forbid"
|
||||
extra = "forbid" # pyright: ignore
|
||||
|
||||
|
||||
async def process_item(db: AsyncSession, payload: dict):
|
||||
async def process_item(db: AsyncSession, payload: dict[str, Any]):
|
||||
# Validate payload structure and types before processing
|
||||
try:
|
||||
validated_payload = PayloadSchema(**payload)
|
||||
|
|
@ -57,22 +57,21 @@ async def process_item(db: AsyncSession, payload: dict):
|
|||
validated_payload.session_name,
|
||||
)
|
||||
|
||||
processing_args = [
|
||||
validated_payload.content,
|
||||
validated_payload.workspace_name,
|
||||
validated_payload.sender_name,
|
||||
validated_payload.target_name,
|
||||
validated_payload.session_name,
|
||||
validated_payload.message_id,
|
||||
db,
|
||||
]
|
||||
if validated_payload.task_type == "representation":
|
||||
logger.debug(
|
||||
"Processing message %s in %s",
|
||||
validated_payload.message_id,
|
||||
validated_payload.session_name,
|
||||
)
|
||||
await process_message(*processing_args)
|
||||
await process_message(
|
||||
validated_payload.content,
|
||||
validated_payload.workspace_name,
|
||||
validated_payload.sender_name,
|
||||
validated_payload.target_name,
|
||||
validated_payload.session_name,
|
||||
validated_payload.message_id,
|
||||
db,
|
||||
)
|
||||
logger.debug(
|
||||
"Finished processing message %s in %s %s",
|
||||
validated_payload.message_id,
|
||||
|
|
@ -140,7 +139,8 @@ async def process_message(
|
|||
# Extract facts from chat history
|
||||
logger.debug("Extracting facts from chat history")
|
||||
extract_start = os.times()[4]
|
||||
facts = await extract_facts_long_term(chat_history_str)
|
||||
fact_extraction = await extract_facts_long_term(chat_history_str)
|
||||
facts: list[str] = fact_extraction.facts or []
|
||||
extract_time = os.times()[4] - extract_start
|
||||
console.print(f"Extracted Facts: {facts}", style="bright_blue")
|
||||
logger.debug(f"Extracted {len(facts)} facts in {extract_time:.2f}s")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import asyncio
|
||||
import signal
|
||||
from _asyncio import Task
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
|
@ -45,14 +46,14 @@ class WorkUnit:
|
|||
|
||||
class QueueManager:
|
||||
def __init__(self):
|
||||
self.shutdown_event = asyncio.Event()
|
||||
self.active_tasks: set[asyncio.Task] = set()
|
||||
self.shutdown_event: asyncio.Event = asyncio.Event()
|
||||
self.active_tasks: set[asyncio.Task[None]] = set()
|
||||
self.owned_work_units: set[WorkUnit] = set()
|
||||
self.queue_empty_flag = asyncio.Event()
|
||||
self.queue_empty_flag: asyncio.Event = asyncio.Event()
|
||||
|
||||
# Initialize from settings
|
||||
self.workers = settings.DERIVER.WORKERS
|
||||
self.semaphore = asyncio.Semaphore(self.workers)
|
||||
self.workers: int = settings.DERIVER.WORKERS
|
||||
self.semaphore: asyncio.Semaphore = asyncio.Semaphore(self.workers)
|
||||
|
||||
# Initialize Sentry if enabled, using settings
|
||||
if settings.SENTRY.ENABLED:
|
||||
|
|
@ -64,7 +65,7 @@ class QueueManager:
|
|||
integrations=[AsyncioIntegration()],
|
||||
)
|
||||
|
||||
def add_task(self, task: asyncio.Task):
|
||||
def add_task(self, task: asyncio.Task[None]):
|
||||
"""Track a new task"""
|
||||
self.active_tasks.add(task)
|
||||
task.add_done_callback(self.active_tasks.discard)
|
||||
|
|
@ -241,7 +242,7 @@ class QueueManager:
|
|||
|
||||
# Create a new task for processing this work unit
|
||||
if not self.shutdown_event.is_set():
|
||||
task = asyncio.create_task(
|
||||
task: Task[None] = asyncio.create_task(
|
||||
self.process_work_unit(work_unit)
|
||||
)
|
||||
self.add_task(task)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
from .conversational import (
|
||||
get_tom_inference_conversational,
|
||||
get_user_representation_conversational,
|
||||
tom_inference_conversational,
|
||||
user_representation_conversational,
|
||||
)
|
||||
from .long_term import get_user_representation_long_term
|
||||
from .single_prompt import (
|
||||
get_tom_inference_single_prompt,
|
||||
get_user_representation_single_prompt,
|
||||
TomInferenceOutput,
|
||||
UserRepresentationOutput,
|
||||
)
|
||||
from .single_prompt import (
|
||||
tom_inference as tom_inference_single_prompt,
|
||||
)
|
||||
from .single_prompt import (
|
||||
user_representation as user_representation_single_prompt,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -13,16 +19,12 @@ async def get_tom_inference(
|
|||
chat_history: str,
|
||||
user_representation: str = "None",
|
||||
method: str = "conversational",
|
||||
**kwargs,
|
||||
) -> str:
|
||||
) -> TomInferenceOutput:
|
||||
if method == "conversational":
|
||||
return await get_tom_inference_conversational(
|
||||
chat_history, user_representation, **kwargs
|
||||
)
|
||||
return await tom_inference_conversational(chat_history, user_representation)
|
||||
elif method == "single_prompt":
|
||||
return await get_tom_inference_single_prompt(
|
||||
chat_history, user_representation, **kwargs
|
||||
)
|
||||
return await tom_inference_single_prompt(chat_history, user_representation)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Invalid method: {method}")
|
||||
|
||||
|
|
@ -32,19 +34,18 @@ async def get_user_representation(
|
|||
user_representation: str = "None",
|
||||
tom_inference: str = "None",
|
||||
method: str = "conversational",
|
||||
**kwargs,
|
||||
) -> str:
|
||||
) -> UserRepresentationOutput:
|
||||
if method == "conversational":
|
||||
return await get_user_representation_conversational(
|
||||
chat_history, user_representation, tom_inference, **kwargs
|
||||
return await user_representation_conversational(
|
||||
chat_history, user_representation, tom_inference
|
||||
)
|
||||
elif method == "single_prompt":
|
||||
return await get_user_representation_single_prompt(
|
||||
chat_history, user_representation, tom_inference, **kwargs
|
||||
return await user_representation_single_prompt(
|
||||
chat_history, user_representation, tom_inference
|
||||
)
|
||||
elif method == "long_term":
|
||||
return await get_user_representation_long_term(
|
||||
chat_history, user_representation, tom_inference, **kwargs
|
||||
chat_history, user_representation, tom_inference
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Invalid method: {method}")
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ... import crud, schemas
|
||||
from ...dependencies import tracked_db
|
||||
|
|
@ -8,14 +9,13 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class CollectionEmbeddingStore:
|
||||
def __init__(self, workspace_name: str, peer_name: str, collection_name: str):
|
||||
self.workspace_name = workspace_name
|
||||
self.peer_name = peer_name
|
||||
self.collection_name = collection_name
|
||||
self.workspace_name: str = workspace_name
|
||||
self.peer_name: str = peer_name
|
||||
self.collection_name: str = collection_name
|
||||
|
||||
async def save_facts(
|
||||
self,
|
||||
facts: list[str],
|
||||
replace_duplicates: bool = True,
|
||||
similarity_threshold: float = 0.85,
|
||||
message_id: int | None = None,
|
||||
) -> None:
|
||||
|
|
@ -30,7 +30,7 @@ class CollectionEmbeddingStore:
|
|||
for fact in facts:
|
||||
# Create document with duplicate checking
|
||||
try:
|
||||
metadata = {}
|
||||
metadata: dict[str, Any] = {}
|
||||
if message_id is not None:
|
||||
metadata["message_id"] = message_id
|
||||
await crud.create_document(
|
||||
|
|
@ -86,7 +86,7 @@ class CollectionEmbeddingStore:
|
|||
Returns:
|
||||
List of facts that are not duplicates of existing facts
|
||||
"""
|
||||
unique_facts = []
|
||||
unique_facts: list[str] = []
|
||||
|
||||
async with tracked_db("embedding_store.remove_duplicates") as db:
|
||||
for fact in facts:
|
||||
|
|
|
|||
|
|
@ -1,41 +1,70 @@
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from langfuse.decorators import observe
|
||||
from sentry_sdk.ai.monitoring import ai_track
|
||||
from mirascope import llm
|
||||
from mirascope.integrations.langfuse import with_langfuse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.utils import parse_xml_content
|
||||
from src.utils.model_client import ModelClient, ModelProvider
|
||||
from src.config import settings
|
||||
from src.deriver.tom.single_prompt import UserRepresentationOutput
|
||||
from src.utils.clients import clients
|
||||
from src.utils.types import track
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constants for fact extraction
|
||||
FACT_EXTRACTION_PROVIDER = ModelProvider.GEMINI
|
||||
FACT_EXTRACTION_MODEL = "gemini-2.0-flash-lite"
|
||||
|
||||
USER_REPRESENTATION_PROVIDER = ModelProvider.GROQ
|
||||
USER_REPRESENTATION_MODEL = "llama-3.3-70b-versatile"
|
||||
|
||||
MAX_FACT_DISTANCE = 0.85
|
||||
class PotentialSurprise(BaseModel):
|
||||
content: str
|
||||
reason: str
|
||||
confidence_level: float
|
||||
|
||||
|
||||
@ai_track("User Representation")
|
||||
@observe()
|
||||
class UserRepresentation(BaseModel):
|
||||
current_state: str
|
||||
tentative_patterns: list[str]
|
||||
knowledge_gaps: list[str]
|
||||
expectation_violations: list[PotentialSurprise]
|
||||
updates: list[str]
|
||||
|
||||
|
||||
class InformationPiece(BaseModel):
|
||||
quote: str
|
||||
category: str
|
||||
explanation: str
|
||||
semantic_retrieval: str
|
||||
|
||||
|
||||
class InformationExtraction(BaseModel):
|
||||
pieces: list[InformationPiece]
|
||||
challenge: str
|
||||
|
||||
|
||||
class FactExtraction(BaseModel):
|
||||
information_extraction: InformationExtraction
|
||||
facts: list[str]
|
||||
|
||||
|
||||
@track("User Representation")
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.TOM_INFERENCE_PROVIDER
|
||||
if settings.LLM.TOM_INFERENCE_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.TOM_INFERENCE_MODEL,
|
||||
response_model=UserRepresentationOutput,
|
||||
client=clients[settings.LLM.TOM_INFERENCE_PROVIDER],
|
||||
)
|
||||
async def get_user_representation_long_term(
|
||||
chat_history: str,
|
||||
user_representation: str = "None",
|
||||
tom_inference: str = "None",
|
||||
facts: Optional[list[str]] = None,
|
||||
) -> str:
|
||||
if facts is None:
|
||||
facts = []
|
||||
facts_str = "\n".join([f"- {fact}" for fact in facts])
|
||||
logger.debug(f"Facts: {facts_str}")
|
||||
|
||||
system_prompt = """You are a system for maintaining factual user representations based on conversation history and theory of mind analysis.
|
||||
facts: list[str] | None = None,
|
||||
):
|
||||
facts_str = "\n".join(f"- {fact}" for fact in facts) if facts else "None available"
|
||||
return f"""
|
||||
You are a system for maintaining factual user representations based on conversation history and theory of mind analysis.
|
||||
|
||||
Your job is to update the existing user representation (if provided) with the new information from the conversation history and theory of mind analysis.
|
||||
|
||||
|
|
@ -45,82 +74,60 @@ REQUIREMENTS:
|
|||
3. Track certainty levels for all information
|
||||
4. Maintain areas of uncertainty explicitly
|
||||
5. Update representation incrementally
|
||||
6. DO NOT generate persistent information - it will be injected separately. Always include the <KNOWN_FACTS> tag in your response in order to inject the facts.
|
||||
6. Use the persistent facts provided to inform your understanding of the user
|
||||
|
||||
OUTPUT FORMAT:
|
||||
<representation>
|
||||
CURRENT STATE:
|
||||
- Active Context: Current situation/activity
|
||||
- Temporary Conditions: Immediate circumstances
|
||||
<CURRENT_CURSOR_POSITION>
|
||||
- Present Mood/Activity: What user is doing right now
|
||||
current_state: str
|
||||
- Active Context: Current situation/activity
|
||||
- Temporary Conditions: Immediate circumstances
|
||||
- Present Mood/Activity: What user is doing right now
|
||||
tentative_patterns: list[str]
|
||||
- Possible Traits: Mark confidence (Low/Medium/High)
|
||||
- Potential Interests: Need more evidence
|
||||
- Speculative Elements: Clearly marked as unconfirmed
|
||||
knowledge_gaps: list[str]
|
||||
- List key missing information
|
||||
- Note areas needing clarification
|
||||
expectation_violations: list
|
||||
content: str
|
||||
reason: str
|
||||
confidence_level: float
|
||||
- Based on the above information, if the next message were to surprise you, what could it contain?
|
||||
- Include 3-5 possible surprises
|
||||
updates: list[str]
|
||||
- New Information: Recent observations
|
||||
- Changes: Modified interpretations
|
||||
- Removals: Information no longer supported
|
||||
|
||||
<KNOWN_FACTS>
|
||||
CONVERSATION:
|
||||
{chat_history}
|
||||
|
||||
TENTATIVE PATTERNS:
|
||||
- Possible Traits: Mark confidence (Low/Medium/High)
|
||||
- Potential Interests: Need more evidence
|
||||
- Speculative Elements: Clearly marked as unconfirmed
|
||||
PREDICTION OF USER MENTAL STATE - MIGHT BE INCORRECT:
|
||||
{tom_inference or "Doesn't exist"}
|
||||
|
||||
KNOWLEDGE GAPS:
|
||||
- List key missing information
|
||||
- Note areas needing clarification
|
||||
EXISTING USER REPRESENTATION - INCOMPLETE, TO BE UPDATED:
|
||||
{user_representation or "Doesn't exist"}
|
||||
|
||||
EXPECTATION VIOLATIONS:
|
||||
- Based on the above information, if the next message were to surprise you, what could it contain?
|
||||
- Format: "POTENTIAL SURPRISE: [possible content] [reason] [confidence level]"
|
||||
- Include 3-5 possible surprises
|
||||
|
||||
UPDATES:
|
||||
- New Information: Recent observations
|
||||
- Changes: Modified interpretations
|
||||
- Removals: Information no longer supported
|
||||
</representation>
|
||||
PERSISTENT FACTS ABOUT USER:
|
||||
{facts_str}
|
||||
"""
|
||||
|
||||
# Build the context message
|
||||
context_str = f"CONVERSATION:\n{chat_history}\n\n"
|
||||
if tom_inference != "None":
|
||||
context_str += f"PREDICTION OF USER MENTAL STATE - MIGHT BE INCORRECT:\n{tom_inference}\n\n"
|
||||
if user_representation != "None":
|
||||
context_str += f"EXISTING USER REPRESENTATION - INCOMPLETE, TO BE UPDATED:\n{user_representation}"
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Please analyze this information and provide an updated user representation. DO NOT generate persistent information - it will be injected separately:\n{context_str}",
|
||||
}
|
||||
]
|
||||
|
||||
# Create a new model client
|
||||
client = ModelClient(
|
||||
provider=USER_REPRESENTATION_PROVIDER, model=USER_REPRESENTATION_MODEL
|
||||
)
|
||||
|
||||
# Generate the response with caching enabled
|
||||
response = await client.generate(
|
||||
messages=messages,
|
||||
system=system_prompt,
|
||||
max_tokens=1000,
|
||||
temperature=0,
|
||||
use_caching=True, # Enable caching for the system prompt
|
||||
)
|
||||
|
||||
# Inject the facts into the response
|
||||
persistent_info = f"""PERSISTENT INFORMATION:
|
||||
{facts_str}"""
|
||||
|
||||
return response.replace("<KNOWN_FACTS>", persistent_info)
|
||||
|
||||
|
||||
@ai_track("Fact Extraction")
|
||||
@observe()
|
||||
async def extract_facts_long_term(chat_history: str) -> list[str]:
|
||||
logger.debug("Starting fact extraction from chat history")
|
||||
extract_start = time.time()
|
||||
|
||||
system_prompt = """
|
||||
You are an AI assistant specialized in extracting and formatting relevant information about users from conversations. Your task is to analyze a given conversation and create a list of concise, factual statements about the user. These statements will be stored in a vector embedding database to enhance future interactions.
|
||||
@track("Fact Extraction")
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.TOM_INFERENCE_PROVIDER
|
||||
if settings.LLM.TOM_INFERENCE_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.TOM_INFERENCE_MODEL,
|
||||
response_model=FactExtraction,
|
||||
client=clients[settings.LLM.TOM_INFERENCE_PROVIDER],
|
||||
)
|
||||
async def extract_facts_long_term(chat_history: str):
|
||||
return f"""
|
||||
You are an AI assistant specialized in extracting and formatting relevant information about users from conversations. Your task is to analyze a given conversation and create a list of concise, factual statements about the user. These statements will be stored in a vector embedding database to enhance future interactions.
|
||||
|
||||
Here is the conversation you need to analyze:
|
||||
|
||||
|
|
@ -132,22 +139,22 @@ Instructions:
|
|||
|
||||
1. Carefully read through the conversation. Extract only new facts, from only the last message sent by the user - treat the rest of the conversation only as context. Ignore facts in the last message that are already stated in the conversation.
|
||||
|
||||
2. Identify key new pieces of information from the last message sent by the user that would be valuable for future interactions. Look for:
|
||||
- Personal details (name, age, occupation, location, etc.)
|
||||
- Preferences (likes, dislikes, interests, hobbies)
|
||||
- Experiences (travel, education, work history)
|
||||
- Expressive style (writing style, tone, etc.)
|
||||
- Relationships (family, friends, pets)
|
||||
- Goals or aspirations
|
||||
- Challenges or problems they're facing
|
||||
- Opinions or beliefs
|
||||
2. Identify key new pieces of information from the last message sent by the user that would be valuable for future interactions. Look for:
|
||||
- Personal details (name, age, occupation, location, etc.)
|
||||
- Preferences (likes, dislikes, interests, hobbies)
|
||||
- Experiences (travel, education, work history)
|
||||
- Expressive style (writing style, tone, etc.)
|
||||
- Relationships (family, friends, pets)
|
||||
- Goals or aspirations
|
||||
- Challenges or problems they're facing
|
||||
- Opinions or beliefs
|
||||
|
||||
3. For each piece of information you identify:
|
||||
a. Verify that it is factual and explicitly stated in the conversation, not inferred.
|
||||
b. Formulate it as a concise statement that would aid in semantic retrieval.
|
||||
c. Ensure it is not similar to information previously stated in the conversation.
|
||||
|
||||
4. Before providing your final output, wrap your analysis in <information_extraction> tags. In this analysis:
|
||||
4. Before providing your final output, wrap your analysis in information_extraction. In this analysis:
|
||||
- List each piece of information you've identified.
|
||||
- For each piece of information:
|
||||
* Quote the relevant part of the conversation.
|
||||
|
|
@ -156,57 +163,7 @@ Instructions:
|
|||
* Show how you've formulated the fact for optimal semantic retrieval.
|
||||
- Discuss any challenges you encountered in extracting or formatting the information.
|
||||
|
||||
5. After your analysis, provide your final output as a JSON array of strings. Each string should be a single fact about the user. Wrap the facts in <facts> tags.
|
||||
5. After your analysis, provide your final output as a list of strings. Each string should be a single fact about the user.
|
||||
|
||||
Example of the expected output format:
|
||||
<information_extraction>
|
||||
[Analysis goes here]
|
||||
</information_extraction>
|
||||
<facts>
|
||||
{{
|
||||
"facts":
|
||||
[
|
||||
"User is 28 years old",
|
||||
"User's friend Mary works as a software engineer",
|
||||
"Favorite food is sushi"
|
||||
]
|
||||
}}
|
||||
</facts>
|
||||
|
||||
Remember to focus on clear, concise statements that capture key information about the user. Each fact should be worded in a way that will aid its semantic retrieval from a vector embedding database. It's OK for this section to be quite long.
|
||||
"""
|
||||
message = system_prompt.format(chat_history=chat_history)
|
||||
messages = [{"role": "user", "content": message}]
|
||||
|
||||
logger.debug("Calling LLM for fact extraction")
|
||||
llm_start = time.time()
|
||||
|
||||
# Create a new model client
|
||||
client = ModelClient(provider=FACT_EXTRACTION_PROVIDER, model=FACT_EXTRACTION_MODEL)
|
||||
|
||||
# Generate the response with caching enabled
|
||||
response = await client.generate(
|
||||
messages=messages,
|
||||
max_tokens=1000,
|
||||
temperature=0.0,
|
||||
use_caching=True, # Enable caching for the system prompt
|
||||
)
|
||||
|
||||
llm_time = time.time() - llm_start
|
||||
logger.debug(f"LLM response received in {llm_time:.2f}s")
|
||||
|
||||
try:
|
||||
logger.debug("Parsing JSON response")
|
||||
facts_str = parse_xml_content(response, "facts")
|
||||
response_data = json.loads(facts_str)
|
||||
facts = response_data["facts"]
|
||||
logger.debug(f"Extracted {len(facts)} facts")
|
||||
if facts:
|
||||
logger.debug(f"Sample facts: {facts[:3] if len(facts) > 3 else facts}")
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.error(f"Error parsing response: {str(e)}")
|
||||
facts = []
|
||||
|
||||
total_time = time.time() - extract_start
|
||||
logger.debug(f"Total extraction completed in {total_time:.2f}s")
|
||||
return facts
|
||||
Remember to focus on clear, concise statements that capture key information about the user. Each fact should be worded in a way that will aid its semantic retrieval from a vector embedding database.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -1,18 +1,130 @@
|
|||
import logging
|
||||
from typing import Any, Optional
|
||||
from enum import Enum
|
||||
|
||||
import sentry_sdk
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
from sentry_sdk.ai.monitoring import ai_track
|
||||
from mirascope import llm
|
||||
from mirascope.integrations.langfuse import with_langfuse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from src.utils.model_client import ModelClient, ModelProvider
|
||||
from src.config import settings
|
||||
from src.utils.clients import clients
|
||||
from src.utils.types import track
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEF_PROVIDER = ModelProvider.GROQ
|
||||
DEF_MODEL = "llama-3.3-70b-versatile"
|
||||
|
||||
TOM_SYSTEM_PROMPT = """You are a system for analyzing conversations to make evidence-based inferences about user mental states.
|
||||
# Enums for strongly typed fields
|
||||
class InfoType(str, Enum):
|
||||
STYLE = "STYLE"
|
||||
STATEMENT = "STATEMENT"
|
||||
|
||||
|
||||
class CertaintyLevel(str, Enum):
|
||||
LIKELY = "LIKELY"
|
||||
POTENTIAL = "POTENTIAL"
|
||||
SPECULATIVE = "SPECULATIVE"
|
||||
|
||||
|
||||
# ToM Inference Output Models
|
||||
class CurrentState(BaseModel):
|
||||
immediate_context: str
|
||||
active_goals: str
|
||||
present_mood: str
|
||||
|
||||
|
||||
class SupportedObservation(BaseModel):
|
||||
detail: str
|
||||
source: str
|
||||
|
||||
|
||||
class TentativeInference(BaseModel):
|
||||
interpretation: str
|
||||
basis: str
|
||||
|
||||
|
||||
class KnowledgeGap(BaseModel):
|
||||
topic: str
|
||||
|
||||
|
||||
class ExpectationViolation(BaseModel):
|
||||
possible_surprise: str
|
||||
reason: str
|
||||
confidence_level: float
|
||||
|
||||
|
||||
class TomInferenceOutput(BaseModel):
|
||||
current_state: CurrentState
|
||||
tentative_inferences: list[TentativeInference]
|
||||
knowledge_gaps: list[KnowledgeGap]
|
||||
expectation_violations: list[ExpectationViolation]
|
||||
|
||||
|
||||
# User Representation Output Models
|
||||
class SourcedInfo(BaseModel):
|
||||
detail: str
|
||||
source: str
|
||||
|
||||
|
||||
class UserCurrentState(BaseModel):
|
||||
active_context: SourcedInfo
|
||||
temporary_conditions: SourcedInfo
|
||||
present_mood_activity: SourcedInfo
|
||||
|
||||
|
||||
class PersistentInfo(BaseModel):
|
||||
detail: str
|
||||
source: str
|
||||
info_type: InfoType
|
||||
|
||||
|
||||
class TentativePattern(BaseModel):
|
||||
pattern: str
|
||||
source: str
|
||||
certainty_level: CertaintyLevel
|
||||
|
||||
|
||||
class UserKnowledgeGap(BaseModel):
|
||||
missing_info: str
|
||||
|
||||
|
||||
class UserExpectationViolation(BaseModel):
|
||||
potential_surprise: str
|
||||
reason: str
|
||||
confidence_level: float
|
||||
|
||||
|
||||
class UpdateSection(BaseModel):
|
||||
new_information: list[SourcedInfo]
|
||||
changes: list[SourcedInfo]
|
||||
removals: list[SourcedInfo]
|
||||
|
||||
|
||||
class UserRepresentationOutput(BaseModel):
|
||||
current_state: UserCurrentState
|
||||
persistent_information: list[PersistentInfo]
|
||||
tentative_patterns: list[TentativePattern]
|
||||
knowledge_gaps: list[UserKnowledgeGap]
|
||||
expectation_violations: list[UserExpectationViolation]
|
||||
updates: UpdateSection
|
||||
|
||||
|
||||
@track("Tom Inference")
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.TOM_INFERENCE_PROVIDER
|
||||
if settings.LLM.TOM_INFERENCE_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.TOM_INFERENCE_MODEL,
|
||||
response_model=TomInferenceOutput,
|
||||
client=clients[settings.LLM.TOM_INFERENCE_PROVIDER],
|
||||
)
|
||||
async def tom_inference(
|
||||
chat_history: str,
|
||||
user_representation: str | None = None,
|
||||
):
|
||||
return f"""
|
||||
You are a system for analyzing conversations to make evidence-based inferences about user mental states.
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Only make inferences that are directly supported by conversation evidence
|
||||
|
|
@ -21,47 +133,60 @@ REQUIREMENTS:
|
|||
4. Do not make assumptions about demographics unless explicitly stated
|
||||
5. Focus on current mental state and immediate context
|
||||
6. Consider your own knowledge gaps and violations of expectations (what would surprise you)
|
||||
7. Always wrap your prediction in <prediction> tags.
|
||||
|
||||
OUTPUT FORMAT:
|
||||
<prediction>
|
||||
CURRENT STATE:
|
||||
- Immediate Context: User's current situation
|
||||
- Active Goals: What user is trying to achieve
|
||||
- Present Mood: Observable emotional state
|
||||
current_state:
|
||||
- immediate_context: User's current situation
|
||||
- active_goals: What user is trying to achieve
|
||||
- present_mood: Observable emotional state
|
||||
|
||||
SUPPORTED OBSERVATIONS:
|
||||
- List only behaviors/preferences with direct evidence
|
||||
- Format: "OBSERVATION: [detail] (SOURCE: [exact message])"
|
||||
tentative_inferences: list of objects with:
|
||||
- interpretation: Possible but uncertain interpretation
|
||||
- basis: Supporting message or evidence
|
||||
|
||||
TENTATIVE INFERENCES:
|
||||
- List possible but uncertain interpretations
|
||||
- Format: "POSSIBLE: [interpretation] (BASIS: [supporting message])"
|
||||
knowledge_gaps: list of objects with:
|
||||
- topic: Important unknown information or question
|
||||
|
||||
KNOWLEDGE GAPS:
|
||||
- List important unknown information
|
||||
- Format: "UNKNOWN: [topic/question]"
|
||||
|
||||
EXPECTATION VIOLATIONS:
|
||||
- Based on the above information, if the next message were to surprise you, what could it contain?
|
||||
- Format: "POTENTIAL SURPRISE: [possible content] [reason] [confidence level]"
|
||||
expectation_violations: list of objects with:
|
||||
- possible_surprise: What content could surprise you in the next message
|
||||
- reason: Why this would be surprising based on current information
|
||||
- confidence_level: Float between 0.0 and 1.0 indicating confidence
|
||||
- Include 3-5 possible surprises
|
||||
</prediction>"""
|
||||
|
||||
USER_REPRESENTATION_SYSTEM_PROMPT = """You are a system for maintaining factual user representations based on conversation history and theory of mind analysis.
|
||||
<conversation>
|
||||
{chat_history or "Not provided"}
|
||||
</conversation>
|
||||
|
||||
<user_representation>
|
||||
{user_representation or "Not provided"}
|
||||
</user_representation>
|
||||
"""
|
||||
|
||||
|
||||
@track("User Representation")
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.TOM_INFERENCE_PROVIDER
|
||||
if settings.LLM.TOM_INFERENCE_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.TOM_INFERENCE_MODEL,
|
||||
response_model=UserRepresentationOutput,
|
||||
client=clients[settings.LLM.TOM_INFERENCE_PROVIDER],
|
||||
)
|
||||
async def user_representation(
|
||||
chat_history: str,
|
||||
user_representation: str | None = None,
|
||||
tom_inference: str | None = None,
|
||||
):
|
||||
return f"""
|
||||
You are a system for maintaining factual user representations based on conversation history and theory of mind analysis.
|
||||
|
||||
Your job is to update the existing user representation (if provided) with the new information from the conversation history and theory of mind analysis.
|
||||
|
||||
Copy over information as-is from the existing user representation. Add new information as needed. Only remove content from this section if new information contradicts it. This is especially important for Persistent Information and Tentative Patterns.
|
||||
|
||||
If the existing user representation contains sources, copy them over to the new user representation.
|
||||
|
||||
Sometimes, especially at the beginning of a conversation, the user representation might contain information from previous conversations. In this case, preserve all previous information unless new information contradicts it, especially in regards to traits, interests, style, and other information about the user that is likely to be useful across conversations.
|
||||
|
||||
Always use the format below. If the existing user representation is in another format, convert it to the format below.
|
||||
|
||||
Always wrap your output in <representation> tags.
|
||||
|
||||
REQUIREMENTS:
|
||||
1. Distinguish between temporary states and persistent patterns
|
||||
2. Only incorporate verified information into core profile
|
||||
|
|
@ -70,127 +195,44 @@ REQUIREMENTS:
|
|||
5. Update representation incrementally
|
||||
|
||||
OUTPUT FORMAT:
|
||||
<representation>
|
||||
CURRENT STATE:
|
||||
- ACTIVE CONTEXT: [detail on situation/activity/location] (SOURCE: [exact message]) # Current situation/activity/location
|
||||
- TEMPORARY CONDITIONS: [detail on immediate circumstances] (SOURCE: [exact message]) # Immediate circumstances
|
||||
- PRESENT MOOD/ACTIVITY: [what user is doing right now] (SOURCE: [exact message]) # What user is doing right now
|
||||
current_state:
|
||||
- active_context: object with "detail" (current situation/activity/location) and "source" (exact message)
|
||||
- temporary_conditions: object with "detail" (immediate circumstances) and "source" (exact message)
|
||||
- present_mood_activity: object with "detail" (what user is doing right now) and "source" (exact message)
|
||||
|
||||
PERSISTENT INFORMATION:
|
||||
- STYLE: [pattern] (SOURCE: [exact message]) # Communication style: observed patterns in language use
|
||||
- STATEMENT: [fact] (SOURCE: [exact message]) # Explicitly stated information
|
||||
persistent_information: list of objects with:
|
||||
- detail: The specific information or pattern
|
||||
- source: Exact message that supports this
|
||||
- info_type: Must be exactly "STYLE" for communication patterns or "STATEMENT" for explicit facts
|
||||
|
||||
TENTATIVE PATTERNS:
|
||||
- LIKELY PATTERN: [pattern] (SOURCE: [exact message]) # Patterns that are almost certain to be true
|
||||
- POTENTIAL PATTERN: [pattern] (SOURCE: [exact message]) # Patterns that are possible but less likely
|
||||
- SPECULATIVE PATTERN: [pattern] (SOURCE: [exact message]) # Patterns that are highly uncertain but remotely possible
|
||||
tentative_patterns: list of objects with:
|
||||
- pattern: The observed pattern
|
||||
- source: Supporting evidence from specific message
|
||||
- certainty_level: Must be exactly "LIKELY" (almost certain), "POTENTIAL" (possible), or "SPECULATIVE" (uncertain)
|
||||
|
||||
KNOWLEDGE GAPS:
|
||||
- List key missing information
|
||||
- Note areas needing clarification
|
||||
knowledge_gaps: list of objects with:
|
||||
- missing_info: Key information that is missing or needs clarification
|
||||
|
||||
EXPECTATION VIOLATIONS:
|
||||
- Based on the above information, if the next message were to surprise you, what could it contain?
|
||||
- Format: "POTENTIAL SURPRISE: [possible content] [reason] [confidence level]"
|
||||
expectation_violations: list of objects with:
|
||||
- potential_surprise: What could surprise you in the next message
|
||||
- reason: Why this would be surprising based on current information
|
||||
- confidence_level: Float between 0.0 and 1.0
|
||||
- Include 3-5 possible surprises
|
||||
|
||||
UPDATES:
|
||||
- New Information: Recent observations
|
||||
- Changes: Modified interpretations
|
||||
- Removals: Information no longer supported
|
||||
</representation>"""
|
||||
updates:
|
||||
- new_information: List of objects with "detail" (recent observation) and "source" (supporting message)
|
||||
- changes: List of objects with "detail" (modified interpretation) and "source" (supporting message)
|
||||
- removals: List of objects with "detail" (information no longer supported) and "source" (contradicting message)
|
||||
|
||||
<conversation>
|
||||
{chat_history or "Not provided"}
|
||||
</conversation>
|
||||
|
||||
@ai_track("Tom Inference")
|
||||
@observe()
|
||||
async def get_tom_inference_single_prompt(
|
||||
chat_history: str,
|
||||
user_representation: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
with sentry_sdk.start_transaction(op="tom-inference", name="ToM Inference"):
|
||||
# Create a new model client
|
||||
client = ModelClient(provider=DEF_PROVIDER, model=DEF_MODEL)
|
||||
<existing_user_representation>
|
||||
{user_representation or "Not provided"}
|
||||
</existing_user_representation>
|
||||
|
||||
# Prepare the messages
|
||||
messages: list[dict[str, Any]] = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Please analyze this conversation and provide a prediction following the format above:\n{chat_history}",
|
||||
}
|
||||
]
|
||||
|
||||
# Add existing user representation if available
|
||||
if user_representation:
|
||||
messages.append(
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Consider this existing user representation for context, but focus on current state:\n{user_representation}",
|
||||
}
|
||||
)
|
||||
|
||||
langfuse_context.update_current_observation(input=messages, model=DEF_MODEL)
|
||||
|
||||
# Generate the response with caching enabled
|
||||
try:
|
||||
response = await client.generate(
|
||||
messages=messages,
|
||||
system=TOM_SYSTEM_PROMPT,
|
||||
max_tokens=1000,
|
||||
temperature=0,
|
||||
use_caching=True, # Enable caching for the system prompt
|
||||
)
|
||||
except Exception as e:
|
||||
sentry_sdk.capture_exception(e)
|
||||
logger.error(f"Error generating Tom inference: {e}")
|
||||
raise e
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@ai_track("User Representation")
|
||||
@observe()
|
||||
async def get_user_representation_single_prompt(
|
||||
chat_history: str,
|
||||
user_representation: Optional[str] = None,
|
||||
tom_inference: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
with sentry_sdk.start_transaction(
|
||||
op="user-representation-inference", name="User Representation"
|
||||
):
|
||||
# Create a new model client
|
||||
client = ModelClient(provider=DEF_PROVIDER, model=DEF_MODEL)
|
||||
|
||||
# Build the context message
|
||||
context_str = f"CONVERSATION:\n{chat_history}\n\n"
|
||||
if tom_inference:
|
||||
context_str += f"PREDICTION OF USER MENTAL STATE - MIGHT BE INCORRECT:\n{tom_inference}\n\n"
|
||||
if user_representation:
|
||||
context_str += f"EXISTING USER REPRESENTATION - INCOMPLETE, TO BE UPDATED:\n{user_representation}"
|
||||
|
||||
# Prepare the messages
|
||||
messages: list[dict[str, Any]] = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Please analyze this information and provide an updated user representation:\n{context_str}",
|
||||
}
|
||||
]
|
||||
|
||||
langfuse_context.update_current_observation(input=messages, model=DEF_MODEL)
|
||||
|
||||
# Generate the response with caching enabled
|
||||
try:
|
||||
response = await client.generate(
|
||||
messages=messages,
|
||||
system=USER_REPRESENTATION_SYSTEM_PROMPT,
|
||||
max_tokens=1000,
|
||||
temperature=0,
|
||||
use_caching=True, # Enable caching for the system prompt
|
||||
)
|
||||
except Exception as e:
|
||||
sentry_sdk.capture_exception(e)
|
||||
logger.error(f"Error generating user representation: {e}")
|
||||
raise e
|
||||
|
||||
return response
|
||||
<tom_analysis>
|
||||
{tom_inference or "Not provided"}
|
||||
</tom_analysis>
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -2,21 +2,22 @@
|
|||
Custom exceptions for the Honcho application.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from typing import final
|
||||
|
||||
|
||||
class HonchoException(Exception):
|
||||
"""Base exception for all Honcho-specific errors."""
|
||||
|
||||
status_code = 500
|
||||
detail = "An unexpected error occurred"
|
||||
status_code: int = 500
|
||||
detail: str = "An unexpected error occurred"
|
||||
|
||||
def __init__(self, detail: Optional[str] = None, status_code: Optional[int] = None):
|
||||
def __init__(self, detail: str | None = None, status_code: int | None = None):
|
||||
self.detail = detail or self.detail
|
||||
self.status_code = status_code or self.status_code
|
||||
super().__init__(self.detail)
|
||||
|
||||
|
||||
@final
|
||||
class ResourceNotFoundException(HonchoException):
|
||||
"""Exception raised when a requested resource is not found."""
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ class ResourceNotFoundException(HonchoException):
|
|||
detail = "Resource not found"
|
||||
|
||||
|
||||
@final
|
||||
class ValidationException(HonchoException):
|
||||
"""Exception raised when validation fails."""
|
||||
|
||||
|
|
@ -31,6 +33,7 @@ class ValidationException(HonchoException):
|
|||
detail = "Validation error"
|
||||
|
||||
|
||||
@final
|
||||
class ConflictException(HonchoException):
|
||||
"""Exception raised when there's a resource conflict."""
|
||||
|
||||
|
|
@ -38,6 +41,7 @@ class ConflictException(HonchoException):
|
|||
detail = "Resource conflict"
|
||||
|
||||
|
||||
@final
|
||||
class AuthenticationException(HonchoException):
|
||||
"""Exception raised when authentication fails."""
|
||||
|
||||
|
|
@ -45,6 +49,7 @@ class AuthenticationException(HonchoException):
|
|||
detail = "Authentication failed"
|
||||
|
||||
|
||||
@final
|
||||
class AuthorizationException(HonchoException):
|
||||
"""Exception raised when authorization fails."""
|
||||
|
||||
|
|
@ -52,6 +57,7 @@ class AuthorizationException(HonchoException):
|
|||
detail = "Not authorized to access this resource"
|
||||
|
||||
|
||||
@final
|
||||
class DisabledException(HonchoException):
|
||||
"""Exception raised when a feature is disabled."""
|
||||
|
||||
|
|
|
|||
23
src/main.py
23
src/main.py
|
|
@ -1,13 +1,18 @@
|
|||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_pagination import add_pagination
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sentry_sdk._types import Event, Hint
|
||||
from sentry_sdk.integrations.fastapi import FastApiIntegration
|
||||
from sentry_sdk.integrations.starlette import StarletteIntegration
|
||||
|
||||
|
|
@ -63,9 +68,9 @@ async def setup_admin_jwt():
|
|||
SENTRY_ENABLED = settings.SENTRY.ENABLED
|
||||
if SENTRY_ENABLED:
|
||||
|
||||
def before_send(event, hint):
|
||||
def before_send(event: Event, hint: Hint) -> Event | None:
|
||||
if "exc_info" in hint:
|
||||
exc_type, exc_value, _ = hint["exc_info"]
|
||||
_, exc_value, _ = hint["exc_info"]
|
||||
# Filter out HonchoExceptions from being sent to Sentry
|
||||
if isinstance(exc_value, HonchoException):
|
||||
return None
|
||||
|
|
@ -95,7 +100,7 @@ if SENTRY_ENABLED:
|
|||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
async def lifespan(_: FastAPI):
|
||||
yield
|
||||
await engine.dispose()
|
||||
|
||||
|
|
@ -149,7 +154,7 @@ app.include_router(keys.router, prefix="/v2")
|
|||
|
||||
# Global exception handlers
|
||||
@app.exception_handler(HonchoException)
|
||||
async def honcho_exception_handler(request: Request, exc: HonchoException):
|
||||
async def honcho_exception_handler(_request: Request, exc: HonchoException):
|
||||
"""Handle all Honcho-specific exceptions."""
|
||||
logger.error(f"{exc.__class__.__name__}: {exc.detail}", exc_info=exc)
|
||||
return JSONResponse(
|
||||
|
|
@ -159,7 +164,7 @@ async def honcho_exception_handler(request: Request, exc: HonchoException):
|
|||
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
async def global_exception_handler(_request: Request, exc: Exception):
|
||||
"""Handle all unhandled exceptions."""
|
||||
logger.error(f"Unhandled exception: {str(exc)}", exc_info=True)
|
||||
if SENTRY_ENABLED:
|
||||
|
|
@ -171,9 +176,9 @@ async def global_exception_handler(request: Request, exc: Exception):
|
|||
|
||||
|
||||
@app.middleware("http")
|
||||
async def track_request(request: Request, call_next):
|
||||
if not settings.DB.TRACING:
|
||||
return await call_next(request)
|
||||
async def track_request(
|
||||
request: Request, call_next: Callable[[Request], Awaitable[Response]]
|
||||
):
|
||||
# Create a request ID that includes endpoint information
|
||||
endpoint = re.sub(r"/[A-Za-z0-9_-]{21}", "", request.url.path).replace("/", "_")
|
||||
request_id = f"{request.method}:{endpoint}:{str(uuid.uuid4())[:8]}"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import datetime
|
||||
from logging import getLogger
|
||||
from typing import Any, final
|
||||
|
||||
import tiktoken
|
||||
from dotenv import load_dotenv
|
||||
|
|
@ -23,7 +24,9 @@ from sqlalchemy import (
|
|||
)
|
||||
from sqlalchemy.dialects.postgresql import JSONB, TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from sqlalchemy.orm.properties import MappedColumn
|
||||
from sqlalchemy.sql import func
|
||||
from typing_extensions import override
|
||||
|
||||
from .db import Base
|
||||
|
||||
|
|
@ -93,19 +96,20 @@ session_peers_table = Table(
|
|||
)
|
||||
|
||||
|
||||
@final
|
||||
class Workspace(Base):
|
||||
__tablename__ = "workspaces"
|
||||
__tablename__: str = "workspaces"
|
||||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(TEXT, index=True, unique=True)
|
||||
peers = relationship("Peer", back_populates="workspace")
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), index=True, default=func.now()
|
||||
)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict] = mapped_column(
|
||||
h_metadata: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
|
||||
"internal_metadata", JSONB, default=dict
|
||||
)
|
||||
configuration: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
configuration: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict)
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("length(id) = 21", name="id_length"),
|
||||
|
|
@ -114,12 +118,13 @@ class Workspace(Base):
|
|||
)
|
||||
|
||||
|
||||
@final
|
||||
class Peer(Base):
|
||||
__tablename__ = "peers"
|
||||
__tablename__: str = "peers"
|
||||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(TEXT, index=True)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict] = mapped_column(
|
||||
h_metadata: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
|
||||
"internal_metadata", JSONB, default=dict
|
||||
)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
|
|
@ -128,7 +133,7 @@ class Peer(Base):
|
|||
workspace_name: Mapped[str] = mapped_column(
|
||||
ForeignKey("workspaces.name"), index=True
|
||||
)
|
||||
configuration: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
configuration: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict)
|
||||
|
||||
workspace = relationship("Workspace", back_populates="peers")
|
||||
sessions = relationship(
|
||||
|
|
@ -148,13 +153,14 @@ class Peer(Base):
|
|||
return f"Peer(id={self.id}, name={self.name}, workspace_name={self.workspace_name}, created_at={self.created_at}, h_metadata={self.h_metadata}, configuration={self.configuration})"
|
||||
|
||||
|
||||
@final
|
||||
class Session(Base):
|
||||
__tablename__ = "sessions"
|
||||
__tablename__: str = "sessions"
|
||||
id: Mapped[str] = mapped_column(TEXT, primary_key=True, default=generate_nanoid)
|
||||
name: Mapped[str] = mapped_column(TEXT, index=True)
|
||||
is_active: Mapped[bool] = mapped_column(default=True)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict] = mapped_column(
|
||||
h_metadata: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
|
||||
"internal_metadata", JSONB, default=dict
|
||||
)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
|
|
@ -164,7 +170,7 @@ class Session(Base):
|
|||
workspace_name: Mapped[str] = mapped_column(
|
||||
ForeignKey("workspaces.name"), index=True
|
||||
)
|
||||
configuration: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
configuration: Mapped[dict[str, Any]] = mapped_column(JSONB, default=dict)
|
||||
|
||||
peers = relationship(
|
||||
"Peer", secondary=session_peers_table, back_populates="sessions"
|
||||
|
|
@ -181,8 +187,9 @@ class Session(Base):
|
|||
return f"Session(id={self.id}, name={self.name}, workspace_name={self.workspace_name}, is_active={self.is_active}, created_at={self.created_at}, h_metadata={self.h_metadata})"
|
||||
|
||||
|
||||
@final
|
||||
class Message(Base):
|
||||
__tablename__ = "messages"
|
||||
__tablename__: str = "messages"
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, Identity(), primary_key=True, autoincrement=True
|
||||
)
|
||||
|
|
@ -191,8 +198,8 @@ class Message(Base):
|
|||
)
|
||||
session_name: Mapped[str | None] = mapped_column(index=True, nullable=True)
|
||||
content: Mapped[str] = mapped_column(TEXT)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict] = mapped_column(
|
||||
h_metadata: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
|
||||
"internal_metadata", JSONB, default=dict
|
||||
)
|
||||
token_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
|
||||
|
|
@ -234,26 +241,28 @@ class Message(Base):
|
|||
),
|
||||
)
|
||||
|
||||
@override
|
||||
def __repr__(self) -> str:
|
||||
return f"Message(id={self.id}, session_name={self.session_name}, peer_name={self.peer_name}, content={self.content})"
|
||||
|
||||
|
||||
@event.listens_for(Message, "before_insert")
|
||||
def calculate_token_count_on_insert(_mapper, _connection, target):
|
||||
def calculate_token_count_on_insert(_mapper: Any, _connection: Any, target: Message):
|
||||
"""Calculate token count before inserting a new message."""
|
||||
target.token_count = count_tokens(target.content)
|
||||
|
||||
|
||||
@final
|
||||
class Collection(Base):
|
||||
__tablename__ = "collections"
|
||||
__tablename__: str = "collections"
|
||||
|
||||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(TEXT, index=True)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), index=True, default=func.now()
|
||||
)
|
||||
h_metadata: Mapped[dict] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict] = mapped_column(
|
||||
h_metadata: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict)
|
||||
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
|
||||
"internal_metadata", JSONB, default=dict
|
||||
)
|
||||
documents = relationship(
|
||||
|
|
@ -280,14 +289,15 @@ class Collection(Base):
|
|||
)
|
||||
|
||||
|
||||
@final
|
||||
class Document(Base):
|
||||
__tablename__ = "documents"
|
||||
__tablename__: str = "documents"
|
||||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
internal_metadata: Mapped[dict] = mapped_column(
|
||||
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
|
||||
"internal_metadata", JSONB, default=dict
|
||||
)
|
||||
content: Mapped[str] = mapped_column(TEXT)
|
||||
embedding = mapped_column(Vector(1536))
|
||||
embedding: MappedColumn[Any] = mapped_column(Vector(1536))
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True), index=True, default=func.now()
|
||||
)
|
||||
|
|
@ -326,20 +336,22 @@ class Document(Base):
|
|||
)
|
||||
|
||||
|
||||
@final
|
||||
class QueueItem(Base):
|
||||
__tablename__ = "queue"
|
||||
__tablename__: str = "queue"
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, Identity(), primary_key=True, autoincrement=True
|
||||
)
|
||||
session_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("sessions.id"), index=True, nullable=True
|
||||
)
|
||||
payload: Mapped[dict] = mapped_column(JSONB, nullable=False)
|
||||
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
|
||||
processed: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
|
||||
@final
|
||||
class ActiveQueueSession(Base):
|
||||
__tablename__ = "active_queue_sessions"
|
||||
__tablename__: str = "active_queue_sessions"
|
||||
|
||||
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=True)
|
||||
session_id: Mapped[str | None] = mapped_column(
|
||||
|
|
@ -363,5 +375,15 @@ class ActiveQueueSession(Base):
|
|||
)
|
||||
|
||||
|
||||
@final
|
||||
class SessionPeer(Base):
|
||||
__table__ = session_peers_table
|
||||
__table__: Table = session_peers_table
|
||||
|
||||
# Type annotations for the columns
|
||||
workspace_name: Mapped[str]
|
||||
session_name: Mapped[str]
|
||||
peer_name: Mapped[str]
|
||||
configuration: Mapped[dict[str, Any]]
|
||||
internal_metadata: Mapped[dict[str, Any]]
|
||||
joined_at: Mapped[datetime.datetime]
|
||||
left_at: Mapped[datetime.datetime | None]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import logging
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Body, Depends, Path, Query
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.sql import insert
|
||||
|
||||
from src import crud, schemas
|
||||
|
|
@ -25,11 +26,11 @@ router = APIRouter(
|
|||
|
||||
|
||||
def create_processed_payload(
|
||||
message: dict,
|
||||
sender_name: Optional[str],
|
||||
target_name: Optional[str],
|
||||
message: dict[str, Any],
|
||||
sender_name: str | None,
|
||||
target_name: str | None,
|
||||
task_type: str,
|
||||
) -> dict:
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Create a processed payload from a message for queue processing.
|
||||
|
||||
|
|
@ -53,7 +54,7 @@ def create_processed_payload(
|
|||
return processed_payload
|
||||
|
||||
|
||||
async def enqueue(payload: list[dict]):
|
||||
async def enqueue(payload: list[dict[str, Any]]):
|
||||
"""
|
||||
Add message(s) to the deriver queue for processing.
|
||||
|
||||
|
|
@ -268,7 +269,7 @@ async def create_messages_for_session(
|
|||
messages: schemas.MessageBatchCreate = Body(
|
||||
..., description="Batch of messages to create"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
workspace_name, session_name = workspace_id, session_id
|
||||
"""Bulk create messages for a session while maintaining order. Maximum 100 messages per batch."""
|
||||
|
|
@ -310,13 +311,13 @@ async def create_messages_for_session(
|
|||
async def get_messages(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
options: Optional[schemas.MessageGet] = Body(
|
||||
options: schemas.MessageGet | None = Body(
|
||||
None, description="Filtering options for the messages list"
|
||||
),
|
||||
reverse: Optional[bool] = Query(
|
||||
reverse: bool | None = Query(
|
||||
False, description="Whether to reverse the order of results"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get all messages for a session"""
|
||||
try:
|
||||
|
|
@ -344,7 +345,7 @@ async def get_message(
|
|||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
message_id: str = Path(..., description="ID of the message to retrieve"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get a Message by ID"""
|
||||
honcho_message = await crud.get_message(
|
||||
|
|
@ -364,7 +365,7 @@ async def update_message(
|
|||
message: schemas.MessageUpdate = Body(
|
||||
..., description="Updated message parameters"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Update the metadata of a Message"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from anthropic import AsyncMessageStreamManager
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
BackgroundTasks,
|
||||
|
|
@ -14,6 +12,8 @@ from fastapi.exceptions import HTTPException
|
|||
from fastapi.responses import StreamingResponse
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from mirascope.llm import Stream
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import agent, crud, schemas
|
||||
from src.dependencies import db
|
||||
|
|
@ -39,10 +39,10 @@ router = APIRouter(
|
|||
)
|
||||
async def get_peers(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
options: Optional[schemas.PeerGet] = Body(
|
||||
options: schemas.PeerGet | None = Body(
|
||||
None, description="Filtering options for the peers list"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get All Peers for a Workspace"""
|
||||
filter_param = None
|
||||
|
|
@ -65,7 +65,7 @@ async def get_or_create_peer(
|
|||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer: schemas.PeerCreate = Body(..., description="Peer creation parameters"),
|
||||
jwt_params: JWTParams = Depends(require_auth()),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
Get a Peer by ID
|
||||
|
|
@ -102,7 +102,7 @@ async def update_peer(
|
|||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: str = Path(..., description="ID of the peer to update"),
|
||||
peer: schemas.PeerUpdate = Body(..., description="Updated peer parameters"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Update a Peer's name and/or metadata"""
|
||||
updated_peer = await crud.update_peer(
|
||||
|
|
@ -121,10 +121,10 @@ async def update_peer(
|
|||
async def get_sessions_for_peer(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: str = Path(..., description="ID of the peer"),
|
||||
options: Optional[schemas.SessionGet] = Body(
|
||||
options: schemas.SessionGet | None = Body(
|
||||
None, description="Filtering options for the sessions list"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get All Sessions for a Peer"""
|
||||
filter_param = None
|
||||
|
|
@ -168,7 +168,7 @@ async def chat(
|
|||
options: schemas.DialecticOptions = Body(
|
||||
..., description="Dialectic Endpoint Parameters"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
# Get or create the peer to ensure it exists
|
||||
await crud.get_or_create_peers(
|
||||
|
|
@ -189,10 +189,11 @@ async def chat(
|
|||
options.queries,
|
||||
stream=True,
|
||||
)
|
||||
if isinstance(stream, AsyncMessageStreamManager):
|
||||
async with stream as stream_manager:
|
||||
async for text in stream_manager.text_stream:
|
||||
yield text
|
||||
if isinstance(stream, Stream):
|
||||
async for chunk, _ in stream:
|
||||
yield chunk.content
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Invalid stream type")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in stream: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
|
|
@ -216,7 +217,7 @@ async def create_messages_for_peer(
|
|||
messages: schemas.MessageBatchCreate = Body(
|
||||
..., description="Batch of messages to create"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Create messages for a peer"""
|
||||
workspace_name, peer_name = workspace_id, peer_id
|
||||
|
|
@ -263,13 +264,13 @@ async def create_messages_for_peer(
|
|||
async def get_messages_for_peer(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: str = Path(..., description="ID of the peer"),
|
||||
options: Optional[schemas.MessageGet] = Body(
|
||||
options: schemas.MessageGet | None = Body(
|
||||
None, description="Filtering options for the messages list"
|
||||
),
|
||||
reverse: Optional[bool] = Query(
|
||||
reverse: bool | None = Query(
|
||||
False, description="Whether to reverse the order of results"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get all messages for a peer"""
|
||||
try:
|
||||
|
|
@ -305,7 +306,7 @@ async def get_working_representation(
|
|||
options: schemas.PeerRepresentationGet = Body(
|
||||
..., description="Options for getting the peer representation"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get a peer's working representation for a session.
|
||||
|
||||
|
|
@ -331,7 +332,7 @@ async def search_peer(
|
|||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: str = Path(..., description="ID of the peer"),
|
||||
query: str = Body(..., description="Search query"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Search a Peer"""
|
||||
stmt = await crud.search(query, workspace_name=workspace_id, peer_name=peer_id)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path, Query, Response
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, schemas
|
||||
from src.dependencies import db
|
||||
|
|
@ -33,7 +33,7 @@ async def get_or_create_session(
|
|||
..., description="Session creation parameters"
|
||||
),
|
||||
jwt_params: JWTParams = Depends(require_auth()),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
Get a specific session in a workspace.
|
||||
|
|
@ -77,10 +77,10 @@ async def get_or_create_session(
|
|||
)
|
||||
async def get_sessions(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
options: Optional[schemas.SessionGet] = Body(
|
||||
options: schemas.SessionGet | None = Body(
|
||||
None, description="Filtering and pagination options for the sessions list"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get All Sessions in a Workspace"""
|
||||
filter_param = None
|
||||
|
|
@ -117,7 +117,7 @@ async def update_session(
|
|||
session: schemas.SessionUpdate = Body(
|
||||
..., description="Updated session parameters"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Update the metadata of a Session"""
|
||||
try:
|
||||
|
|
@ -140,7 +140,7 @@ async def update_session(
|
|||
async def delete_session(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session to delete"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Delete a session by marking it as inactive"""
|
||||
try:
|
||||
|
|
@ -164,8 +164,8 @@ async def delete_session(
|
|||
async def clone_session(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session to clone"),
|
||||
db=db,
|
||||
message_id: Optional[str] = Query(
|
||||
db: AsyncSession = db,
|
||||
message_id: str | None = Query(
|
||||
None, description="Message ID to cut off the clone at"
|
||||
),
|
||||
):
|
||||
|
|
@ -198,23 +198,22 @@ async def add_peers_to_session(
|
|||
peers: dict[str, schemas.SessionPeerConfig] = Body(
|
||||
..., description="List of peer IDs to add to the session"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Add peers to a session"""
|
||||
try:
|
||||
workspace_name, session_name = workspace_id, session_id
|
||||
session = await crud.get_or_create_session(
|
||||
db,
|
||||
session=schemas.SessionCreate(
|
||||
name=session_name,
|
||||
name=session_id,
|
||||
peers=peers,
|
||||
),
|
||||
workspace_name=workspace_name,
|
||||
workspace_name=workspace_id,
|
||||
)
|
||||
logger.info(f"Added peers to session {session_name} successfully")
|
||||
logger.info(f"Added peers to session {session_id} successfully")
|
||||
return session
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to add peers to session {session_name}: {str(e)}")
|
||||
logger.warning(f"Failed to add peers to session {session_id}: {str(e)}")
|
||||
raise ResourceNotFoundException("Session not found") from e
|
||||
|
||||
|
||||
|
|
@ -231,27 +230,26 @@ async def set_session_peers(
|
|||
peers: dict[str, schemas.SessionPeerConfig] = Body(
|
||||
..., description="List of peer IDs to set for the session"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Set the peers in a session"""
|
||||
try:
|
||||
workspace_name, session_name = workspace_id, session_id
|
||||
await crud.set_peers_for_session(
|
||||
db,
|
||||
workspace_name=workspace_name,
|
||||
session_name=session_name,
|
||||
workspace_name=workspace_id,
|
||||
session_name=session_id,
|
||||
peer_names=peers,
|
||||
)
|
||||
# Get the session to return
|
||||
session = await crud.get_or_create_session(
|
||||
db,
|
||||
session=schemas.SessionCreate(name=session_name),
|
||||
workspace_name=workspace_name,
|
||||
session=schemas.SessionCreate(name=session_id),
|
||||
workspace_name=workspace_id,
|
||||
)
|
||||
logger.info(f"Set peers for session {session_name} successfully")
|
||||
logger.info(f"Set peers for session {session_id} successfully")
|
||||
return session
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to set peers for session {session_name}: {str(e)}")
|
||||
logger.warning(f"Failed to set peers for session {session_id}: {str(e)}")
|
||||
raise ResourceNotFoundException("Failed to set peers for session") from e
|
||||
|
||||
|
||||
|
|
@ -268,27 +266,26 @@ async def remove_peers_from_session(
|
|||
peers: list[str] = Body(
|
||||
..., description="List of peer IDs to remove from the session"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Remove peers from a session"""
|
||||
try:
|
||||
session_name = session_id
|
||||
await crud.remove_peers_from_session(
|
||||
db,
|
||||
workspace_name=workspace_id,
|
||||
session_name=session_name,
|
||||
session_name=session_id,
|
||||
peer_names=set(peers),
|
||||
)
|
||||
# Get the session to return
|
||||
session = await crud.get_or_create_session(
|
||||
db,
|
||||
session=schemas.SessionCreate(name=session_name),
|
||||
session=schemas.SessionCreate(name=session_id),
|
||||
workspace_name=workspace_id,
|
||||
)
|
||||
logger.info(f"Removed peers from session {session_name} successfully")
|
||||
logger.info(f"Removed peers from session {session_id} successfully")
|
||||
return session
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to remove peers from session {session_name}: {str(e)}")
|
||||
logger.warning(f"Failed to remove peers from session {session_id}: {str(e)}")
|
||||
raise ResourceNotFoundException("Session not found") from e
|
||||
|
||||
|
||||
|
|
@ -303,7 +300,7 @@ async def get_peer_config(
|
|||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
peer_id: str = Path(..., description="ID of the peer"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get the configuration for a peer in a session"""
|
||||
return await crud.get_peer_config(
|
||||
|
|
@ -325,7 +322,7 @@ async def set_peer_config(
|
|||
session_id: str = Path(..., description="ID of the session"),
|
||||
peer_id: str = Path(..., description="ID of the peer"),
|
||||
config: schemas.SessionPeerConfig = Body(..., description="Peer configuration"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Set the configuration for a peer in a session"""
|
||||
try:
|
||||
|
|
@ -357,17 +354,16 @@ async def set_peer_config(
|
|||
async def get_session_peers(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get peers from a session"""
|
||||
try:
|
||||
session_name = session_id
|
||||
peers_query = await crud.get_peers_from_session(
|
||||
workspace_name=workspace_id, session_name=session_name
|
||||
workspace_name=workspace_id, session_name=session_id
|
||||
)
|
||||
return await paginate(db, peers_query)
|
||||
except ValueError as e:
|
||||
logger.warning(f"Failed to get peers from session {session_name}: {str(e)}")
|
||||
logger.warning(f"Failed to get peers from session {session_id}: {str(e)}")
|
||||
raise ResourceNotFoundException("Session not found") from e
|
||||
|
||||
|
||||
|
|
@ -381,7 +377,7 @@ async def get_session_peers(
|
|||
async def get_session_context(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
tokens: Optional[int] = Query(
|
||||
tokens: int | None = Query(
|
||||
None,
|
||||
description="Number of tokens to use for the context. Includes summary if set to true",
|
||||
),
|
||||
|
|
@ -389,7 +385,7 @@ async def get_session_context(
|
|||
False,
|
||||
description="Whether to summarize the session history prior to the cutoff message",
|
||||
), # default to false
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
Produce a context object from the session. The caller provides a token limit which the entire context must fit into.
|
||||
|
|
@ -449,7 +445,7 @@ async def get_session_context(
|
|||
|
||||
return schemas.SessionContext(
|
||||
name=session_id,
|
||||
messages=messages,
|
||||
messages=messages, # pyright: ignore
|
||||
summary=summary_content,
|
||||
)
|
||||
|
||||
|
|
@ -465,7 +461,7 @@ async def search_session(
|
|||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
session_id: str = Path(..., description="ID of the session"),
|
||||
query: str = Body(..., description="Search query"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Search a Session"""
|
||||
stmt = await crud.search(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, schemas
|
||||
from src.dependencies import db
|
||||
|
|
@ -24,7 +24,7 @@ async def get_or_create_workspace(
|
|||
..., description="Workspace creation parameters"
|
||||
),
|
||||
jwt_params: JWTParams = Depends(require_auth()),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""
|
||||
Get a Workspace by ID.
|
||||
|
|
@ -53,10 +53,10 @@ async def get_or_create_workspace(
|
|||
dependencies=[Depends(require_auth(admin=True))],
|
||||
)
|
||||
async def get_all_workspaces(
|
||||
options: Optional[schemas.WorkspaceGet] = Body(
|
||||
options: schemas.WorkspaceGet | None = Body(
|
||||
None, description="Filtering and pagination options for the workspaces list"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get all Workspaces"""
|
||||
filter_param = None
|
||||
|
|
@ -81,7 +81,7 @@ async def update_workspace(
|
|||
workspace: schemas.WorkspaceUpdate = Body(
|
||||
..., description="Updated workspace parameters"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Update a Workspace"""
|
||||
# ResourceNotFoundException will be caught by global handler if workspace not found
|
||||
|
|
@ -99,7 +99,7 @@ async def update_workspace(
|
|||
async def search_workspace(
|
||||
workspace_id: str = Path(..., description="ID of the workspace to search"),
|
||||
query: str = Body(..., description="Search query"),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Search a Workspace"""
|
||||
stmt = await crud.search(query, workspace_name=workspace_id)
|
||||
|
|
@ -114,14 +114,14 @@ async def search_workspace(
|
|||
)
|
||||
async def get_deriver_status(
|
||||
workspace_id: str = Path(..., description="ID of the workspace"),
|
||||
peer_id: Optional[str] = Query(None, description="Optional peer ID to filter by"),
|
||||
session_id: Optional[str] = Query(
|
||||
peer_id: str | None = Query(None, description="Optional peer ID to filter by"),
|
||||
session_id: str | None = Query(
|
||||
None, description="Optional session ID to filter by"
|
||||
),
|
||||
include_sender: bool = Query(
|
||||
False, description="Include work units triggered by this peer"
|
||||
),
|
||||
db=db,
|
||||
db: AsyncSession = db,
|
||||
):
|
||||
"""Get the deriver processing status, optionally scoped to a peer and/or session"""
|
||||
# Validate that at least one of peer_id or session_id is provided
|
||||
|
|
|
|||
155
src/schemas.py
155
src/schemas.py
|
|
@ -1,5 +1,6 @@
|
|||
# pyright: reportUnannotatedClassAttribute=false # pyright: ignore
|
||||
import datetime
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated, Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
|
@ -15,28 +16,32 @@ class WorkspaceCreate(WorkspaceBase):
|
|||
str,
|
||||
Field(alias="id", min_length=1, max_length=100, pattern=RESOURCE_NAME_PATTERN),
|
||||
]
|
||||
metadata: dict = {}
|
||||
configuration: dict = {}
|
||||
metadata: dict[str, Any] = {}
|
||||
configuration: dict[str, Any] = {}
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
model_config = ConfigDict(populate_by_name=True) # pyright: ignore
|
||||
|
||||
|
||||
class WorkspaceGet(WorkspaceBase):
|
||||
filter: dict | None = None
|
||||
filter: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class WorkspaceUpdate(WorkspaceBase):
|
||||
metadata: dict | None = None
|
||||
configuration: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class Workspace(WorkspaceBase):
|
||||
name: str = Field(serialization_alias="id")
|
||||
h_metadata: dict = Field(default_factory=dict, serialization_alias="metadata")
|
||||
configuration: dict = Field(default_factory=dict)
|
||||
h_metadata: dict[str, Any] = Field(
|
||||
default_factory=dict, serialization_alias="metadata"
|
||||
)
|
||||
configuration: dict[str, Any] = Field(default_factory=dict)
|
||||
created_at: datetime.datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class PeerBase(BaseModel):
|
||||
|
|
@ -48,36 +53,40 @@ class PeerCreate(PeerBase):
|
|||
str,
|
||||
Field(alias="id", min_length=1, max_length=100, pattern=RESOURCE_NAME_PATTERN),
|
||||
]
|
||||
metadata: dict | None = None
|
||||
configuration: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: dict[str, Any] | None = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
model_config = ConfigDict(populate_by_name=True) # pyright: ignore
|
||||
|
||||
|
||||
class PeerGet(PeerBase):
|
||||
filter: dict | None = None
|
||||
filter: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class PeerUpdate(PeerBase):
|
||||
metadata: dict | None = None
|
||||
configuration: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class Peer(PeerBase):
|
||||
name: str = Field(serialization_alias="id")
|
||||
workspace_name: str = Field(serialization_alias="workspace_id")
|
||||
created_at: datetime.datetime
|
||||
h_metadata: dict = Field(default_factory=dict, serialization_alias="metadata")
|
||||
configuration: dict = Field(default_factory=dict)
|
||||
h_metadata: dict[str, Any] = Field(
|
||||
default_factory=dict, serialization_alias="metadata"
|
||||
)
|
||||
configuration: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class PeerRepresentationGet(BaseModel):
|
||||
session_id: str = Field(
|
||||
..., description="Get the working representation within this session"
|
||||
)
|
||||
target: Optional[str] = Field(
|
||||
target: str | None = Field(
|
||||
None,
|
||||
description="Optional peer ID to get the representation for, from the perspective of this peer",
|
||||
)
|
||||
|
|
@ -97,15 +106,15 @@ class MessageBase(BaseModel):
|
|||
class MessageCreate(MessageBase):
|
||||
content: Annotated[str, Field(min_length=0, max_length=50000)]
|
||||
peer_name: str = Field(alias="peer_id")
|
||||
metadata: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class MessageGet(MessageBase):
|
||||
filter: dict | None = None
|
||||
filter: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class MessageUpdate(MessageBase):
|
||||
metadata: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class Message(MessageBase):
|
||||
|
|
@ -113,12 +122,16 @@ class Message(MessageBase):
|
|||
content: str
|
||||
peer_name: str = Field(serialization_alias="peer_id")
|
||||
session_name: str | None = Field(serialization_alias="session_id")
|
||||
h_metadata: dict = Field(default_factory=dict, serialization_alias="metadata")
|
||||
h_metadata: dict[str, Any] = Field(
|
||||
default_factory=dict, serialization_alias="metadata"
|
||||
)
|
||||
created_at: datetime.datetime
|
||||
workspace_name: str = Field(serialization_alias="workspace_id")
|
||||
token_count: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class MessageBatchCreate(BaseModel):
|
||||
|
|
@ -147,32 +160,36 @@ class SessionCreate(SessionBase):
|
|||
str,
|
||||
Field(alias="id", min_length=1, max_length=100, pattern=RESOURCE_NAME_PATTERN),
|
||||
]
|
||||
metadata: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
peer_names: dict[str, SessionPeerConfig] | None = Field(default=None, alias="peers")
|
||||
configuration: dict | None = None
|
||||
configuration: dict[str, Any] | None = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
model_config = ConfigDict(populate_by_name=True) # pyright: ignore
|
||||
|
||||
|
||||
class SessionGet(SessionBase):
|
||||
filter: dict | None = None
|
||||
filter: dict[str, Any] | None = None
|
||||
is_active: bool = False
|
||||
|
||||
|
||||
class SessionUpdate(SessionBase):
|
||||
metadata: dict | None = None
|
||||
configuration: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
configuration: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class Session(SessionBase):
|
||||
name: str = Field(serialization_alias="id")
|
||||
is_active: bool
|
||||
workspace_name: str = Field(serialization_alias="workspace_id")
|
||||
h_metadata: dict = Field(default_factory=dict, serialization_alias="metadata")
|
||||
configuration: dict = Field(default_factory=dict)
|
||||
h_metadata: dict[str, Any] = Field(
|
||||
default_factory=dict, serialization_alias="metadata"
|
||||
)
|
||||
configuration: dict[str, Any] = Field(default_factory=dict)
|
||||
created_at: datetime.datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class SessionContext(SessionBase):
|
||||
|
|
@ -180,7 +197,9 @@ class SessionContext(SessionBase):
|
|||
messages: list[Message]
|
||||
summary: str
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
||||
model_config = ConfigDict( # pyright: ignore
|
||||
from_attributes=True, populate_by_name=True
|
||||
)
|
||||
|
||||
|
||||
class DocumentBase(BaseModel):
|
||||
|
|
@ -189,19 +208,19 @@ class DocumentBase(BaseModel):
|
|||
|
||||
class DocumentCreate(DocumentBase):
|
||||
content: Annotated[str, Field(min_length=1, max_length=100000)]
|
||||
metadata: dict = {}
|
||||
metadata: dict[str, Any] = {}
|
||||
|
||||
|
||||
class DocumentUpdate(DocumentBase):
|
||||
content: Annotated[str, Field(min_length=1, max_length=100000)]
|
||||
metadata: dict | None = None
|
||||
metadata: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class DialecticOptions(BaseModel):
|
||||
session_id: Optional[str] = Field(
|
||||
session_id: str | None = Field(
|
||||
None, description="ID of the session to scope the representation to"
|
||||
)
|
||||
target: Optional[str] = Field(
|
||||
target: str | None = Field(
|
||||
None,
|
||||
description="Optional peer to get the representation for, from the perspective of this peer",
|
||||
)
|
||||
|
|
@ -209,13 +228,13 @@ class DialecticOptions(BaseModel):
|
|||
stream: bool = False
|
||||
|
||||
@field_validator("queries")
|
||||
def validate_queries(cls, v):
|
||||
def validate_queries(cls, v: str | list[str]) -> str | list[str]:
|
||||
MAX_STRING_LENGTH = 10000
|
||||
MAX_LIST_LENGTH = 25
|
||||
if isinstance(v, str):
|
||||
if len(v) > MAX_STRING_LENGTH:
|
||||
raise ValueError("Query too long")
|
||||
elif isinstance(v, list):
|
||||
else:
|
||||
if len(v) > MAX_LIST_LENGTH:
|
||||
raise ValueError("Too many queries")
|
||||
if any(len(q) > MAX_STRING_LENGTH for q in v):
|
||||
|
|
@ -227,12 +246,60 @@ class DialecticResponse(BaseModel):
|
|||
content: str
|
||||
|
||||
|
||||
class SessionCounts(BaseModel):
|
||||
"""Counts for a specific session in queue processing."""
|
||||
completed: int
|
||||
in_progress: int
|
||||
pending: int
|
||||
|
||||
|
||||
class QueueCounts(BaseModel):
|
||||
"""Aggregated counts for queue processing status."""
|
||||
total: int
|
||||
completed: int
|
||||
in_progress: int
|
||||
pending: int
|
||||
sessions: dict[str, SessionCounts]
|
||||
|
||||
|
||||
class QueueStatusRow(BaseModel):
|
||||
"""Represents a row from the queue status SQL query result."""
|
||||
session_id: str | None
|
||||
total: int
|
||||
completed: int
|
||||
in_progress: int
|
||||
pending: int
|
||||
session_total: int
|
||||
session_completed: int
|
||||
session_in_progress: int
|
||||
session_pending: int
|
||||
|
||||
|
||||
class PeerConfigResult(BaseModel):
|
||||
"""Result from querying peer configuration data."""
|
||||
peer_name: str
|
||||
peer_configuration: dict[str, Any]
|
||||
session_peer_configuration: dict[str, Any]
|
||||
|
||||
|
||||
class SessionPeerData(BaseModel):
|
||||
"""Data for managing session peer relationships."""
|
||||
peer_names: dict[str, SessionPeerConfig]
|
||||
|
||||
|
||||
class MessageBulkData(BaseModel):
|
||||
"""Data for bulk message operations."""
|
||||
messages: list[MessageCreate]
|
||||
session_name: str
|
||||
workspace_name: str
|
||||
|
||||
|
||||
class DeriverStatus(BaseModel):
|
||||
peer_id: Optional[str] = Field(
|
||||
peer_id: str | None = Field(
|
||||
default=None,
|
||||
description="ID of the peer (optional when filtering by session only)",
|
||||
)
|
||||
session_id: Optional[str] = Field(
|
||||
session_id: str | None = Field(
|
||||
default=None, description="Session ID if filtered by session"
|
||||
)
|
||||
total_work_units: int = Field(description="Total work units")
|
||||
|
|
@ -241,6 +308,6 @@ class DeriverStatus(BaseModel):
|
|||
description="Work units currently being processed"
|
||||
)
|
||||
pending_work_units: int = Field(description="Work units waiting to be processed")
|
||||
sessions: Optional[dict[str, "DeriverStatus"]] = Field(
|
||||
sessions: dict[str, "DeriverStatus"] | None = Field(
|
||||
default=None, description="Per-session status when not filtered by session"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import datetime
|
||||
import logging
|
||||
from typing import Annotated, Optional
|
||||
from typing import Annotated
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.config import settings
|
||||
from src.dependencies import get_db
|
||||
|
||||
from .exceptions import AuthenticationException
|
||||
|
||||
|
|
@ -57,11 +55,11 @@ class JWTParams(BaseModel):
|
|||
"""
|
||||
|
||||
t: str = datetime.datetime.now().isoformat()
|
||||
exp: Optional[str] = None
|
||||
ad: Optional[bool] = None
|
||||
w: Optional[str] = None
|
||||
p: Optional[str] = None
|
||||
s: Optional[str] = None
|
||||
exp: str | None = None
|
||||
ad: bool | None = None
|
||||
w: str | None = None
|
||||
p: str | None = None
|
||||
s: str | None = None
|
||||
|
||||
|
||||
def create_admin_jwt() -> str:
|
||||
|
|
@ -76,7 +74,9 @@ def create_jwt(params: JWTParams) -> str:
|
|||
payload = {k: v for k, v in params.__dict__.items() if v is not None}
|
||||
if not settings.AUTH.JWT_SECRET:
|
||||
raise ValueError("AUTH_JWT_SECRET is not set, cannot create JWT.")
|
||||
return jwt.encode(payload, settings.AUTH.JWT_SECRET.encode("utf-8"), algorithm="HS256")
|
||||
return jwt.encode( # pyright: ignore
|
||||
payload, settings.AUTH.JWT_SECRET.encode("utf-8"), algorithm="HS256"
|
||||
)
|
||||
|
||||
|
||||
async def verify_jwt(token: str) -> JWTParams:
|
||||
|
|
@ -86,7 +86,7 @@ async def verify_jwt(token: str) -> JWTParams:
|
|||
try:
|
||||
if not settings.AUTH.JWT_SECRET:
|
||||
raise ValueError("AUTH_JWT_SECRET is not set, cannot verify JWT.")
|
||||
decoded = jwt.decode(
|
||||
decoded = jwt.decode( # pyright: ignore
|
||||
token, settings.AUTH.JWT_SECRET.encode("utf-8"), algorithms=["HS256"]
|
||||
)
|
||||
if "t" in decoded:
|
||||
|
|
@ -113,10 +113,10 @@ async def verify_jwt(token: str) -> JWTParams:
|
|||
|
||||
|
||||
def require_auth(
|
||||
admin: Optional[bool] = None,
|
||||
workspace_name: Optional[str] = None,
|
||||
peer_name: Optional[str] = None,
|
||||
session_name: Optional[str] = None,
|
||||
admin: bool | None = None,
|
||||
workspace_name: str | None = None,
|
||||
peer_name: str | None = None,
|
||||
session_name: str | None = None,
|
||||
):
|
||||
"""
|
||||
Generate a dependency that requires authentication for the given parameters.
|
||||
|
|
@ -125,7 +125,6 @@ def require_auth(
|
|||
async def auth_dependency(
|
||||
request: Request,
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
workspace_name_param = (
|
||||
request.path_params.get(workspace_name)
|
||||
|
|
@ -158,10 +157,10 @@ def require_auth(
|
|||
|
||||
async def auth(
|
||||
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
|
||||
admin: Optional[bool] = None,
|
||||
workspace_name: Optional[str] = None,
|
||||
peer_name: Optional[str] = None,
|
||||
session_name: Optional[str] = None,
|
||||
admin: bool | None = None,
|
||||
workspace_name: str | None = None,
|
||||
peer_name: str | None = None,
|
||||
session_name: str | None = None,
|
||||
) -> JWTParams:
|
||||
"""Authenticate the given JWT and return the decoded parameters."""
|
||||
if not settings.AUTH.USE_AUTH:
|
||||
|
|
|
|||
|
|
@ -1,99 +0,0 @@
|
|||
import asyncio
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class LRUCache:
|
||||
"""
|
||||
A simple asyncio-safe LRU cache implementation.
|
||||
|
||||
This cache automatically evicts the least recently used items when it reaches
|
||||
its capacity limit. All operations are protected by an asyncio lock for
|
||||
concurrent access safety.
|
||||
"""
|
||||
|
||||
def __init__(self, capacity=100):
|
||||
"""
|
||||
Initialize the LRU cache.
|
||||
|
||||
Args:
|
||||
capacity (int): Maximum number of items to store in the cache
|
||||
"""
|
||||
self._cache = OrderedDict()
|
||||
self._capacity = max(1, capacity)
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
|
||||
async def get(self, key):
|
||||
"""
|
||||
Get a value from the cache.
|
||||
|
||||
Args:
|
||||
key: The key to look up
|
||||
|
||||
Returns:
|
||||
The cached value or None if not found
|
||||
"""
|
||||
async with self._lock:
|
||||
if key not in self._cache:
|
||||
return None
|
||||
|
||||
# Move accessed item to the end to mark as most recently used
|
||||
value = self._cache.pop(key)
|
||||
self._cache[key] = value
|
||||
return value
|
||||
|
||||
async def put(self, key, value):
|
||||
"""
|
||||
Add or update an entry in the cache.
|
||||
|
||||
Args:
|
||||
key: The key to store
|
||||
value: The value to store
|
||||
|
||||
Returns:
|
||||
The value that was stored
|
||||
"""
|
||||
async with self._lock:
|
||||
# If key already exists, remove it first to update its position
|
||||
if key in self._cache:
|
||||
self._cache.pop(key)
|
||||
|
||||
# If at capacity, remove the least recently used item
|
||||
elif len(self._cache) >= self._capacity:
|
||||
self._cache.popitem(last=False)
|
||||
|
||||
# Add the new item
|
||||
self._cache[key] = value
|
||||
return value
|
||||
|
||||
async def delete(self, key):
|
||||
"""
|
||||
Remove an item from the cache.
|
||||
|
||||
Args:
|
||||
key: The key to remove
|
||||
|
||||
Returns:
|
||||
bool: True if the key was removed, False if it didn't exist
|
||||
"""
|
||||
async with self._lock:
|
||||
if key in self._cache:
|
||||
self._cache.pop(key)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def generate_cache_key(model_type: str, app_id: str, user_id: str, model_id: str):
|
||||
"""
|
||||
Generate a cache key for a given model type, app ID, user ID, and model ID.
|
||||
|
||||
Args:
|
||||
model_type (str): The type of model to generate a cache key for (e.g. "collection", "document", "message", "session", "user")
|
||||
app_id (str): The ID of the app to generate a cache key for
|
||||
user_id (str): The ID of the user to generate a cache key for
|
||||
model_id (str): The ID of the model to generate a cache key for
|
||||
|
||||
Returns:
|
||||
str: A cache key for the given model type, app ID, user ID, and model ID
|
||||
"""
|
||||
return f"{model_type}:{app_id}:{user_id}:{model_id}"
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
from anthropic import AsyncAnthropic
|
||||
from google import genai
|
||||
from groq import Groq
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
from src.config import settings
|
||||
from src.utils.types import Providers
|
||||
|
||||
clients: dict[Providers, AsyncAnthropic | AsyncOpenAI | genai.Client | Groq] = {}
|
||||
|
||||
if settings.LLM.ANTHROPIC_API_KEY:
|
||||
anthropic = AsyncAnthropic(api_key=settings.LLM.ANTHROPIC_API_KEY)
|
||||
clients["anthropic"] = anthropic
|
||||
|
||||
if settings.LLM.OPENAI_API_KEY:
|
||||
openai_client = AsyncOpenAI(
|
||||
api_key=settings.LLM.OPENAI_API_KEY,
|
||||
)
|
||||
clients["openai"] = openai_client
|
||||
|
||||
if settings.LLM.OPENAI_COMPATIBLE_BASE_URL:
|
||||
clients["custom"] = AsyncOpenAI(
|
||||
api_key=settings.LLM.OPENAI_COMPATIBLE_API_KEY,
|
||||
base_url=settings.LLM.OPENAI_COMPATIBLE_BASE_URL,
|
||||
)
|
||||
|
||||
if settings.LLM.GEMINI_API_KEY:
|
||||
google = genai.Client(api_key=settings.LLM.GEMINI_API_KEY)
|
||||
clients["google"] = google
|
||||
|
||||
if settings.LLM.GROQ_API_KEY:
|
||||
groq = Groq(api_key=settings.LLM.GROQ_API_KEY)
|
||||
clients["groq"] = groq
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
import datetime
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import Optional, TypedDict
|
||||
from typing import TypedDict
|
||||
|
||||
from mirascope import llm
|
||||
from mirascope.integrations.langfuse import with_langfuse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.config import settings
|
||||
from src.utils.model_client import ModelClient, ModelProvider
|
||||
from src.utils.clients import clients
|
||||
|
||||
from .. import crud, models
|
||||
|
||||
|
|
@ -58,9 +60,89 @@ class SummaryType(Enum):
|
|||
LONG = "honcho_chat_summary_long"
|
||||
|
||||
|
||||
# Default model settings for summary generation
|
||||
# DEFAULT_PROVIDER = ModelProvider.GEMINI
|
||||
# DEFAULT_MODEL = "gemini-2.0-flash-lite"
|
||||
# Mirascope functions for summaries
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.SUMMARY_PROVIDER
|
||||
if settings.LLM.SUMMARY_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.SUMMARY_MODEL,
|
||||
call_params={"max_tokens": 1000},
|
||||
client=clients[settings.LLM.SUMMARY_PROVIDER],
|
||||
)
|
||||
async def create_short_summary(
|
||||
messages: list[models.Message],
|
||||
previous_summary: str | None = None,
|
||||
):
|
||||
return f"""
|
||||
You are a system that summarizes parts of a conversation to create a concise and accurate summary.
|
||||
Focus on capturing:
|
||||
1. Key facts and information shared
|
||||
2. User preferences, opinions, and questions
|
||||
3. Important context and requests
|
||||
4. Core topics discussed
|
||||
5. User's apparent emotional state
|
||||
|
||||
It is very important that you clearly distinguish between the user's messages and the assistant's messages, and that only the user's literal words are attributed to them.
|
||||
|
||||
Provide a concise, factual summary that captures the essence of the conversation.
|
||||
Your summary should be detailed enough to serve as context for future messages,
|
||||
but brief enough to be helpful.
|
||||
|
||||
Return only the summary without any explanation or meta-commentary.
|
||||
|
||||
<conversation>
|
||||
{format_messages(messages)}
|
||||
</conversation>
|
||||
|
||||
<previous_summary>
|
||||
{previous_summary or ""}
|
||||
</previous_summary>
|
||||
"""
|
||||
|
||||
|
||||
@with_langfuse()
|
||||
@llm.call(
|
||||
provider=(
|
||||
settings.LLM.SUMMARY_PROVIDER
|
||||
if settings.LLM.SUMMARY_PROVIDER != "custom"
|
||||
else "openai"
|
||||
),
|
||||
model=settings.LLM.SUMMARY_MODEL,
|
||||
call_params={"max_tokens": 2000},
|
||||
client=clients[settings.LLM.SUMMARY_PROVIDER],
|
||||
)
|
||||
async def create_long_summary(
|
||||
messages: list[models.Message],
|
||||
previous_summary: str | None = None,
|
||||
):
|
||||
return f"""
|
||||
You are a system that creates comprehensive summaries of conversations.
|
||||
Focus on capturing:
|
||||
1. Key facts and information shared
|
||||
2. User preferences, opinions, and questions
|
||||
3. Important context and requests
|
||||
4. Core topics discussed in detail
|
||||
5. User's apparent emotional state and personality traits
|
||||
6. Important themes and patterns across the conversation
|
||||
|
||||
It is very important that you clearly distinguish between the user's messages and the assistant's messages, and that only the user's literal words are attributed to them.
|
||||
|
||||
Provide a thorough and detailed summary that captures the essence of the conversation.
|
||||
Your summary should serve as a comprehensive record of the important information in this conversation.
|
||||
|
||||
Return only the summary without any explanation or meta-commentary.
|
||||
|
||||
<conversation>
|
||||
{format_messages(messages)}
|
||||
</conversation>
|
||||
|
||||
<previous_summary>
|
||||
{previous_summary or ""}
|
||||
</previous_summary>
|
||||
"""
|
||||
|
||||
|
||||
async def get_summary(
|
||||
|
|
@ -68,7 +150,7 @@ async def get_summary(
|
|||
workspace_name: str,
|
||||
session_name: str,
|
||||
summary_type: SummaryType = SummaryType.SHORT,
|
||||
) -> Optional[Summary]:
|
||||
) -> Summary | None:
|
||||
"""
|
||||
Get summary for a given session or peer.
|
||||
|
||||
|
|
@ -95,7 +177,7 @@ async def get_summary(
|
|||
# If session doesn't exist, there's no summary to retrieve
|
||||
return None
|
||||
|
||||
summaries = session.internal_metadata.get("summaries", {})
|
||||
summaries: dict[str, Summary] = session.internal_metadata.get("summaries", {})
|
||||
if not summaries or label not in summaries:
|
||||
return None
|
||||
return summaries[label]
|
||||
|
|
@ -103,9 +185,9 @@ async def get_summary(
|
|||
|
||||
async def create_summary(
|
||||
messages: list[models.Message],
|
||||
previous_summary_text: Optional[str] = None,
|
||||
previous_summary_text: str | None = None,
|
||||
summary_type: SummaryType = SummaryType.SHORT,
|
||||
max_tokens: Optional[int] = None,
|
||||
max_tokens: int | None = None,
|
||||
) -> Summary:
|
||||
"""
|
||||
Generate a summary of the provided messages using an LLM.
|
||||
|
|
@ -119,80 +201,17 @@ async def create_summary(
|
|||
Returns:
|
||||
A summary of the conversation
|
||||
"""
|
||||
# Combine messages into a conversation format string
|
||||
conversation = format_messages(messages)
|
||||
|
||||
# Adjust system prompt based on summary type
|
||||
if summary_type == SummaryType.LONG:
|
||||
system_prompt = """You are a system that creates comprehensive summaries of conversations.
|
||||
Focus on capturing:
|
||||
1. Key facts and information shared
|
||||
2. User preferences, opinions, and questions
|
||||
3. Important context and requests
|
||||
4. Core topics discussed in detail
|
||||
5. User's apparent emotional state and personality traits
|
||||
6. Important themes and patterns across the conversation
|
||||
|
||||
It is very important that you clearly distinguish between the user's messages and the assistant's messages, and that only the user's literal words are attributed to them.
|
||||
|
||||
Provide a thorough and detailed summary that captures the essence of the conversation.
|
||||
Your summary should serve as a comprehensive record of the important information in this conversation.
|
||||
|
||||
Return only the summary without any explanation or meta-commentary."""
|
||||
else: # short summary
|
||||
system_prompt = """You are a system that summarizes parts of a conversation to create a concise and accurate summary.
|
||||
Focus on capturing:
|
||||
1. Key facts and information shared
|
||||
2. User preferences, opinions, and questions
|
||||
3. Important context and requests
|
||||
4. Core topics discussed
|
||||
5. User's apparent emotional state
|
||||
|
||||
It is very important that you clearly distinguish between the user's messages and the assistant's messages, and that only the user's literal words are attributed to them.
|
||||
|
||||
Provide a concise, factual summary that captures the essence of the conversation.
|
||||
Your summary should be detailed enough to serve as context for future messages,
|
||||
but brief enough to be helpful.
|
||||
|
||||
Return only the summary without any explanation or meta-commentary."""
|
||||
|
||||
# Include previous summary if available
|
||||
if previous_summary_text:
|
||||
user_prompt = f"""Here is a previous summary of the conversation:
|
||||
{previous_summary_text}
|
||||
Now please summarize these additional messages, incorporating the context from the previous summary.
|
||||
|
||||
Your summary should summarize the entire conversation in a self-contained way, such that someone could read it and understand the entire conversation.
|
||||
{conversation}
|
||||
Provide a {"comprehensive" if summary_type == SummaryType.LONG else "concise"} summary that captures both the previous context and the new information."""
|
||||
else:
|
||||
user_prompt = f"""Please summarize the following conversation segment:
|
||||
{conversation}
|
||||
Provide a {"comprehensive" if summary_type == SummaryType.LONG else "concise"} summary that captures the key points and context."""
|
||||
|
||||
# Create a model client
|
||||
client = ModelClient(
|
||||
provider=ModelProvider(settings.LLM.SUMMARY_PROVIDER),
|
||||
model=settings.LLM.SUMMARY_MODEL,
|
||||
)
|
||||
|
||||
# Generate the summary
|
||||
llm_messages = [{"role": "user", "content": user_prompt}]
|
||||
|
||||
max_tokens = max_tokens or (
|
||||
settings.LLM.SUMMARY_MAX_TOKENS_SHORT
|
||||
if summary_type == SummaryType.SHORT
|
||||
else settings.LLM.SUMMARY_MAX_TOKENS_LONG
|
||||
)
|
||||
|
||||
try:
|
||||
summary_text = await client.generate(
|
||||
messages=llm_messages,
|
||||
system=system_prompt,
|
||||
max_tokens=max_tokens,
|
||||
temperature=0.0,
|
||||
use_caching=True,
|
||||
if summary_type == SummaryType.SHORT:
|
||||
response = await create_short_summary(messages, previous_summary_text)
|
||||
else:
|
||||
response = await create_long_summary(messages, previous_summary_text)
|
||||
|
||||
summary_text = str(response)
|
||||
calculated_max_tokens = max_tokens or (
|
||||
1000 if summary_type == SummaryType.SHORT else 2000
|
||||
)
|
||||
|
||||
logger.info("Successfully generated summary for session")
|
||||
return Summary(
|
||||
content=summary_text,
|
||||
|
|
@ -200,15 +219,11 @@ Provide a {"comprehensive" if summary_type == SummaryType.LONG else "concise"} s
|
|||
summary_type=summary_type.value,
|
||||
created_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||
message_id=messages[-1].id,
|
||||
# cheating: just use the max tokens instead of counting
|
||||
# tokens in response. in the future we can update client
|
||||
# to return tokens if that's doable.
|
||||
token_count=max_tokens,
|
||||
token_count=calculated_max_tokens,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating summary: {str(e)}")
|
||||
# Fallback to a basic summary in case of error
|
||||
# Do not save this failed summary to the session metadata.
|
||||
return Summary(
|
||||
content=(
|
||||
f"Conversation with {len(messages)} messages about {messages[-1].content[:30]}..."
|
||||
|
|
@ -322,7 +337,7 @@ async def get_latest_summary_and_messages_since(
|
|||
peer_name: str,
|
||||
cutoff: int | None = None,
|
||||
summary_type: SummaryType = SummaryType.SHORT,
|
||||
) -> tuple[list[models.Message], Optional[Summary]]:
|
||||
) -> tuple[list[models.Message], Summary | None]:
|
||||
"""
|
||||
Get all messages since the latest summary for a session or peer.
|
||||
|
||||
|
|
@ -376,7 +391,7 @@ async def should_create_summary(
|
|||
peer_name: str,
|
||||
message_id: int,
|
||||
summary_type: SummaryType = SummaryType.SHORT,
|
||||
) -> tuple[bool, list[models.Message], Optional[Summary]]:
|
||||
) -> tuple[bool, list[models.Message], Summary | None]:
|
||||
"""
|
||||
Determine if a new summary should be created for this object (peer or session).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,591 +0,0 @@
|
|||
"""
|
||||
Utility functions for interacting with various language model APIs.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Protocol
|
||||
|
||||
import sentry_sdk
|
||||
from anthropic import AsyncAnthropic
|
||||
from google import genai
|
||||
from google.genai import types as genai_types
|
||||
from langfuse.decorators import langfuse_context, observe
|
||||
|
||||
# from openai import AsyncOpenAI
|
||||
from langfuse.openai import AsyncOpenAI
|
||||
|
||||
from src.config import settings
|
||||
|
||||
|
||||
# Supported model providers
|
||||
class ModelProvider(str, Enum):
|
||||
ANTHROPIC = "anthropic"
|
||||
OPENAI = "openai"
|
||||
OPENROUTER = "openrouter"
|
||||
CEREBRAS = "cerebras"
|
||||
GROQ = "groq"
|
||||
GEMINI = "gemini"
|
||||
# Add other providers as needed
|
||||
|
||||
|
||||
# Default models for each provider
|
||||
DEFAULT_MODELS = {
|
||||
ModelProvider.ANTHROPIC: "claude-3-7-sonnet-20250219",
|
||||
ModelProvider.OPENAI: "gpt-4o",
|
||||
ModelProvider.OPENROUTER: "meta-llama/Llama-3.3-70B-Instruct",
|
||||
ModelProvider.CEREBRAS: "llama-3.3-70b",
|
||||
ModelProvider.GROQ: "llama-3.3-70b-versatile",
|
||||
ModelProvider.GEMINI: "gemini-2.0-flash-lite",
|
||||
}
|
||||
|
||||
OPENAI_COMPATIBLE_PROVIDERS = [
|
||||
ModelProvider.OPENAI,
|
||||
ModelProvider.OPENROUTER,
|
||||
ModelProvider.CEREBRAS,
|
||||
ModelProvider.GROQ,
|
||||
]
|
||||
|
||||
DEFAULT_TEMPERATURE: float = settings.LLM.DEFAULT_TEMPERATURE
|
||||
DEFAULT_MAX_TOKENS: int = settings.LLM.DEFAULT_MAX_TOKENS
|
||||
|
||||
# Default embedding model
|
||||
DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small"
|
||||
|
||||
|
||||
class Message(Protocol):
|
||||
"""Protocol for a message that works with any provider."""
|
||||
|
||||
role: str
|
||||
content: str
|
||||
|
||||
|
||||
class ModelClient:
|
||||
"""A client for interacting with various language model APIs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
provider: ModelProvider = ModelProvider.ANTHROPIC,
|
||||
model: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the model client.
|
||||
|
||||
Args:
|
||||
provider: The model provider to use
|
||||
model: The specific model to use, or None to use the default model for the provider
|
||||
api_key: The API key to use, or None to read from environment variables
|
||||
base_url: Custom base URL for the API endpoints (used for OpenRouter)
|
||||
"""
|
||||
self.provider = provider
|
||||
self.model = model or DEFAULT_MODELS[provider]
|
||||
self.base_url = base_url
|
||||
self.openai_client = None
|
||||
self.gemini_client = None
|
||||
|
||||
# Setup provider-specific clients
|
||||
if provider == ModelProvider.ANTHROPIC:
|
||||
self.api_key = api_key or settings.LLM.ANTHROPIC_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("Anthropic API key is required")
|
||||
self.client = AsyncAnthropic(api_key=self.api_key)
|
||||
elif provider in OPENAI_COMPATIBLE_PROVIDERS:
|
||||
# Use specific API key based on provider
|
||||
if provider == ModelProvider.OPENAI:
|
||||
self.api_key = api_key or settings.LLM.OPENAI_API_KEY
|
||||
elif provider == ModelProvider.GROQ:
|
||||
self.api_key = api_key or settings.LLM.GROQ_API_KEY
|
||||
else:
|
||||
self.api_key = api_key or settings.LLM.OPENAI_COMPATIBLE_API_KEY
|
||||
|
||||
self.base_url = base_url or settings.LLM.OPENAI_COMPATIBLE_BASE_URL
|
||||
|
||||
if not self.api_key:
|
||||
raise ValueError(f"{provider.value} API key is required")
|
||||
|
||||
self.openai_client = AsyncOpenAI(
|
||||
api_key=self.api_key, base_url=self.base_url
|
||||
)
|
||||
elif provider == ModelProvider.GEMINI:
|
||||
self.api_key = api_key or settings.LLM.GEMINI_API_KEY
|
||||
if not self.api_key:
|
||||
raise ValueError("Gemini API key is required")
|
||||
self.gemini_client = genai.Client(api_key=self.api_key)
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {provider}")
|
||||
|
||||
def create_message(self, role: str, content: str) -> dict[str, str]:
|
||||
"""
|
||||
Create a message that works with the current provider.
|
||||
|
||||
Args:
|
||||
role: The role of the message (e.g., "user", "assistant")
|
||||
content: The message content
|
||||
|
||||
Returns:
|
||||
A message compatible with the current provider
|
||||
"""
|
||||
# For now, just return a dictionary that works with both Anthropic and OpenAI
|
||||
return {"role": role, "content": content}
|
||||
|
||||
@observe()
|
||||
async def generate(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
extra_headers: Optional[dict[str, str]] = None,
|
||||
use_caching: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a response using the configured model.
|
||||
|
||||
Args:
|
||||
messages: The conversation history
|
||||
system: Optional system prompt
|
||||
max_tokens: Maximum number of tokens to generate
|
||||
temperature: Temperature for generation
|
||||
extra_headers: Optional headers to add to the request
|
||||
use_caching: Whether to use provider-side caching for the response
|
||||
|
||||
Returns:
|
||||
The generated text
|
||||
"""
|
||||
with sentry_sdk.start_transaction(
|
||||
op="llm-api", name=f"{self.provider} API Call"
|
||||
):
|
||||
# Log to langfuse
|
||||
langfuse_context.update_current_observation(
|
||||
input=messages, model=self.model
|
||||
)
|
||||
|
||||
if self.provider == ModelProvider.ANTHROPIC:
|
||||
return await self._generate_anthropic(
|
||||
messages,
|
||||
system,
|
||||
max_tokens,
|
||||
temperature,
|
||||
extra_headers,
|
||||
use_caching,
|
||||
)
|
||||
elif self.provider in OPENAI_COMPATIBLE_PROVIDERS:
|
||||
return await self._generate_openai(
|
||||
messages, system, max_tokens, temperature
|
||||
)
|
||||
elif self.provider == ModelProvider.GEMINI:
|
||||
return await self._generate_gemini(
|
||||
messages, system, max_tokens, temperature
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {self.provider}")
|
||||
|
||||
@observe(as_type="generation")
|
||||
async def _generate_anthropic(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
extra_headers: Optional[dict[str, str]] = None,
|
||||
use_caching: bool = False,
|
||||
) -> str:
|
||||
"""Generate a response using the Anthropic API."""
|
||||
if not self.client:
|
||||
raise ValueError("Anthropic client not initialized.")
|
||||
|
||||
params = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
# Handle system prompt with caching if enabled
|
||||
if system:
|
||||
if use_caching:
|
||||
params["system"] = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": system,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
]
|
||||
else:
|
||||
params["system"] = system
|
||||
|
||||
langfuse_context.update_current_observation(input=messages, model=self.model)
|
||||
|
||||
response = await self.client.messages.create(**params)
|
||||
|
||||
# Extract the text from the response
|
||||
if response.content and len(response.content) > 0:
|
||||
content_block = response.content[0]
|
||||
# Check content_block by checking for attribute 'type' instead of using isinstance
|
||||
if (
|
||||
content_block
|
||||
and hasattr(content_block, "type")
|
||||
and content_block.type == "text"
|
||||
):
|
||||
return content_block.text
|
||||
return str(content_block)
|
||||
return ""
|
||||
|
||||
@observe(as_type="generation")
|
||||
async def _generate_openai(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
) -> str:
|
||||
"""Generate text using OpenAI or OpenRouter API."""
|
||||
if not self.openai_client:
|
||||
raise ValueError("OpenAI client not initialized")
|
||||
|
||||
# Prepare messages
|
||||
formatted_messages = []
|
||||
|
||||
# Add system message if provided
|
||||
if system:
|
||||
formatted_messages.append({"role": "system", "content": system})
|
||||
|
||||
# Add the rest of the messages
|
||||
formatted_messages.extend(messages)
|
||||
|
||||
# Make the API call with the OpenAI client
|
||||
response = await self.openai_client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=formatted_messages,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
)
|
||||
|
||||
# Extract the generated text
|
||||
choice = response.choices[0]
|
||||
if choice and choice.message and choice.message.content:
|
||||
return choice.message.content
|
||||
return ""
|
||||
|
||||
@observe()
|
||||
async def stream(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
extra_headers: Optional[dict[str, str]] = None,
|
||||
use_caching: bool = False,
|
||||
) -> Any:
|
||||
"""
|
||||
Stream a response using the configured model.
|
||||
|
||||
Args:
|
||||
messages: The conversation history
|
||||
system: Optional system prompt
|
||||
max_tokens: Maximum number of tokens to generate
|
||||
temperature: Temperature for generation
|
||||
extra_headers: Optional headers to add to the request
|
||||
use_caching: Whether to use provider-side caching for the response
|
||||
|
||||
Returns:
|
||||
A streaming response from the provider
|
||||
"""
|
||||
with sentry_sdk.start_transaction(
|
||||
op="llm-api-stream", name=f"{self.provider} API Stream"
|
||||
):
|
||||
# Log to langfuse
|
||||
langfuse_context.update_current_observation(
|
||||
input=messages, model=self.model
|
||||
)
|
||||
|
||||
if self.provider == ModelProvider.ANTHROPIC:
|
||||
return await self._stream_anthropic(
|
||||
messages,
|
||||
system,
|
||||
max_tokens,
|
||||
temperature,
|
||||
extra_headers,
|
||||
use_caching,
|
||||
)
|
||||
elif self.provider in OPENAI_COMPATIBLE_PROVIDERS:
|
||||
return await self._stream_openai(
|
||||
messages, system, max_tokens, temperature
|
||||
)
|
||||
elif self.provider == ModelProvider.GEMINI:
|
||||
return await self._stream_gemini(
|
||||
messages, system, max_tokens, temperature
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider: {self.provider}")
|
||||
|
||||
@observe(as_type="generation")
|
||||
async def _stream_anthropic(
|
||||
self,
|
||||
messages: list[dict[str, Any]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
extra_headers: Optional[dict[str, str]] = None,
|
||||
use_caching: bool = False,
|
||||
) -> Any:
|
||||
"""Stream text using Anthropic API."""
|
||||
if not self.client:
|
||||
raise ValueError("Anthropic client not initialized.")
|
||||
|
||||
params = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
"max_tokens": max_tokens,
|
||||
"temperature": temperature,
|
||||
}
|
||||
|
||||
# Handle system prompt with caching if enabled
|
||||
if system:
|
||||
if use_caching:
|
||||
params["system"] = [
|
||||
{
|
||||
"type": "text",
|
||||
"text": system,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
]
|
||||
else:
|
||||
params["system"] = system
|
||||
|
||||
langfuse_context.update_current_observation(input=messages, model=self.model)
|
||||
|
||||
# Return the stream directly without awaiting it
|
||||
return self.client.messages.stream(**params)
|
||||
|
||||
@observe(as_type="generation")
|
||||
async def _stream_openai(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
) -> Any:
|
||||
"""Stream text using OpenAI or OpenRouter API."""
|
||||
if not self.openai_client:
|
||||
raise ValueError("OpenAI client not initialized")
|
||||
|
||||
# Prepare messages
|
||||
formatted_messages = []
|
||||
|
||||
# Add system message if provided
|
||||
if system:
|
||||
formatted_messages.append({"role": "system", "content": system})
|
||||
|
||||
# Add the rest of the messages
|
||||
formatted_messages.extend(messages)
|
||||
|
||||
# Make the API call with the OpenAI client
|
||||
stream = await self.openai_client.chat.completions.create(
|
||||
model=self.model,
|
||||
messages=formatted_messages,
|
||||
max_tokens=max_tokens,
|
||||
temperature=temperature,
|
||||
stream=True,
|
||||
)
|
||||
|
||||
return stream
|
||||
|
||||
@observe(as_type="generation")
|
||||
async def _generate_gemini(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
) -> str:
|
||||
"""Generate text using Gemini API."""
|
||||
if not self.gemini_client:
|
||||
raise ValueError("Gemini client not initialized")
|
||||
|
||||
# Format messages for Gemini
|
||||
gemini_messages = []
|
||||
|
||||
# Convert messages to Gemini format
|
||||
for message in messages:
|
||||
role = message["role"]
|
||||
# Map roles to what Gemini expects
|
||||
if role == "user":
|
||||
gemini_role = "user"
|
||||
elif role == "assistant":
|
||||
gemini_role = "model"
|
||||
else:
|
||||
# Skip system messages as they're handled through config
|
||||
continue
|
||||
|
||||
gemini_messages.append(
|
||||
genai_types.Content(
|
||||
role=gemini_role,
|
||||
parts=[genai_types.Part.from_text(text=message["content"])],
|
||||
)
|
||||
)
|
||||
|
||||
# Set generation config
|
||||
generate_content_config = genai_types.GenerateContentConfig(
|
||||
temperature=temperature,
|
||||
max_output_tokens=max_tokens,
|
||||
response_mime_type="text/plain",
|
||||
)
|
||||
|
||||
# Add system instruction if provided
|
||||
if system:
|
||||
generate_content_config.system_instruction = system
|
||||
|
||||
# Make the API call
|
||||
if not gemini_messages:
|
||||
# If we have no messages but have a system prompt, create a default user message
|
||||
if system:
|
||||
default_content = genai_types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
genai_types.Part.from_text(
|
||||
text="Please respond based on the system instructions."
|
||||
)
|
||||
],
|
||||
)
|
||||
# model = self.gemini_client.get_model(self.model)
|
||||
response = await self.gemini_client.aio.models.generate_content(
|
||||
model=self.model,
|
||||
contents=default_content,
|
||||
config=generate_content_config,
|
||||
)
|
||||
else:
|
||||
raise ValueError("No messages provided for Gemini generation")
|
||||
else:
|
||||
# Normal case with messages
|
||||
# model = get_model(self.model)
|
||||
response = await self.gemini_client.aio.models.generate_content(
|
||||
model=self.model,
|
||||
contents=gemini_messages
|
||||
if len(gemini_messages) > 1
|
||||
else gemini_messages[0],
|
||||
config=generate_content_config,
|
||||
)
|
||||
|
||||
# Extract text from response
|
||||
if response and response.text:
|
||||
return response.text
|
||||
return ""
|
||||
|
||||
@observe(as_type="generation")
|
||||
async def _stream_gemini(
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
system: Optional[str] = None,
|
||||
max_tokens: int = DEFAULT_MAX_TOKENS,
|
||||
temperature: float = DEFAULT_TEMPERATURE,
|
||||
) -> Any:
|
||||
"""Stream text using Gemini API."""
|
||||
if not self.gemini_client:
|
||||
raise ValueError("Gemini client not initialized")
|
||||
|
||||
# Format messages for Gemini
|
||||
gemini_messages = []
|
||||
|
||||
# Convert messages to Gemini format
|
||||
for message in messages:
|
||||
role = message["role"]
|
||||
# Map roles to what Gemini expects
|
||||
if role == "user":
|
||||
gemini_role = "user"
|
||||
elif role == "assistant":
|
||||
gemini_role = "model"
|
||||
else:
|
||||
# Skip system messages as they're handled through config
|
||||
continue
|
||||
|
||||
gemini_messages.append(
|
||||
genai_types.Content(
|
||||
role=gemini_role,
|
||||
parts=[genai_types.Part.from_text(text=message["content"])],
|
||||
)
|
||||
)
|
||||
|
||||
# Set generation config
|
||||
generate_content_config = genai_types.GenerateContentConfig(
|
||||
temperature=temperature,
|
||||
max_output_tokens=max_tokens,
|
||||
response_mime_type="text/plain",
|
||||
)
|
||||
|
||||
# Add system instruction if provided
|
||||
if system:
|
||||
generate_content_config.system_instruction = system
|
||||
|
||||
# Make the streaming API call
|
||||
if not gemini_messages:
|
||||
# If we have no messages but have a system prompt, create a default user message
|
||||
if system:
|
||||
default_content = genai_types.Content(
|
||||
role="user",
|
||||
parts=[
|
||||
genai_types.Part.from_text(
|
||||
text="Please respond based on the system instructions."
|
||||
)
|
||||
],
|
||||
)
|
||||
stream = await self.gemini_client.aio.models.generate_content_stream(
|
||||
model=self.model,
|
||||
contents=default_content,
|
||||
config=generate_content_config,
|
||||
)
|
||||
else:
|
||||
raise ValueError("No messages provided for Gemini streaming")
|
||||
else:
|
||||
# Normal case with messages
|
||||
stream = await self.gemini_client.aio.models.generate_content_stream(
|
||||
model=self.model,
|
||||
contents=gemini_messages
|
||||
if len(gemini_messages) > 1
|
||||
else gemini_messages[0],
|
||||
config=generate_content_config,
|
||||
)
|
||||
|
||||
return stream
|
||||
|
||||
@observe()
|
||||
async def embed(
|
||||
self,
|
||||
text: str,
|
||||
model: Optional[str] = None,
|
||||
) -> list[float]:
|
||||
"""
|
||||
Generate embeddings for the given text.
|
||||
|
||||
Args:
|
||||
text: The text to embed
|
||||
model: The embedding model to use (optional, defaults to text-embedding-3-small)
|
||||
|
||||
Returns:
|
||||
The embedding vector as a list of floats
|
||||
"""
|
||||
# Currently only supports OpenAI-compatible providers for embeddings
|
||||
if self.provider not in [ModelProvider.OPENAI, ModelProvider.OPENROUTER]:
|
||||
raise ValueError(
|
||||
f"Embeddings not supported for provider: {self.provider}. "
|
||||
"Please use OpenAI or OpenRouter for embeddings."
|
||||
)
|
||||
|
||||
if not self.openai_client:
|
||||
raise ValueError("OpenAI client not initialized")
|
||||
|
||||
embedding_model = model or DEFAULT_EMBEDDING_MODEL
|
||||
|
||||
with sentry_sdk.start_transaction(
|
||||
op="embedding-api", name=f"{self.provider} Embedding Call"
|
||||
):
|
||||
langfuse_context.update_current_observation(
|
||||
input=text, model=embedding_model
|
||||
)
|
||||
|
||||
response = await self.openai_client.embeddings.create(
|
||||
model=embedding_model, input=text
|
||||
)
|
||||
|
||||
return response.data[0].embedding
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
from collections.abc import Callable
|
||||
from typing import Literal, ParamSpec, TypeVar
|
||||
|
||||
from mirascope import Provider
|
||||
from sentry_sdk.ai.monitoring import ai_track
|
||||
|
||||
R = TypeVar("R")
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
def track(description: str) -> Callable[[Callable[P, R]], Callable[P, R]]:
|
||||
def decorator(f: Callable[P, R]) -> Callable[P, R]:
|
||||
def _inner(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
result: R = ai_track(description)(f)(*args, **kwargs)
|
||||
return result
|
||||
|
||||
return _inner
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
Providers = Provider | Literal["custom"]
|
||||
|
|
@ -1,35 +1,48 @@
|
|||
import logging # noqa: I001
|
||||
import jwt
|
||||
from nanoid import generate as generate_nanoid
|
||||
from unittest.mock import patch, MagicMock, AsyncMock
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from fastapi import Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from sqlalchemy.engine.url import URL, make_url
|
||||
from sqlalchemy.exc import OperationalError, ProgrammingError
|
||||
from sqlalchemy_utils import create_database, database_exists, drop_database
|
||||
from sqlalchemy.ext.asyncio import (
|
||||
AsyncEngine,
|
||||
AsyncSession,
|
||||
async_sessionmaker,
|
||||
create_async_engine,
|
||||
)
|
||||
from sqlalchemy_utils import (
|
||||
create_database, # pyright: ignore[reportUnknownVariableType]
|
||||
database_exists, # pyright: ignore[reportUnknownVariableType]
|
||||
drop_database, # pyright: ignore[reportUnknownVariableType]
|
||||
)
|
||||
|
||||
from src import models
|
||||
from src.db import Base
|
||||
from src.dependencies import get_db
|
||||
from src.exceptions import HonchoException
|
||||
from src.security import create_admin_jwt, create_jwt, JWTParams
|
||||
from src.main import app
|
||||
from src.config import settings
|
||||
from src.models import Peer, Workspace
|
||||
from src.security import JWTParams, create_admin_jwt, create_jwt
|
||||
|
||||
|
||||
# Create a custom handler that doesn't get closed prematurely
|
||||
class TestHandler(logging.Handler):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.records = []
|
||||
self.records: list[logging.LogRecord] = []
|
||||
|
||||
def emit(self, record):
|
||||
def emit(self, record: logging.LogRecord):
|
||||
self.records.append(record)
|
||||
|
||||
|
||||
|
|
@ -57,7 +70,7 @@ DEFAULT_DB_URL = str(CONNECTION_URI.set(database="postgres"))
|
|||
# We'll use settings.AUTH directly where needed
|
||||
|
||||
|
||||
def create_test_database(db_url):
|
||||
def create_test_database(db_url: URL):
|
||||
"""Helper function create a database if it does not already exist
|
||||
uses the `sqlalchemy_utils` library to create the database and takes a DB URL
|
||||
as the input
|
||||
|
|
@ -78,7 +91,7 @@ def create_test_database(db_url):
|
|||
raise
|
||||
|
||||
|
||||
async def setup_test_database(db_url):
|
||||
async def setup_test_database(db_url: URL):
|
||||
"""Helper function to setup the test database
|
||||
takes a DB URL as input and returns a SQLAlchemy engine
|
||||
|
||||
|
|
@ -120,7 +133,7 @@ async def db_engine():
|
|||
# Save the original schema to restore later
|
||||
original_schema = Base.metadata.schema
|
||||
Base.metadata.schema = "public"
|
||||
|
||||
|
||||
# Update all table schemas to public
|
||||
for table in Base.metadata.tables.values():
|
||||
table.schema = "public"
|
||||
|
|
@ -144,7 +157,7 @@ async def db_engine():
|
|||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def db_session(db_engine):
|
||||
async def db_session(db_engine: AsyncEngine):
|
||||
"""Create a database session for the scope of a single test function"""
|
||||
Session = async_sessionmaker(bind=db_engine, expire_on_commit=False)
|
||||
async with Session() as session:
|
||||
|
|
@ -153,12 +166,14 @@ async def db_session(db_engine):
|
|||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def client(db_session):
|
||||
async def client(db_session: AsyncSession):
|
||||
"""Create a FastAPI TestClient for the scope of a single test function"""
|
||||
|
||||
# Register exception handlers for tests
|
||||
@app.exception_handler(HonchoException)
|
||||
async def test_exception_handler(request: Request, exc: HonchoException):
|
||||
async def test_exception_handler( # pyright: ignore
|
||||
_: Request, exc: HonchoException
|
||||
):
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"detail": exc.detail},
|
||||
|
|
@ -176,7 +191,13 @@ async def client(db_session):
|
|||
|
||||
|
||||
def create_invalid_jwt() -> str:
|
||||
return jwt.encode({"ad": "invalid"}, "this is not the secret", algorithm="HS256")
|
||||
return jwt.encode( # pyright: ignore[reportUnknownMemberType]
|
||||
{"ad": "invalid"}, "this is not the secret", algorithm="HS256"
|
||||
)
|
||||
|
||||
|
||||
class AuthClient(TestClient):
|
||||
auth_type: str | None = None
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
|
|
@ -187,7 +208,11 @@ def create_invalid_jwt() -> str:
|
|||
("admin", create_admin_jwt), # Admin JWT
|
||||
]
|
||||
)
|
||||
def auth_client(client, request, monkeypatch):
|
||||
def auth_client(
|
||||
client: AuthClient,
|
||||
request: pytest.FixtureRequest,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
"""
|
||||
Fixture that provides a client with different authentication states.
|
||||
Always ensures USE_AUTH is set to True.
|
||||
|
|
@ -210,7 +235,9 @@ def auth_client(client, request, monkeypatch):
|
|||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def sample_data(db_session):
|
||||
async def sample_data(
|
||||
db_session: AsyncSession,
|
||||
) -> AsyncGenerator[tuple[Workspace, Peer], Any]:
|
||||
"""Helper function to create test data"""
|
||||
# Create test app
|
||||
test_workspace = models.Workspace(name=str(generate_nanoid()))
|
||||
|
|
@ -237,7 +264,10 @@ def mock_langfuse():
|
|||
patch("langfuse.decorators.langfuse_context") as mock_context,
|
||||
):
|
||||
# Mock the decorator to just return the function
|
||||
mock_observe.return_value = lambda func: func
|
||||
def return_value(func: Callable[..., Any]):
|
||||
return func
|
||||
|
||||
mock_observe.return_value = return_value
|
||||
|
||||
# Mock the context object
|
||||
mock_context_obj = MagicMock()
|
||||
|
|
@ -260,56 +290,98 @@ def mock_langfuse():
|
|||
@pytest.fixture(autouse=True)
|
||||
def mock_openai_embeddings():
|
||||
"""Mock OpenAI embeddings API calls for testing"""
|
||||
with patch("src.crud.openai_client.embeddings.create") as mock_create:
|
||||
mock_response = AsyncMock()
|
||||
mock_response.data = [MagicMock(embedding=[0.1] * 1536)]
|
||||
mock_create.return_value = mock_response
|
||||
with patch("src.crud.embedding_client.embed") as mock_create:
|
||||
mock_create.return_value = [0.1] * 1536
|
||||
yield mock_create
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_model_client(request):
|
||||
"""Mock ModelClient to avoid needing API keys during tests"""
|
||||
# Skip mocking for ModelClient unit tests
|
||||
if "test_model_client" in request.node.name or "test_model_client.py" in str(
|
||||
request.fspath
|
||||
):
|
||||
yield None
|
||||
return
|
||||
|
||||
# Create a mock instance
|
||||
mock_client_instance = MagicMock()
|
||||
mock_client_instance.generate = AsyncMock(return_value="Test summary content")
|
||||
mock_client_instance.stream = AsyncMock()
|
||||
def mock_mirascope_functions():
|
||||
"""Mock Mirascope LLM functions to avoid needing API keys during tests"""
|
||||
|
||||
# Create mock responses for different function types
|
||||
with (
|
||||
patch("src.utils.history.ModelClient") as mock_history_client,
|
||||
patch("src.deriver.tom.single_prompt.ModelClient") as mock_single_prompt_client,
|
||||
patch("src.deriver.tom.long_term.ModelClient") as mock_long_term_client,
|
||||
patch("src.agent.ModelClient") as mock_agent_client,
|
||||
patch(
|
||||
"src.utils.history.create_short_summary", new_callable=AsyncMock
|
||||
) as mock_short_summary,
|
||||
patch(
|
||||
"src.utils.history.create_long_summary", new_callable=AsyncMock
|
||||
) as mock_long_summary,
|
||||
patch(
|
||||
"src.deriver.tom.single_prompt.tom_inference", new_callable=AsyncMock
|
||||
) as mock_tom_inference,
|
||||
patch(
|
||||
"src.deriver.tom.tom_inference_single_prompt",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_tom_inference_conversational,
|
||||
patch(
|
||||
"src.deriver.tom.single_prompt.user_representation", new_callable=AsyncMock
|
||||
) as mock_user_rep_inference,
|
||||
patch(
|
||||
"src.deriver.tom.long_term.get_user_representation_long_term",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_long_rep,
|
||||
patch(
|
||||
"src.deriver.tom.long_term.extract_facts_long_term",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_extract_facts,
|
||||
patch(
|
||||
"src.agent.dialectic_call", new_callable=AsyncMock
|
||||
) as mock_dialectic_call,
|
||||
patch(
|
||||
"src.agent.dialectic_stream", new_callable=AsyncMock
|
||||
) as mock_dialectic_stream,
|
||||
patch(
|
||||
"src.agent.generate_semantic_queries_llm", new_callable=AsyncMock
|
||||
) as mock_semantic_queries,
|
||||
):
|
||||
# Make all class constructors return our mock instance
|
||||
mock_history_client.return_value = mock_client_instance
|
||||
mock_single_prompt_client.return_value = mock_client_instance
|
||||
mock_long_term_client.return_value = mock_client_instance
|
||||
mock_agent_client.return_value = mock_client_instance
|
||||
# Mock return values for different function types
|
||||
mock_short_summary.return_value = "Test short summary content"
|
||||
mock_long_summary.return_value = "Test long summary content"
|
||||
mock_tom_inference.return_value = AsyncMock(inference="Test tom inference")
|
||||
mock_tom_inference_conversational.return_value = AsyncMock(
|
||||
inference="Test tom inference"
|
||||
)
|
||||
mock_user_rep_inference.return_value = "Test user representation"
|
||||
# Mock single_tom to return a proper Pydantic object
|
||||
mock_long_rep.return_value = MagicMock(
|
||||
current_state="Test state",
|
||||
tentative_patterns=[],
|
||||
knowledge_gaps=[],
|
||||
expectation_violations=[],
|
||||
updates=[],
|
||||
)
|
||||
mock_extract_facts.return_value = MagicMock(facts=["fact 1", "fact 2"])
|
||||
|
||||
# Create a proper async mock result for dialectic_call
|
||||
mock_dialectic_result = MagicMock()
|
||||
mock_dialectic_result.content = "Test dialectic response"
|
||||
mock_dialectic_call.return_value = mock_dialectic_result
|
||||
|
||||
mock_dialectic_stream.return_value = AsyncMock()
|
||||
mock_semantic_queries.return_value = ["test query 1", "test query 2"]
|
||||
|
||||
yield {
|
||||
"history": mock_history_client,
|
||||
"single_prompt": mock_single_prompt_client,
|
||||
"long_term": mock_long_term_client,
|
||||
"agent": mock_agent_client,
|
||||
"instance": mock_client_instance,
|
||||
"short_summary": mock_short_summary,
|
||||
"long_summary": mock_long_summary,
|
||||
"tom_inference": mock_tom_inference,
|
||||
"tom_inference_conversational": mock_tom_inference_conversational,
|
||||
"user_rep_inference": mock_user_rep_inference,
|
||||
"long_rep": mock_long_rep,
|
||||
"extract_facts": mock_extract_facts,
|
||||
"dialectic_call": mock_dialectic_call,
|
||||
"dialectic_stream": mock_dialectic_stream,
|
||||
"semantic_queries": mock_semantic_queries,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_tracked_db(db_session):
|
||||
def mock_tracked_db(db_session: AsyncSession):
|
||||
"""Mock tracked_db to use the test database session"""
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
@asynccontextmanager
|
||||
async def mock_tracked_db_context(operation_name=None):
|
||||
async def mock_tracked_db_context(_: str | None = None):
|
||||
yield db_session
|
||||
|
||||
with patch("src.dependencies.tracked_db", mock_tracked_db_context):
|
||||
|
|
@ -324,11 +396,11 @@ def mock_crud_collection_operations():
|
|||
from src import models
|
||||
|
||||
async def mock_get_or_create_collection(
|
||||
db, workspace_name, peer_name, collection_name
|
||||
_: AsyncSession, workspace_name: str, peer_name: str, collection_name: str
|
||||
):
|
||||
# Create a mock collection object that doesn't require database commit
|
||||
mock_collection = models.Collection(
|
||||
name="honcho",
|
||||
name=collection_name,
|
||||
workspace_name=workspace_name,
|
||||
peer_name=peer_name,
|
||||
)
|
||||
|
|
@ -343,27 +415,23 @@ def mock_crud_collection_operations():
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_agent_api_calls(request):
|
||||
def mock_agent_api_calls():
|
||||
"""Mock API calls made by the agent during tests"""
|
||||
# Mock the agent-specific functions
|
||||
with (
|
||||
patch("src.agent.generate_semantic_queries") as mock_generate_queries,
|
||||
patch("src.deriver.tom.get_tom_inference") as mock_tom_inference,
|
||||
patch("src.agent.get_user_representation_long_term") as mock_user_rep,
|
||||
patch(
|
||||
"src.deriver.tom.embeddings.CollectionEmbeddingStore.get_relevant_facts"
|
||||
) as mock_get_facts,
|
||||
patch("src.agent.Dialectic.call") as mock_dialectic_call,
|
||||
patch("src.agent.Dialectic.stream") as mock_dialectic_stream,
|
||||
patch(
|
||||
"src.agent.dialectic_call", new_callable=AsyncMock
|
||||
) as mock_dialectic_call,
|
||||
patch("src.agent.dialectic_stream") as mock_dialectic_stream,
|
||||
):
|
||||
# Mock semantic query generation
|
||||
mock_generate_queries.return_value = ["test query 1", "test query 2"]
|
||||
|
||||
# Mock ToM inference
|
||||
mock_tom_inference.return_value = (
|
||||
"<prediction>Test prediction about user mental state</prediction>"
|
||||
)
|
||||
|
||||
# Mock user representation generation
|
||||
mock_user_rep.return_value = (
|
||||
"<representation>Test user representation</representation>"
|
||||
|
|
@ -373,12 +441,13 @@ def mock_agent_api_calls(request):
|
|||
mock_get_facts.return_value = ["fact 1", "fact 2", "fact 3"]
|
||||
|
||||
# Mock Dialectic API calls
|
||||
mock_dialectic_call.return_value = [{"text": "Test dialectic response"}]
|
||||
mock_dialectic_result = MagicMock()
|
||||
mock_dialectic_result.content = "Test dialectic response"
|
||||
mock_dialectic_call.return_value = mock_dialectic_result
|
||||
mock_dialectic_stream.return_value = AsyncMock()
|
||||
|
||||
yield {
|
||||
"generate_queries": mock_generate_queries,
|
||||
"tom_inference": mock_tom_inference,
|
||||
"user_rep": mock_user_rep,
|
||||
"get_facts": mock_get_facts,
|
||||
"dialectic_call": mock_dialectic_call,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from typing import Optional
|
||||
from unittest.mock import patch
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud, models, schemas
|
||||
from src.models import QueueItem
|
||||
from src.models import Peer, QueueItem, Workspace
|
||||
from src.routers.messages import enqueue
|
||||
|
||||
|
||||
|
|
@ -17,10 +18,10 @@ class TestEnqueueFunction:
|
|||
# Helper methods
|
||||
def create_sample_payload(
|
||||
self,
|
||||
workspace_name="test_workspace",
|
||||
session_name: Optional[str] = "test_session",
|
||||
peer_name="test_peer",
|
||||
count=1,
|
||||
workspace_name: str = "test_workspace",
|
||||
session_name: str | None = "test_session",
|
||||
peer_name: str = "test_peer",
|
||||
count: int = 1,
|
||||
):
|
||||
"""Create sample payload for testing"""
|
||||
return [
|
||||
|
|
@ -35,14 +36,14 @@ class TestEnqueueFunction:
|
|||
for i in range(count)
|
||||
]
|
||||
|
||||
async def count_queue_items(self, db_session):
|
||||
async def count_queue_items(self, db_session: AsyncSession):
|
||||
"""Helper to count queue items in database"""
|
||||
result = await db_session.execute(select(QueueItem))
|
||||
return len(result.scalars().all())
|
||||
|
||||
# Edge Cases & Input Validation Tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_payload_skips_enqueue(self, caplog):
|
||||
async def test_empty_payload_skips_enqueue(self, caplog: pytest.LogCaptureFixture):
|
||||
"""Test that empty payload list is handled gracefully"""
|
||||
with caplog.at_level("DEBUG"):
|
||||
await enqueue([])
|
||||
|
|
@ -50,7 +51,7 @@ class TestEnqueueFunction:
|
|||
assert "Empty payload list, skipping enqueue" in caplog.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_malformed_payload_logs_error(self, caplog):
|
||||
async def test_malformed_payload_logs_error(self, caplog: pytest.LogCaptureFixture):
|
||||
"""Test that malformed payload logs appropriate error"""
|
||||
malformed_payload = [{"incomplete": "data"}]
|
||||
|
||||
|
|
@ -64,7 +65,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_none_session_with_observe_me_true(
|
||||
self, mock_tracked_db, db_session, sample_data
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test peer-only processing when observe_me=True"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -99,7 +103,11 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_none_session_with_observe_me_false(
|
||||
self, mock_tracked_db, db_session, sample_data, caplog
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Test peer-only processing when observe_me=False"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -131,7 +139,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_session_with_deriver_disabled(
|
||||
self, mock_tracked_db, db_session, sample_data, caplog
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that deriver disabled sessions skip enqueue"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -163,7 +174,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_session_normal_processing_single_peer(
|
||||
self, mock_tracked_db, db_session, sample_data
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
||||
|
|
@ -204,7 +218,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_session_with_multiple_peers_none_observe_others(
|
||||
self, mock_tracked_db, db_session, sample_data
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test session processing with multiple peers where some observe others"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -254,7 +271,7 @@ class TestEnqueueFunction:
|
|||
|
||||
# Explicitly match up payloads by sender/target/task_type
|
||||
# For each message, we expect a representation and a summary queue item
|
||||
expected_payloads = []
|
||||
expected_payloads: list[dict[str, Any]] = []
|
||||
for _ in range(NUM_MESSAGES):
|
||||
expected_payloads.append(
|
||||
{
|
||||
|
|
@ -280,7 +297,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_session_with_multiple_peers_all_observe_others(
|
||||
self, mock_tracked_db, db_session, sample_data
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test session processing with multiple peers where some observe others"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -329,7 +349,7 @@ class TestEnqueueFunction:
|
|||
|
||||
# Explicitly match up payloads by sender/target/task_type
|
||||
# For each message, we expect a representation and a summary queue item
|
||||
expected_payloads = []
|
||||
expected_payloads: list[dict[str, Any]] = []
|
||||
for _ in range(NUM_MESSAGES):
|
||||
expected_payloads.append(
|
||||
{
|
||||
|
|
@ -362,7 +382,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_session_with_multiple_peers_some_observe_others(
|
||||
self, mock_tracked_db, db_session, sample_data
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test session processing with multiple peers where some observe others"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -419,7 +442,7 @@ class TestEnqueueFunction:
|
|||
|
||||
# Explicitly match up payloads by sender/target/task_type
|
||||
# For each message, we expect a representation and a summary queue item
|
||||
expected_payloads = []
|
||||
expected_payloads: list[dict[str, Any]] = []
|
||||
for _ in range(NUM_MESSAGES):
|
||||
expected_payloads.append(
|
||||
{
|
||||
|
|
@ -456,7 +479,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_session_peer_config_overrides_peer_config(
|
||||
self, mock_tracked_db, db_session, sample_data
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test that session peer config overrides peer config"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -500,7 +526,10 @@ class TestEnqueueFunction:
|
|||
@pytest.mark.asyncio
|
||||
@patch("src.routers.messages.tracked_db")
|
||||
async def test_multi_sender_scenario(
|
||||
self, mock_tracked_db, db_session, sample_data
|
||||
self,
|
||||
mock_tracked_db: AsyncMock,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
):
|
||||
"""Test complex scenario with multiple peers and mixed configurations"""
|
||||
mock_tracked_db.return_value.__aenter__.return_value = db_session
|
||||
|
|
@ -562,7 +591,7 @@ class TestEnqueueFunction:
|
|||
queue_items = result.all()
|
||||
|
||||
# Build expected payloads for this scenario
|
||||
expected_payloads = []
|
||||
expected_payloads: list[dict[str, Any]] = []
|
||||
# 2 messages: one from test_peer1, one from additional_sender_peer
|
||||
for sender in [test_peer1.name, additional_sender_peer.name]:
|
||||
# representation for sender (self)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
def test_create_key_no_params(auth_client):
|
||||
from src.models import Peer, Workspace
|
||||
from tests.conftest import AuthClient
|
||||
|
||||
|
||||
def test_create_key_no_params(auth_client: AuthClient):
|
||||
"""Test creating a key with no parameters"""
|
||||
response = auth_client.post("/v2/keys")
|
||||
|
||||
|
|
@ -10,7 +14,9 @@ def test_create_key_no_params(auth_client):
|
|||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_create_key_with_params(auth_client, sample_data):
|
||||
def test_create_key_with_params(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating a key with specific parameters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -46,7 +52,9 @@ def test_create_key_with_params(auth_client, sample_data):
|
|||
assert "key" in response.json()
|
||||
|
||||
|
||||
def test_create_key_with_expires_at(auth_client, sample_data):
|
||||
def test_create_key_with_expires_at(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating a key with an expiration date"""
|
||||
response = auth_client.post("/v2/keys", params={"expires_at": "2025-01-01"})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models # Import your SQLAlchemy models
|
||||
from src.models import Peer, Workspace
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_message(client, db_session, sample_data):
|
||||
async def test_create_message(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session
|
||||
|
|
@ -39,7 +44,9 @@ async def test_create_message(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_batch_messages_with_metadata(client, db_session, sample_data):
|
||||
async def test_create_batch_messages_with_metadata(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test batch message creation with metadata for each message"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -81,7 +88,9 @@ async def test_create_batch_messages_with_metadata(client, db_session, sample_da
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_batch_messages_without_metadata(client, db_session, sample_data):
|
||||
async def test_create_batch_messages_without_metadata(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test batch message creation without metadata (should default to empty dict)"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -112,7 +121,7 @@ async def test_create_batch_messages_without_metadata(client, db_session, sample
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_batch_messages_with_null_metadata(
|
||||
client, db_session, sample_data
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test batch message creation with null metadata"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -144,7 +153,9 @@ async def test_create_batch_messages_with_null_metadata(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages(client, db_session, sample_data):
|
||||
async def test_get_messages(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session and message
|
||||
|
|
@ -178,7 +189,9 @@ async def test_get_messages(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_with_reverse(client, db_session, sample_data):
|
||||
async def test_get_messages_with_reverse(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages with reverse parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -232,7 +245,9 @@ async def test_get_messages_with_reverse(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_with_empty_filter(client, db_session, sample_data):
|
||||
async def test_get_messages_with_empty_filter(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages with empty filter object"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -263,7 +278,9 @@ async def test_get_messages_with_empty_filter(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_with_null_filter(client, db_session, sample_data):
|
||||
async def test_get_messages_with_null_filter(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages with null filter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -294,7 +311,9 @@ async def test_get_messages_with_null_filter(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_no_body(client, db_session, sample_data):
|
||||
async def test_get_messages_no_body(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages without any request body"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -324,7 +343,9 @@ async def test_get_messages_no_body(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_filtered_messages(client, db_session, sample_data):
|
||||
async def test_get_filtered_messages(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session and messages
|
||||
|
|
@ -368,7 +389,7 @@ async def test_get_filtered_messages(client, db_session, sample_data):
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_filtered_messages_with_complex_filter(
|
||||
client, db_session, sample_data
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages with complex metadata filter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -419,7 +440,9 @@ async def test_get_filtered_messages_with_complex_filter(
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message(client, db_session, sample_data):
|
||||
async def test_update_message(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session and message
|
||||
|
|
@ -448,7 +471,9 @@ async def test_update_message(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_with_complex_metadata(client, db_session, sample_data):
|
||||
async def test_update_message_with_complex_metadata(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test updating message with complex metadata structure"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -485,7 +510,9 @@ async def test_update_message_with_complex_metadata(client, db_session, sample_d
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_empty_metadata(client, db_session, sample_data):
|
||||
async def test_update_message_empty_metadata(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
# note that this should not change the metadata of the message.
|
||||
# this test is to ensure that the metadata is not changed when it is set to None.
|
||||
|
||||
|
|
@ -524,7 +551,9 @@ async def test_update_message_empty_metadata(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_with_empty_dict_metadata(client, db_session, sample_data):
|
||||
async def test_update_message_with_empty_dict_metadata(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test updating message with empty dictionary metadata"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -555,7 +584,9 @@ async def test_update_message_with_empty_dict_metadata(client, db_session, sampl
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_single_message(client, db_session, sample_data):
|
||||
async def test_get_single_message(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session and message
|
||||
|
|
@ -587,9 +618,11 @@ async def test_get_single_message(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_message(client, db_session, sample_data):
|
||||
async def test_get_nonexistent_message(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting a message that doesn't exist"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
|
|
@ -606,9 +639,11 @@ async def test_get_nonexistent_message(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nonexistent_message(client, db_session, sample_data):
|
||||
async def test_update_nonexistent_message(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test updating a message that doesn't exist"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
|
|
@ -626,7 +661,9 @@ async def test_update_nonexistent_message(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_messages_for_nonexistent_session(client, sample_data):
|
||||
async def test_create_messages_for_nonexistent_session(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating messages for a session that doesn't exist - should create the session"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -653,9 +690,11 @@ async def test_create_messages_for_nonexistent_session(client, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_messages_for_nonexistent_session(client, sample_data):
|
||||
async def test_get_messages_for_nonexistent_session(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages for a session that doesn't exist - should return empty list"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
nonexistent_session_id = str(generate_nanoid())
|
||||
response = client.post(
|
||||
|
|
@ -670,9 +709,11 @@ async def test_get_messages_for_nonexistent_session(client, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_empty_batch_messages(client, db_session, sample_data):
|
||||
async def test_create_empty_batch_messages(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating an empty batch of messages"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
|
|
@ -690,7 +731,9 @@ async def test_create_empty_batch_messages(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_batch_messages_max_limit(client, db_session, sample_data):
|
||||
async def test_create_batch_messages_max_limit(
|
||||
client: TestClient, db_session: AsyncSession, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test creating batch messages at the maximum limit (100 messages)"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
|
||||
from src.models import Peer, Workspace
|
||||
|
||||
def test_get_or_create_peer(client, sample_data):
|
||||
|
||||
def test_get_or_create_peer(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, _ = sample_data
|
||||
name = str(generate_nanoid())
|
||||
response = client.post(
|
||||
|
|
@ -15,7 +18,9 @@ def test_get_or_create_peer(client, sample_data):
|
|||
assert "id" in data
|
||||
|
||||
|
||||
def test_get_or_create_peer_with_configuration(client, sample_data):
|
||||
def test_get_or_create_peer_with_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer creation with configuration parameter"""
|
||||
test_workspace, _ = sample_data
|
||||
name = str(generate_nanoid())
|
||||
|
|
@ -31,7 +36,9 @@ def test_get_or_create_peer_with_configuration(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_get_or_create_peer_with_all_optional_params(client, sample_data):
|
||||
def test_get_or_create_peer_with_all_optional_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer creation with all optional parameters"""
|
||||
test_workspace, _ = sample_data
|
||||
name = str(generate_nanoid())
|
||||
|
|
@ -49,7 +56,9 @@ def test_get_or_create_peer_with_all_optional_params(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_get_or_create_existing_peer(client, sample_data):
|
||||
def test_get_or_create_existing_peer(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, _ = sample_data
|
||||
name = str(generate_nanoid())
|
||||
|
||||
|
|
@ -74,7 +83,7 @@ def test_get_or_create_existing_peer(client, sample_data):
|
|||
assert peer1["metadata"] == peer2["metadata"]
|
||||
|
||||
|
||||
def test_get_peers(client, sample_data):
|
||||
def test_get_peers(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
# Create a few peers with metadata
|
||||
|
|
@ -113,7 +122,9 @@ def test_get_peers(client, sample_data):
|
|||
assert data["items"][0]["metadata"]["peer_key"] == "peer_value"
|
||||
|
||||
|
||||
def test_get_peers_with_empty_filter(client, sample_data):
|
||||
def test_get_peers_with_empty_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer listing with empty filter object"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -126,7 +137,9 @@ def test_get_peers_with_empty_filter(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_get_peers_with_null_filter(client, sample_data):
|
||||
def test_get_peers_with_null_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer listing with null filter"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -139,7 +152,7 @@ def test_get_peers_with_null_filter(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_update_peer(client, sample_data):
|
||||
def test_update_peer(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
response = client.put(
|
||||
f"/v2/workspaces/{test_workspace.name}/peers/{test_peer.name}",
|
||||
|
|
@ -150,7 +163,9 @@ def test_update_peer(client, sample_data):
|
|||
assert data["metadata"] == {"new_key": "new_value"}
|
||||
|
||||
|
||||
def test_update_peer_with_configuration(client, sample_data):
|
||||
def test_update_peer_with_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer update with configuration parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
configuration = {"new_feature": True, "legacy_feature": False}
|
||||
|
|
@ -164,7 +179,9 @@ def test_update_peer_with_configuration(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_update_peer_with_all_optional_params(client, sample_data):
|
||||
def test_update_peer_with_all_optional_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer update with both metadata and configuration"""
|
||||
test_workspace, test_peer = sample_data
|
||||
metadata = {"updated_key": "updated_value", "count": 100}
|
||||
|
|
@ -180,7 +197,9 @@ def test_update_peer_with_all_optional_params(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_update_peer_with_null_metadata(client, sample_data):
|
||||
def test_update_peer_with_null_metadata(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer update with null metadata"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -200,7 +219,9 @@ def test_update_peer_with_null_metadata(client, sample_data):
|
|||
assert "metadata" in data
|
||||
|
||||
|
||||
def test_update_peer_with_null_configuration(client, sample_data):
|
||||
def test_update_peer_with_null_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer update with null configuration"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -213,7 +234,9 @@ def test_update_peer_with_null_configuration(client, sample_data):
|
|||
assert "configuration" in data
|
||||
|
||||
|
||||
def test_get_sessions_for_peer_no_sessions(client, sample_data):
|
||||
def test_get_sessions_for_peer_no_sessions(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Get sessions for the peer
|
||||
|
|
@ -226,7 +249,7 @@ def test_get_sessions_for_peer_no_sessions(client, sample_data):
|
|||
assert "items" in data
|
||||
|
||||
|
||||
def test_get_sessions_for_peer(client, sample_data):
|
||||
def test_get_sessions_for_peer(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create session for the peer
|
||||
|
|
@ -252,7 +275,9 @@ def test_get_sessions_for_peer(client, sample_data):
|
|||
assert len(data["items"]) == 1
|
||||
|
||||
|
||||
def test_get_sessions_for_peer_with_is_active_filter(client, sample_data):
|
||||
def test_get_sessions_for_peer_with_is_active_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting sessions for peer with is_active parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -277,7 +302,9 @@ def test_get_sessions_for_peer_with_is_active_filter(client, sample_data):
|
|||
assert len(inactive_sessions) > 0
|
||||
|
||||
|
||||
def test_get_sessions_for_peer_with_empty_filter(client, sample_data):
|
||||
def test_get_sessions_for_peer_with_empty_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting sessions for peer with empty filter object"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -291,7 +318,9 @@ def test_get_sessions_for_peer_with_empty_filter(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_create_and_get_messages_for_peer(client, sample_data):
|
||||
def test_create_and_get_messages_for_peer(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create messages for the peer
|
||||
|
|
@ -334,7 +363,9 @@ def test_create_and_get_messages_for_peer(client, sample_data):
|
|||
assert data["items"][1]["metadata"] == {"message_key": "message_value2"}
|
||||
|
||||
|
||||
def test_get_messages_for_peer_with_reverse(client, sample_data):
|
||||
def test_get_messages_for_peer_with_reverse(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages for peer with reverse parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -370,7 +401,9 @@ def test_get_messages_for_peer_with_reverse(client, sample_data):
|
|||
assert len(reversed_data["items"]) > 0
|
||||
|
||||
|
||||
def test_get_messages_for_peer_with_empty_filter(client, sample_data):
|
||||
def test_get_messages_for_peer_with_empty_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages for peer with empty filter object"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -384,7 +417,9 @@ def test_get_messages_for_peer_with_empty_filter(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_get_messages_for_peer_with_null_filter(client, sample_data):
|
||||
def test_get_messages_for_peer_with_null_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test getting messages for peer with null filter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -398,7 +433,7 @@ def test_get_messages_for_peer_with_null_filter(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_chat(client, sample_data):
|
||||
def test_chat(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
target_peer = str(generate_nanoid())
|
||||
|
||||
|
|
@ -416,7 +451,9 @@ def test_chat(client, sample_data):
|
|||
assert "content" in data
|
||||
|
||||
|
||||
def test_chat_with_optional_params(client, sample_data):
|
||||
def test_chat_with_optional_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test chat endpoint with optional parameters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -442,7 +479,9 @@ def test_chat_with_optional_params(client, sample_data):
|
|||
assert "content" in data
|
||||
|
||||
|
||||
def test_get_peer_representation_with_session(client, sample_data):
|
||||
def test_get_peer_representation_with_session(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer representation with session_id parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -464,7 +503,7 @@ def test_get_peer_representation_with_session(client, sample_data):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_search_peer(client, sample_data):
|
||||
def test_search_peer(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
"""Test the peer search functionality"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -495,7 +534,9 @@ def test_search_peer(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_search_peer_empty_query(client, sample_data):
|
||||
def test_search_peer_empty_query(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test peer search with empty query"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -511,7 +552,9 @@ def test_search_peer_empty_query(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_search_peer_nonexistent(client, sample_data):
|
||||
def test_search_peer_nonexistent(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test searching a peer that doesn't exist"""
|
||||
test_workspace, _ = sample_data
|
||||
nonexistent_peer_id = str(generate_nanoid())
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import models
|
||||
|
||||
|
|
@ -8,7 +10,11 @@ from src import models
|
|||
class TestDeriverStatusEndpoint:
|
||||
"""Test suite for the /deriver/status endpoint"""
|
||||
|
||||
async def test_get_deriver_status_peer_only(self, client, db_session, sample_data):
|
||||
async def test_get_deriver_status_peer_only(
|
||||
self,
|
||||
client: TestClient,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test getting deriver status filtered by peer only"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -31,10 +37,13 @@ class TestDeriverStatusEndpoint:
|
|||
assert isinstance(data["pending_work_units"], int)
|
||||
|
||||
async def test_get_deriver_status_session_only(
|
||||
self, client, db_session, sample_data
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test getting deriver status filtered by session only"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
# Create a test session
|
||||
test_session = models.Session(
|
||||
|
|
@ -59,7 +68,10 @@ class TestDeriverStatusEndpoint:
|
|||
assert isinstance(data["total_work_units"], int)
|
||||
|
||||
async def test_get_deriver_status_peer_and_session(
|
||||
self, client, db_session, sample_data
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test getting deriver status filtered by both peer and session"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -86,7 +98,9 @@ class TestDeriverStatusEndpoint:
|
|||
assert "pending_work_units" in data
|
||||
|
||||
async def test_get_deriver_status_with_include_sender_true(
|
||||
self, client, db_session, sample_data
|
||||
self,
|
||||
client: TestClient,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test getting deriver status with include_sender=True"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -101,7 +115,9 @@ class TestDeriverStatusEndpoint:
|
|||
assert "total_work_units" in data
|
||||
|
||||
async def test_get_deriver_status_with_include_sender_false(
|
||||
self, client, db_session, sample_data
|
||||
self,
|
||||
client: TestClient,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test getting deriver status with include_sender=False (default)"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -115,7 +131,9 @@ class TestDeriverStatusEndpoint:
|
|||
assert data["peer_id"] == test_peer.name
|
||||
assert "total_work_units" in data
|
||||
|
||||
async def test_get_deriver_status_no_parameters(self, client, sample_data):
|
||||
async def test_get_deriver_status_no_parameters(
|
||||
self, client: TestClient, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Test getting deriver status without required parameters returns 400"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -128,7 +146,9 @@ class TestDeriverStatusEndpoint:
|
|||
in data["detail"]
|
||||
)
|
||||
|
||||
async def test_get_deriver_status_nonexistent_peer(self, client, sample_data):
|
||||
async def test_get_deriver_status_nonexistent_peer(
|
||||
self, client: TestClient, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Test getting deriver status for nonexistent peer returns empty result"""
|
||||
test_workspace, _ = sample_data
|
||||
nonexistent_peer = str(generate_nanoid())
|
||||
|
|
@ -144,7 +164,9 @@ class TestDeriverStatusEndpoint:
|
|||
assert data["in_progress_work_units"] == 0
|
||||
assert data["pending_work_units"] == 0
|
||||
|
||||
async def test_get_deriver_status_nonexistent_session(self, client, sample_data):
|
||||
async def test_get_deriver_status_nonexistent_session(
|
||||
self, client: TestClient, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Test getting deriver status for nonexistent session returns empty result"""
|
||||
test_workspace, _ = sample_data
|
||||
nonexistent_session = str(generate_nanoid())
|
||||
|
|
@ -160,7 +182,7 @@ class TestDeriverStatusEndpoint:
|
|||
assert data["in_progress_work_units"] == 0
|
||||
assert data["pending_work_units"] == 0
|
||||
|
||||
async def test_get_deriver_status_nonexistent_workspace(self, client):
|
||||
async def test_get_deriver_status_nonexistent_workspace(self, client: TestClient):
|
||||
"""Test getting deriver status for nonexistent workspace returns empty result"""
|
||||
nonexistent_workspace = str(generate_nanoid())
|
||||
fake_peer = str(generate_nanoid())
|
||||
|
|
@ -178,7 +200,10 @@ class TestDeriverStatusEndpoint:
|
|||
assert data["pending_work_units"] == 0
|
||||
|
||||
async def test_get_deriver_status_with_queue_items(
|
||||
self, client, db_session, sample_data
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test getting deriver status when there are actual queue items"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -230,7 +255,10 @@ class TestDeriverStatusEndpoint:
|
|||
assert data["pending_work_units"] >= 1
|
||||
|
||||
async def test_get_deriver_status_with_sessions_breakdown(
|
||||
self, client, db_session, sample_data
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test getting deriver status shows sessions breakdown when appropriate"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -288,7 +316,9 @@ class TestDeriverStatusEndpoint:
|
|||
assert "in_progress_work_units" in session_data
|
||||
assert "pending_work_units" in session_data
|
||||
|
||||
async def test_get_deriver_status_empty_parameters(self, client, sample_data):
|
||||
async def test_get_deriver_status_empty_parameters(
|
||||
self, client: TestClient, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Test various edge cases with empty or invalid parameters"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -305,7 +335,7 @@ class TestDeriverStatusEndpoint:
|
|||
assert response.status_code == 400
|
||||
|
||||
async def test_get_deriver_status_boolean_parameter_variations(
|
||||
self, client, sample_data
|
||||
self, client: TestClient, sample_data: tuple[models.Workspace, models.Peer]
|
||||
):
|
||||
"""Test different boolean parameter formats for include_sender"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
@ -335,7 +365,10 @@ class TestDeriverStatusEndpoint:
|
|||
assert response.status_code == 200
|
||||
|
||||
async def test_get_deriver_status_response_consistency(
|
||||
self, client, db_session, sample_data
|
||||
self,
|
||||
client: TestClient,
|
||||
db_session: AsyncSession,
|
||||
sample_data: tuple[models.Workspace, models.Peer],
|
||||
):
|
||||
"""Test that response structure is consistent across different parameter combinations"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
from nanoid import generate as generate_nanoid
|
||||
|
||||
from src.models import Peer, Workspace
|
||||
from src.security import JWTParams, create_jwt
|
||||
from tests.conftest import AuthClient
|
||||
|
||||
|
||||
def test_create_workspace_with_auth(auth_client):
|
||||
def test_create_workspace_with_auth(auth_client: AuthClient):
|
||||
name = str(generate_nanoid())
|
||||
|
||||
response = auth_client.post(
|
||||
|
|
@ -18,7 +20,7 @@ def test_create_workspace_with_auth(auth_client):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_auth_response_time(auth_client):
|
||||
def test_auth_response_time(auth_client: AuthClient):
|
||||
name = str(generate_nanoid())
|
||||
|
||||
import time
|
||||
|
|
@ -43,7 +45,7 @@ def test_auth_response_time(auth_client):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_get_or_create_workspace_with_auth(auth_client):
|
||||
def test_get_or_create_workspace_with_auth(auth_client: AuthClient):
|
||||
name = str(generate_nanoid())
|
||||
|
||||
response = auth_client.post(
|
||||
|
|
@ -57,7 +59,9 @@ def test_get_or_create_workspace_with_auth(auth_client):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_get_workspace_with_auth(auth_client, sample_data):
|
||||
def test_get_workspace_with_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
if auth_client.auth_type == "empty":
|
||||
|
|
@ -75,7 +79,9 @@ def test_get_workspace_with_auth(auth_client, sample_data):
|
|||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_update_workspace_with_auth(auth_client, sample_data):
|
||||
def test_update_workspace_with_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
if auth_client.auth_type == "empty":
|
||||
|
|
@ -97,7 +103,9 @@ def test_update_workspace_with_auth(auth_client, sample_data):
|
|||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_update_workspace_with_wrong_auth(auth_client, sample_data):
|
||||
def test_update_workspace_with_wrong_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
different_workspace = str(generate_nanoid())
|
||||
|
|
@ -122,7 +130,9 @@ def test_update_workspace_with_wrong_auth(auth_client, sample_data):
|
|||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_create_peer_with_auth(auth_client, sample_data):
|
||||
def test_create_peer_with_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
if auth_client.auth_type == "empty":
|
||||
|
|
@ -144,7 +154,9 @@ def test_create_peer_with_auth(auth_client, sample_data):
|
|||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_get_peer_by_name_with_auth(auth_client, sample_data):
|
||||
def test_get_peer_by_name_with_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
if auth_client.auth_type == "empty":
|
||||
|
|
@ -179,7 +191,9 @@ def test_get_peer_by_name_with_auth(auth_client, sample_data):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_update_peer_with_auth(auth_client, sample_data):
|
||||
def test_update_peer_with_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
if auth_client.auth_type == "empty":
|
||||
|
|
@ -217,7 +231,9 @@ def test_update_peer_with_auth(auth_client, sample_data):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_create_session_with_auth(auth_client, sample_data):
|
||||
def test_create_session_with_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
if auth_client.auth_type == "empty":
|
||||
|
|
@ -253,7 +269,9 @@ def test_create_session_with_auth(auth_client, sample_data):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_get_session_by_name_with_auth(auth_client, sample_data):
|
||||
def test_get_session_by_name_with_auth(
|
||||
auth_client: AuthClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# First create a session
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
|
||||
from src.models import Peer, Workspace
|
||||
|
||||
def test_get_or_create_session(client, sample_data):
|
||||
|
||||
def test_get_or_create_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
# Test get or create session
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -40,7 +46,9 @@ def test_get_or_create_session(client, sample_data):
|
|||
assert data3["workspace_id"] == test_workspace.name
|
||||
|
||||
|
||||
def test_create_session_with_metadata(client, sample_data):
|
||||
def test_create_session_with_metadata(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
response = client.post(
|
||||
|
|
@ -59,7 +67,9 @@ def test_create_session_with_metadata(client, sample_data):
|
|||
assert data["workspace_id"] == test_workspace.name
|
||||
|
||||
|
||||
def test_create_session_with_configuration(client, sample_data):
|
||||
def test_create_session_with_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session creation with configuration parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -80,7 +90,9 @@ def test_create_session_with_configuration(client, sample_data):
|
|||
assert data["workspace_id"] == test_workspace.name
|
||||
|
||||
|
||||
def test_create_session_with_all_optional_params(client, sample_data):
|
||||
def test_create_session_with_all_optional_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session creation with all optional parameters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -104,7 +116,11 @@ def test_create_session_with_all_optional_params(client, sample_data):
|
|||
assert data["workspace_id"] == test_workspace.name
|
||||
|
||||
|
||||
def test_create_session_with_too_many_peers(client, sample_data, caplog):
|
||||
def test_create_session_with_too_many_peers(
|
||||
client: TestClient,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
# create 10 peers
|
||||
peer_names = [test_peer.name]
|
||||
|
|
@ -151,7 +167,7 @@ def test_create_session_with_too_many_peers(client, sample_data, caplog):
|
|||
assert data["workspace_id"] == test_workspace.name
|
||||
|
||||
|
||||
def test_get_sessions(client, sample_data):
|
||||
def test_get_sessions(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -180,7 +196,9 @@ def test_get_sessions(client, sample_data):
|
|||
assert data["items"][0]["workspace_id"] == test_workspace.name
|
||||
|
||||
|
||||
def test_get_sessions_with_is_active_filter(client, sample_data):
|
||||
def test_get_sessions_with_is_active_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session listing with is_active parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
|
|
@ -204,9 +222,11 @@ def test_get_sessions_with_is_active_filter(client, sample_data):
|
|||
assert len(inactive_sessions) > 0
|
||||
|
||||
|
||||
def test_get_sessions_with_empty_filter(client, sample_data):
|
||||
def test_get_sessions_with_empty_filter(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session listing with empty filter object"""
|
||||
test_workspace, test_peer = sample_data
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
response = client.post(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/list", json={"filter": {}}
|
||||
|
|
@ -217,7 +237,9 @@ def test_get_sessions_with_empty_filter(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_update_delete_metadata(client, sample_data):
|
||||
def test_update_delete_metadata(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -240,7 +262,7 @@ def test_update_delete_metadata(client, sample_data):
|
|||
assert data["metadata"] == {}
|
||||
|
||||
|
||||
def test_update_session(client, sample_data):
|
||||
def test_update_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -262,7 +284,9 @@ def test_update_session(client, sample_data):
|
|||
assert data["metadata"] == {"new_key": "new_value"}
|
||||
|
||||
|
||||
def test_update_session_with_configuration(client, sample_data):
|
||||
def test_update_session_with_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session update with configuration parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -284,7 +308,9 @@ def test_update_session_with_configuration(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_update_session_with_all_optional_params(client, sample_data):
|
||||
def test_update_session_with_all_optional_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session update with both metadata and configuration"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -308,7 +334,9 @@ def test_update_session_with_all_optional_params(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_update_session_with_null_configuration(client, sample_data):
|
||||
def test_update_session_with_null_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session update with null configuration"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -333,7 +361,7 @@ def test_update_session_with_null_configuration(client, sample_data):
|
|||
assert "configuration" in data
|
||||
|
||||
|
||||
def test_delete_session(client, sample_data):
|
||||
def test_delete_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -363,7 +391,7 @@ def test_delete_session(client, sample_data):
|
|||
assert inactive_session["is_active"] is False
|
||||
|
||||
|
||||
def test_clone_session(client, sample_data):
|
||||
def test_clone_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -422,7 +450,9 @@ def test_clone_session(client, sample_data):
|
|||
assert data["items"][1]["metadata"] == {"key": "value2"}
|
||||
|
||||
|
||||
def test_clone_session_with_cutoff(client, sample_data):
|
||||
def test_clone_session_with_cutoff(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session cloning with message cutoff parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -457,7 +487,7 @@ def test_clone_session_with_cutoff(client, sample_data):
|
|||
assert "id" in data
|
||||
|
||||
|
||||
def test_add_peers_to_session(client, sample_data):
|
||||
def test_add_peers_to_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create another peer
|
||||
peer2_name = str(generate_nanoid())
|
||||
|
|
@ -486,7 +516,7 @@ def test_add_peers_to_session(client, sample_data):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_get_session_peers(client, sample_data):
|
||||
def test_get_session_peers(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create another peer
|
||||
peer2_name = str(generate_nanoid())
|
||||
|
|
@ -520,7 +550,7 @@ def test_get_session_peers(client, sample_data):
|
|||
assert peer2_name in peer_names
|
||||
|
||||
|
||||
def test_set_session_peers(client, sample_data):
|
||||
def test_set_session_peers(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create another peer
|
||||
peer2_name = str(generate_nanoid())
|
||||
|
|
@ -559,7 +589,11 @@ def test_set_session_peers(client, sample_data):
|
|||
assert data["items"][0]["id"] == peer2_name
|
||||
|
||||
|
||||
def test_set_session_peers_with_limit(client, sample_data, caplog):
|
||||
def test_set_session_peers_with_limit(
|
||||
client: TestClient,
|
||||
sample_data: tuple[Workspace, Peer],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
|
||||
# Create a test session with multiple peers
|
||||
|
|
@ -584,7 +618,7 @@ def test_set_session_peers_with_limit(client, sample_data, caplog):
|
|||
peer_names.append(peer_name)
|
||||
|
||||
# set peers with 11 peers (as a dict of peer_name: {})
|
||||
peers_dict = {peer_name: {} for peer_name in peer_names}
|
||||
peers_dict: dict[str, dict[Any, Any]] = {peer_name: {} for peer_name in peer_names}
|
||||
response = client.put(
|
||||
f"/v2/workspaces/{test_workspace.name}/sessions/{session_id}/peers",
|
||||
json=peers_dict,
|
||||
|
|
@ -593,7 +627,9 @@ def test_set_session_peers_with_limit(client, sample_data, caplog):
|
|||
assert "Failed to set peers for session" in caplog.text
|
||||
|
||||
|
||||
def test_remove_peers_from_session(client, sample_data):
|
||||
def test_remove_peers_from_session(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create another peer
|
||||
peer2_name = str(generate_nanoid())
|
||||
|
|
@ -633,7 +669,7 @@ def test_remove_peers_from_session(client, sample_data):
|
|||
assert data["items"][0]["id"] == peer2_name
|
||||
|
||||
|
||||
def test_get_session_context(client, sample_data):
|
||||
def test_get_session_context(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
"""Test the session context endpoint"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -670,7 +706,9 @@ def test_get_session_context(client, sample_data):
|
|||
assert data["summary"] == "" # Default is empty when summary=False
|
||||
|
||||
|
||||
def test_get_session_context_with_summary(client, sample_data):
|
||||
def test_get_session_context_with_summary(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with summary parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -690,7 +728,9 @@ def test_get_session_context_with_summary(client, sample_data):
|
|||
assert "summary" in data
|
||||
|
||||
|
||||
def test_get_session_context_with_tokens(client, sample_data):
|
||||
def test_get_session_context_with_tokens(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with token limit parameter"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -711,7 +751,9 @@ def test_get_session_context_with_tokens(client, sample_data):
|
|||
assert isinstance(data["messages"], list)
|
||||
|
||||
|
||||
def test_get_session_context_with_all_params(client, sample_data):
|
||||
def test_get_session_context_with_all_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session context with both summary and tokens parameters"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -732,7 +774,7 @@ def test_get_session_context_with_all_params(client, sample_data):
|
|||
assert "summary" in data
|
||||
|
||||
|
||||
def test_search_session(client, sample_data):
|
||||
def test_search_session(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
"""Test the session search functionality"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -770,7 +812,9 @@ def test_search_session(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_search_session_empty_query(client, sample_data):
|
||||
def test_search_session_empty_query(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test session search with empty query"""
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -793,7 +837,9 @@ def test_search_session_empty_query(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_search_session_nonexistent(client, sample_data):
|
||||
def test_search_session_nonexistent(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test searching a session that doesn't exist"""
|
||||
test_workspace, _ = sample_data
|
||||
nonexistent_session_id = str(generate_nanoid())
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
|
||||
from src.models import Peer, Workspace
|
||||
|
||||
def test_workspace_validations_api(client):
|
||||
|
||||
def test_workspace_validations_api(client: TestClient):
|
||||
# Test name too short
|
||||
response = client.post("/v2/workspaces", json={"name": "", "metadata": {}})
|
||||
assert response.status_code == 422
|
||||
|
|
@ -28,7 +31,7 @@ def test_workspace_validations_api(client):
|
|||
assert error["type"] == "dict_type"
|
||||
|
||||
|
||||
def test_peer_validations_api(client, sample_data):
|
||||
def test_peer_validations_api(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
# Test name too short
|
||||
|
|
@ -53,7 +56,9 @@ def test_peer_validations_api(client, sample_data):
|
|||
assert error["type"] == "string_too_long"
|
||||
|
||||
|
||||
def test_message_validations_api(client, sample_data):
|
||||
def test_message_validations_api(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session first
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -79,7 +84,9 @@ def test_message_validations_api(client, sample_data):
|
|||
assert error["type"] == "string_too_long"
|
||||
|
||||
|
||||
def test_session_validations_api(client, sample_data):
|
||||
def test_session_validations_api(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a test session first
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -125,7 +132,9 @@ def test_session_validations_api(client, sample_data):
|
|||
assert data["configuration"] == {"test_flag": "test_value"}
|
||||
|
||||
|
||||
def test_agent_query_validations_api(client, sample_data):
|
||||
def test_agent_query_validations_api(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a session first since agent queries are session-based
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
@ -195,7 +204,9 @@ def test_agent_query_validations_api(client, sample_data):
|
|||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_required_field_validations_api(client, sample_data):
|
||||
def test_required_field_validations_api(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
session_id = str(generate_nanoid())
|
||||
session_response = client.post(
|
||||
|
|
@ -225,7 +236,9 @@ def test_required_field_validations_api(client, sample_data):
|
|||
assert error["type"] == "missing"
|
||||
|
||||
|
||||
def test_filter_validations_api(client, sample_data):
|
||||
def test_filter_validations_api(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
test_workspace, test_peer = sample_data
|
||||
# Create a session first
|
||||
session_id = str(generate_nanoid())
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from nanoid import generate as generate_nanoid
|
||||
|
||||
from src.models import Peer, Workspace
|
||||
|
||||
def test_get_or_create_workspace(client):
|
||||
|
||||
def test_get_or_create_workspace(client: TestClient):
|
||||
name = str(generate_nanoid())
|
||||
|
||||
# This should create the workspace using POST /v2/workspaces
|
||||
|
|
@ -13,7 +16,7 @@ def test_get_or_create_workspace(client):
|
|||
assert "id" in data
|
||||
|
||||
|
||||
def test_get_or_create_workspace_with_configuration(client):
|
||||
def test_get_or_create_workspace_with_configuration(client: TestClient):
|
||||
"""Test workspace creation with configuration parameter"""
|
||||
name = str(generate_nanoid())
|
||||
configuration = {"feature1": True, "feature2": False}
|
||||
|
|
@ -27,7 +30,7 @@ def test_get_or_create_workspace_with_configuration(client):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_get_or_create_workspace_with_all_optional_params(client):
|
||||
def test_get_or_create_workspace_with_all_optional_params(client: TestClient):
|
||||
"""Test workspace creation with all optional parameters"""
|
||||
name = str(generate_nanoid())
|
||||
metadata = {"key": "value", "number": 42}
|
||||
|
|
@ -44,7 +47,7 @@ def test_get_or_create_workspace_with_all_optional_params(client):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_get_or_create_existing_workspace(client):
|
||||
def test_get_or_create_existing_workspace(client: TestClient):
|
||||
name = str(generate_nanoid())
|
||||
|
||||
# Create the workspace
|
||||
|
|
@ -67,7 +70,7 @@ def test_get_or_create_existing_workspace(client):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_workspaces(client, db_session, sample_data):
|
||||
async def test_get_all_workspaces(client: TestClient):
|
||||
# create a test workspace with metadata
|
||||
response = client.post(
|
||||
"/v2/workspaces",
|
||||
|
|
@ -98,7 +101,7 @@ async def test_get_all_workspaces(client, db_session, sample_data):
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_workspaces_with_empty_filter(client, db_session, sample_data):
|
||||
async def test_get_all_workspaces_with_empty_filter(client: TestClient):
|
||||
"""Test workspace listing with empty filter object"""
|
||||
response = client.post("/v2/workspaces/list", json={"filter": {}})
|
||||
assert response.status_code == 200
|
||||
|
|
@ -108,7 +111,7 @@ async def test_get_all_workspaces_with_empty_filter(client, db_session, sample_d
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_all_workspaces_with_null_filter(client, db_session, sample_data):
|
||||
async def test_get_all_workspaces_with_null_filter(client: TestClient):
|
||||
"""Test workspace listing with null filter"""
|
||||
response = client.post("/v2/workspaces/list", json={"filter": None})
|
||||
assert response.status_code == 200
|
||||
|
|
@ -117,7 +120,7 @@ async def test_get_all_workspaces_with_null_filter(client, db_session, sample_da
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_update_workspace(client, sample_data):
|
||||
def test_update_workspace(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
test_workspace, _ = sample_data
|
||||
_new_name = str(generate_nanoid())
|
||||
response = client.put(
|
||||
|
|
@ -129,7 +132,9 @@ def test_update_workspace(client, sample_data):
|
|||
assert data["metadata"] == {"new_key": "new_value"}
|
||||
|
||||
|
||||
def test_update_workspace_with_configuration(client, sample_data):
|
||||
def test_update_workspace_with_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test workspace update with configuration parameter"""
|
||||
test_workspace, _ = sample_data
|
||||
configuration = {"new_feature": True, "legacy_feature": False}
|
||||
|
|
@ -142,7 +147,9 @@ def test_update_workspace_with_configuration(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_update_workspace_with_all_optional_params(client, sample_data):
|
||||
def test_update_workspace_with_all_optional_params(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test workspace update with both metadata and configuration"""
|
||||
test_workspace, _ = sample_data
|
||||
metadata = {"updated_key": "updated_value", "count": 100}
|
||||
|
|
@ -158,7 +165,9 @@ def test_update_workspace_with_all_optional_params(client, sample_data):
|
|||
assert data["configuration"] == configuration
|
||||
|
||||
|
||||
def test_update_workspace_with_null_metadata(client, sample_data):
|
||||
def test_update_workspace_with_null_metadata(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test workspace update with null metadata (should clear metadata)"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -178,7 +187,9 @@ def test_update_workspace_with_null_metadata(client, sample_data):
|
|||
assert "metadata" in data
|
||||
|
||||
|
||||
def test_update_workspace_with_null_configuration(client, sample_data):
|
||||
def test_update_workspace_with_null_configuration(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test workspace update with null configuration"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -190,7 +201,7 @@ def test_update_workspace_with_null_configuration(client, sample_data):
|
|||
assert "configuration" in data
|
||||
|
||||
|
||||
def test_create_duplicate_workspace_name(client):
|
||||
def test_create_duplicate_workspace_name(client: TestClient):
|
||||
# Create an workspace
|
||||
name = str(generate_nanoid())
|
||||
response = client.post("/v2/workspaces", json={"name": name})
|
||||
|
|
@ -205,7 +216,7 @@ def test_create_duplicate_workspace_name(client):
|
|||
assert data["id"] == name
|
||||
|
||||
|
||||
def test_search_workspace(client, sample_data):
|
||||
def test_search_workspace(client: TestClient, sample_data: tuple[Workspace, Peer]):
|
||||
"""Test the workspace search functionality"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -224,7 +235,9 @@ def test_search_workspace(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_search_workspace_empty_query(client, sample_data):
|
||||
def test_search_workspace_empty_query(
|
||||
client: TestClient, sample_data: tuple[Workspace, Peer]
|
||||
):
|
||||
"""Test the workspace search with empty query"""
|
||||
test_workspace, _ = sample_data
|
||||
|
||||
|
|
@ -238,7 +251,7 @@ def test_search_workspace_empty_query(client, sample_data):
|
|||
assert isinstance(data["items"], list)
|
||||
|
||||
|
||||
def test_search_workspace_nonexistent(client):
|
||||
def test_search_workspace_nonexistent(client: TestClient):
|
||||
"""Test searching a workspace that doesn't exist"""
|
||||
nonexistent_workspace_id = str(generate_nanoid())
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src import agent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dialectic_call_function_exists():
|
||||
"""Test that dialectic_call function exists and can be mocked"""
|
||||
with patch("src.agent.dialectic_call", new_callable=AsyncMock) as mock_call:
|
||||
mock_call.return_value = MagicMock(content="test response")
|
||||
|
||||
# This would normally make an LLM call, but it's mocked
|
||||
result = await agent.dialectic_call(
|
||||
query="test query",
|
||||
working_representation="test representation",
|
||||
additional_context="test context",
|
||||
)
|
||||
|
||||
assert mock_call.called
|
||||
assert result.content == "test response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dialectic_stream_function_exists():
|
||||
"""Test that dialectic_stream function exists and can be mocked"""
|
||||
with patch("src.agent.dialectic_stream", new_callable=AsyncMock) as mock_stream:
|
||||
mock_stream.return_value = AsyncMock()
|
||||
|
||||
# This would normally make a streaming LLM call, but it's mocked
|
||||
result = await agent.dialectic_stream(
|
||||
query="test query",
|
||||
working_representation="test representation",
|
||||
additional_context="test context",
|
||||
)
|
||||
|
||||
assert mock_stream.called
|
||||
assert result is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_semantic_queries_llm_function_exists():
|
||||
"""Test that generate_semantic_queries_llm function exists and can be mocked"""
|
||||
with patch("src.agent.generate_semantic_queries_llm") as mock_queries:
|
||||
mock_queries.return_value = ["query1", "query2", "query3"]
|
||||
|
||||
# This would normally make an LLM call, but it's mocked
|
||||
result = await agent.generate_semantic_queries_llm("test query")
|
||||
|
||||
assert mock_queries.called
|
||||
assert result == ["query1", "query2", "query3"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_tom_inference_function():
|
||||
"""Test that run_tom_inference function works with new Pydantic objects"""
|
||||
with patch("src.agent.get_tom_inference") as mock_tom:
|
||||
from src.deriver.tom.single_prompt import (
|
||||
CurrentState,
|
||||
TentativeInference,
|
||||
TomInferenceOutput,
|
||||
)
|
||||
|
||||
# Mock the function to return a proper Pydantic object
|
||||
mock_tom_response = TomInferenceOutput(
|
||||
current_state=CurrentState(
|
||||
immediate_context="test context",
|
||||
active_goals="test goals",
|
||||
present_mood="test mood",
|
||||
),
|
||||
tentative_inferences=[
|
||||
TentativeInference(interpretation="test inference", basis="test basis")
|
||||
],
|
||||
knowledge_gaps=[],
|
||||
expectation_violations=[],
|
||||
)
|
||||
mock_tom.return_value = mock_tom_response
|
||||
|
||||
# Test the function
|
||||
result = await agent.run_tom_inference("test chat history")
|
||||
|
||||
# Verify it extracted the right information from the Pydantic object
|
||||
assert "test context" in result
|
||||
assert "test inference" in result
|
||||
assert mock_tom.called
|
||||
|
|
@ -30,7 +30,10 @@ class TestWorkspaceValidations:
|
|||
|
||||
def test_app_invalid_metadata_type(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
WorkspaceCreate(name="test", metadata="not a dict")
|
||||
WorkspaceCreate(
|
||||
name="test",
|
||||
metadata="not a dict", # pyright: ignore
|
||||
)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "dict_type"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,198 +0,0 @@
|
|||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from src.utils.model_client import (
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
ModelClient,
|
||||
ModelProvider,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_env(monkeypatch):
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", "mock-anthropic-api-key")
|
||||
monkeypatch.setenv("OPENAI_COMPATIBLE_API_KEY", "mock-openai-api-key")
|
||||
monkeypatch.setenv("OPENAI_API_KEY", "mock-openai-api-key")
|
||||
monkeypatch.setenv("GROQ_API_KEY", "mock-groq-api-key")
|
||||
monkeypatch.setenv("CEREBRAS_API_KEY", "mock-cerebras-api-key")
|
||||
monkeypatch.setenv("OPENROUTER_API_KEY", "mock-openrouter-api-key")
|
||||
|
||||
|
||||
# Test fixtures
|
||||
@pytest.fixture
|
||||
def mock_anthropic_response():
|
||||
mock_response = MagicMock()
|
||||
mock_content = MagicMock()
|
||||
mock_content.type = "text"
|
||||
mock_content.text = "Test response"
|
||||
mock_response.content = [mock_content]
|
||||
return mock_response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_openai_response():
|
||||
mock_response = MagicMock()
|
||||
mock_message = MagicMock()
|
||||
mock_message.content = "Test response"
|
||||
mock_choice = MagicMock()
|
||||
mock_choice.message = mock_message
|
||||
mock_response.choices = [mock_choice]
|
||||
return mock_response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_anthropic_client():
|
||||
with patch("src.utils.model_client.AsyncAnthropic") as mock:
|
||||
client = MagicMock()
|
||||
client.messages.create = AsyncMock()
|
||||
client.messages.stream = AsyncMock()
|
||||
mock.return_value = client
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_openai_client():
|
||||
with patch("src.utils.model_client.AsyncOpenAI") as mock:
|
||||
client = MagicMock()
|
||||
client.chat.completions.create = AsyncMock()
|
||||
mock.return_value = client
|
||||
yield client
|
||||
|
||||
|
||||
# Initialization Tests
|
||||
def test_default_initialization(mock_env):
|
||||
"""Test default initialization with Anthropic provider."""
|
||||
client = ModelClient()
|
||||
assert client.provider == ModelProvider.ANTHROPIC
|
||||
assert client.model == DEFAULT_MODELS[ModelProvider.ANTHROPIC]
|
||||
assert client.base_url is None
|
||||
|
||||
|
||||
def test_custom_model_initialization():
|
||||
"""Test initialization with custom model name."""
|
||||
custom_model = "custom-model"
|
||||
client = ModelClient(model=custom_model)
|
||||
assert client.model == custom_model
|
||||
|
||||
|
||||
def test_custom_api_key_initialization():
|
||||
"""Test initialization with custom API key."""
|
||||
custom_key = "test-api-key"
|
||||
client = ModelClient(api_key=custom_key)
|
||||
assert client.api_key == custom_key
|
||||
|
||||
|
||||
def test_custom_base_url_initialization():
|
||||
"""Test initialization with custom base URL."""
|
||||
custom_url = "https://custom-api.example.com"
|
||||
client = ModelClient(base_url=custom_url)
|
||||
assert client.base_url == custom_url
|
||||
|
||||
|
||||
def test_unsupported_provider_initialization():
|
||||
"""Test initialization with unsupported provider."""
|
||||
with pytest.raises(ValueError, match="is not a valid ModelProvider"):
|
||||
ModelClient(provider=ModelProvider("unsupported"))
|
||||
|
||||
|
||||
def test_missing_api_key_initialization():
|
||||
"""Test initialization without required API key."""
|
||||
with (
|
||||
patch("src.utils.model_client.settings") as mock_settings,
|
||||
pytest.raises(ValueError, match="API key is required"),
|
||||
):
|
||||
# Mock the settings to return None for all API keys
|
||||
mock_settings.LLM.ANTHROPIC_API_KEY = None
|
||||
mock_settings.LLM.OPENAI_API_KEY = None
|
||||
mock_settings.LLM.OPENAI_COMPATIBLE_API_KEY = None
|
||||
mock_settings.LLM.GROQ_API_KEY = None
|
||||
mock_settings.LLM.GEMINI_API_KEY = None
|
||||
ModelClient()
|
||||
|
||||
|
||||
# Message Creation Tests
|
||||
def test_create_message():
|
||||
"""Test message creation with different roles."""
|
||||
client = ModelClient()
|
||||
message = client.create_message("user", "Hello")
|
||||
assert message == {"role": "user", "content": "Hello"}
|
||||
|
||||
|
||||
# Generation Tests
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_anthropic(mock_anthropic_client, mock_anthropic_response):
|
||||
"""Test generation with Anthropic provider."""
|
||||
mock_anthropic_client.messages.create.return_value = mock_anthropic_response
|
||||
|
||||
client = ModelClient(provider=ModelProvider.ANTHROPIC)
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
response = await client.generate(messages)
|
||||
assert response == "Test response"
|
||||
|
||||
# Verify the API call
|
||||
mock_anthropic_client.messages.create.assert_called_once()
|
||||
call_args = mock_anthropic_client.messages.create.call_args[1]
|
||||
assert call_args["model"] == DEFAULT_MODELS[ModelProvider.ANTHROPIC]
|
||||
assert call_args["messages"] == messages
|
||||
assert call_args["max_tokens"] == DEFAULT_MAX_TOKENS
|
||||
assert call_args["temperature"] == DEFAULT_TEMPERATURE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_openai(mock_openai_client, mock_openai_response, mock_env):
|
||||
"""Test generation with OpenAI provider."""
|
||||
mock_openai_client.chat.completions.create.return_value = mock_openai_response
|
||||
|
||||
client = ModelClient(provider=ModelProvider.OPENAI)
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
response = await client.generate(messages)
|
||||
assert response == "Test response"
|
||||
|
||||
# Verify the API call
|
||||
mock_openai_client.chat.completions.create.assert_called_once()
|
||||
call_args = mock_openai_client.chat.completions.create.call_args[1]
|
||||
assert call_args["model"] == DEFAULT_MODELS[ModelProvider.OPENAI]
|
||||
assert call_args["messages"] == messages
|
||||
assert call_args["max_tokens"] == DEFAULT_MAX_TOKENS
|
||||
assert call_args["temperature"] == DEFAULT_TEMPERATURE
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_with_system_prompt(
|
||||
mock_anthropic_client, mock_anthropic_response
|
||||
):
|
||||
"""Test generation with system prompt."""
|
||||
mock_anthropic_client.messages.create.return_value = mock_anthropic_response
|
||||
|
||||
client = ModelClient(provider=ModelProvider.ANTHROPIC)
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
system = "You are a helpful assistant"
|
||||
response = await client.generate(messages, system=system)
|
||||
assert response == "Test response"
|
||||
|
||||
# Verify the API call
|
||||
mock_anthropic_client.messages.create.assert_called_once()
|
||||
call_args = mock_anthropic_client.messages.create.call_args[1]
|
||||
assert call_args["system"] == system
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generate_with_caching(mock_anthropic_client, mock_anthropic_response):
|
||||
"""Test generation with caching enabled."""
|
||||
mock_anthropic_client.messages.create.return_value = mock_anthropic_response
|
||||
|
||||
client = ModelClient(provider=ModelProvider.ANTHROPIC)
|
||||
messages = [{"role": "user", "content": "Hello"}]
|
||||
system = "You are a helpful assistant"
|
||||
response = await client.generate(messages, system=system, use_caching=True)
|
||||
assert response == "Test response"
|
||||
|
||||
# Verify the API call
|
||||
mock_anthropic_client.messages.create.assert_called_once()
|
||||
call_args = mock_anthropic_client.messages.create.call_args[1]
|
||||
assert call_args["system"] == [
|
||||
{"type": "text", "text": system, "cache_control": {"type": "ephemeral"}}
|
||||
]
|
||||
520
uv.lock
520
uv.lock
|
|
@ -1,9 +1,5 @@
|
|||
version = 1
|
||||
requires-python = ">=3.9"
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.13'",
|
||||
"python_full_version >= '3.13'",
|
||||
]
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
|
|
@ -31,7 +27,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.54.0"
|
||||
version = "0.55.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -42,9 +38,9 @@ dependencies = [
|
|||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/28/80cb9bb6e7ce77d404145b51da4257455805c17f0a6be528ff3286e3882f/anthropic-0.54.0.tar.gz", hash = "sha256:5e6f997d97ce8e70eac603c3ec2e7f23addeff953fbbb76b19430562bb6ba815", size = 312376 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/19/e2e09bc7fc0c4562ae865b3e5d487931c254c517e1c739b0c8aef2cf3186/anthropic-0.55.0.tar.gz", hash = "sha256:61826efa1bda0e4c7dc6f6a0d82b7d99b3fda970cd048d40ef5fca08a5eabd33", size = 408192 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/b9/6ffb48e82c5e97b03cecee872d134a6b6666c2767b2d32ed709f3a60a8fe/anthropic-0.54.0-py3-none-any.whl", hash = "sha256:c1062a0a905daeec17ca9c06c401e4b3f24cb0495841d29d752568a1d4018d56", size = 288774 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/8f/ba982f539db40f49a610f61562e9b54fb9c85e7b9ede9a46ff6f9e79042f/anthropic-0.55.0-py3-none-any.whl", hash = "sha256:3518433fc0372a13f2b793b4cabecc7734ec9176e063a0f28dac19aa17c57f94", size = 289318 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -80,6 +76,18 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basedpyright"
|
||||
version = "1.29.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "nodejs-wheel-binaries" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/80/fb/bd92196a07e3b4ccee4ff2761a26a05bff77d4da089b67b4b1a547868099/basedpyright-1.29.4.tar.gz", hash = "sha256:2df1976f8591eedf4b4ce8f9d123f43e810cc8cb7cc83c53eec0e2f8044073d0", size = 21961481 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/dc/180fe721a2574fb3aad4051adcca196ac2d18adaf75122f5eeb47436cca2/basedpyright-1.29.4-py3-none-any.whl", hash = "sha256:e087513979972f83010639c6c1a1c13dd3b1d24ee45f8ecff747962cc2063d6f", size = 11476859 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.5.2"
|
||||
|
|
@ -156,32 +164,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
version = "8.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -253,16 +248,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d6/c41dd9b02bf16ec001aaf1cbef665537606899a3db1094e78f5ae17540ca/coverage-7.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f424507f57878e424d9a95dc4ead3fbdd72fd201e404e861e465f28ea469951", size = 212029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/c0/40420d81d731f84c3916dcdf0506b3e6c6570817bff2576b83f780914ae6/coverage-7.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:535fde4001b2783ac80865d90e7cc7798b6b126f4cd8a8c54acfe76804e54e58", size = 212407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/87/f0db7d62d0e09f14d6d2f6ae8c7274a2f09edf74895a34b412a0601e375a/coverage-7.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02532fd3290bb8fa6bec876520842428e2a6ed6c27014eca81b031c2d30e3f71", size = 241160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/b7/3337c064f058a5d7696c4867159651a5b5fb01a5202bcf37362f0c51400e/coverage-7.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56f5eb308b17bca3bbff810f55ee26d51926d9f89ba92707ee41d3c061257e55", size = 239027 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/a9/5898a283f66d1bd413c32c2e0e05408196fd4f37e206e2b06c6e0c626e0e/coverage-7.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa447506c1a52271f1b0de3f42ea0fa14676052549095e378d5bff1c505ff7b", size = 240145 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/33/d96e3350078a3c423c549cb5b2ba970de24c5257954d3e4066e2b2152d30/coverage-7.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9ca8e220006966b4a7b68e8984a6aee645a0384b0769e829ba60281fe61ec4f7", size = 239871 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/6e/6fb946072455f71a820cac144d49d11747a0f1a21038060a68d2d0200499/coverage-7.9.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49f1d0788ba5b7ba65933f3a18864117c6506619f5ca80326b478f72acf3f385", size = 238122 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/5c/bc43f25c8586840ce25a796a8111acf6a2b5f0909ba89a10d41ccff3920d/coverage-7.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68cd53aec6f45b8e4724c0950ce86eacb775c6be01ce6e3669fe4f3a21e768ed", size = 239058 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/d8/ce2007418dd7fd00ff8c8b898bb150bb4bac2d6a86df05d7b88a07ff595f/coverage-7.9.1-cp39-cp39-win32.whl", hash = "sha256:95335095b6c7b1cc14c3f3f17d5452ce677e8490d101698562b2ffcacc304c8d", size = 214532 },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/21/334e76fa246e92e6d69cab217f7c8a70ae0cc8f01438bd0544103f29528e/coverage-7.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:e1b5191d1648acc439b24721caab2fd0c86679d8549ed2c84d5a7ec1bedcc244", size = 215439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000 },
|
||||
]
|
||||
|
|
@ -285,6 +270,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/12/9c22a58c0b1e29271051222d8906257616da84135af9ed167c9e28f85cb3/docstring_parser-0.16.tar.gz", hash = "sha256:538beabd0af1e2db0146b6bd3caa526c35a34d61af9fd2887f3a8a27a739aa6e", size = 26565 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "2.2.0"
|
||||
|
|
@ -449,16 +443,23 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/67/10/b2a4b63d3f08362662e89c103f7fe28894a51ae0bc890fabf37d1d780e52/greenlet-3.2.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02b0df6f63cd15012bed5401b47829cfd2e97052dc89da3cfaf2c779124eb892", size = 692995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/c6/ad82f148a4e3ce9564056453a71529732baf5448ad53fc323e37efe34f66/greenlet-3.2.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86c2d68e87107c1792e2e8d5399acec2487a4e993ab76c792408e59394d52141", size = 655320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/4f/aab73ecaa6b3086a4c89863d94cf26fa84cbff63f52ce9bc4342b3087a06/greenlet-3.2.3-cp314-cp314-win_amd64.whl", hash = "sha256:8c47aae8fbbfcf82cc13327ae802ba13c9c36753b67e760023fd116bc124a62a", size = 301236 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/d9/a3114df5fba2bf9823e0acc01e9e2abdcd8ea4c5487cf1c3dcd4cc0b48cf/greenlet-3.2.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:42efc522c0bd75ffa11a71e09cd8a399d83fafe36db250a87cf1dacfaa15dc64", size = 267769 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/da/47dfc50f6e5673116e66a737dc58d1eca651db9a9aa8797c1d27e940e211/greenlet-3.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d760f9bdfe79bff803bad32b4d8ffb2c1d2ce906313fc10a83976ffb73d64ca7", size = 625472 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/74/f6ef9f85d981b2fcd665bbee3e69e3c0a10fb962eb4c6a5889ac3b6debfa/greenlet-3.2.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:8324319cbd7b35b97990090808fdc99c27fe5338f87db50514959f8059999805", size = 637253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/69/4919bb1c9e43bfc16dc886e7a37fe1bc04bfa4101aba177936a10f313cad/greenlet-3.2.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8c37ef5b3787567d322331d5250e44e42b58c8c713859b8a04c6065f27efbf72", size = 632611 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/8d/97d988d019f40b6b360b0c71c99e5b4c877a3d92666fe48b081d0e1ea1cd/greenlet-3.2.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ce539fb52fb774d0802175d37fcff5c723e2c7d249c65916257f0a940cee8904", size = 631843 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/24/d5e1504ec00768755d4ccc2168b76d9f4524e96694a14ad45bd87796e9bb/greenlet-3.2.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:003c930e0e074db83559edc8705f3a2d066d4aa8c2f198aff1e454946efd0f26", size = 580781 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/df/d009bcca566dbfd2283b306b4e424f4c0e59bf984868f8b789802fe9e607/greenlet-3.2.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7e70ea4384b81ef9e84192e8a77fb87573138aa5d4feee541d8014e452b434da", size = 1109903 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/54/5036097197a78388aa6901a5b90b562f3a154a9fbee89c301a26f56f3942/greenlet-3.2.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:22eb5ba839c4b2156f18f76768233fe44b23a31decd9cc0d4cc8141c211fd1b4", size = 1133975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/15/b001456a430805fdd8b600a788d19a790664eee8863739523395f68df752/greenlet-3.2.3-cp39-cp39-win32.whl", hash = "sha256:4532f0d25df67f896d137431b13f4cdce89f7e3d4a96387a41290910df4d3a57", size = 279320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4c/bf2100cbc1bd07f39bee3b09e7eef39beffe29f5453dc2477a2693737913/greenlet-3.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:aaa7aae1e7f75eaa3ae400ad98f8644bb81e1dc6ba47ce8a93d3f17274e08322", size = 296444 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "groq"
|
||||
version = "0.28.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/7d/bb053ba75357bf5e8c33def63fb31c8b0bb86dce07759a0cd8e3232d2df9/groq-0.28.0.tar.gz", hash = "sha256:65e1cab9184cbb32380d62eca50d6162269c7ec0c77e4cc868069cfe93450f9f", size = 131730 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/24/20fc18d1b3e0883aeb24286ca8f26dc1970561b07d9c4412c84561bdf307/groq-0.28.0-py3-none-any.whl", hash = "sha256:c6f86638371c2cba2ca337232e76c8d412e75965ed7e3058d30c9aa5dfe84303", size = 130217 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -476,17 +477,16 @@ version = "2.0.1"
|
|||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "alembic" },
|
||||
{ name = "anthropic" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "fastapi-pagination" },
|
||||
{ name = "google-genai" },
|
||||
{ name = "greenlet" },
|
||||
{ name = "httpx" },
|
||||
{ name = "langfuse" },
|
||||
{ name = "mirascope", extra = ["anthropic", "google", "groq", "langfuse"] },
|
||||
{ name = "nanoid" },
|
||||
{ name = "openai" },
|
||||
{ name = "pgvector" },
|
||||
{ name = "psycopg", extra = ["binary"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "python-dotenv" },
|
||||
|
|
@ -494,11 +494,11 @@ dependencies = [
|
|||
{ name = "sentry-sdk", extra = ["anthropic", "fastapi", "sqlalchemy"] },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "tiktoken" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "basedpyright" },
|
||||
{ name = "coverage" },
|
||||
{ name = "interrogate" },
|
||||
{ name = "py-spy" },
|
||||
|
|
@ -511,29 +511,28 @@ dev = [
|
|||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "alembic", specifier = ">=1.14.0" },
|
||||
{ name = "anthropic", specifier = ">=0.36.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.111.0" },
|
||||
{ name = "fastapi-pagination", specifier = ">=0.12.24" },
|
||||
{ name = "google-genai", specifier = ">=1.10.0" },
|
||||
{ name = "greenlet", specifier = ">=3.0.3" },
|
||||
{ name = "httpx", specifier = ">=0.27.0" },
|
||||
{ name = "langfuse", specifier = ">=2.57.1,<3.0.0" },
|
||||
{ name = "mirascope", extras = ["anthropic", "google", "groq", "langfuse"], specifier = ">=1.25.1" },
|
||||
{ name = "nanoid", specifier = ">=2.0.0" },
|
||||
{ name = "openai", specifier = ">=1.43.0" },
|
||||
{ name = "openai", specifier = ">=1.91.0" },
|
||||
{ name = "pgvector", specifier = ">=0.2.5" },
|
||||
{ name = "psycopg", extras = ["binary"], specifier = ">=3.1.19" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.9.1" },
|
||||
{ name = "pydantic", specifier = ">=2.11.7" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.10.1" },
|
||||
{ name = "pyjwt", specifier = ">=2.10.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||
{ name = "rich", specifier = ">=13.7.1" },
|
||||
{ name = "sentry-sdk", extras = ["anthropic", "fastapi", "sqlalchemy"], specifier = ">=2.3.1" },
|
||||
{ name = "sqlalchemy", specifier = ">=2.0.30" },
|
||||
{ name = "tiktoken", specifier = ">=0.9.0" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "basedpyright", specifier = ">=1.29.4" },
|
||||
{ name = "coverage", specifier = ">=7.6.0" },
|
||||
{ name = "interrogate", specifier = ">=1.7.0" },
|
||||
{ name = "py-spy", specifier = ">=0.3.14" },
|
||||
|
|
@ -590,13 +589,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/b1/4fc6f52afdf93b7c4304e21f6add9e981e4f857c2fa622a55dfe21b6059e/httptools-0.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85797e37e8eeaa5439d33e556662cc370e474445d5fab24dcadc65a8ffb04003", size = 201123 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/01/e6ecb40ac8fdfb76607c7d3b74a41b464458d5c8710534d8f163b0c15f29/httptools-0.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:db353d22843cf1028f43c3651581e4bb49374d85692a85f95f7b9a130e1b2cab", size = 104507 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/24/c70c34119d209bf08199d938dc9c69164f585ed3029237b4bdb90f673cb9/httptools-0.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1ffd262a73d7c28424252381a5b854c19d9de5f56f075445d33919a637e3547", size = 449615 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/62/e7f317fed3703bd81053840cacba4e40bcf424b870e4197f94bd1cf9fe7a/httptools-0.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:703c346571fa50d2e9856a37d7cd9435a25e7fd15e236c397bf224afaa355fe9", size = 448819 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/13/68337d3be6b023260139434c49d7aa466aaa98f9aee7ed29270ac7dde6a2/httptools-0.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aafe0f1918ed07b67c1e838f950b1c1fabc683030477e60b335649b8020e1076", size = 422093 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b3/3a1bc45be03dda7a60c7858e55b6cd0489a81613c1908fb81cf21d34ae50/httptools-0.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0e563e54979e97b6d13f1bbc05a96109923e76b901f786a5eae36e99c01237bd", size = 423898 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/72/2ddc2ae5f7ace986f7e68a326215b2e7c32e32fd40e6428fa8f1d8065c7e/httptools-0.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:b799de31416ecc589ad79dd85a0b2657a8fe39327944998dea368c1d4c9e55e6", size = 89552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -731,18 +723,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127 },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/fd/aced428e2bd3c6c1132f67c5a708f9e7fd161d0ca8f8c5862b17b93cdf0a/jiter-0.10.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bd6292a43c0fc09ce7c154ec0fa646a536b877d1e8f2f96c19707f65355b5a4d", size = 317665 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2e/47d42f15d53ed382aef8212a737101ae2720e3697a954f9b95af06d34e89/jiter-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39de429dcaeb6808d75ffe9effefe96a4903c6a4b376b2f6d08d77c1aaee2f18", size = 312152 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/02/aae834228ef4834fc18718724017995ace8da5f70aa1ec225b9bc2b2d7aa/jiter-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ce124f13a7a616fad3bb723f2bfb537d78239d1f7f219566dc52b6f2a9e48d", size = 346708 },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d4/6ff39dee2d0a9abd69d8a3832ce48a3aa644eed75e8515b5ff86c526ca9a/jiter-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:166f3606f11920f9a1746b2eea84fa2c0a5d50fd313c38bdea4edc072000b0af", size = 371360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/67/c749d962b4eb62445867ae4e64a543cbb5d63cc7d78ada274ac515500a7f/jiter-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28dcecbb4ba402916034fc14eba7709f250c4d24b0c43fc94d187ee0580af181", size = 492105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d3/8fe1b1bae5161f27b1891c256668f598fa4c30c0a7dacd668046a6215fca/jiter-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86c5aa6910f9bebcc7bc4f8bc461aff68504388b43bfe5e5c0bd21efa33b52f4", size = 389577 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/28/ecb19d789b4777898a4252bfaac35e3f8caf16c93becd58dcbaac0dc24ad/jiter-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceeb52d242b315d7f1f74b441b6a167f78cea801ad7c11c36da77ff2d42e8a28", size = 353849 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/69/261f798f84790da6482ebd8c87ec976192b8c846e79444d0a2e0d33ebed8/jiter-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ff76d8887c8c8ee1e772274fcf8cc1071c2c58590d13e33bd12d02dc9a560397", size = 392029 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/08/b8d15140d4d91f16faa2f5d416c1a71ab1bbe2b66c57197b692d04c0335f/jiter-0.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a9be4d0fa2b79f7222a88aa488bd89e2ae0a0a5b189462a12def6ece2faa45f1", size = 524386 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/1d/23c41765cc95c0e23ac492a88450d34bf0fd87a37218d1b97000bffe0f53/jiter-0.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab7fd8738094139b6c1ab1822d6f2000ebe41515c537235fd45dabe13ec9324", size = 515234 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/14/381d8b151132e79790579819c3775be32820569f23806769658535fe467f/jiter-0.10.0-cp39-cp39-win32.whl", hash = "sha256:5f51e048540dd27f204ff4a87f5d79294ea0aa3aa552aca34934588cf27023cf", size = 211436 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/66/f23ae51dea8ee8ce429027b60008ca895d0fa0704f0c7fe5f09014a6cffb/jiter-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b28302349dc65703a9e4ead16f163b1c339efffbe1049c30a44b001a2a4fff9", size = 208777 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -844,16 +824,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -865,6 +835,37 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mirascope"
|
||||
version = "1.25.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "docstring-parser" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/60/e7/4a196658d3e9ffe5840872922e0ea5e9f583bc3cb204c769e55347e142c1/mirascope-1.25.2.tar.gz", hash = "sha256:88df7232c8b41150e0f788371ba5657c08c27eb544dec93aa4d8354adf04f4be", size = 627149 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/1d/cf7f99294c19552f4a80ba092d8b883ab34161f7030c66c38a5c43e27c0d/mirascope-1.25.2-py3-none-any.whl", hash = "sha256:9d96bec0bcb7fa3dd767803d978b71167eb1fd657be865b9f3bfbd23f24d2187", size = 373194 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
anthropic = [
|
||||
{ name = "anthropic" },
|
||||
]
|
||||
google = [
|
||||
{ name = "google-genai" },
|
||||
{ name = "pillow" },
|
||||
{ name = "proto-plus" },
|
||||
]
|
||||
groq = [
|
||||
{ name = "groq" },
|
||||
]
|
||||
langfuse = [
|
||||
{ name = "langfuse" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanoid"
|
||||
version = "2.0.0"
|
||||
|
|
@ -875,60 +876,86 @@ wheels = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.0.2"
|
||||
name = "nodejs-wheel-binaries"
|
||||
version = "22.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/c6/66f36b7b0d528660dfb4a59cb9b8dd6a3f4c0a3939cd49c404a775ea4a63/nodejs_wheel_binaries-22.16.0.tar.gz", hash = "sha256:d695832f026df3a0cf9a089d222225939de9d1b67f8f0a353b79f015aabbe7e2", size = 8061 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/dc/417a5c5f99e53a5d2b3be122506312731eb90fb9630c248e327e2e38cc6b/nodejs_wheel_binaries-22.16.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:986b715a96ed703f8ce0c15712f76fc42895cf09067d72b6ef29e8b334eccf64", size = 50957501 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/dd/d6ce48209ed15f5d1fccb29eeaa111f962557123eaf4fd03a7316c42734c/nodejs_wheel_binaries-22.16.0-py2.py3-none-macosx_11_0_x86_64.whl", hash = "sha256:4ae3cf22138891cb44c3ee952862a257ce082b098b29024d7175684a9a77b0c0", size = 51891634 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/fa/a07e622fd87717eec3e5cff41575f85ad62717e8698884d28ca809266ca1/nodejs_wheel_binaries-22.16.0-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f2de4dc0b64ae43e146897ce811f80ac4f9acfbae6ccf814226282bf4ef174", size = 57857862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/80/52736f9570a93f8e6b7942981dc9770eca2bc7aa1d200c1d54198374a6ca/nodejs_wheel_binaries-22.16.0-py2.py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbfccbcd558d2f142ccf66d8c3a098022bf4436db9525b5b8d32169ce185d99e", size = 58395868 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/0e/53616a5ed8fc1fbe9e48bf132862da5a9abf5cc7f8483dab1722ec257187/nodejs_wheel_binaries-22.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:447ad796850eb52ca20356ad39b2d296ed8fef3f214921f84a1ccdad49f2eba1", size = 59712469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/cd/e2b5083df581fc1d08eb93feb6f8fbd3d56b113cef9b59d8e0fb7d4dd4f3/nodejs_wheel_binaries-22.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7f526ca6a132b0caf633566a2a78c6985fe92857e7bfdb37380f76205a10b808", size = 60763005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/8d/57112b49214e8bd636f3cc3386eba6be4d23552ec8a0f6efbe814013caa7/nodejs_wheel_binaries-22.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:2fffb4bf1066fb5f660da20819d754f1b424bca1b234ba0f4fa901c52e3975fb", size = 41313324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/03/a852711aec73dfb965844592dfe226024c0da28e37d1ee54083342e38f57/nodejs_wheel_binaries-22.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:2728972d336d436d39ee45988978d8b5d963509e06f063e80fe41b203ee80b28", size = 38828154 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.2.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "1.88.0"
|
||||
version = "1.91.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
|
|
@ -940,9 +967,9 @@ dependencies = [
|
|||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/ea/bbeef604d1fe0f7e9111745bb8a81362973a95713b28855beb9a9832ab12/openai-1.88.0.tar.gz", hash = "sha256:122d35e42998255cf1fc84560f6ee49a844e65c054cd05d3e42fda506b832bb1", size = 470963 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/e2/a22f2973b729eff3f1f429017bdf717930c5de0fbf9e14017bae330e4e7a/openai-1.91.0.tar.gz", hash = "sha256:d6b07730d2f7c6745d0991997c16f85cddfc90ddcde8d569c862c30716b9fc90", size = 472529 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/03/ef68d77a38dd383cbed7fc898857d394d5a8b0520a35f054e7fe05dc3ac1/openai-1.88.0-py3-none-any.whl", hash = "sha256:7edd7826b3b83f5846562a6f310f040c79576278bf8e3687b30ba05bb5dff978", size = 734293 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d2/f99bdd6fc737d6b3cf0df895508d621fc9a386b375a1230ee81d46c5436e/openai-1.91.0-py3-none-any.whl", hash = "sha256:207f87aa3bc49365e014fac2f7e291b99929f4fe126c4654143440e0ad446a5f", size = 735837 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -966,6 +993,65 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/bf/21/b5735d5982892c878ff3d01bb06e018c43fc204428361ee9fc25a1b2125c/pgvector-0.4.1-py3-none-any.whl", hash = "sha256:34bb4e99e1b13d08a2fe82dda9f860f15ddcd0166fbb25bffe15821cbfeb7362", size = 27086 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163 },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655 },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219 },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
|
|
@ -975,6 +1061,32 @@ 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 = "proto-plus"
|
||||
version = "1.26.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "6.31.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.2.9"
|
||||
|
|
@ -1042,17 +1154,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/4a/e095884dd016b2bde2796043c61cd383b79e5d2a820c33e2c47293707ca8/psycopg_binary-3.2.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587a3f19954d687a14e0c8202628844db692dbf00bba0e6d006659bf1ca91cbe", size = 4034274 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/e9/ab3fad6033de260a620f6481e66092417ce31fa194dbf9ac292ab8cb9fd0/psycopg_binary-3.2.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:791759138380df21d356ff991265fde7fe5997b0c924a502847a9f9141e68786", size = 4083015 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/c8/6cd54a349d0b62b080761eb7bda43190003ecbbf17920d57254d5c780e11/psycopg_binary-3.2.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95315b8c8ddfa2fdcb7fe3ddea8a595c1364524f512160c604e3be368be9dd07", size = 4679369 },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/34/35c65ac413c485e9340d62f14adcb34420acae44425f77aee591d49e6647/psycopg_binary-3.2.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18ac08475c9b971237fcc395b0a6ee4e8580bb5cf6247bc9b8461644bef5d9f4", size = 4500889 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/a9/f691b8037b0bcef481b09ae4283beedbf048f79b6fe9bda1445dbb14ed18/psycopg_binary-3.2.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac2c04b6345e215e65ca6aef5c05cc689a960b16674eaa1f90a8f86dfaee8c04", size = 4769218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/38/25afc811c1dfb664b31d66d6f5c070326a1f89f768f1b673273a3abe6912/psycopg_binary-3.2.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1ab25e3134774f1e476d4bb9050cdec25f10802e63e92153906ae934578734", size = 4462834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/e2/eb4a8230e13f691d6e386e22b16d4b90f454839b78ac547be3f399562ee4/psycopg_binary-3.2.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4bfec4a73e8447d8fe8854886ffa78df2b1c279a7592241c2eb393d4499a17e2", size = 3779527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/39/0f79c7d42f0c5711861ce9db55c65e14e7f1e52bd40304b4d6e7cd505e61/psycopg_binary-3.2.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:166acc57af5d2ff0c0c342aed02e69a0cd5ff216cae8820c1059a6f3b7cf5f78", size = 3337958 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/ce/28b1d98aed9337a721b271778d07c5ac7f85730d96f0185cc6d22684536d/psycopg_binary-3.2.9-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:413f9e46259fe26d99461af8e1a2b4795a4e27cc8ac6f7919ec19bcee8945074", size = 3440567 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/54/40a3a8175566f8c1268af0bacf5d7b26371697b6cefa87352c1df4b435e1/psycopg_binary-3.2.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:354dea21137a316b6868ee41c2ae7cce001e104760cf4eab3ec85627aed9b6cd", size = 3498637 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/ee/51748bc8af0ba08e7415fcbbd00b7d069c068f8c08509e8dd0dd0a066394/psycopg_binary-3.2.9-cp39-cp39-win_amd64.whl", hash = "sha256:24ddb03c1ccfe12d000d950c9aba93a7297993c4e3905d9f2c9795bb0764d523", size = 2938614 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1182,19 +1283,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/ea/bbe9095cdd771987d13c82d104a9c8559ae9aec1e29f139e286fd2e9256e/pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", size = 2028677 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1d/4ac5ed228078737d457a609013e8f7edc64adc37b91d619ea965758369e5/pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", size = 1864735 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/9a/2e70d6388d7cda488ae38f57bc2f7b03ee442fbcf0d75d848304ac7e405b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", size = 1898467 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/2e/1568934feb43370c1ffb78a77f0baaa5a8b6897513e7a91051af707ffdc4/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", size = 1983041 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/1a/1a1118f38ab64eac2f6269eb8c120ab915be30e387bb561e3af904b12499/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", size = 2136503 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/da/44754d1d7ae0f22d6d3ce6c6b1486fc07ac2c524ed8f6eca636e2e1ee49b/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", size = 2736079 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/98/f43cd89172220ec5aa86654967b22d862146bc4d736b1350b4c41e7c9c03/pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", size = 2006508 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/cc/f77e8e242171d2158309f830f7d5d07e0531b756106f36bc18712dc439df/pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", size = 2113693 },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7a/7be6a7bd43e0a47c147ba7fbf124fe8aaf1200bc587da925509641113b2d/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", size = 2074224 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/31cf8fadffbb03be1cb520850e00a8490c0927ec456e8293cafda0726184/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", size = 2245403 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/8d/bbaf4c6721b668d44f01861f297eb01c9b35f612f6b8e14173cb204e6240/pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", size = 2242331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/93/3cc157026bca8f5006250e74515119fcaa6d6858aceee8f67ab6dc548c16/pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", size = 1910571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/90/7edc3b2a0d9f0dda8806c04e511a67b0b7a41d2187e2003673a996fb4310/pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", size = 1956504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412 },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749 },
|
||||
|
|
@ -1213,38 +1301,29 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/98/dbf3fdfabaf81cda5622154fda78ea9965ac467e3239078e0dcd6df159e7/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", size = 2024034 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/99/7810aa9256e7f2ccd492590f86b79d370df1e9292f1f80b000b6a75bd2fb/pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", size = 1858578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/60/bc06fa9027c7006cc6dd21e48dbf39076dc39d9abbaf718a1604973a9670/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", size = 1892858 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/40/9d03997d9518816c68b4dfccb88969756b9146031b61cd37f781c74c9b6a/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", size = 2068498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/62/d490198d05d2d86672dc269f52579cad7261ced64c2df213d5c16e0aecb1/pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", size = 2108428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/ec/4cd215534fd10b8549015f12ea650a1a973da20ce46430b68fc3185573e8/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", size = 2069854 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/1a/abbd63d47e1d9b0d632fee6bb15785d0889c8a6e0a6c3b5a8e28ac1ec5d2/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", size = 2237859 },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1c/fa883643429908b1c90598fd2642af8839efd1d835b65af1f75fba4d94fe/pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", size = 2239059 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/29/3cade8a924a61f60ccfa10842f75eb12787e1440e2b8660ceffeb26685e7/pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", size = 2066661 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-settings"
|
||||
version = "2.9.1"
|
||||
version = "2.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.1"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
|
||||
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/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1280,7 +1359,6 @@ version = "1.0.0"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960 }
|
||||
wheels = [
|
||||
|
|
@ -1289,11 +1367,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1347,15 +1425,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593 },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1425,22 +1494,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/ed/e3/c446a64984ea9f69982ba1a69d4658d5014bc7a0ea468a07e1a1265db6e2/regex-2024.11.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f51f88c126370dcec4908576c5a627220da6c09d0bff31cfa89f2523843316d", size = 787733 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f1/e40c8373e3480e4f29f2692bd21b3e05f296d3afebc7e5dcf21b9756ca1c/regex-2024.11.6-cp313-cp313-win32.whl", hash = "sha256:63b13cfd72e9601125027202cad74995ab26921d8cd935c25f09c630436348ff", size = 262122 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/23/c4a86df398e57e26f93b13ae63acce58771e04bdde86092502496fa57f9c/regex-2024.11.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5704e174f8ccab2026bd2f1ab6c510345ae8eac818b613d7d73e785f1310f839", size = 482682 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/8b/45c24ab7a51a1658441b961b86209c43e6bb9d39caf1e63f46ce6ea03bc7/regex-2024.11.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:220902c3c5cc6af55d4fe19ead504de80eb91f786dc102fbd74894b1551f095e", size = 287679 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/d1/598de10b17fdafc452d11f7dada11c3be4e379a8671393e4e3da3c4070df/regex-2024.11.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7e351589da0850c125f1600a4c4ba3c722efefe16b297de54300f08d734fbf", size = 284578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/70/c7eaa219efa67a215846766fde18d92d54cb590b6a04ffe43cef30057622/regex-2024.11.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5056b185ca113c88e18223183aa1a50e66507769c9640a6ff75859619d73957b", size = 782012 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/e5/ef52c7eb117dd20ff1697968219971d052138965a4d3d9b95e92e549f505/regex-2024.11.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e34b51b650b23ed3354b5a07aab37034d9f923db2a40519139af34f485f77d0", size = 820580 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/3f/9f5da81aff1d4167ac52711acf789df13e789fe6ac9545552e49138e3282/regex-2024.11.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5670bce7b200273eee1840ef307bfa07cda90b38ae56e9a6ebcc9f50da9c469b", size = 809110 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/44/2101cc0890c3621b90365c9ee8d7291a597c0722ad66eccd6ffa7f1bcc09/regex-2024.11.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08986dce1339bc932923e7d1232ce9881499a0e02925f7402fb7c982515419ef", size = 780919 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/2e/3e0668d8d1c7c3c0d397bf54d92fc182575b3a26939aed5000d3cc78760f/regex-2024.11.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93c0b12d3d3bc25af4ebbf38f9ee780a487e8bf6954c115b9f015822d3bb8e48", size = 771515 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/49/1bc4584254355e3dba930a3a2fd7ad26ccba3ebbab7d9100db0aff2eedb0/regex-2024.11.6-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:764e71f22ab3b305e7f4c21f1a97e1526a25ebdd22513e251cf376760213da13", size = 696957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/dd/42879c1fc8a37a887cd08e358af3d3ba9e23038cd77c7fe044a86d9450ba/regex-2024.11.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f056bf21105c2515c32372bbc057f43eb02aae2fda61052e2f7622c801f0b4e2", size = 768088 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/96/c05a0fe173cd2acd29d5e13c1adad8b706bcaa71b169e1ee57dcf2e74584/regex-2024.11.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:69ab78f848845569401469da20df3e081e6b5a11cb086de3eed1d48f5ed57c95", size = 774752 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/f3/a757748066255f97f14506483436c5f6aded7af9e37bca04ec30c90ca683/regex-2024.11.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:86fddba590aad9208e2fa8b43b4c098bb0ec74f15718bb6a704e3c63e2cef3e9", size = 838862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/93/c6d2092fd479dcaeea40fc8fa673822829181ded77d294a7f950f1dda6e2/regex-2024.11.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:684d7a212682996d21ca12ef3c17353c021fe9de6049e19ac8481ec35574a70f", size = 842622 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/9c/daa99532c72f25051a90ef90e1413a8d54413a9e64614d9095b0c1c154d0/regex-2024.11.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a03e02f48cd1abbd9f3b7e3586d97c8f7a9721c436f51a5245b3b9483044480b", size = 772713 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/5d/61a533ccb8c231b474ac8e3a7d70155b00dfc61af6cafdccd1947df6d735/regex-2024.11.6-cp39-cp39-win32.whl", hash = "sha256:41758407fc32d5c3c5de163888068cfee69cb4c2be844e7ac517a52770f9af57", size = 261756 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/7b/e59b7f7c91ae110d154370c24133f947262525b5d6406df65f23422acc17/regex-2024.11.6-cp39-cp39-win_amd64.whl", hash = "sha256:b2837718570f95dd41675328e111345f9b7095d821bac435aac173ac80b19983", size = 274110 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1607,14 +1660,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/1c/3d2a893c020fcc18463794e0a687de58044d1c8a9892d23548ca7e71274a/sqlalchemy-2.0.41-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9a420a91913092d1e20c86a2f5f1fc85c1a8924dbcaf5e0586df8aceb09c9cc2", size = 2121327 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/84/389c8f7c7b465682c4e5ba97f6e7825149a6625c629e09b5e872ec3b378f/sqlalchemy-2.0.41-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:906e6b0d7d452e9a98e5ab8507c0da791856b2380fdee61b765632bb8698026f", size = 2110739 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/3d/036e84ecb46d6687fa57dc25ab366dff50773a19364def210b8770fd1516/sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a373a400f3e9bac95ba2a06372c4fd1412a7cee53c37fc6c05f829bf672b8769", size = 3198018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/de/112e2142bf730a16a6cb43efc87e36dd62426e155727490c041130c6e852/sqlalchemy-2.0.41-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:087b6b52de812741c27231b5a3586384d60c353fbd0e2f81405a814b5591dc8b", size = 3197074 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/be/a766c78ec3050cb5b734c3087cd20bafd7370b0ab0c8636a87652631af1f/sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:34ea30ab3ec98355235972dadc497bb659cc75f8292b760394824fab9cf39826", size = 3138698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/c3/245e39ec45e1a8c86ff1ac3a88b13d0457307ac728eaeb217834a3ac6813/sqlalchemy-2.0.41-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8280856dd7c6a68ab3a164b4a4b1c51f7691f6d04af4d4ca23d6ecf2261b7923", size = 3160877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/0c/cda8631405f6417208e160070b513bb752da0885e462fce42ac200c8262f/sqlalchemy-2.0.41-cp39-cp39-win32.whl", hash = "sha256:b50eab9994d64f4a823ff99a0ed28a6903224ddbe7fef56a6dd865eec9243440", size = 2089270 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/1f/f68c58970d80ea5a1868ca5dc965d154a3b711f9ab06376ad9840d1475b8/sqlalchemy-2.0.41-cp39-cp39-win_amd64.whl", hash = "sha256:5e22575d169529ac3e0a120cf050ec9daa94b6a9597993d1702884f6954a7d71", size = 2113134 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224 },
|
||||
]
|
||||
|
||||
|
|
@ -1636,7 +1681,6 @@ version = "0.46.2"
|
|||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.10'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 }
|
||||
wheels = [
|
||||
|
|
@ -1695,12 +1739,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/92/4d681b5c066d417b98f22a0176358d9e606e183c6b61c337d61fb54accb4/tiktoken-0.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c6386ca815e7d96ef5b4ac61e0048cd32ca5a92d5781255e13b31381d28667dc", size = 1066217 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/dd/af27bbe186df481666de48cf0f2f4e0643ba9c78b472e7bf70144c663b22/tiktoken-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75f6d5db5bc2c6274b674ceab1615c1778e6416b14705827d19b40e6355f03e0", size = 1009441 },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/35/2792b7dcb8b150d2767322637513c73a3e80833c19212efea80b31087894/tiktoken-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b16f61e6f4625a57a36496d28dd182a8a60ec20a534c5343ba3cafa156ac7", size = 1144423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/ae/4d1682510172ce3500bbed3b206ebc4efefe280f0bf1179cfb043f88cc16/tiktoken-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebcec91babf21297022882344c3f7d9eed855931466c3311b1ad6b64befb3df", size = 1199002 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/2e/df2dc31dd161190f315829775a9652ea01d60f307af8f98e35bdd14a6a93/tiktoken-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e5fd49e7799579240f03913447c0cdfa1129625ebd5ac440787afc4345990427", size = 1260610 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/22/e8fc1bf9cdecc439b7ddc28a45b976a8c699a38874c070749d855696368a/tiktoken-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:26242ca9dc8b58e875ff4ca078b9a94d2f0813e6a535dcd2205df5d49d927cc7", size = 894215 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1863,12 +1901,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a4/646a9d0edff7cde25fc1734695d3dfcee0501140dd0e723e4df3f0a50acb/uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b", size = 1439646 },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/2e/e128c66106af9728f86ebfeeb52af27ecd3cb09336f3e2f3e06053707a15/uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2", size = 800931 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/1a/9fbc2b1543d0df11f7aed1632f64bdf5ecc4053cf98cdc9edb91a65494f9/uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0", size = 3829660 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c0/392e235e4100ae3b95b5c6dac77f82b529d2760942b1e7e0981e5d8e895d/uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75", size = 3827185 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/24/a5da6aba58f99aed5255eca87d58d1760853e8302d390820cc29058408e3/uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd", size = 3705833 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/5c/6ba221bb60f1e6474474102e17e38612ec7a06dc320e22b687ab563d877f/uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff", size = 3804696 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1961,18 +1993,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/69/c4/088825b75489cb5b6a761a4542645718893d395d8c530b38734f19da44d2/watchfiles-1.1.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05686b5487cfa2e2c28ff1aa370ea3e6c5accfe6435944ddea1e10d93872147", size = 452240 },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8c/22b074814970eeef43b7c44df98c3e9667c1f7bf5b83e0ff0201b0bd43f9/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d0e10e6f8f6dc5762adee7dece33b722282e1f59aa6a55da5d493a97282fedd8", size = 625607 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/fa/a4f5c2046385492b2273213ef815bf71a0d4c1943b784fb904e184e30201/watchfiles-1.1.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:af06c863f152005c7592df1d6a7009c836a247c9d8adb78fef8575a5a98699db", size = 623315 },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/8a/a45db804b9f0740f8408626ab2bca89c3136432e57c4673b50180bf85dd9/watchfiles-1.1.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:865c8e95713744cf5ae261f3067861e9da5f1370ba91fc536431e29b418676fa", size = 406400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/06/a08684f628fb41addd451845aceedc2407dc3d843b4b060a7c4350ddee0c/watchfiles-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42f92befc848bb7a19658f21f3e7bae80d7d005d13891c62c2cd4d4d0abb3433", size = 397920 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/e6/e10d5675af653b1b07d4156906858041149ca222edaf8995877f2605ba9e/watchfiles-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0cc8365ab29487eb4f9979fd41b22549853389e22d5de3f134a6796e1b05a4", size = 451196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/8a/facd6988100cd0f39e89f6c550af80edb28e3a529e1ee662e750663e6b36/watchfiles-1.1.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90ebb429e933645f3da534c89b29b665e285048973b4d2b6946526888c3eb2c7", size = 458218 },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/26/34cbcbc4d0f2f8f9cc243007e65d741ae039f7a11ef8ec6e9cd25bee08d1/watchfiles-1.1.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c588c45da9b08ab3da81d08d7987dae6d2a3badd63acdb3e206a42dbfa7cb76f", size = 484851 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/1f/f59faa9fc4b0e36dbcdd28a18c430416443b309d295d8b82e18192d120ad/watchfiles-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c55b0f9f68590115c25272b06e63f0824f03d4fc7d6deed43d8ad5660cabdbf", size = 599520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/72/3637abecb3bf590529f5154ca000924003e5f4bbb9619744feeaf6f0b70b/watchfiles-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd17a1e489f02ce9117b0de3c0b1fab1c3e2eedc82311b299ee6b6faf6c23a29", size = 477956 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/f3/d14ffd9acc0c1bd4790378995e320981423263a5d70bd3929e2e0dc87fff/watchfiles-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da71945c9ace018d8634822f16cbc2a78323ef6c876b1d34bbf5d5222fd6a72e", size = 453196 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/38/78ad77bd99e20c0fdc82262be571ef114fc0beef9b43db52adb939768c38/watchfiles-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:51556d5004887045dba3acdd1fdf61dddea2be0a7e18048b5e853dcd37149b86", size = 627479 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/cf/549d50a22fcc83f1017c6427b1c76c053233f91b526f4ad7a45971e70c0b/watchfiles-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04e4ed5d1cd3eae68c89bcc1a485a109f39f2fd8de05f705e98af6b5f1861f1f", size = 624414 },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/de/57d6e40dc9140af71c12f3a9fc2d3efc5529d93981cd4d265d484d7c9148/watchfiles-1.1.0-cp39-cp39-win32.whl", hash = "sha256:c600e85f2ffd9f1035222b1a312aff85fd11ea39baff1d705b9b047aad2ce267", size = 280020 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/bb/7d287fc2a762396b128a0fca2dbae29386e0a242b81d1046daf389641db3/watchfiles-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3aba215958d88182e8d2acba0fdaf687745180974946609119953c0e112397dc", size = 292758 },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/7c/a3d7c55cfa377c2f62c4ae3c6502b997186bc5e38156bafcb9b653de9a6d/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a6fd40bbb50d24976eb275ccb55cd1951dfb63dbc27cae3066a6ca5f4beabd5", size = 406748 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/d0/c46f1b2c0ca47f3667b144de6f0515f6d1c670d72f2ca29861cac78abaa1/watchfiles-1.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9f811079d2f9795b5d48b55a37aa7773680a5659afe34b54cc1d86590a51507d", size = 398801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/9c/9a6a42e97f92eeed77c3485a43ea96723900aefa3ac739a8c73f4bff2cd7/watchfiles-1.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2726d7bfd9f76158c84c10a409b77a320426540df8c35be172444394b17f7ea", size = 451528 },
|
||||
|
|
@ -1981,10 +2001,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f3/d3/71c2dcf81dc1edcf8af9f4d8d63b1316fb0a2dd90cbfd427e8d9dd584a90/watchfiles-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:51b81e55d40c4b4aa8658427a3ee7ea847c591ae9e8b81ef94a90b668999353c", size = 398816 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/fa/12269467b2fc006f8fce4cd6c3acfa77491dd0777d2a747415f28ccc8c60/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2bcdc54ea267fe72bfc7d83c041e4eb58d7d8dc6f578dfddb52f037ce62f432", size = 451584 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/d3/254cea30f918f489db09d6a8435a7de7047f8cb68584477a515f160541d6/watchfiles-1.1.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923fec6e5461c42bd7e3fd5ec37492c6f3468be0499bc0707b4bbbc16ac21792", size = 454009 },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/93/5c96bdb65e7f88f7da40645f34c0a3c317a2931ed82161e93c91e8eddd27/watchfiles-1.1.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b3443f4ec3ba5aa00b0e9fa90cf31d98321cbff8b925a7c7b84161619870bc9", size = 406640 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/25/09204836e93e1b99cce88802ce87264a1d20610c7a8f6de24def27ad95b1/watchfiles-1.1.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7049e52167fc75fc3cc418fc13d39a8e520cbb60ca08b47f6cedb85e181d2f2a", size = 398543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/dc/6f324a6f32c5ab73b54311b5f393a79df34c1584b8d2404cf7e6d780aa5d/watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54062ef956807ba806559b3c3d52105ae1827a0d4ab47b621b31132b6b7e2866", size = 451787 },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/5d/1d02ef4caa4ec02389e72d5594cdf9c67f1800a7c380baa55063c30c6598/watchfiles-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a7bd57a1bb02f9d5c398c0c1675384e7ab1dd39da0ca50b7f09af45fa435277", size = 454272 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2037,29 +2053,12 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/db/3fff0bcbe339a6fa6a3b9e3fbc2bfb321ec2f4cd233692272c5a8d6cf801/websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5", size = 175424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/e6/519054c2f477def4165b0ec060ad664ed174e140b0d1cbb9fafa4a54f6db/websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a", size = 173077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/21/c0712e382df64c93a0d16449ecbf87b647163485ca1cc3f6cbadb36d2b03/websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b", size = 173324 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/cb/51ba82e59b3a664df54beed8ad95517c1b4dc1a913730e7a7db778f21291/websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770", size = 182094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/0f/bf3788c03fec679bcdaef787518dbe60d12fe5615a544a6d4cf82f045193/websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb", size = 181094 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/da/9fb8c21edbc719b66763a571afbaf206cb6d3736d28255a46fc2fe20f902/websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054", size = 181397 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/65/65f379525a2719e91d9d90c38fe8b8bc62bd3c702ac651b7278609b696c4/websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee", size = 181794 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/26/31ac2d08f8e9304d81a1a7ed2851c0300f636019a57cbaa91342015c72cc/websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed", size = 181194 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/72/1090de20d6c91994cd4b357c3f75a4f25ee231b63e03adea89671cc12a3f/websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880", size = 181164 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/37/098f2e1c103ae8ed79b0e77f08d83b0ec0b241cf4b7f2f10edd0126472e1/websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411", size = 176381 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/8b/a32978a3ab42cebb2ebdd5b05df0696a09f4d436ce69def11893afa301f0/websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4", size = 176841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155 },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/48/4b67623bac4d79beb3a6bb27b803ba75c1bdedc06bd827e465803690a4b2/websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940", size = 173106 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/f0/adb07514a49fe5728192764e04295be78859e4a537ab8fcc518a3dbb3281/websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e", size = 173339 },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/28/bd23c6344b18fb43df40d0700f6d3fffcd7cef14a6995b4f976978b52e62/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9", size = 174597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/79/ca288495863d0f23a60f546f0905ae8f3ed467ad87f8b6aceb65f4c013e4/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b", size = 174205 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/e4/120ff3180b0872b1fe6637f6f995bcb009fb5c87d597c1fc21456f50c848/websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f", size = 174150 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/c3/30e2f9c539b8da8b1d76f64012f3b19253271a63413b2d3adb94b143407f/websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123", size = 176877 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
|
||||
]
|
||||
|
||||
|
|
@ -2124,16 +2123,5 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/f4/6ed2b8f6f1c832933283974839b88ec7c983fd12905e01e97889dadf7559/wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a", size = 53308 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/a9/712a53f8f4f4545768ac532619f6e56d5d0364a87b2212531685e89aeef8/wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061", size = 38489 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/9b/e172c8f28a489a2888df18f953e2f6cb8d33b1a2e78c9dfc52d8bf6a5ead/wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82", size = 38776 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/cb/7a07b51762dcd59bdbe07aa97f87b3169766cadf240f48d1cbe70a1be9db/wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9", size = 83050 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/51/a42757dd41032afd6d8037617aa3bc6803ba971850733b24dfb7d5c627c4/wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f", size = 74718 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/bb/d552bfe47db02fcfc950fc563073a33500f8108efa5f7b41db2f83a59028/wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b", size = 82590 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/99/77b06b3c3c410dbae411105bf22496facf03a5496bfaca8fbcf9da381889/wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f", size = 81462 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/21/cf0bd85ae66f92600829ea1de8e1da778e5e9f6e574ccbe74b66db0d95db/wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8", size = 74309 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/16/112d25e9092398a0dd6fec50ab7ac1b775a0c19b428f049785096067ada9/wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9", size = 81081 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/49/364a615a0cc0872685646c495c7172e4fc7bf1959e3b12a1807a03014e05/wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb", size = 36423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/ad/5d2c1b34ba3202cd833d9221833e74d6500ce66730974993a8dc9a94fb8c/wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb", size = 38772 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 },
|
||||
]
|
||||
|
|
|
|||
Loading…
Reference in New Issue