Skip to content

Commit 6d30cf0

Browse files
initial commit
0 parents  commit 6d30cf0

File tree

14 files changed

+4105
-0
lines changed

14 files changed

+4105
-0
lines changed

.gitignore

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Python-generated files
2+
__pycache__/
3+
*.py[oc]
4+
build/
5+
dist/
6+
wheels/
7+
*.egg-info
8+
*.whl
9+
10+
# Virtual environments
11+
.venv
12+
venv/
13+
env/
14+
15+
# OS files
16+
.DS_Store
17+
Thumbs.db
18+
19+
# Testing
20+
.pytest_cache/
21+
.coverage
22+
htmlcov/
23+
24+
# Temporary files
25+
*.tmp
26+
*.temp
27+
28+
# Cache directories
29+
.pytest_cache/
30+
.ruff_cache/

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

.vscode/settings.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"python.defaultInterpreterPath": "./.venv/bin/python",
3+
"python.terminal.activateEnvironment": true,
4+
"[python]": {
5+
"editor.defaultFormatter": "charliermarsh.ruff",
6+
"editor.formatOnSave": true,
7+
"editor.codeActionsOnSave": {
8+
"source.fixAll.ruff": "explicit",
9+
"source.organizeImports.ruff": "explicit"
10+
}
11+
},
12+
"python.linting.enabled": true,
13+
"editor.rulers": [88]
14+
}

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Piotr Bednarski
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# structllm
2+
3+
<div style="text-align: center;">
4+
<img width="100%" src="structllm.svg" alt="Logo">
5+
</div>
6+
7+
[![PyPI version](https://badge.fury.io/py/structllm.svg)](https://badge.fury.io/py/structllm)
8+
[![Python Support](https://img.shields.io/pypi/pyversions/structllm.svg)](https://pypi.org/project/structllm/)
9+
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
10+
11+
**structllm** is a universal and lightweight Python library that provides [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses) functionality for any LLM provider (OpenAI, Anthropic, Mistral, local models, etc.), not just OpenAI. It guarantees that LLM responses conform to your provided JSON schema using Pydantic models.
12+
13+
If your LLM model has 7B parameters or more, it can be used with structllm.
14+
15+
## Installation
16+
17+
```bash
18+
pip install structllm
19+
```
20+
21+
Or using uv (recommended):
22+
23+
```bash
24+
uv add structllm
25+
```
26+
27+
## Quick Start
28+
29+
```python
30+
from pydantic import BaseModel
31+
from structllm import StructLLM
32+
from typing import List
33+
34+
class CalendarEvent(BaseModel):
35+
name: str
36+
date: str
37+
participants: List[str]
38+
39+
client = StructLLM(
40+
api_base="https://openrouter.ai/api/v1",
41+
api_key="sk-or-v1-...",
42+
)
43+
44+
messages = [
45+
{"role": "system", "content": "Extract the event information."},
46+
{"role": "user", "content": "Alice and Bob are going to a science fair on Friday."},
47+
]
48+
49+
response = client.parse(
50+
model="openrouter/moonshotai/kimi-k2",
51+
messages=messages,
52+
response_format=CalendarEvent,
53+
)
54+
55+
if response.output_parsed:
56+
print(response.output_parsed)
57+
# {"name": "science fair", "date": "Friday", "participants": ["Alice", "Bob"]}
58+
else:
59+
print("Failed to parse structured output")
60+
```
61+
62+
## Provider Support
63+
64+
StructLLM works with **100+ LLM providers** through LiteLLM. Check the [LiteLLM documentation](https://docs.litellm.ai/docs/providers) for the full list of supported providers.
65+
66+
## Advanced Usage
67+
68+
### Complex Data Structures
69+
70+
```python
71+
from pydantic import BaseModel, Field
72+
from typing import List, Optional
73+
from enum import Enum
74+
75+
class Priority(str, Enum):
76+
LOW = "low"
77+
MEDIUM = "medium"
78+
HIGH = "high"
79+
80+
class Task(BaseModel):
81+
title: str = Field(description="The task title")
82+
description: Optional[str] = Field(default=None, description="Task description")
83+
priority: Priority = Field(description="Task priority level")
84+
assignees: List[str] = Field(description="List of assigned people")
85+
due_date: Optional[str] = Field(default=None, description="Due date in YYYY-MM-DD format")
86+
87+
client = StructLLM(
88+
api_base="https://openrouter.ai/api/v1",
89+
api_key="sk-or-v1-...",
90+
)
91+
92+
response = client.parse(
93+
model="gpt-4o-2024-08-06",
94+
messages=[
95+
{
96+
"role": "user",
97+
"content": "Create a high-priority task for John and Sarah to review the quarterly report by next Friday."
98+
}
99+
],
100+
response_format=Task,
101+
)
102+
103+
task = response.output_parsed
104+
print(f"Task: {task.title}")
105+
print(f"Priority: {task.priority}")
106+
print(f"Assignees: {task.assignees}")
107+
```
108+
109+
### Error Handling
110+
111+
```python
112+
response = client.parse(
113+
model="gpt-4o-2024-08-06",
114+
messages=messages,
115+
response_format=CalendarEvent,
116+
)
117+
118+
if response.output_parsed:
119+
# Successfully parsed
120+
event = response.output_parsed
121+
print(f"Parsed event: {event}")
122+
else:
123+
# Parsing failed, but raw response is available
124+
print("Failed to parse structured output")
125+
print(f"Raw response: {response.raw_response.choices[0].message.content}")
126+
```
127+
128+
### Custom Configuration
129+
130+
```python
131+
client = StructLLM(
132+
api_base="https://api.custom-provider.com/v1",
133+
api_key="your-api-key"
134+
)
135+
136+
response = client.parse(
137+
model="custom/model-name",
138+
messages=messages,
139+
response_format=YourModel,
140+
temperature=0.1,
141+
top_p=0.1,
142+
max_tokens=1000,
143+
# Any additional parameters supported by the LiteLLM interface
144+
custom_parameter="value"
145+
)
146+
```
147+
148+
## How It Works
149+
150+
StructLLM uses prompt engineering to ensure structured outputs:
151+
152+
1. **Schema Injection**: Automatically injects your Pydantic model's JSON schema into the system prompt
153+
2. **Format Instructions**: Adds specific instructions for JSON-only responses
154+
3. **Intelligent Parsing**: Extracts JSON from responses even when wrapped in additional text
155+
4. **Validation**: Uses Pydantic for robust type checking and validation
156+
5. **Fallback Handling**: Gracefully handles parsing failures while preserving raw responses
157+
158+
By default it uses low `temperature` and `top_p` settings to ensure consistent outputs, but you can customize these parameters as needed.
159+
160+
## Testing
161+
162+
Run the test suite:
163+
164+
```bash
165+
# Install dependencies
166+
uv sync
167+
168+
# Run tests
169+
uv run pytest
170+
uv run pytest -m "not integration"
171+
172+
# Run integration tests (requires external services)
173+
uv run pytest -m "integration"
174+
175+
# Run linting
176+
uv run ruff check .
177+
```
178+
179+
## Contributing
180+
181+
Contributions are welcome! Please feel free to submit a Pull Request.
182+
183+
1. Fork the repository
184+
2. Create a feature branch: `git checkout -b feature/amazing-feature`
185+
3. Make your changes with tests
186+
4. Run the test suite: `uv run pytest`
187+
5. Run linting: `uv run ruff check .`
188+
6. Submit a pull request
189+
190+
## License
191+
192+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
193+
194+
## Acknowledgments
195+
196+
- [LiteLLM](https://github.com/BerriAI/litellm) for providing the universal LLM interface
197+
- [Pydantic](https://github.com/pydantic/pydantic) for structured data validation

example.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Example usage of StructLLM library."""
2+
3+
from typing import List
4+
5+
from pydantic import BaseModel
6+
7+
from structllm import StructLLM
8+
9+
10+
class CalendarEvent(BaseModel):
11+
"""A calendar event with name, date, and participants."""
12+
13+
name: str
14+
date: str
15+
participants: List[str]
16+
17+
18+
def main():
19+
"""Demonstrate StructLLM usage."""
20+
# Initialize the client for OpenRouter
21+
client = StructLLM(
22+
api_base="https://openrouter.ai/api/v1",
23+
api_key="sk-or-v1-...",
24+
)
25+
26+
# Define the conversation
27+
messages = [
28+
{"role": "system", "content": "Extract the event information."},
29+
{
30+
"role": "user",
31+
"content": "Alice and Bob are going to a science fair on Friday.",
32+
},
33+
]
34+
35+
try:
36+
# Parse structured output
37+
response = client.parse(
38+
model="openrouter/moonshotai/kimi-k2",
39+
messages=messages,
40+
response_format=CalendarEvent,
41+
)
42+
43+
print("Raw response content:")
44+
print(f"{response.raw_response.choices[0].message.content}\n")
45+
# {"name": "science fair", "date": "Friday", "participants": ["Alice", "Bob"]}
46+
47+
if response.output_parsed:
48+
print("Parsed structured output:")
49+
print(f"Name: {response.output_parsed.name}") # science fair
50+
print(f"Date: {response.output_parsed.date}") # Friday
51+
print(
52+
f"Participants: {response.output_parsed.participants}"
53+
) # ['Alice', 'Bob']
54+
else:
55+
print("Failed to parse structured output")
56+
57+
except Exception as e:
58+
print(f"Error: {e}")
59+
print("Make sure you have a valid OpenRouter API key and internet connection")
60+
61+
62+
if __name__ == "__main__":
63+
main()

0 commit comments

Comments
 (0)