speedy-utils 1.1.27__py3-none-any.whl → 1.1.29__py3-none-any.whl
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.
- llm_utils/__init__.py +16 -4
- llm_utils/chat_format/__init__.py +10 -10
- llm_utils/chat_format/display.py +33 -21
- llm_utils/chat_format/transform.py +17 -19
- llm_utils/chat_format/utils.py +6 -4
- llm_utils/group_messages.py +17 -14
- llm_utils/lm/__init__.py +6 -5
- llm_utils/lm/async_lm/__init__.py +1 -0
- llm_utils/lm/async_lm/_utils.py +10 -9
- llm_utils/lm/async_lm/async_llm_task.py +141 -137
- llm_utils/lm/async_lm/async_lm.py +48 -42
- llm_utils/lm/async_lm/async_lm_base.py +59 -60
- llm_utils/lm/async_lm/lm_specific.py +4 -3
- llm_utils/lm/base_prompt_builder.py +93 -70
- llm_utils/lm/llm.py +126 -108
- llm_utils/lm/llm_signature.py +4 -2
- llm_utils/lm/lm_base.py +72 -73
- llm_utils/lm/mixins.py +102 -62
- llm_utils/lm/openai_memoize.py +124 -87
- llm_utils/lm/signature.py +105 -92
- llm_utils/lm/utils.py +42 -23
- llm_utils/scripts/vllm_load_balancer.py +23 -30
- llm_utils/scripts/vllm_serve.py +8 -7
- llm_utils/vector_cache/__init__.py +9 -3
- llm_utils/vector_cache/cli.py +1 -1
- llm_utils/vector_cache/core.py +59 -63
- llm_utils/vector_cache/types.py +7 -5
- llm_utils/vector_cache/utils.py +12 -8
- speedy_utils/__imports.py +244 -0
- speedy_utils/__init__.py +90 -194
- speedy_utils/all.py +125 -227
- speedy_utils/common/clock.py +37 -42
- speedy_utils/common/function_decorator.py +6 -12
- speedy_utils/common/logger.py +43 -52
- speedy_utils/common/notebook_utils.py +13 -21
- speedy_utils/common/patcher.py +21 -17
- speedy_utils/common/report_manager.py +42 -44
- speedy_utils/common/utils_cache.py +152 -169
- speedy_utils/common/utils_io.py +137 -103
- speedy_utils/common/utils_misc.py +15 -21
- speedy_utils/common/utils_print.py +22 -28
- speedy_utils/multi_worker/process.py +66 -79
- speedy_utils/multi_worker/thread.py +78 -155
- speedy_utils/scripts/mpython.py +38 -36
- speedy_utils/scripts/openapi_client_codegen.py +10 -10
- {speedy_utils-1.1.27.dist-info → speedy_utils-1.1.29.dist-info}/METADATA +1 -1
- speedy_utils-1.1.29.dist-info/RECORD +57 -0
- vision_utils/README.md +202 -0
- vision_utils/__init__.py +4 -0
- vision_utils/io_utils.py +735 -0
- vision_utils/plot.py +345 -0
- speedy_utils-1.1.27.dist-info/RECORD +0 -52
- {speedy_utils-1.1.27.dist-info → speedy_utils-1.1.29.dist-info}/WHEEL +0 -0
- {speedy_utils-1.1.27.dist-info → speedy_utils-1.1.29.dist-info}/entry_points.txt +0 -0
llm_utils/lm/openai_memoize.py
CHANGED
|
@@ -1,91 +1,128 @@
|
|
|
1
|
-
from
|
|
2
|
-
from typing import
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
3
|
|
|
4
4
|
from speedy_utils.common.utils_cache import memoize
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
OpenAI
|
|
20
|
-
|
|
21
|
-
the
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from openai import AsyncOpenAI, OpenAI
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_mopenai_class():
|
|
12
|
+
"""Lazily create MOpenAI class to avoid importing openai at module load."""
|
|
13
|
+
from openai import OpenAI
|
|
14
|
+
|
|
15
|
+
class MOpenAI(OpenAI):
|
|
16
|
+
"""
|
|
17
|
+
MOpenAI(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
Subclass of OpenAI that transparently memoizes the instance's `post` method.
|
|
20
|
+
|
|
21
|
+
This class forwards all constructor arguments to the OpenAI base class and then
|
|
22
|
+
replaces the instance's `post` method with a memoized wrapper:
|
|
23
|
+
|
|
24
|
+
Behavior
|
|
25
|
+
- The memoized `post` caches responses based on the arguments with which it is
|
|
26
|
+
invoked, preventing repeated identical requests from invoking the underlying
|
|
27
|
+
OpenAI API repeatedly.
|
|
28
|
+
- Because `post` is replaced on the instance, the cache is by-default tied to
|
|
29
|
+
the MOpenAI instance (per-instance cache).
|
|
30
|
+
- Any initialization arguments are passed unchanged to OpenAI.__init__.
|
|
31
|
+
|
|
32
|
+
Notes and cautions
|
|
33
|
+
- The exact semantics of caching (cache key construction, expiry, max size,
|
|
34
|
+
persistence) depend on the implementation of `memoize`. Ensure that the
|
|
35
|
+
provided `memoize` supports the desired behavior (e.g., hashing of mutable
|
|
36
|
+
inputs, thread-safety, TTL, cache invalidation).
|
|
37
|
+
- If the original `post` method has important side effects or relies on
|
|
38
|
+
non-deterministic behavior, memoization may change program behavior.
|
|
39
|
+
- If you need a shared cache across instances, or more advanced cache controls,
|
|
40
|
+
modify `memoize` or wrap at a class/static level instead of assigning to the
|
|
41
|
+
bound method.
|
|
42
|
+
- Type information is now fully preserved by the memoize decorator, eliminating
|
|
43
|
+
the need for type casting.
|
|
44
|
+
|
|
45
|
+
Example
|
|
46
|
+
m = MOpenAI(api_key="...", model="gpt-4")
|
|
47
|
+
r1 = m.post("Hello") # executes API call and caches result
|
|
48
|
+
r2 = m.post("Hello") # returns cached result (no API call)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, *args, cache=True, **kwargs):
|
|
52
|
+
super().__init__(*args, **kwargs)
|
|
53
|
+
self._orig_post = self.post
|
|
54
|
+
if cache:
|
|
55
|
+
self.set_cache(cache)
|
|
56
|
+
|
|
57
|
+
def set_cache(self, cache: bool) -> None:
|
|
58
|
+
"""Enable or disable caching of the post method."""
|
|
59
|
+
if cache and self.post == self._orig_post:
|
|
60
|
+
self.post = memoize(self._orig_post) # type: ignore
|
|
61
|
+
elif not cache and self.post != self._orig_post:
|
|
62
|
+
self.post = self._orig_post
|
|
63
|
+
|
|
64
|
+
return MOpenAI
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _get_masyncopenai_class():
|
|
68
|
+
"""Lazily create MAsyncOpenAI class to avoid importing openai at module load."""
|
|
69
|
+
from openai import AsyncOpenAI
|
|
70
|
+
|
|
71
|
+
class MAsyncOpenAI(AsyncOpenAI):
|
|
72
|
+
"""
|
|
73
|
+
MAsyncOpenAI(*args, **kwargs)
|
|
74
|
+
|
|
75
|
+
Async subclass of AsyncOpenAI that transparently memoizes the instance's `post` method.
|
|
76
|
+
|
|
77
|
+
This class forwards all constructor arguments to the AsyncOpenAI base class and then
|
|
78
|
+
replaces the instance's `post` method with a memoized wrapper:
|
|
79
|
+
|
|
80
|
+
Behavior
|
|
81
|
+
- The memoized `post` caches responses based on the arguments with which it is
|
|
82
|
+
invoked, preventing repeated identical requests from invoking the underlying
|
|
83
|
+
OpenAI API repeatedly.
|
|
84
|
+
- Because `post` is replaced on the instance, the cache is by-default tied to
|
|
85
|
+
the MAsyncOpenAI instance (per-instance cache).
|
|
86
|
+
- Any initialization arguments are passed unchanged to AsyncOpenAI.__init__.
|
|
87
|
+
|
|
88
|
+
Example
|
|
89
|
+
m = MAsyncOpenAI(api_key="...", model="gpt-4")
|
|
90
|
+
r1 = await m.post("Hello") # executes API call and caches result
|
|
91
|
+
r2 = await m.post("Hello") # returns cached result (no API call)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def __init__(self, *args, cache=True, **kwargs):
|
|
95
|
+
super().__init__(*args, **kwargs)
|
|
96
|
+
self._orig_post = self.post
|
|
97
|
+
if cache:
|
|
98
|
+
self.set_cache(cache)
|
|
99
|
+
|
|
100
|
+
def set_cache(self, cache: bool) -> None:
|
|
101
|
+
"""Enable or disable caching of the post method."""
|
|
102
|
+
if cache and self.post == self._orig_post:
|
|
103
|
+
self.post = memoize(self._orig_post) # type: ignore
|
|
104
|
+
elif not cache and self.post != self._orig_post:
|
|
105
|
+
self.post = self._orig_post
|
|
106
|
+
|
|
107
|
+
return MAsyncOpenAI
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# Cache the classes so they're only created once
|
|
111
|
+
_MOpenAI_class = None
|
|
112
|
+
_MAsyncOpenAI_class = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def MOpenAI(*args, **kwargs):
|
|
116
|
+
"""Factory function for MOpenAI that lazily loads openai module."""
|
|
117
|
+
global _MOpenAI_class
|
|
118
|
+
if _MOpenAI_class is None:
|
|
119
|
+
_MOpenAI_class = _get_mopenai_class()
|
|
120
|
+
return _MOpenAI_class(*args, **kwargs)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def MAsyncOpenAI(*args, **kwargs):
|
|
124
|
+
"""Factory function for MAsyncOpenAI that lazily loads openai module."""
|
|
125
|
+
global _MAsyncOpenAI_class
|
|
126
|
+
if _MAsyncOpenAI_class is None:
|
|
127
|
+
_MAsyncOpenAI_class = _get_masyncopenai_class()
|
|
128
|
+
return _MAsyncOpenAI_class(*args, **kwargs)
|
llm_utils/lm/signature.py
CHANGED
|
@@ -5,14 +5,25 @@ This module provides a declarative way to define LLM input/output schemas
|
|
|
5
5
|
with field descriptions and type annotations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any, Dict, List, Type, get_type_hints, Annotated, get_origin, get_args, cast
|
|
9
|
-
from pydantic import BaseModel, Field
|
|
10
8
|
import inspect
|
|
9
|
+
from typing import (
|
|
10
|
+
Annotated,
|
|
11
|
+
Any,
|
|
12
|
+
Dict,
|
|
13
|
+
List,
|
|
14
|
+
Type,
|
|
15
|
+
cast,
|
|
16
|
+
get_args,
|
|
17
|
+
get_origin,
|
|
18
|
+
get_type_hints,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field
|
|
11
22
|
|
|
12
23
|
|
|
13
24
|
class _FieldProxy:
|
|
14
25
|
"""Proxy that stores field information while appearing type-compatible."""
|
|
15
|
-
|
|
26
|
+
|
|
16
27
|
def __init__(self, field_type: str, desc: str = "", **kwargs):
|
|
17
28
|
self.field_type = field_type # 'input' or 'output'
|
|
18
29
|
self.desc = desc
|
|
@@ -21,12 +32,12 @@ class _FieldProxy:
|
|
|
21
32
|
|
|
22
33
|
def InputField(desc: str = "", **kwargs) -> Any:
|
|
23
34
|
"""Create an input field descriptor."""
|
|
24
|
-
return cast(Any, _FieldProxy(
|
|
35
|
+
return cast(Any, _FieldProxy("input", desc=desc, **kwargs))
|
|
25
36
|
|
|
26
37
|
|
|
27
38
|
def OutputField(desc: str = "", **kwargs) -> Any:
|
|
28
|
-
"""Create an output field descriptor."""
|
|
29
|
-
return cast(Any, _FieldProxy(
|
|
39
|
+
"""Create an output field descriptor."""
|
|
40
|
+
return cast(Any, _FieldProxy("output", desc=desc, **kwargs))
|
|
30
41
|
|
|
31
42
|
|
|
32
43
|
# Type aliases for cleaner syntax
|
|
@@ -42,19 +53,19 @@ def Output(desc: str = "", **kwargs) -> Any:
|
|
|
42
53
|
|
|
43
54
|
class SignatureMeta(type):
|
|
44
55
|
"""Metaclass for Signature that processes field annotations."""
|
|
45
|
-
|
|
56
|
+
|
|
46
57
|
def __new__(cls, name, bases, namespace, **kwargs):
|
|
47
58
|
# Get type hints for this class
|
|
48
|
-
annotations = namespace.get(
|
|
49
|
-
|
|
59
|
+
annotations = namespace.get("__annotations__", {})
|
|
60
|
+
|
|
50
61
|
# Store field information
|
|
51
62
|
input_fields = {}
|
|
52
63
|
output_fields = {}
|
|
53
|
-
|
|
64
|
+
|
|
54
65
|
for field_name, field_type in annotations.items():
|
|
55
66
|
field_value = namespace.get(field_name)
|
|
56
67
|
field_desc = None
|
|
57
|
-
|
|
68
|
+
|
|
58
69
|
# Handle Annotated[Type, Field(...)] syntax using get_origin/get_args
|
|
59
70
|
if get_origin(field_type) is Annotated:
|
|
60
71
|
# Extract args from Annotated type
|
|
@@ -67,163 +78,163 @@ class SignatureMeta(type):
|
|
|
67
78
|
if isinstance(metadata, _FieldProxy):
|
|
68
79
|
field_desc = metadata
|
|
69
80
|
break
|
|
70
|
-
|
|
81
|
+
|
|
71
82
|
# Handle old syntax with direct assignment
|
|
72
83
|
if field_desc is None and isinstance(field_value, _FieldProxy):
|
|
73
84
|
field_desc = field_value
|
|
74
|
-
|
|
85
|
+
|
|
75
86
|
# Store field information
|
|
76
|
-
if field_desc and field_desc.field_type ==
|
|
87
|
+
if field_desc and field_desc.field_type == "input":
|
|
77
88
|
input_fields[field_name] = {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
**field_desc.kwargs
|
|
89
|
+
"type": field_type,
|
|
90
|
+
"desc": field_desc.desc,
|
|
91
|
+
**field_desc.kwargs,
|
|
81
92
|
}
|
|
82
|
-
elif field_desc and field_desc.field_type ==
|
|
93
|
+
elif field_desc and field_desc.field_type == "output":
|
|
83
94
|
output_fields[field_name] = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
**field_desc.kwargs
|
|
95
|
+
"type": field_type,
|
|
96
|
+
"desc": field_desc.desc,
|
|
97
|
+
**field_desc.kwargs,
|
|
87
98
|
}
|
|
88
|
-
|
|
99
|
+
|
|
89
100
|
# Store in class attributes
|
|
90
|
-
namespace[
|
|
91
|
-
namespace[
|
|
92
|
-
|
|
101
|
+
namespace["_input_fields"] = input_fields
|
|
102
|
+
namespace["_output_fields"] = output_fields
|
|
103
|
+
|
|
93
104
|
return super().__new__(cls, name, bases, namespace)
|
|
94
105
|
|
|
95
106
|
|
|
96
107
|
class Signature(metaclass=SignatureMeta):
|
|
97
108
|
"""Base class for defining LLM signatures with input and output fields."""
|
|
98
|
-
|
|
99
|
-
_input_fields:
|
|
100
|
-
_output_fields:
|
|
101
|
-
|
|
109
|
+
|
|
110
|
+
_input_fields: dict[str, dict[str, Any]] = {}
|
|
111
|
+
_output_fields: dict[str, dict[str, Any]] = {}
|
|
112
|
+
|
|
102
113
|
def __init__(self, **kwargs):
|
|
103
114
|
"""Initialize signature with field values."""
|
|
104
115
|
for field_name, value in kwargs.items():
|
|
105
116
|
setattr(self, field_name, value)
|
|
106
|
-
|
|
117
|
+
|
|
107
118
|
@classmethod
|
|
108
119
|
def get_instruction(cls) -> str:
|
|
109
120
|
"""Generate instruction text from docstring and field descriptions."""
|
|
110
121
|
instruction = cls.__doc__ or "Complete the following task."
|
|
111
122
|
instruction = instruction.strip()
|
|
112
|
-
|
|
123
|
+
|
|
113
124
|
# Add input field descriptions
|
|
114
125
|
if cls._input_fields:
|
|
115
126
|
instruction += "\n\n**Input Fields:**\n"
|
|
116
127
|
for field_name, field_info in cls._input_fields.items():
|
|
117
|
-
desc = field_info.get(
|
|
118
|
-
field_type = field_info[
|
|
119
|
-
type_str = getattr(field_type,
|
|
128
|
+
desc = field_info.get("desc", "")
|
|
129
|
+
field_type = field_info["type"]
|
|
130
|
+
type_str = getattr(field_type, "__name__", str(field_type))
|
|
120
131
|
instruction += f"- {field_name} ({type_str}): {desc}\n"
|
|
121
|
-
|
|
132
|
+
|
|
122
133
|
# Add output field descriptions
|
|
123
134
|
if cls._output_fields:
|
|
124
135
|
instruction += "\n**Output Fields:**\n"
|
|
125
136
|
for field_name, field_info in cls._output_fields.items():
|
|
126
|
-
desc = field_info.get(
|
|
127
|
-
field_type = field_info[
|
|
128
|
-
type_str = getattr(field_type,
|
|
137
|
+
desc = field_info.get("desc", "")
|
|
138
|
+
field_type = field_info["type"]
|
|
139
|
+
type_str = getattr(field_type, "__name__", str(field_type))
|
|
129
140
|
instruction += f"- {field_name} ({type_str}): {desc}\n"
|
|
130
|
-
|
|
141
|
+
|
|
131
142
|
return instruction
|
|
132
|
-
|
|
143
|
+
|
|
133
144
|
@classmethod
|
|
134
|
-
def get_input_model(cls) ->
|
|
145
|
+
def get_input_model(cls) -> type[BaseModel]:
|
|
135
146
|
"""Generate Pydantic input model from input fields."""
|
|
136
147
|
if not cls._input_fields:
|
|
137
|
-
raise ValueError(
|
|
138
|
-
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"Signature {cls.__name__} must have at least one input field"
|
|
150
|
+
)
|
|
151
|
+
|
|
139
152
|
fields = {}
|
|
140
153
|
annotations = {}
|
|
141
|
-
|
|
154
|
+
|
|
142
155
|
for field_name, field_info in cls._input_fields.items():
|
|
143
|
-
field_type = field_info[
|
|
144
|
-
desc = field_info.get(
|
|
145
|
-
|
|
156
|
+
field_type = field_info["type"]
|
|
157
|
+
desc = field_info.get("desc", "")
|
|
158
|
+
|
|
146
159
|
# Create Pydantic field
|
|
147
|
-
field_kwargs = {
|
|
148
|
-
|
|
160
|
+
field_kwargs = {
|
|
161
|
+
k: v for k, v in field_info.items() if k not in ["type", "desc"]
|
|
162
|
+
}
|
|
149
163
|
if desc:
|
|
150
|
-
field_kwargs[
|
|
151
|
-
|
|
164
|
+
field_kwargs["description"] = desc
|
|
165
|
+
|
|
152
166
|
fields[field_name] = Field(**field_kwargs) if field_kwargs else Field()
|
|
153
167
|
annotations[field_name] = field_type
|
|
154
|
-
|
|
168
|
+
|
|
155
169
|
# Create dynamic Pydantic model
|
|
156
170
|
input_model = type(
|
|
157
171
|
f"{cls.__name__}Input",
|
|
158
172
|
(BaseModel,),
|
|
159
|
-
{
|
|
160
|
-
'__annotations__': annotations,
|
|
161
|
-
**fields
|
|
162
|
-
}
|
|
173
|
+
{"__annotations__": annotations, **fields},
|
|
163
174
|
)
|
|
164
|
-
|
|
175
|
+
|
|
165
176
|
return input_model
|
|
166
|
-
|
|
177
|
+
|
|
167
178
|
@classmethod
|
|
168
|
-
def get_output_model(cls) ->
|
|
179
|
+
def get_output_model(cls) -> type[BaseModel]:
|
|
169
180
|
"""Generate Pydantic output model from output fields."""
|
|
170
181
|
if not cls._output_fields:
|
|
171
|
-
raise ValueError(
|
|
172
|
-
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"Signature {cls.__name__} must have at least one output field"
|
|
184
|
+
)
|
|
185
|
+
|
|
173
186
|
fields = {}
|
|
174
187
|
annotations = {}
|
|
175
|
-
|
|
188
|
+
|
|
176
189
|
for field_name, field_info in cls._output_fields.items():
|
|
177
|
-
field_type = field_info[
|
|
178
|
-
desc = field_info.get(
|
|
179
|
-
|
|
190
|
+
field_type = field_info["type"]
|
|
191
|
+
desc = field_info.get("desc", "")
|
|
192
|
+
|
|
180
193
|
# Create Pydantic field
|
|
181
|
-
field_kwargs = {
|
|
182
|
-
|
|
194
|
+
field_kwargs = {
|
|
195
|
+
k: v for k, v in field_info.items() if k not in ["type", "desc"]
|
|
196
|
+
}
|
|
183
197
|
if desc:
|
|
184
|
-
field_kwargs[
|
|
185
|
-
|
|
198
|
+
field_kwargs["description"] = desc
|
|
199
|
+
|
|
186
200
|
fields[field_name] = Field(**field_kwargs) if field_kwargs else Field()
|
|
187
201
|
annotations[field_name] = field_type
|
|
188
|
-
|
|
202
|
+
|
|
189
203
|
# Create dynamic Pydantic model
|
|
190
204
|
output_model = type(
|
|
191
205
|
f"{cls.__name__}Output",
|
|
192
206
|
(BaseModel,),
|
|
193
|
-
{
|
|
194
|
-
'__annotations__': annotations,
|
|
195
|
-
**fields
|
|
196
|
-
}
|
|
207
|
+
{"__annotations__": annotations, **fields},
|
|
197
208
|
)
|
|
198
|
-
|
|
209
|
+
|
|
199
210
|
return output_model
|
|
200
|
-
|
|
211
|
+
|
|
201
212
|
def format_input(self, **kwargs) -> str:
|
|
202
213
|
"""Format input fields as a string."""
|
|
203
214
|
input_data = {}
|
|
204
|
-
|
|
215
|
+
|
|
205
216
|
# Collect input field values
|
|
206
217
|
for field_name in self._input_fields:
|
|
207
218
|
if field_name in kwargs:
|
|
208
219
|
input_data[field_name] = kwargs[field_name]
|
|
209
220
|
elif hasattr(self, field_name):
|
|
210
221
|
input_data[field_name] = getattr(self, field_name)
|
|
211
|
-
|
|
222
|
+
|
|
212
223
|
# Format as key-value pairs
|
|
213
224
|
formatted_lines = []
|
|
214
225
|
for field_name, value in input_data.items():
|
|
215
226
|
field_info = self._input_fields[field_name]
|
|
216
|
-
desc = field_info.get(
|
|
227
|
+
desc = field_info.get("desc", "")
|
|
217
228
|
if desc:
|
|
218
229
|
formatted_lines.append(f"{field_name} ({desc}): {value}")
|
|
219
230
|
else:
|
|
220
231
|
formatted_lines.append(f"{field_name}: {value}")
|
|
221
|
-
|
|
222
|
-
return
|
|
232
|
+
|
|
233
|
+
return "\n".join(formatted_lines)
|
|
223
234
|
|
|
224
235
|
|
|
225
236
|
# Export functions for easier importing
|
|
226
|
-
__all__ = [
|
|
237
|
+
__all__ = ["Signature", "InputField", "OutputField", "Input", "Output"]
|
|
227
238
|
|
|
228
239
|
|
|
229
240
|
# Example usage for testing
|
|
@@ -231,41 +242,43 @@ if __name__ == "__main__":
|
|
|
231
242
|
# Define a signature like DSPy - using Annotated approach
|
|
232
243
|
class FactJudge(Signature):
|
|
233
244
|
"""Judge if the answer is factually correct based on the context."""
|
|
234
|
-
|
|
245
|
+
|
|
235
246
|
context: Annotated[str, Input("Context for the prediction")]
|
|
236
247
|
question: Annotated[str, Input("Question to be answered")]
|
|
237
248
|
answer: Annotated[str, Input("Answer for the question")]
|
|
238
|
-
factually_correct: Annotated[
|
|
239
|
-
|
|
249
|
+
factually_correct: Annotated[
|
|
250
|
+
bool, Output("Is the answer factually correct based on the context?")
|
|
251
|
+
]
|
|
252
|
+
|
|
240
253
|
# Alternative syntax still works but will show type warnings
|
|
241
254
|
class FactJudgeOldSyntax(Signature):
|
|
242
255
|
"""Judge if the answer is factually correct based on the context."""
|
|
243
|
-
|
|
256
|
+
|
|
244
257
|
context: str = InputField(desc="Context for the prediction") # type: ignore
|
|
245
258
|
question: str = InputField(desc="Question to be answered") # type: ignore
|
|
246
259
|
answer: str = InputField(desc="Answer for the question") # type: ignore
|
|
247
260
|
factually_correct: bool = OutputField(desc="Is the answer factually correct based on the context?") # type: ignore
|
|
248
|
-
|
|
261
|
+
|
|
249
262
|
# Test both signatures
|
|
250
263
|
for judge_class in [FactJudge, FactJudgeOldSyntax]:
|
|
251
264
|
print(f"\n=== Testing {judge_class.__name__} ===")
|
|
252
265
|
print("Instruction:")
|
|
253
266
|
print(judge_class.get_instruction())
|
|
254
|
-
|
|
267
|
+
|
|
255
268
|
print("\nInput Model:")
|
|
256
269
|
input_model = judge_class.get_input_model()
|
|
257
270
|
print(input_model.model_json_schema())
|
|
258
|
-
|
|
271
|
+
|
|
259
272
|
print("\nOutput Model:")
|
|
260
273
|
output_model = judge_class.get_output_model()
|
|
261
274
|
print(output_model.model_json_schema())
|
|
262
|
-
|
|
275
|
+
|
|
263
276
|
# Test instance usage
|
|
264
277
|
judge = judge_class()
|
|
265
278
|
input_text = judge.format_input(
|
|
266
279
|
context="The sky is blue during daytime.",
|
|
267
280
|
question="What color is the sky?",
|
|
268
|
-
answer="Blue"
|
|
281
|
+
answer="Blue",
|
|
269
282
|
)
|
|
270
283
|
print("\nFormatted Input:")
|
|
271
|
-
print(input_text)
|
|
284
|
+
print(input_text)
|