Bulk Message Creation Endpoint (#82)
* fix: increase db pool limit and optimize crud requests * fix: Sentry tracing and fly concurrency * feat: Add alembic and indexes * feat: Batch insert method * fix: Pydantic Validation * fix: Added Pydantic based API validation and Associated Test Cases * chore: Update Changelog * fix(docs): Update Docs with new API Method and OpenAPI Spec * chore(ci): Add Environment Variable for Anthropic
This commit is contained in:
parent
6995f878a9
commit
d6194df824
|
|
@ -51,6 +51,7 @@ jobs:
|
|||
SENTRY_ENABLED: false
|
||||
OPENTELEMETRY_ENABLED: false
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
- Alembic for handling database migrations
|
||||
- Additional indexes for reading Messages and Metamessages
|
||||
|
||||
### Changed
|
||||
|
||||
- API validation using Pydantic
|
||||
|
||||
### Fixed
|
||||
|
||||
- Dialectic Streaming Endpoint properly sends text in `StreamingResponse`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
openapi: post /v1/apps/{app_id}/users/{user_id}/sessions/{session_id}/messages/batch
|
||||
---
|
||||
|
|
@ -118,6 +118,7 @@
|
|||
"pages": [
|
||||
"api-reference/endpoint/messages/get-messages",
|
||||
"api-reference/endpoint/messages/create-message-for-session",
|
||||
"api-reference/endpoint/messages/create-batch-messages-for-session",
|
||||
"api-reference/endpoint/messages/get-message",
|
||||
"api-reference/endpoint/messages/update-message"
|
||||
]
|
||||
|
|
@ -160,7 +161,7 @@
|
|||
"github": "https://github.com/plastic-labs",
|
||||
"linkedin": "https://www.linkedin.com/company/plasticlabs"
|
||||
},
|
||||
"openapi": ["/openapi.yml"],
|
||||
"openapi": ["/openapi.json"],
|
||||
"analytics": {
|
||||
"posthog": {
|
||||
"apiKey": "phc_1yrzzcgywqXGcerkkI4g7C0YfyPMcAKNOOvGcjTCiUk"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
4016
docs/openapi.yml
4016
docs/openapi.yml
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,7 @@
|
|||
"main": ".pnp.js",
|
||||
"scripts": {
|
||||
"dev": "mintlify dev",
|
||||
"openapi": "npx @mintlify/scraping openapi-file openapi.yml -o api-reference/endpoint",
|
||||
"openapi": "npx @mintlify/scraping openapi-file openapi.json -o api-reference/endpoint",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
|
|
|
|||
34
src/crud.py
34
src/crud.py
|
|
@ -421,6 +421,40 @@ async def create_message(
|
|||
return honcho_message
|
||||
|
||||
|
||||
async def create_messages(
|
||||
db: AsyncSession,
|
||||
messages: list[schemas.MessageCreate],
|
||||
app_id: str,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
) -> list[models.Message]:
|
||||
"""Bulk create messages for a session while maintaining order"""
|
||||
# Verify session exists and belongs to user
|
||||
honcho_session = await get_session(
|
||||
db, app_id=app_id, session_id=session_id, user_id=user_id
|
||||
)
|
||||
if honcho_session is None:
|
||||
raise ValueError("Session not found or does not belong to user")
|
||||
|
||||
# Create list of message records
|
||||
message_records = [
|
||||
{
|
||||
"session_id": session_id,
|
||||
"is_user": message.is_user,
|
||||
"content": message.content,
|
||||
"h_metadata": message.metadata,
|
||||
}
|
||||
for message in messages
|
||||
]
|
||||
|
||||
# Bulk insert messages and return them in order
|
||||
stmt = insert(models.Message).returning(models.Message)
|
||||
result = await db.execute(stmt, message_records)
|
||||
await db.commit()
|
||||
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get_messages(
|
||||
db: AsyncSession,
|
||||
app_id: str,
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ class User(Base):
|
|||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"User(id={self.id}, app_id={self.app_id}, created_at={self.created_at}, h_metadata={self.h_metadata})"
|
||||
return f"User(id={self.id}, app_id={self.app_id}, public_id={self.public_id} created_at={self.created_at}, h_metadata={self.h_metadata})"
|
||||
|
||||
|
||||
class Session(Base):
|
||||
|
|
|
|||
|
|
@ -76,11 +76,11 @@ async def create_collection(
|
|||
db=db,
|
||||
):
|
||||
"""Create a new Collection"""
|
||||
if collection.name == "honcho":
|
||||
raise HTTPException(
|
||||
status_code=406,
|
||||
detail="error invalid collection configuration - honcho is a reserved name",
|
||||
)
|
||||
# if collection.name == "honcho":
|
||||
# raise HTTPException(
|
||||
# status_code=406,
|
||||
# detail="error invalid collection configuration - honcho is a reserved name",
|
||||
# )
|
||||
try:
|
||||
return await crud.create_collection(
|
||||
db, collection=collection, app_id=app_id, user_id=user_id
|
||||
|
|
@ -106,11 +106,6 @@ async def update_collection(
|
|||
status_code=406,
|
||||
detail="error invalid collection configuration - atleast 1 field must be provided",
|
||||
)
|
||||
if collection.name is not None and collection.name == "honcho":
|
||||
raise HTTPException(
|
||||
status_code=406,
|
||||
detail="error invalid collection configuration - honcho is a reserved name",
|
||||
)
|
||||
try:
|
||||
honcho_collection = await crud.update_collection(
|
||||
db,
|
||||
|
|
|
|||
|
|
@ -85,8 +85,6 @@ async def query_documents(
|
|||
|
||||
try:
|
||||
top_k = options.top_k
|
||||
if top_k is not None and top_k > 50:
|
||||
top_k = 50 # TODO see if we need to paginate this
|
||||
filter = options.filter
|
||||
if options.filter == {}:
|
||||
filter = None
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from typing import Optional
|
||||
from typing import Optional, List
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException
|
||||
from fastapi_pagination import Page
|
||||
from fastapi_pagination.ext.sqlalchemy import paginate
|
||||
from sqlalchemy.sql import insert
|
||||
|
||||
from src import crud, schemas
|
||||
from src.db import SessionLocal
|
||||
|
|
@ -17,35 +18,83 @@ router = APIRouter(
|
|||
)
|
||||
|
||||
|
||||
async def enqueue(payload: dict):
|
||||
async def enqueue(payload: dict | list[dict]):
|
||||
async with SessionLocal() as db:
|
||||
# Get Session and Check metadata
|
||||
session = await crud.get_session(
|
||||
db,
|
||||
app_id=payload["app_id"],
|
||||
user_id=payload["user_id"],
|
||||
session_id=payload["session_id"],
|
||||
)
|
||||
# Check if metadata has a "deriver" key
|
||||
if session is not None:
|
||||
deriver_disabled = session.h_metadata.get("deriver_disabled")
|
||||
if deriver_disabled is not None and deriver_disabled is not False:
|
||||
print("=====================")
|
||||
print(f"Deriver is not enabled on session {payload['session_id']}")
|
||||
print("=====================")
|
||||
# If deriver is not enabled, do not enqueue
|
||||
return
|
||||
else:
|
||||
# Session doesn't exist return
|
||||
return
|
||||
try:
|
||||
processed_payload = {
|
||||
k: str(v) if isinstance(v, str) else v for k, v in payload.items()
|
||||
}
|
||||
item = QueueItem(payload=processed_payload, session_id=session.id)
|
||||
db.add(item)
|
||||
await db.commit()
|
||||
return
|
||||
if isinstance(payload, list):
|
||||
if not payload: # Empty list check
|
||||
return
|
||||
print("Payload:\n", payload)
|
||||
|
||||
# Check session once since all messages are for same session
|
||||
session = await crud.get_session(
|
||||
db,
|
||||
app_id=payload[0]["app_id"],
|
||||
user_id=payload[0]["user_id"],
|
||||
session_id=payload[0]["session_id"],
|
||||
)
|
||||
print("Session found:", session is not None)
|
||||
if session:
|
||||
print("Session metadata:", session.h_metadata)
|
||||
|
||||
if session is None or (
|
||||
session.h_metadata.get("deriver_disabled") is not None
|
||||
and session.h_metadata.get("deriver_disabled") is not False
|
||||
):
|
||||
print("Skipping enqueue due to session check")
|
||||
return
|
||||
|
||||
# Process all payloads
|
||||
queue_records = [
|
||||
{
|
||||
"payload": {
|
||||
k: str(v) if isinstance(v, str) else v for k, v in p.items()
|
||||
},
|
||||
"session_id": session.id,
|
||||
}
|
||||
for p in payload
|
||||
]
|
||||
|
||||
print("Number of queue records to insert:", len(queue_records))
|
||||
|
||||
# Use insert to maintain order
|
||||
stmt = insert(QueueItem).returning(QueueItem)
|
||||
result = await db.execute(stmt, queue_records)
|
||||
await db.commit()
|
||||
print("Queue items inserted successfully")
|
||||
return
|
||||
else:
|
||||
# Original single insert logic
|
||||
session = await crud.get_session(
|
||||
db,
|
||||
app_id=payload["app_id"],
|
||||
user_id=payload["user_id"],
|
||||
session_id=payload["session_id"],
|
||||
)
|
||||
if session is not None:
|
||||
deriver_disabled = session.h_metadata.get("deriver_disabled")
|
||||
if deriver_disabled is not None and deriver_disabled is not False:
|
||||
print("=====================")
|
||||
print(
|
||||
f"Deriver is not enabled on session {payload['session_id']}"
|
||||
)
|
||||
print("=====================")
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
processed_payload = {
|
||||
k: str(v) if isinstance(v, str) else v for k, v in payload.items()
|
||||
}
|
||||
# Use insert for consistency
|
||||
stmt = (
|
||||
insert(QueueItem)
|
||||
.values(payload=processed_payload, session_id=session.id)
|
||||
.returning(QueueItem)
|
||||
)
|
||||
await db.execute(stmt)
|
||||
await db.commit()
|
||||
return
|
||||
except Exception as e:
|
||||
print("=====================")
|
||||
print("FAILURE: in enqueue")
|
||||
|
|
@ -87,6 +136,43 @@ async def create_message_for_session(
|
|||
raise HTTPException(status_code=404, detail="Session not found") from None
|
||||
|
||||
|
||||
@router.post("/batch", response_model=List[schemas.Message])
|
||||
async def create_batch_messages_for_session(
|
||||
app_id: str,
|
||||
user_id: str,
|
||||
session_id: str,
|
||||
batch: schemas.MessageBatchCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
db=db,
|
||||
):
|
||||
"""Bulk create messages for a session while maintaining order. Maximum 100 messages per batch."""
|
||||
try:
|
||||
created_messages = await crud.create_messages(
|
||||
db, messages=batch.messages, app_id=app_id, user_id=user_id, session_id=session_id
|
||||
)
|
||||
|
||||
# Create payloads for all messages
|
||||
payloads = [
|
||||
{
|
||||
"app_id": app_id,
|
||||
"user_id": user_id,
|
||||
"session_id": session_id,
|
||||
"message_id": message.public_id,
|
||||
"is_user": message.is_user,
|
||||
"content": message.content,
|
||||
"metadata": message.h_metadata,
|
||||
}
|
||||
for message in created_messages
|
||||
]
|
||||
|
||||
# Enqueue all messages in one call
|
||||
background_tasks.add_task(enqueue, payloads) # type: ignore
|
||||
|
||||
return created_messages
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=404, detail="Session not found") from None
|
||||
|
||||
|
||||
@router.post("/list", response_model=Page[schemas.Message])
|
||||
async def get_messages(
|
||||
app_id: str,
|
||||
|
|
@ -143,8 +229,6 @@ async def update_message(
|
|||
db=db,
|
||||
):
|
||||
"""Update the metadata of a Message"""
|
||||
if message.metadata is None:
|
||||
raise HTTPException(status_code=400, detail="Message metadata cannot be empty")
|
||||
try:
|
||||
return await crud.update_message(
|
||||
db,
|
||||
|
|
|
|||
|
|
@ -68,8 +68,6 @@ async def update_session(
|
|||
db=db,
|
||||
):
|
||||
"""Update the metadata of a Session"""
|
||||
if session.metadata is None:
|
||||
raise HTTPException(status_code=400, detail="Session metadata cannot be empty")
|
||||
try:
|
||||
return await crud.update_session(
|
||||
db, app_id=app_id, user_id=user_id, session_id=session_id, session=session
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ async def create_user(
|
|||
db=db,
|
||||
):
|
||||
"""Create a new User"""
|
||||
print("running create_user")
|
||||
try:
|
||||
return await crud.create_user(db, app_id=app_id, user=user)
|
||||
except IntegrityError as e:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
|
@ -8,7 +9,7 @@ class AppBase(BaseModel):
|
|||
|
||||
|
||||
class AppCreate(AppBase):
|
||||
name: str
|
||||
name: Annotated[str, Field(min_length=1, max_length=100)]
|
||||
metadata: dict = {}
|
||||
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ class UserBase(BaseModel):
|
|||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
name: str
|
||||
name: Annotated[str, Field(min_length=1, max_length=100)]
|
||||
metadata: dict = {}
|
||||
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ class UserGet(UserBase):
|
|||
|
||||
class UserUpdate(UserBase):
|
||||
name: str | None = None
|
||||
metadata: dict | None = None
|
||||
metadata: dict | None = None # Allow user to explicitly set metadata to empty
|
||||
|
||||
|
||||
class User(UserBase):
|
||||
|
|
@ -85,7 +86,7 @@ class MessageBase(BaseModel):
|
|||
|
||||
|
||||
class MessageCreate(MessageBase):
|
||||
content: str
|
||||
content: Annotated[str, Field(min_length=0, max_length=50000)]
|
||||
is_user: bool
|
||||
metadata: dict = {}
|
||||
|
||||
|
|
@ -95,7 +96,7 @@ class MessageGet(MessageBase):
|
|||
|
||||
|
||||
class MessageUpdate(MessageBase):
|
||||
metadata: dict | None = None
|
||||
metadata: dict
|
||||
|
||||
|
||||
class Message(MessageBase):
|
||||
|
|
@ -136,7 +137,7 @@ class SessionGet(SessionBase):
|
|||
|
||||
|
||||
class SessionUpdate(SessionBase):
|
||||
metadata: dict | None = None
|
||||
metadata: dict
|
||||
|
||||
|
||||
class Session(SessionBase):
|
||||
|
|
@ -169,10 +170,10 @@ class MetamessageBase(BaseModel):
|
|||
|
||||
|
||||
class MetamessageCreate(MetamessageBase):
|
||||
metamessage_type: str
|
||||
content: str
|
||||
metamessage_type: Annotated[str, Field(min_length=1, max_length=50)]
|
||||
content: Annotated[str, Field(min_length=0, max_length=50000)]
|
||||
message_id: str
|
||||
metadata: dict | None = {}
|
||||
metadata: dict = {}
|
||||
|
||||
|
||||
class MetamessageGet(MetamessageBase):
|
||||
|
|
@ -221,8 +222,14 @@ class CollectionBase(BaseModel):
|
|||
|
||||
|
||||
class CollectionCreate(CollectionBase):
|
||||
name: str
|
||||
metadata: dict | None = {}
|
||||
name: Annotated[str, Field(min_length=1, max_length=100)]
|
||||
metadata: dict = {}
|
||||
|
||||
@field_validator("name")
|
||||
def validate_name(cls, v):
|
||||
if v.lower() == "honcho":
|
||||
raise ValueError("Collection name cannot be 'honcho'")
|
||||
return v
|
||||
|
||||
|
||||
class CollectionGet(CollectionBase):
|
||||
|
|
@ -233,6 +240,12 @@ class CollectionUpdate(CollectionBase):
|
|||
name: str | None = None
|
||||
metadata: dict | None = None
|
||||
|
||||
@field_validator("name")
|
||||
def validate_name(cls, v):
|
||||
if v is not None and v.lower() == "honcho":
|
||||
raise ValueError("Collection name cannot be 'honcho'")
|
||||
return v
|
||||
|
||||
|
||||
class Collection(CollectionBase):
|
||||
public_id: str = Field(exclude=True)
|
||||
|
|
@ -262,8 +275,8 @@ class DocumentBase(BaseModel):
|
|||
|
||||
|
||||
class DocumentCreate(DocumentBase):
|
||||
content: str
|
||||
metadata: dict | None = {}
|
||||
content: Annotated[str, Field(min_length=1, max_length=100000)]
|
||||
metadata: dict = {}
|
||||
|
||||
|
||||
class DocumentGet(DocumentBase):
|
||||
|
|
@ -271,14 +284,14 @@ class DocumentGet(DocumentBase):
|
|||
|
||||
|
||||
class DocumentQuery(DocumentBase):
|
||||
query: str
|
||||
query: Annotated[str, Field(min_length=1, max_length=1000)]
|
||||
filter: dict | None = None
|
||||
top_k: int = 5
|
||||
top_k: int = Field(default=5, ge=1, le=50)
|
||||
|
||||
|
||||
class DocumentUpdate(DocumentBase):
|
||||
metadata: dict | None = None
|
||||
content: str | None = None
|
||||
metadata: dict | None = Field(None, max_length=10000)
|
||||
content: Annotated[str | None, Field(min_length=1, max_length=100000)] = None
|
||||
|
||||
|
||||
class Document(DocumentBase):
|
||||
|
|
@ -306,8 +319,25 @@ class Document(DocumentBase):
|
|||
|
||||
class AgentQuery(BaseModel):
|
||||
queries: str | list[str]
|
||||
# collections: str | list[str] = "honcho"
|
||||
|
||||
@field_validator('queries')
|
||||
def validate_queries(cls, v):
|
||||
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):
|
||||
if len(v) > MAX_LIST_LENGTH:
|
||||
raise ValueError('Too many queries')
|
||||
if any(len(q) > MAX_STRING_LENGTH for q in v):
|
||||
raise ValueError('One or more queries too long')
|
||||
return v
|
||||
|
||||
class AgentChat(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
class MessageBatchCreate(BaseModel):
|
||||
"""Schema for batch message creation with a max of 100 messages"""
|
||||
messages: list[MessageCreate] = Field(..., max_length=100)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,20 @@ def test_create_app(client):
|
|||
assert "id" in data
|
||||
|
||||
|
||||
def test_create_app_no_metadata(client):
|
||||
name = str(generate_nanoid())
|
||||
response = client.post("/v1/apps", json={"name": name})
|
||||
print(response)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
print("===================")
|
||||
print(data)
|
||||
print("===================")
|
||||
assert data["name"] == name
|
||||
assert data["metadata"] == {}
|
||||
assert "id" in data
|
||||
|
||||
|
||||
def test_get_or_create_app(client):
|
||||
name = str(generate_nanoid())
|
||||
response = client.get(f"/v1/apps/name/{name}")
|
||||
|
|
|
|||
|
|
@ -110,3 +110,101 @@ async def test_update_message(client, db_session, sample_data):
|
|||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["metadata"] == {"new_key": "new_value"}
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_message_empty_metadata(client, db_session, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a test session and message
|
||||
test_session = models.Session(user_id=test_user.public_id)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
test_message = models.Message(
|
||||
session_id=test_session.public_id, content="Test message", is_user=True
|
||||
)
|
||||
db_session.add(test_message)
|
||||
await db_session.commit()
|
||||
|
||||
response = client.put(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{test_session.public_id}/messages/{test_message.public_id}",
|
||||
json={"metadata": None},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
print(data)
|
||||
# assert data["detail"] == "Message metadata cannot be empty"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_batch_messages(client, db_session, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a test session
|
||||
test_session = models.Session(user_id=test_user.public_id)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create batch of test messages
|
||||
test_messages = {
|
||||
"messages": [
|
||||
{
|
||||
"content": f"Test message {i}",
|
||||
"is_user": i % 2 == 0, # Alternating user/non-user messages
|
||||
"metadata": {"batch_index": i}
|
||||
} for i in range(3)
|
||||
]
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{test_session.public_id}/messages/batch",
|
||||
json=test_messages,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
|
||||
# Verify the response contains all messages
|
||||
assert len(data) == 3
|
||||
|
||||
# Verify messages are in the correct order and have correct content
|
||||
for i, message in enumerate(data):
|
||||
assert message["content"] == f"Test message {i}"
|
||||
assert message["is_user"] == (i % 2 == 0)
|
||||
assert message["metadata"] == {"batch_index": i}
|
||||
assert "id" in message
|
||||
assert message["session_id"] == test_session.public_id
|
||||
|
||||
# Verify messages were actually saved to the database
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{test_session.public_id}/messages/list",
|
||||
json={},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
saved_messages = response.json()["items"]
|
||||
assert len(saved_messages) == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_batch_messages_limit(client, db_session, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
test_session = models.Session(user_id=test_user.public_id)
|
||||
db_session.add(test_session)
|
||||
await db_session.commit()
|
||||
|
||||
# Create batch with more than 100 messages
|
||||
test_messages = {
|
||||
"messages": [
|
||||
{
|
||||
"content": f"Test message {i}",
|
||||
"is_user": i % 2 == 0,
|
||||
"metadata": {"batch_index": i}
|
||||
} for i in range(101) # 101 messages
|
||||
]
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{test_session.public_id}/messages/batch",
|
||||
json=test_messages,
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
data = response.json()
|
||||
assert "messages" in data["detail"][0]["loc"] # Error should mention messages field
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ async def test_empty_update_session(client, db_session, sample_data):
|
|||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{test_session.public_id}",
|
||||
json={},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -0,0 +1,516 @@
|
|||
import pytest
|
||||
from nanoid import generate as generate_nanoid
|
||||
|
||||
|
||||
def test_app_validations_api(client):
|
||||
# Test name too short
|
||||
response = client.post("/v1/apps", json={"name": "", "metadata": {}})
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "String should have at least 1 character"
|
||||
assert error["type"] == "string_too_short"
|
||||
|
||||
# Test name too long
|
||||
response = client.post("/v1/apps", json={"name": "a" * 101, "metadata": {}})
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "String should have at most 100 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
# Test invalid metadata type
|
||||
response = client.post("/v1/apps", json={"name": "test", "metadata": "not a dict"})
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "metadata"]
|
||||
assert error["type"] == "dict_type"
|
||||
|
||||
|
||||
def test_user_validations_api(client, sample_data):
|
||||
test_app, _ = sample_data
|
||||
|
||||
# Test name too short
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users",
|
||||
json={"name": "", "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "String should have at least 1 character"
|
||||
assert error["type"] == "string_too_short"
|
||||
|
||||
# Test name too long
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users",
|
||||
json={"name": "a" * 101, "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "String should have at most 100 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
|
||||
def test_message_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a test session first
|
||||
session_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
session_id = session_response.json()["id"]
|
||||
|
||||
# Test content too long
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages",
|
||||
json={
|
||||
"content": "a" * 50001,
|
||||
"is_user": True,
|
||||
"metadata": {}
|
||||
}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "content"]
|
||||
assert error["msg"] == "String should have at most 50000 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
# Test invalid is_user type
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages",
|
||||
json={
|
||||
"content": "test",
|
||||
"is_user": "not a bool",
|
||||
"metadata": {}
|
||||
}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "is_user"]
|
||||
assert error["type"] == "bool_parsing"
|
||||
|
||||
|
||||
def test_collection_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
|
||||
# Test name too short
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"name": "", "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "String should have at least 1 character"
|
||||
assert error["type"] == "string_too_short"
|
||||
|
||||
# Test name too long
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"name": "a" * 101, "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "String should have at most 100 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
# Test 'honcho' name restriction
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"name": "honcho", "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "Value error, Collection name cannot be 'honcho'"
|
||||
assert error["type"] == "value_error"
|
||||
|
||||
|
||||
def test_document_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a collection first
|
||||
collection_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"name": str(generate_nanoid()), "metadata": {}}
|
||||
)
|
||||
collection_id = collection_response.json()["id"]
|
||||
|
||||
# Test content too short
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents",
|
||||
json={"content": "", "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "content"]
|
||||
assert error["msg"] == "String should have at least 1 character"
|
||||
assert error["type"] == "string_too_short"
|
||||
|
||||
# Test content too long
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents",
|
||||
json={"content": "a" * 100001, "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "content"]
|
||||
assert error["msg"] == "String should have at most 100000 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
|
||||
def test_document_query_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a collection first
|
||||
collection_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"name": str(generate_nanoid()), "metadata": {}}
|
||||
)
|
||||
collection_id = collection_response.json()["id"]
|
||||
|
||||
# Test query too short
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query",
|
||||
json={"query": "", "top_k": 5}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "query"]
|
||||
assert error["msg"] == "String should have at least 1 character"
|
||||
assert error["type"] == "string_too_short"
|
||||
|
||||
# Test query too long
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query",
|
||||
json={"query": "a" * 1001, "top_k": 5}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "query"]
|
||||
assert error["msg"] == "String should have at most 1000 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
# Test top_k too small
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query",
|
||||
json={"query": "test", "top_k": 0}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "top_k"]
|
||||
assert error["msg"] == "Input should be greater than or equal to 1"
|
||||
assert error["type"] == "greater_than_equal"
|
||||
|
||||
# Test top_k too large
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query",
|
||||
json={"query": "test", "top_k": 51}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "top_k"]
|
||||
assert error["msg"] == "Input should be less than or equal to 50"
|
||||
assert error["type"] == "less_than_equal"
|
||||
|
||||
|
||||
def test_message_batch_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a test session first
|
||||
session_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
session_id = session_response.json()["id"]
|
||||
|
||||
# Test batch too large
|
||||
messages = [
|
||||
{
|
||||
"content": f"test message {i}",
|
||||
"is_user": True,
|
||||
"metadata": {}
|
||||
}
|
||||
for i in range(101) # Create 101 messages
|
||||
]
|
||||
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages/batch",
|
||||
json={"messages": messages}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "messages"]
|
||||
assert "List should have at most 100 items after validation" in error["msg"]
|
||||
assert error["type"] == "too_long"
|
||||
|
||||
|
||||
def test_metamessage_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create session and message first
|
||||
session_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
session_id = session_response.json()["id"]
|
||||
|
||||
message_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages",
|
||||
json={"content": "test message", "is_user": True, "metadata": {}}
|
||||
)
|
||||
message_id = message_response.json()["id"]
|
||||
|
||||
# Test metamessage_type too short
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/metamessages",
|
||||
json={
|
||||
"metamessage_type": "",
|
||||
"content": "test content",
|
||||
"message_id": message_id,
|
||||
"metadata": {}
|
||||
}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "metamessage_type"]
|
||||
assert error["msg"] == "String should have at least 1 character"
|
||||
assert error["type"] == "string_too_short"
|
||||
|
||||
# Test metamessage_type too long
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/metamessages",
|
||||
json={
|
||||
"metamessage_type": "a" * 51,
|
||||
"content": "test content",
|
||||
"message_id": message_id,
|
||||
"metadata": {}
|
||||
}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "metamessage_type"]
|
||||
assert error["msg"] == "String should have at most 50 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
# Test content too long
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/metamessages",
|
||||
json={
|
||||
"metamessage_type": "test_type",
|
||||
"content": "a" * 50001,
|
||||
"message_id": message_id,
|
||||
"metadata": {}
|
||||
}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "content"]
|
||||
assert error["msg"] == "String should have at most 50000 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
|
||||
def test_collection_update_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a collection first
|
||||
collection_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"name": str(generate_nanoid()), "metadata": {}}
|
||||
)
|
||||
collection_id = collection_response.json()["id"]
|
||||
|
||||
# Test honcho name in update
|
||||
response = client.put(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}",
|
||||
json={"name": "honcho", "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["msg"] == "Value error, Collection name cannot be 'honcho'"
|
||||
assert error["type"] == "value_error"
|
||||
|
||||
|
||||
def test_document_update_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create collection and document first
|
||||
collection_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"name": str(generate_nanoid()), "metadata": {}}
|
||||
)
|
||||
collection_id = collection_response.json()["id"]
|
||||
|
||||
document_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents",
|
||||
json={"content": "test content", "metadata": {}}
|
||||
)
|
||||
document_id = document_response.json()["id"]
|
||||
|
||||
# Test content too long in update
|
||||
response = client.put(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/{document_id}",
|
||||
json={"content": "a" * 100001, "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "content"]
|
||||
assert error["msg"] == "String should have at most 100000 characters"
|
||||
assert error["type"] == "string_too_long"
|
||||
|
||||
|
||||
def test_session_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a test session first
|
||||
session_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
session_id = session_response.json()["id"]
|
||||
|
||||
# Test invalid metadata type
|
||||
response = client.put(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}",
|
||||
json={"metadata": "not a dict"}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "metadata"]
|
||||
assert error["type"] == "dict_type"
|
||||
|
||||
# Test empty update
|
||||
response = client.put(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}",
|
||||
json={}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
def test_agent_query_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a session first since agent queries are likely session-based
|
||||
session_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
session_id = session_response.json()["id"]
|
||||
|
||||
# Test valid string query (under 10000 chars)
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat",
|
||||
json={"queries": "a" * 9999}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test string query too long (over 10000 chars)
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat",
|
||||
json={"queries": "a" * 10001}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "queries"]
|
||||
assert error["msg"] == "Value error, Query too long"
|
||||
assert error["type"] == "value_error"
|
||||
|
||||
# Test valid list query (under 25 items, each under 10000 chars)
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat",
|
||||
json={"queries": ["a" * 9999 for _ in range(25)]}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Test list too long (over 25 items)
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat",
|
||||
json={"queries": ["test" for _ in range(26)]}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "queries"]
|
||||
assert error["type"] == "value_error"
|
||||
|
||||
# Test list item too long (item over 10000 chars)
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat",
|
||||
json={"queries": ["a" * 10001]}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "queries"]
|
||||
assert error["msg"] == "Value error, One or more queries too long"
|
||||
assert error["type"] == "value_error"
|
||||
|
||||
# Test that strings over 20 chars are allowed
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat",
|
||||
json={"queries": "a" * 100} # 100 chars should be fine
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_required_field_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
session_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
session_id = session_response.json()["id"]
|
||||
|
||||
# Test missing required content in message
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages",
|
||||
json={"is_user": True, "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "content"]
|
||||
assert error["type"] == "missing"
|
||||
|
||||
# Test missing required is_user in message
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages",
|
||||
json={"content": "test", "metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "is_user"]
|
||||
assert error["type"] == "missing"
|
||||
|
||||
# Test missing required name in collection
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "name"]
|
||||
assert error["type"] == "missing"
|
||||
|
||||
|
||||
def test_filter_validations_api(client, sample_data):
|
||||
test_app, test_user = sample_data
|
||||
# Create a session first
|
||||
session_response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions",
|
||||
json={"metadata": {}}
|
||||
)
|
||||
session_id = session_response.json()["id"]
|
||||
|
||||
# Test invalid filter type in message list (at session level)
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages/list",
|
||||
json={"filter": "not a dict"}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "filter"]
|
||||
assert error["type"] == "dict_type"
|
||||
|
||||
# Test invalid filter type in collection list (at user level)
|
||||
response = client.post(
|
||||
f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/list",
|
||||
json={"filter": "not a dict"}
|
||||
)
|
||||
assert response.status_code == 422
|
||||
error = response.json()["detail"][0]
|
||||
assert error["loc"] == ["body", "filter"]
|
||||
assert error["type"] == "dict_type"
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
import pytest
|
||||
from pydantic import ValidationError
|
||||
from src.schemas import (
|
||||
AppCreate,
|
||||
UserCreate,
|
||||
MessageCreate,
|
||||
MetamessageCreate,
|
||||
CollectionCreate,
|
||||
DocumentCreate,
|
||||
DocumentQuery,
|
||||
MessageBatchCreate,
|
||||
)
|
||||
|
||||
|
||||
class TestAppValidations:
|
||||
def test_valid_app_create(self):
|
||||
app = AppCreate(name="test", metadata={})
|
||||
assert app.name == "test"
|
||||
assert app.metadata == {}
|
||||
|
||||
def test_app_name_too_short(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
AppCreate(name="", metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_short"
|
||||
|
||||
def test_app_name_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
AppCreate(name="a" * 101, metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
def test_app_invalid_metadata_type(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
AppCreate(name="test", metadata="not a dict")
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "dict_type"
|
||||
|
||||
|
||||
class TestUserValidations:
|
||||
def test_valid_user_create(self):
|
||||
user = UserCreate(name="test", metadata={})
|
||||
assert user.name == "test"
|
||||
assert user.metadata == {}
|
||||
|
||||
def test_user_name_too_short(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
UserCreate(name="", metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_short"
|
||||
|
||||
def test_user_name_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
UserCreate(name="a" * 101, metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
|
||||
class TestMessageValidations:
|
||||
def test_valid_message_create(self):
|
||||
msg = MessageCreate(content="test", is_user=True, metadata={})
|
||||
assert msg.content == "test"
|
||||
assert msg.is_user is True
|
||||
assert msg.metadata == {}
|
||||
|
||||
def test_message_content_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MessageCreate(content="a" * 50001, is_user=True, metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
def test_message_invalid_is_user_type(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MessageCreate(content="test", is_user="not a bool", metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "bool_parsing"
|
||||
|
||||
|
||||
class TestMetamessageValidations:
|
||||
def test_valid_metamessage_create(self):
|
||||
meta = MetamessageCreate(
|
||||
metamessage_type="test",
|
||||
content="test content",
|
||||
message_id="123",
|
||||
metadata={},
|
||||
)
|
||||
assert meta.metamessage_type == "test"
|
||||
assert meta.content == "test content"
|
||||
assert meta.message_id == "123"
|
||||
|
||||
def test_metamessage_type_too_short(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MetamessageCreate(
|
||||
metamessage_type="",
|
||||
content="test",
|
||||
message_id="123",
|
||||
metadata={},
|
||||
)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_short"
|
||||
|
||||
def test_metamessage_type_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MetamessageCreate(
|
||||
metamessage_type="a" * 51,
|
||||
content="test",
|
||||
message_id="123",
|
||||
metadata={},
|
||||
)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
def test_metamessage_content_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MetamessageCreate(
|
||||
metamessage_type="test",
|
||||
content="a" * 50001,
|
||||
message_id="123",
|
||||
metadata={},
|
||||
)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
|
||||
class TestCollectionValidations:
|
||||
def test_valid_collection_create(self):
|
||||
collection = CollectionCreate(name="test", metadata={})
|
||||
assert collection.name == "test"
|
||||
assert collection.metadata == {}
|
||||
|
||||
def test_collection_name_too_short(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CollectionCreate(name="", metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_short"
|
||||
|
||||
def test_collection_name_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CollectionCreate(name="a" * 101, metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
def test_collection_name_honcho(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
CollectionCreate(name="honcho", metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "value_error"
|
||||
|
||||
|
||||
class TestDocumentValidations:
|
||||
def test_valid_document_create(self):
|
||||
doc = DocumentCreate(content="test content", metadata={})
|
||||
assert doc.content == "test content"
|
||||
assert doc.metadata == {}
|
||||
|
||||
def test_document_content_too_short(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
DocumentCreate(content="", metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_short"
|
||||
|
||||
def test_document_content_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
DocumentCreate(content="a" * 100001, metadata={})
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
|
||||
class TestDocumentQueryValidations:
|
||||
def test_valid_document_query(self):
|
||||
query = DocumentQuery(query="test query", top_k=5)
|
||||
assert query.query == "test query"
|
||||
assert query.top_k == 5
|
||||
|
||||
def test_query_too_short(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
DocumentQuery(query="", top_k=5)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_short"
|
||||
|
||||
def test_query_too_long(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
DocumentQuery(query="a" * 1001, top_k=5)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "string_too_long"
|
||||
|
||||
def test_top_k_too_small(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
DocumentQuery(query="test", top_k=0)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "greater_than_equal"
|
||||
|
||||
def test_top_k_too_large(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
DocumentQuery(query="test", top_k=51)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "less_than_equal"
|
||||
|
||||
|
||||
class TestMessageBatchValidations:
|
||||
def test_valid_message_batch(self):
|
||||
batch = MessageBatchCreate(
|
||||
messages=[
|
||||
MessageCreate(content="test", is_user=True, metadata={})
|
||||
]
|
||||
)
|
||||
assert len(batch.messages) == 1
|
||||
|
||||
def test_message_batch_too_large(self):
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
MessageBatchCreate(
|
||||
messages=[
|
||||
MessageCreate(content="test", is_user=True, metadata={})
|
||||
for _ in range(101)
|
||||
]
|
||||
)
|
||||
error_dict = exc_info.value.errors()[0]
|
||||
assert error_dict["type"] == "too_long"
|
||||
Loading…
Reference in New Issue