Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions promptlens/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field


class ProviderConfig(BaseModel):
Expand Down Expand Up @@ -114,9 +114,7 @@ class RunConfig(BaseModel):
execution: ExecutionConfig = Field(default_factory=ExecutionConfig)
output: OutputConfig = Field(default_factory=OutputConfig)

class Config:
"""Pydantic config."""
json_schema_extra = {
model_config = ConfigDict(json_schema_extra={
"example": {
"golden_set": "./examples/golden_sets/customer_support.yaml",
"models": [
Expand All @@ -142,4 +140,4 @@ class Config:
"formats": ["html", "json"],
},
}
}
})
14 changes: 5 additions & 9 deletions promptlens/models/test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field

from promptlens.models.tools import ToolDefinition, ExpectedToolCall

Expand Down Expand Up @@ -50,17 +50,15 @@ class TestCase(BaseModel):
description="Whether to actually execute tools (default: False, evaluation only)"
)

class Config:
"""Pydantic config."""
json_schema_extra = {
model_config = ConfigDict(json_schema_extra={
"example": {
"id": "cs-001",
"query": "How do I reset my password?",
"expected_behavior": "Provide clear step-by-step instructions",
"category": "account_management",
"tags": ["password", "account"],
}
}
})


class GoldenSet(BaseModel):
Expand All @@ -80,9 +78,7 @@ class GoldenSet(BaseModel):
test_cases: List[TestCase]
metadata: Dict[str, Any] = Field(default_factory=dict)

class Config:
"""Pydantic config."""
json_schema_extra = {
model_config = ConfigDict(json_schema_extra={
"example": {
"name": "Customer Support Tests",
"description": "Test cases for customer support chatbot",
Expand All @@ -97,4 +93,4 @@ class Config:
}
],
}
}
})
5 changes: 2 additions & 3 deletions promptlens/models/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"""

from typing import Any, Dict, List, Optional
from pydantic import BaseModel, Field
from pydantic import BaseModel, ConfigDict, Field


class ToolParameter(BaseModel):
Expand All @@ -24,8 +24,7 @@ class ToolParameter(BaseModel):
properties: Optional[Dict[str, "ToolParameter"]] = Field(None, description="For object types, nested properties")
items: Optional["ToolParameter"] = Field(None, description="For array types, the item schema")

class Config:
extra = "allow" # Allow additional JSON Schema fields
model_config = ConfigDict(extra="allow") # Allow additional JSON Schema fields


class ToolDefinition(BaseModel):
Expand Down
15 changes: 14 additions & 1 deletion promptlens/providers/http.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Generic HTTP provider for local models (Ollama, LM Studio, etc.)."""

import json
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
Expand Down Expand Up @@ -119,7 +120,19 @@ async def _make_request() -> ModelResponse:
timeout=aiohttp.ClientTimeout(total=self.config.timeout),
) as response:
response.raise_for_status()
data = await response.json()
raw_text = await response.text()

try:
data = json.loads(raw_text)
except json.JSONDecodeError as exc:
logger.error(
"HTTP provider returned non-JSON response from %s: %s",
self.endpoint,
raw_text[:500],
)
raise ValueError(
"HTTP provider expected JSON response but received non-JSON content"
) from exc

# Extract content (try common response formats)
content = self._extract_content(data)
Expand Down
38 changes: 38 additions & 0 deletions tests/test_http_provider_response_parsing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from promptlens.models.config import ProviderConfig
from promptlens.providers.http import HTTPProvider

Expand Down Expand Up @@ -57,3 +59,39 @@ def test_extract_content_returns_empty_for_unknown_shape() -> None:
provider = _provider()

assert provider._extract_content({"foo": "bar"}) == ""


@pytest.mark.asyncio
async def test_http_provider_returns_clear_error_for_non_json_response() -> None:
provider = _provider()

class MockResponse:
async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
return False

def raise_for_status(self) -> None:
return None

async def text(self) -> str:
return "not-json"

class MockSession:
async def __aenter__(self):
return self

async def __aexit__(self, exc_type, exc, tb):
return False

def post(self, *args, **kwargs):
return MockResponse()

from unittest.mock import patch

with patch("promptlens.providers.http.aiohttp.ClientSession", return_value=MockSession()):
result = await provider.generate("hello")

assert result.error is not None
assert "expected JSON response" in result.error