schemez 0.1.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {schemez-0.1.1 → schemez-0.2.2}/PKG-INFO +6 -1
- {schemez-0.1.1 → schemez-0.2.2}/pyproject.toml +6 -5
- schemez-0.2.2/src/schemez/__init__.py +27 -0
- schemez-0.2.2/src/schemez/helpers.py +171 -0
- schemez-0.2.2/src/schemez/pydantic_types.py +42 -0
- schemez-0.2.2/src/schemez/schema.py +246 -0
- schemez-0.2.2/src/schemez/schemadef/__init__.py +0 -0
- schemez-0.2.2/src/schemez/schemadef/schemadef.py +120 -0
- schemez-0.1.1/src/schemez/__init__.py +0 -7
- schemez-0.1.1/src/schemez/helpers.py +0 -35
- schemez-0.1.1/src/schemez/schema.py +0 -102
- {schemez-0.1.1 → schemez-0.2.2}/.copier-answers.yml +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/.github/FUNDING.yml +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/.github/copilot-instructions.md +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/.github/dependabot.yml +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/.github/workflows/build.yml +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/.github/workflows/documentation.yml +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/.gitignore +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/.pre-commit-config.yaml +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/LICENSE +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/README.md +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/docs/.empty +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/duties.py +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/mkdocs.yml +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/overrides/_dummy.txt +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/src/schemez/code.py +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/src/schemez/convert.py +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/src/schemez/docstrings.py +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/src/schemez/py.typed +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/tests/__init__.py +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/tests/conftest.py +0 -0
- {schemez-0.1.1 → schemez-0.2.2}/tests/test_schema.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: schemez
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2.2
|
4
4
|
Summary: Pydantic shim for config stuff
|
5
5
|
Project-URL: Documentation, https://phil65.github.io/schemez/
|
6
6
|
Project-URL: Source, https://github.com/phil65/schemez
|
@@ -49,6 +49,11 @@ Requires-Python: >=3.12
|
|
49
49
|
Requires-Dist: griffe>=1.7.3
|
50
50
|
Requires-Dist: pydantic
|
51
51
|
Requires-Dist: universal-pathlib>=0.2.6
|
52
|
+
Provides-Extra: ai
|
53
|
+
Requires-Dist: anyenv>=0.4.14; extra == 'ai'
|
54
|
+
Requires-Dist: llmling-agent; extra == 'ai'
|
55
|
+
Provides-Extra: yaml
|
56
|
+
Requires-Dist: yamling; extra == 'yaml'
|
52
57
|
Description-Content-Type: text/markdown
|
53
58
|
|
54
59
|
# Schemez
|
@@ -27,9 +27,9 @@ classifiers = [
|
|
27
27
|
"Topic :: Documentation",
|
28
28
|
"Topic :: Software Development",
|
29
29
|
"Topic :: Utilities",
|
30
|
-
"Typing :: Typed",
|
30
|
+
"Typing :: Typed",
|
31
|
+
"Framework :: Pydantic",
|
31
32
|
"Framework :: Pydantic :: 2",
|
32
|
-
|
33
33
|
]
|
34
34
|
keywords = []
|
35
35
|
requires-python = ">=3.12"
|
@@ -41,8 +41,9 @@ dependencies = [
|
|
41
41
|
"universal-pathlib>=0.2.6",
|
42
42
|
]
|
43
43
|
|
44
|
-
[project
|
44
|
+
[project.optional-dependencies]
|
45
45
|
yaml = ["yamling"]
|
46
|
+
ai = ["llmling-agent", "anyenv>=0.4.14"]
|
46
47
|
|
47
48
|
[tool.uv]
|
48
49
|
default-groups = ["dev", "lint", "docs"]
|
@@ -158,7 +159,7 @@ select = [
|
|
158
159
|
# "TD", # flake8-todos
|
159
160
|
"T10", # flake8-debugger
|
160
161
|
# "T20", # flake8-print
|
161
|
-
"TC",
|
162
|
+
"TC", # flake8-type-checking
|
162
163
|
"TID", # flake8-tidy-imports
|
163
164
|
"TRY", # tryceratops
|
164
165
|
"UP", # PyUpgrade
|
@@ -198,7 +199,7 @@ ignore = [
|
|
198
199
|
# "PLR2004", # Magic values instead of named consts
|
199
200
|
"SLF001", # Private member accessed
|
200
201
|
"TRY003", # Avoid specifying long messages outside the exception class
|
201
|
-
"TC006",
|
202
|
+
"TC006", # runtime-cast-value
|
202
203
|
]
|
203
204
|
|
204
205
|
[tool.ruff.lint.flake8-quotes]
|
@@ -0,0 +1,27 @@
|
|
1
|
+
__version__ = "0.2.2"
|
2
|
+
|
3
|
+
|
4
|
+
from schemez.schema import Schema
|
5
|
+
from schemez.code import PythonCode, JSONCode, TOMLCode, YAMLCode
|
6
|
+
from schemez.schemadef.schemadef import (
|
7
|
+
SchemaDef,
|
8
|
+
SchemaField,
|
9
|
+
ImportedSchemaDef,
|
10
|
+
InlineSchemaDef,
|
11
|
+
)
|
12
|
+
from schemez.pydantic_types import ModelIdentifier, ModelTemperature, MimeType
|
13
|
+
|
14
|
+
__all__ = [
|
15
|
+
"ImportedSchemaDef",
|
16
|
+
"InlineSchemaDef",
|
17
|
+
"JSONCode",
|
18
|
+
"MimeType",
|
19
|
+
"ModelIdentifier",
|
20
|
+
"ModelTemperature",
|
21
|
+
"PythonCode",
|
22
|
+
"Schema",
|
23
|
+
"SchemaDef",
|
24
|
+
"SchemaField",
|
25
|
+
"TOMLCode",
|
26
|
+
"YAMLCode",
|
27
|
+
]
|
@@ -0,0 +1,171 @@
|
|
1
|
+
"""Helpers for BaseModels."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import importlib
|
6
|
+
import os
|
7
|
+
from typing import TYPE_CHECKING, Any
|
8
|
+
|
9
|
+
from pydantic import BaseModel
|
10
|
+
|
11
|
+
|
12
|
+
StrPath = str | os.PathLike[str]
|
13
|
+
|
14
|
+
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from collections.abc import Callable
|
17
|
+
|
18
|
+
|
19
|
+
def import_callable(path: str) -> Callable[..., Any]:
|
20
|
+
"""Import a callable from a dotted path.
|
21
|
+
|
22
|
+
Supports both dot and colon notation:
|
23
|
+
- Dot notation: module.submodule.Class.method
|
24
|
+
- Colon notation: module.submodule:Class.method
|
25
|
+
|
26
|
+
Args:
|
27
|
+
path: Import path using dots and/or colon
|
28
|
+
|
29
|
+
Raises:
|
30
|
+
ValueError: If path cannot be imported or result isn't callable
|
31
|
+
"""
|
32
|
+
if not path:
|
33
|
+
msg = "Import path cannot be empty"
|
34
|
+
raise ValueError(msg)
|
35
|
+
|
36
|
+
# Normalize path - replace colon with dot if present
|
37
|
+
normalized_path = path.replace(":", ".")
|
38
|
+
parts = normalized_path.split(".")
|
39
|
+
|
40
|
+
# Try importing progressively smaller module paths
|
41
|
+
for i in range(len(parts), 0, -1):
|
42
|
+
try:
|
43
|
+
# Try current module path
|
44
|
+
module_path = ".".join(parts[:i])
|
45
|
+
module = importlib.import_module(module_path)
|
46
|
+
|
47
|
+
# Walk remaining parts as attributes
|
48
|
+
obj = module
|
49
|
+
for part in parts[i:]:
|
50
|
+
obj = getattr(obj, part)
|
51
|
+
|
52
|
+
# Check if we got a callable
|
53
|
+
if callable(obj):
|
54
|
+
return obj
|
55
|
+
|
56
|
+
msg = f"Found object at {path} but it isn't callable"
|
57
|
+
raise ValueError(msg)
|
58
|
+
|
59
|
+
except ImportError:
|
60
|
+
# Try next shorter path
|
61
|
+
continue
|
62
|
+
except AttributeError:
|
63
|
+
# Attribute not found - try next shorter path
|
64
|
+
continue
|
65
|
+
|
66
|
+
# If we get here, no import combination worked
|
67
|
+
msg = f"Could not import callable from path: {path}"
|
68
|
+
raise ValueError(msg)
|
69
|
+
|
70
|
+
|
71
|
+
def import_class(path: str) -> type:
|
72
|
+
"""Import a class from a dotted path.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
path: Dot-separated path to the class
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
The imported class
|
79
|
+
|
80
|
+
Raises:
|
81
|
+
ValueError: If path is invalid or doesn't point to a class
|
82
|
+
"""
|
83
|
+
try:
|
84
|
+
obj = import_callable(path)
|
85
|
+
if not isinstance(obj, type):
|
86
|
+
msg = f"{path} is not a class"
|
87
|
+
raise TypeError(msg) # noqa: TRY301
|
88
|
+
except Exception as exc:
|
89
|
+
msg = f"Failed to import class from {path}"
|
90
|
+
raise ValueError(msg) from exc
|
91
|
+
else:
|
92
|
+
return obj
|
93
|
+
|
94
|
+
|
95
|
+
def merge_models[T: BaseModel](base: T, overlay: T) -> T:
|
96
|
+
"""Deep merge two Pydantic models."""
|
97
|
+
if not isinstance(overlay, type(base)):
|
98
|
+
msg = f"Cannot merge different types: {type(base)} and {type(overlay)}"
|
99
|
+
raise TypeError(msg)
|
100
|
+
|
101
|
+
merged_data = base.model_dump()
|
102
|
+
overlay_data = overlay.model_dump(exclude_none=True)
|
103
|
+
for field_name, field_value in overlay_data.items():
|
104
|
+
base_value = merged_data.get(field_name)
|
105
|
+
|
106
|
+
match (base_value, field_value):
|
107
|
+
case (list(), list()):
|
108
|
+
merged_data[field_name] = [
|
109
|
+
*base_value,
|
110
|
+
*(item for item in field_value if item not in base_value),
|
111
|
+
]
|
112
|
+
case (dict(), dict()):
|
113
|
+
merged_data[field_name] = base_value | field_value
|
114
|
+
case _:
|
115
|
+
merged_data[field_name] = field_value
|
116
|
+
|
117
|
+
return base.__class__.model_validate(merged_data)
|
118
|
+
|
119
|
+
|
120
|
+
def resolve_type_string(type_string: str, safe: bool = True) -> type:
|
121
|
+
"""Convert a string representation to an actual Python type.
|
122
|
+
|
123
|
+
Args:
|
124
|
+
type_string: String representation of a type (e.g. "list[str]", "int")
|
125
|
+
safe: If True, uses a limited set of allowed types. If False, allows any valid
|
126
|
+
Python type expression but has potential security implications
|
127
|
+
if input is untrusted
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
The corresponding Python type
|
131
|
+
|
132
|
+
Raises:
|
133
|
+
ValueError: If the type string cannot be resolved
|
134
|
+
"""
|
135
|
+
if safe:
|
136
|
+
# Create a safe context with just the allowed types
|
137
|
+
type_context = {
|
138
|
+
"str": str,
|
139
|
+
"int": int,
|
140
|
+
"float": float,
|
141
|
+
"bool": bool,
|
142
|
+
"list": list,
|
143
|
+
"dict": dict,
|
144
|
+
"set": set,
|
145
|
+
"tuple": tuple,
|
146
|
+
"Any": Any,
|
147
|
+
# Add other safe types as needed
|
148
|
+
}
|
149
|
+
|
150
|
+
try:
|
151
|
+
return eval(type_string, {"__builtins__": {}}, type_context)
|
152
|
+
except Exception as e:
|
153
|
+
msg = f"Failed to resolve type {type_string} in safe mode"
|
154
|
+
raise ValueError(msg) from e
|
155
|
+
else: # unsafe mode
|
156
|
+
# Import common typing modules to make them available
|
157
|
+
import collections.abc
|
158
|
+
import typing
|
159
|
+
|
160
|
+
# Create a context with full typing module available
|
161
|
+
type_context = {
|
162
|
+
**vars(typing),
|
163
|
+
**vars(collections.abc),
|
164
|
+
**{t.__name__: t for t in __builtins__.values() if isinstance(t, type)}, # type: ignore
|
165
|
+
}
|
166
|
+
|
167
|
+
try:
|
168
|
+
return eval(type_string, {"__builtins__": {}}, type_context)
|
169
|
+
except Exception as e:
|
170
|
+
msg = f"Failed to resolve type {type_string} in unsafe mode"
|
171
|
+
raise ValueError(msg) from e
|
@@ -0,0 +1,42 @@
|
|
1
|
+
"""Custom field types with 'field_type' metadata for UI rendering hints."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Annotated
|
6
|
+
|
7
|
+
from pydantic import Field
|
8
|
+
|
9
|
+
|
10
|
+
ModelIdentifier = Annotated[
|
11
|
+
str,
|
12
|
+
Field(
|
13
|
+
json_schema_extra={"field_type": "model_identifier"},
|
14
|
+
pattern=r"^[a-zA-Z0-9\-]+(/[a-zA-Z0-9\-]+)*(:[\w\-\.]+)?$",
|
15
|
+
examples=["openai:gpt-o1-mini", "anthropic/claude-3-opus"],
|
16
|
+
description="Identifier for an AI model, optionally including provider.",
|
17
|
+
),
|
18
|
+
]
|
19
|
+
|
20
|
+
ModelTemperature = Annotated[
|
21
|
+
float,
|
22
|
+
Field(
|
23
|
+
json_schema_extra={"field_type": "temperature", "step": 0.1},
|
24
|
+
ge=0.0,
|
25
|
+
le=2.0,
|
26
|
+
description=(
|
27
|
+
"Controls randomness in model responses.\n"
|
28
|
+
"Lower values are more deterministic, higher values more creative"
|
29
|
+
),
|
30
|
+
examples=[0.0, 0.7, 1.0],
|
31
|
+
),
|
32
|
+
]
|
33
|
+
|
34
|
+
MimeType = Annotated[
|
35
|
+
str,
|
36
|
+
Field(
|
37
|
+
json_schema_extra={"field_type": "mime_type"},
|
38
|
+
pattern=r"^[a-z]+/[a-z0-9\-+.]+$",
|
39
|
+
examples=["text/plain", "application/pdf", "image/jpeg", "application/json"],
|
40
|
+
description="Standard MIME type identifying file formats and content types",
|
41
|
+
),
|
42
|
+
]
|
@@ -0,0 +1,246 @@
|
|
1
|
+
"""Configuration models for Schemez."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import os
|
6
|
+
from typing import TYPE_CHECKING, Any, Literal, Self
|
7
|
+
|
8
|
+
import anyenv
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
10
|
+
import upath
|
11
|
+
|
12
|
+
|
13
|
+
if TYPE_CHECKING:
|
14
|
+
from collections.abc import Callable
|
15
|
+
|
16
|
+
from llmling_agent.agent.agent import AgentType
|
17
|
+
from llmling_agent.models.content import BaseContent
|
18
|
+
|
19
|
+
|
20
|
+
StrPath = str | os.PathLike[str]
|
21
|
+
SourceType = Literal["pdf", "image"]
|
22
|
+
|
23
|
+
DEFAULT_SYSTEM_PROMPT = "You are a schema extractor for {name} BaseModels."
|
24
|
+
DEFAULT_USER_PROMPT = "Extract information from this document:"
|
25
|
+
|
26
|
+
|
27
|
+
class Schema(BaseModel):
|
28
|
+
"""Base class configuration models.
|
29
|
+
|
30
|
+
Provides:
|
31
|
+
- Common Pydantic settings
|
32
|
+
- YAML serialization
|
33
|
+
- Basic merge functionality
|
34
|
+
"""
|
35
|
+
|
36
|
+
model_config = ConfigDict(extra="forbid", use_attribute_docstrings=True)
|
37
|
+
|
38
|
+
def merge(self, other: Self) -> Self:
|
39
|
+
"""Merge with another instance by overlaying its non-None values."""
|
40
|
+
from schemez.helpers import merge_models
|
41
|
+
|
42
|
+
return merge_models(self, other)
|
43
|
+
|
44
|
+
@classmethod
|
45
|
+
def from_yaml(cls, content: str, inherit_path: StrPath | None = None) -> Self:
|
46
|
+
"""Create from YAML string."""
|
47
|
+
import yamling
|
48
|
+
|
49
|
+
data = yamling.load_yaml(content, resolve_inherit=inherit_path or False)
|
50
|
+
return cls.model_validate(data)
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def for_function(
|
54
|
+
cls, func: Callable[..., Any], *, name: str | None = None
|
55
|
+
) -> type[Schema]:
|
56
|
+
"""Create a schema model from a function's signature.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
func: The function to create a schema from
|
60
|
+
name: Optional name for the model
|
61
|
+
|
62
|
+
Returns:
|
63
|
+
A new schema model class based on the function parameters
|
64
|
+
"""
|
65
|
+
from schemez.convert import get_function_model
|
66
|
+
|
67
|
+
return get_function_model(func, name=name)
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def from_vision_llm_sync(
|
71
|
+
cls,
|
72
|
+
file_content: bytes,
|
73
|
+
source_type: SourceType = "pdf",
|
74
|
+
model: str = "google-gla:gemini-2.0-flash",
|
75
|
+
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
76
|
+
user_prompt: str = DEFAULT_USER_PROMPT,
|
77
|
+
provider: AgentType = "pydantic_ai",
|
78
|
+
) -> Self:
|
79
|
+
"""Create a schema model from a document using AI.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
file_content: The document content to create a schema from
|
83
|
+
source_type: The type of the document
|
84
|
+
model: The AI model to use for schema extraction
|
85
|
+
system_prompt: The system prompt to use for schema extraction
|
86
|
+
user_prompt: The user prompt to use for schema extraction
|
87
|
+
provider: The provider to use for schema extraction
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
A new schema model class based on the document
|
91
|
+
"""
|
92
|
+
from llmling_agent import Agent, ImageBase64Content, PDFBase64Content
|
93
|
+
|
94
|
+
if source_type == "pdf":
|
95
|
+
content: BaseContent = PDFBase64Content.from_bytes(file_content)
|
96
|
+
else:
|
97
|
+
content = ImageBase64Content.from_bytes(file_content)
|
98
|
+
agent = Agent[None]( # type:ignore[var-annotated]
|
99
|
+
model=model,
|
100
|
+
system_prompt=system_prompt.format(name=cls.__name__),
|
101
|
+
provider=provider,
|
102
|
+
).to_structured(cls)
|
103
|
+
chat_message = anyenv.run_sync(agent.run(user_prompt, content))
|
104
|
+
return chat_message.content
|
105
|
+
|
106
|
+
@classmethod
|
107
|
+
async def from_vision_llm(
|
108
|
+
cls,
|
109
|
+
file_content: bytes,
|
110
|
+
source_type: SourceType = "pdf",
|
111
|
+
model: str = "google-gla:gemini-2.0-flash",
|
112
|
+
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
113
|
+
user_prompt: str = DEFAULT_USER_PROMPT,
|
114
|
+
provider: AgentType = "pydantic_ai",
|
115
|
+
) -> Self:
|
116
|
+
"""Create a schema model from a document using AI.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
file_content: The document content to create a schema from
|
120
|
+
source_type: The type of the document
|
121
|
+
model: The AI model to use for schema extraction
|
122
|
+
system_prompt: The system prompt to use for schema extraction
|
123
|
+
user_prompt: The user prompt to use for schema extraction
|
124
|
+
provider: The provider to use for schema extraction
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
A new schema model class based on the document
|
128
|
+
"""
|
129
|
+
from llmling_agent import Agent, ImageBase64Content, PDFBase64Content
|
130
|
+
|
131
|
+
if source_type == "pdf":
|
132
|
+
content: BaseContent = PDFBase64Content.from_bytes(file_content)
|
133
|
+
else:
|
134
|
+
content = ImageBase64Content.from_bytes(file_content)
|
135
|
+
agent = Agent[None]( # type:ignore[var-annotated]
|
136
|
+
model=model,
|
137
|
+
system_prompt=system_prompt.format(name=cls.__name__),
|
138
|
+
provider=provider,
|
139
|
+
).to_structured(cls)
|
140
|
+
chat_message = await agent.run(user_prompt, content)
|
141
|
+
return chat_message.content
|
142
|
+
|
143
|
+
@classmethod
|
144
|
+
def from_llm_sync(
|
145
|
+
cls,
|
146
|
+
text: str,
|
147
|
+
model: str = "google-gla:gemini-2.0-flash",
|
148
|
+
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
149
|
+
user_prompt: str = DEFAULT_USER_PROMPT,
|
150
|
+
provider: AgentType = "pydantic_ai",
|
151
|
+
) -> Self:
|
152
|
+
"""Create a schema model from a text snippet using AI.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
text: The text to create a schema from
|
156
|
+
model: The AI model to use for schema extraction
|
157
|
+
system_prompt: The system prompt to use for schema extraction
|
158
|
+
user_prompt: The user prompt to use for schema extraction
|
159
|
+
provider: The provider to use for schema extraction
|
160
|
+
|
161
|
+
Returns:
|
162
|
+
A new schema model class based on the document
|
163
|
+
"""
|
164
|
+
from llmling_agent import Agent
|
165
|
+
|
166
|
+
agent = Agent[None]( # type:ignore[var-annotated]
|
167
|
+
model=model,
|
168
|
+
system_prompt=system_prompt.format(name=cls.__name__),
|
169
|
+
provider=provider,
|
170
|
+
).to_structured(cls)
|
171
|
+
chat_message = anyenv.run_sync(agent.run(user_prompt, text))
|
172
|
+
return chat_message.content
|
173
|
+
|
174
|
+
@classmethod
|
175
|
+
async def from_llm(
|
176
|
+
cls,
|
177
|
+
text: str,
|
178
|
+
model: str = "google-gla:gemini-2.0-flash",
|
179
|
+
system_prompt: str = DEFAULT_SYSTEM_PROMPT,
|
180
|
+
user_prompt: str = DEFAULT_USER_PROMPT,
|
181
|
+
provider: AgentType = "pydantic_ai",
|
182
|
+
) -> Self:
|
183
|
+
"""Create a schema model from a text snippet using AI.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
text: The text to create a schema from
|
187
|
+
model: The AI model to use for schema extraction
|
188
|
+
system_prompt: The system prompt to use for schema extraction
|
189
|
+
user_prompt: The user prompt to use for schema extraction
|
190
|
+
provider: The provider to use for schema extraction
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
A new schema model class based on the document
|
194
|
+
"""
|
195
|
+
from llmling_agent import Agent
|
196
|
+
|
197
|
+
agent = Agent[None]( # type:ignore[var-annotated]
|
198
|
+
model=model,
|
199
|
+
system_prompt=system_prompt.format(name=cls.__name__),
|
200
|
+
provider=provider,
|
201
|
+
).to_structured(cls)
|
202
|
+
chat_message = await agent.run(user_prompt, text)
|
203
|
+
return chat_message.content
|
204
|
+
|
205
|
+
@classmethod
|
206
|
+
def for_class_ctor(cls, target_cls: type) -> type[Schema]:
|
207
|
+
"""Create a schema model from a class constructor.
|
208
|
+
|
209
|
+
Args:
|
210
|
+
target_cls: The class whose constructor to convert
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
A new schema model class based on the constructor parameters
|
214
|
+
"""
|
215
|
+
from schemez.convert import get_ctor_basemodel
|
216
|
+
|
217
|
+
return get_ctor_basemodel(target_cls)
|
218
|
+
|
219
|
+
def model_dump_yaml(self) -> str:
|
220
|
+
"""Dump configuration to YAML string."""
|
221
|
+
import yamling
|
222
|
+
|
223
|
+
return yamling.dump_yaml(self.model_dump(exclude_none=True))
|
224
|
+
|
225
|
+
def save(self, path: StrPath, overwrite: bool = False) -> None:
|
226
|
+
"""Save configuration to a YAML file.
|
227
|
+
|
228
|
+
Args:
|
229
|
+
path: Path to save the configuration to
|
230
|
+
overwrite: Whether to overwrite an existing file
|
231
|
+
|
232
|
+
Raises:
|
233
|
+
OSError: If file cannot be written
|
234
|
+
ValueError: If path is invalid
|
235
|
+
"""
|
236
|
+
yaml_str = self.model_dump_yaml()
|
237
|
+
try:
|
238
|
+
file_path = upath.UPath(path)
|
239
|
+
if file_path.exists() and not overwrite:
|
240
|
+
msg = f"File already exists: {path}"
|
241
|
+
raise FileExistsError(msg) # noqa: TRY301
|
242
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
243
|
+
file_path.write_text(yaml_str)
|
244
|
+
except Exception as exc:
|
245
|
+
msg = f"Failed to save configuration to {path}"
|
246
|
+
raise ValueError(msg) from exc
|
File without changes
|
@@ -0,0 +1,120 @@
|
|
1
|
+
"""Models for schema fields and definitions."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
from typing import Annotated, Any, Literal
|
6
|
+
|
7
|
+
from pydantic import BaseModel, Field, create_model
|
8
|
+
|
9
|
+
from schemez import Schema, helpers
|
10
|
+
|
11
|
+
|
12
|
+
class SchemaField(Schema):
|
13
|
+
"""Field definition for inline response types.
|
14
|
+
|
15
|
+
Defines a single field in an inline response definition, including:
|
16
|
+
- Data type specification
|
17
|
+
- Optional description
|
18
|
+
- Validation constraints
|
19
|
+
|
20
|
+
Used by InlineSchemaDef to structure response fields.
|
21
|
+
"""
|
22
|
+
|
23
|
+
type: str
|
24
|
+
"""Data type of the response field"""
|
25
|
+
|
26
|
+
description: str | None = None
|
27
|
+
"""Optional description of what this field represents"""
|
28
|
+
|
29
|
+
constraints: dict[str, Any] = Field(default_factory=dict)
|
30
|
+
"""Optional validation constraints for the field"""
|
31
|
+
|
32
|
+
|
33
|
+
class BaseSchemaDef(Schema):
|
34
|
+
"""Base class for response definitions."""
|
35
|
+
|
36
|
+
type: str = Field(init=False)
|
37
|
+
|
38
|
+
description: str | None = None
|
39
|
+
"""A description for this response definition."""
|
40
|
+
|
41
|
+
|
42
|
+
class InlineSchemaDef(BaseSchemaDef):
|
43
|
+
"""Inline definition of schema.
|
44
|
+
|
45
|
+
Allows defining response types directly in the configuration using:
|
46
|
+
- Field definitions with types and descriptions
|
47
|
+
- Optional validation constraints
|
48
|
+
- Custom field descriptions
|
49
|
+
|
50
|
+
Example:
|
51
|
+
schemas:
|
52
|
+
BasicResult:
|
53
|
+
type: inline
|
54
|
+
fields:
|
55
|
+
success: {type: bool, description: "Operation success"}
|
56
|
+
message: {type: str, description: "Result details"}
|
57
|
+
"""
|
58
|
+
|
59
|
+
type: Literal["inline"] = Field("inline", init=False)
|
60
|
+
"""Inline response definition."""
|
61
|
+
|
62
|
+
fields: dict[str, SchemaField]
|
63
|
+
"""A dictionary containing all fields."""
|
64
|
+
|
65
|
+
def get_schema(self) -> type[Schema]: # type: ignore
|
66
|
+
"""Create Pydantic model from inline definition."""
|
67
|
+
fields = {}
|
68
|
+
for name, field in self.fields.items():
|
69
|
+
python_type = helpers.resolve_type_string(field.type)
|
70
|
+
if not python_type:
|
71
|
+
msg = f"Unsupported field type: {field.type}"
|
72
|
+
raise ValueError(msg)
|
73
|
+
|
74
|
+
field_info = Field(description=field.description, **(field.constraints))
|
75
|
+
fields[name] = (python_type, field_info)
|
76
|
+
|
77
|
+
cls_name = self.description or "ResponseType"
|
78
|
+
return create_model(cls_name, **fields, __base__=Schema, __doc__=self.description) # type: ignore[call-overload]
|
79
|
+
|
80
|
+
|
81
|
+
class ImportedSchemaDef(BaseSchemaDef):
|
82
|
+
"""Response definition that imports an existing Pydantic model.
|
83
|
+
|
84
|
+
Allows using externally defined Pydantic models as response types.
|
85
|
+
Benefits:
|
86
|
+
- Reuse existing model definitions
|
87
|
+
- Full Python type support
|
88
|
+
- Complex validation logic
|
89
|
+
- IDE support for imported types
|
90
|
+
|
91
|
+
Example:
|
92
|
+
responses:
|
93
|
+
AnalysisResult:
|
94
|
+
type: import
|
95
|
+
import_path: myapp.models.AnalysisResult
|
96
|
+
"""
|
97
|
+
|
98
|
+
type: Literal["import"] = Field("import", init=False)
|
99
|
+
"""Import-path based response definition."""
|
100
|
+
|
101
|
+
import_path: str
|
102
|
+
"""The path to the pydantic model to use as the response type."""
|
103
|
+
|
104
|
+
# mypy is confused about "type"
|
105
|
+
# TODO: convert BaseModel to Schema?
|
106
|
+
def get_schema(self) -> type[BaseModel]: # type: ignore
|
107
|
+
"""Import and return the model class."""
|
108
|
+
try:
|
109
|
+
model_class = helpers.import_class(self.import_path)
|
110
|
+
if not issubclass(model_class, BaseModel):
|
111
|
+
msg = f"{self.import_path} must be a Pydantic model"
|
112
|
+
raise TypeError(msg) # noqa: TRY301
|
113
|
+
except Exception as e:
|
114
|
+
msg = f"Failed to import response type {self.import_path}"
|
115
|
+
raise ValueError(msg) from e
|
116
|
+
else:
|
117
|
+
return model_class
|
118
|
+
|
119
|
+
|
120
|
+
SchemaDef = Annotated[InlineSchemaDef | ImportedSchemaDef, Field(discriminator="type")]
|
@@ -1,35 +0,0 @@
|
|
1
|
-
"""Helpers for BaseModels."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import os
|
6
|
-
|
7
|
-
from pydantic import BaseModel
|
8
|
-
|
9
|
-
|
10
|
-
StrPath = str | os.PathLike[str]
|
11
|
-
|
12
|
-
|
13
|
-
def merge_models[T: BaseModel](base: T, overlay: T) -> T:
|
14
|
-
"""Deep merge two Pydantic models."""
|
15
|
-
if not isinstance(overlay, type(base)):
|
16
|
-
msg = f"Cannot merge different types: {type(base)} and {type(overlay)}"
|
17
|
-
raise TypeError(msg)
|
18
|
-
|
19
|
-
merged_data = base.model_dump()
|
20
|
-
overlay_data = overlay.model_dump(exclude_none=True)
|
21
|
-
for field_name, field_value in overlay_data.items():
|
22
|
-
base_value = merged_data.get(field_name)
|
23
|
-
|
24
|
-
match (base_value, field_value):
|
25
|
-
case (list(), list()):
|
26
|
-
merged_data[field_name] = [
|
27
|
-
*base_value,
|
28
|
-
*(item for item in field_value if item not in base_value),
|
29
|
-
]
|
30
|
-
case (dict(), dict()):
|
31
|
-
merged_data[field_name] = base_value | field_value
|
32
|
-
case _:
|
33
|
-
merged_data[field_name] = field_value
|
34
|
-
|
35
|
-
return base.__class__.model_validate(merged_data)
|
@@ -1,102 +0,0 @@
|
|
1
|
-
"""Configuration models for Schemez."""
|
2
|
-
|
3
|
-
from __future__ import annotations
|
4
|
-
|
5
|
-
import os
|
6
|
-
from typing import TYPE_CHECKING, Any, Self
|
7
|
-
|
8
|
-
from pydantic import BaseModel, ConfigDict
|
9
|
-
import upath
|
10
|
-
|
11
|
-
|
12
|
-
if TYPE_CHECKING:
|
13
|
-
from collections.abc import Callable
|
14
|
-
|
15
|
-
|
16
|
-
StrPath = str | os.PathLike[str]
|
17
|
-
|
18
|
-
|
19
|
-
class Schema(BaseModel):
|
20
|
-
"""Base class configuration models.
|
21
|
-
|
22
|
-
Provides:
|
23
|
-
- Common Pydantic settings
|
24
|
-
- YAML serialization
|
25
|
-
- Basic merge functionality
|
26
|
-
"""
|
27
|
-
|
28
|
-
model_config = ConfigDict(extra="forbid", use_attribute_docstrings=True)
|
29
|
-
|
30
|
-
def merge(self, other: Self) -> Self:
|
31
|
-
"""Merge with another instance by overlaying its non-None values."""
|
32
|
-
from schemez.helpers import merge_models
|
33
|
-
|
34
|
-
return merge_models(self, other)
|
35
|
-
|
36
|
-
@classmethod
|
37
|
-
def from_yaml(cls, content: str, inherit_path: StrPath | None = None) -> Self:
|
38
|
-
"""Create from YAML string."""
|
39
|
-
import yamling
|
40
|
-
|
41
|
-
data = yamling.load_yaml(content, resolve_inherit=inherit_path or False)
|
42
|
-
return cls.model_validate(data)
|
43
|
-
|
44
|
-
@classmethod
|
45
|
-
def for_function(
|
46
|
-
cls, func: Callable[..., Any], *, name: str | None = None
|
47
|
-
) -> type[Schema]:
|
48
|
-
"""Create a schema model from a function's signature.
|
49
|
-
|
50
|
-
Args:
|
51
|
-
func: The function to create a schema from
|
52
|
-
name: Optional name for the model
|
53
|
-
|
54
|
-
Returns:
|
55
|
-
A new schema model class based on the function parameters
|
56
|
-
"""
|
57
|
-
from schemez.convert import get_function_model
|
58
|
-
|
59
|
-
return get_function_model(func, name=name)
|
60
|
-
|
61
|
-
@classmethod
|
62
|
-
def for_class_ctor(cls, target_cls: type) -> type[Schema]:
|
63
|
-
"""Create a schema model from a class constructor.
|
64
|
-
|
65
|
-
Args:
|
66
|
-
target_cls: The class whose constructor to convert
|
67
|
-
|
68
|
-
Returns:
|
69
|
-
A new schema model class based on the constructor parameters
|
70
|
-
"""
|
71
|
-
from schemez.convert import get_ctor_basemodel
|
72
|
-
|
73
|
-
return get_ctor_basemodel(target_cls)
|
74
|
-
|
75
|
-
def model_dump_yaml(self) -> str:
|
76
|
-
"""Dump configuration to YAML string."""
|
77
|
-
import yamling
|
78
|
-
|
79
|
-
return yamling.dump_yaml(self.model_dump(exclude_none=True))
|
80
|
-
|
81
|
-
def save(self, path: StrPath, overwrite: bool = False) -> None:
|
82
|
-
"""Save configuration to a YAML file.
|
83
|
-
|
84
|
-
Args:
|
85
|
-
path: Path to save the configuration to
|
86
|
-
overwrite: Whether to overwrite an existing file
|
87
|
-
|
88
|
-
Raises:
|
89
|
-
OSError: If file cannot be written
|
90
|
-
ValueError: If path is invalid
|
91
|
-
"""
|
92
|
-
yaml_str = self.model_dump_yaml()
|
93
|
-
try:
|
94
|
-
file_path = upath.UPath(path)
|
95
|
-
if file_path.exists() and not overwrite:
|
96
|
-
msg = f"File already exists: {path}"
|
97
|
-
raise FileExistsError(msg) # noqa: TRY301
|
98
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
99
|
-
file_path.write_text(yaml_str)
|
100
|
-
except Exception as exc:
|
101
|
-
msg = f"Failed to save configuration to {path}"
|
102
|
-
raise ValueError(msg) from exc
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|