Skip to content

Commit 1b5891b

Browse files
committed
fixed a lot of bugs and created comprehensive example
1 parent 7ab4d6c commit 1b5891b

8 files changed

Lines changed: 2212 additions & 74 deletions

File tree

examples/comprehensive_demo.py

Lines changed: 890 additions & 0 deletions
Large diffs are not rendered by default.

poetry.lock

Lines changed: 1254 additions & 65 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dependencies = [
3232
"aiohttp>=3.9.0",
3333
"pyyaml>=6.0.0",
3434
"scipy>=1.10.0",
35+
"python-dotenv (>=1.2.1,<2.0.0)",
3536
]
3637

3738
[project.urls]

src/parsec/cache/keys.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from typing import Any, Optional
33
import hashlib
44
import json
5+
from pydantic import BaseModel
56

67

78
def generate_cache_key(
@@ -44,10 +45,20 @@ def generate_cache_key(
4445
- All components are sorted to ensure deterministic ordering
4546
"""
4647
normalized_prompt = prompt.strip()
48+
49+
# Handle Pydantic models by converting to JSON schema
50+
if schema:
51+
if isinstance(schema, type) and issubclass(schema, BaseModel):
52+
schema_str = json.dumps(schema.model_json_schema(), sort_keys=True)
53+
else:
54+
schema_str = json.dumps(schema, sort_keys=True)
55+
else:
56+
schema_str = ""
57+
4758
key_components = {
4859
"prompt": normalized_prompt,
4960
"model": model,
50-
"schema": json.dumps(schema, sort_keys=True) if schema else "",
61+
"schema": schema_str,
5162
"temperature": temperature,
5263
**kwargs
5364
}

src/parsec/enforcement/engine.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,14 @@ async def _generate():
107107

108108
if validation.status == ValidationStatus.VALID:
109109
if self.collector:
110+
# Convert Pydantic model to JSON schema for storage
111+
schema_for_storage = schema
112+
if isinstance(schema, type) and issubclass(schema, BaseModel):
113+
schema_for_storage = schema.model_json_schema()
114+
110115
self.collector.collect({
111116
"prompt": prompt,
112-
"json_schema": schema,
117+
"json_schema": schema_for_storage,
113118
"response": generation.output,
114119
"parsed_output": validation.parsed_output,
115120
"success": True,
@@ -153,4 +158,18 @@ async def _generate():
153158

154159
# Will retry on next iteration
155160
retry_count += 1
156-
continue
161+
continue
162+
163+
# If we exit the loop without returning, all retries were exhausted
164+
# Return the last failed result
165+
if last_validation:
166+
return EnforcedOutput(
167+
data=last_validation.parsed_output,
168+
generation=generation,
169+
validation=last_validation,
170+
retry_count=retry_count,
171+
success=False
172+
)
173+
else:
174+
# Should never reach here, but handle it gracefully
175+
raise RuntimeError("Enforcement failed: no validation results available")

src/parsec/models/adapters/anthropic_adapter.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
from parsec.core import BaseLLMAdapter, GenerationResponse, ModelProviders
33
from typing import AsyncIterator
44
from parsec.logging import get_logger
5+
from pydantic import BaseModel
56
import time
7+
import json
68

79
class AnthropicAdapter(BaseLLMAdapter):
810
"""Adapter for Anthropic's API with custom configurations."""
@@ -47,8 +49,12 @@ async def generate(self, prompt: str, schema=None, temperature=0.7,
4749
# Anthropic doesn't have response_format parameter
4850
# Instead, we need to instruct it via the prompt
4951
if schema:
50-
import json
51-
schema_str = json.dumps(schema, indent=2)
52+
# Convert Pydantic model to JSON schema if needed
53+
if isinstance(schema, type) and issubclass(schema, BaseModel):
54+
schema_dict = schema.model_json_schema()
55+
else:
56+
schema_dict = schema
57+
schema_str = json.dumps(schema_dict, indent=2)
5258
message_params["messages"][0]["content"] = (
5359
f"{prompt}\n\n"
5460
f"Please respond with valid JSON matching this schema:\n{schema_str}\n"

src/parsec/models/adapters/gemini_adapter.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from parsec.core import BaseLLMAdapter, GenerationResponse, ModelProviders
33
from typing import AsyncIterator
44
from parsec.logging import get_logger
5+
from pydantic import BaseModel
56
import time
67
import json
78

@@ -69,7 +70,12 @@ async def generate(
6970
# If schema is provided, use JSON mode and add schema to prompt
7071
if schema:
7172
generation_config["response_mime_type"] = "application/json"
72-
schema_str = json.dumps(schema, indent=2)
73+
# Convert Pydantic model to JSON schema if needed
74+
if isinstance(schema, type) and issubclass(schema, BaseModel):
75+
schema_dict = schema.model_json_schema()
76+
else:
77+
schema_dict = schema
78+
schema_str = json.dumps(schema_dict, indent=2)
7379
prompt = (
7480
f"{prompt}\n\n"
7581
f"Please respond with valid JSON matching this schema:\n{schema_str}\n"
@@ -140,7 +146,12 @@ async def generate_stream(
140146
# If schema is provided, use JSON mode and add schema to prompt
141147
if schema:
142148
generation_config["response_mime_type"] = "application/json"
143-
schema_str = json.dumps(schema, indent=2)
149+
# Convert Pydantic model to JSON schema if needed
150+
if isinstance(schema, type) and issubclass(schema, BaseModel):
151+
schema_dict = schema.model_json_schema()
152+
else:
153+
schema_dict = schema
154+
schema_str = json.dumps(schema_dict, indent=2)
144155
prompt = (
145156
f"{prompt}\n\n"
146157
f"Please respond with valid JSON matching this schema:\n{schema_str}\n"

src/parsec/models/adapters/openai_adapter.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from parsec.core import BaseLLMAdapter, GenerationResponse, ModelProviders
33
from typing import AsyncIterator
44
from parsec.logging import get_logger
5+
from pydantic import BaseModel
56
import time
67
import json
78

@@ -41,8 +42,13 @@ async def generate(self, prompt: str, schema=None, temperature=0.7,
4142
extra_args = {}
4243
if schema and self.supports_native_structure_output():
4344
extra_args["response_format"] = {"type": "json_object"}
45+
# Convert Pydantic model to JSON schema if needed
46+
if isinstance(schema, type) and issubclass(schema, BaseModel):
47+
schema_dict = schema.model_json_schema()
48+
else:
49+
schema_dict = schema
4450
# Add schema to prompt
45-
messages[0]["content"] = f"{prompt}\n\nReturn valid JSON matching this schema: {json.dumps(schema)}"
51+
messages[0]["content"] = f"{prompt}\n\nReturn valid JSON matching this schema: {json.dumps(schema_dict)}"
4652
try:
4753
response = await client.chat.completions.create(
4854
model=self.model,
@@ -85,7 +91,12 @@ async def generate_stream(
8591
extra_args = {}
8692
if schema and self.supports_native_structure_output():
8793
extra_args["response_format"] = {"type": "json_object"}
88-
messages[0]["content"] = f"{prompt}\n\nReturn valid JSON matching this schema: {json.dumps(schema)}"
94+
# Convert Pydantic model to JSON schema if needed
95+
if isinstance(schema, type) and issubclass(schema, BaseModel):
96+
schema_dict = schema.model_json_schema()
97+
else:
98+
schema_dict = schema
99+
messages[0]["content"] = f"{prompt}\n\nReturn valid JSON matching this schema: {json.dumps(schema_dict)}"
89100

90101
stream = await client.chat.completions.create(
91102
model=self.model,

0 commit comments

Comments
 (0)