langwatch 0.2.18__py3-none-any.whl → 0.3.0__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.
- langwatch/__init__.py +5 -5
- langwatch/__version__.py +1 -1
- langwatch/generated/langwatch_rest_api_client/models/__init__.py +2 -18
- langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2.py +7 -115
- langwatch/generated/langwatch_rest_api_client/models/{post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item_function.py → post_api_scenario_events_body_type_2_messages_item.py} +23 -22
- langwatch/prompts/__init__.py +2 -2
- langwatch/prompts/decorators/prompt_service_tracing.py +6 -4
- langwatch/prompts/local_loader.py +170 -0
- langwatch/prompts/prompt.py +41 -43
- langwatch/prompts/{service.py → prompt_api_service.py} +23 -33
- langwatch/prompts/prompt_facade.py +139 -0
- langwatch/prompts/types/__init__.py +27 -0
- langwatch/prompts/types/prompt_data.py +93 -0
- langwatch/prompts/types/structures.py +37 -0
- langwatch/prompts/types.py +16 -24
- langwatch/utils/initialization.py +8 -2
- langwatch/utils/transformation.py +16 -5
- {langwatch-0.2.18.dist-info → langwatch-0.3.0.dist-info}/METADATA +1 -1
- {langwatch-0.2.18.dist-info → langwatch-0.3.0.dist-info}/RECORD +20 -22
- langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_0.py +0 -88
- langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_1.py +0 -88
- langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_2.py +0 -120
- langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item.py +0 -87
- langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_3.py +0 -88
- langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_4.py +0 -85
- langwatch/prompts/formatter.py +0 -31
- {langwatch-0.2.18.dist-info → langwatch-0.3.0.dist-info}/WHEEL +0 -0
langwatch/__init__.py
CHANGED
|
@@ -19,11 +19,11 @@ if TYPE_CHECKING:
|
|
|
19
19
|
import langwatch.dataset as dataset
|
|
20
20
|
import langwatch.dspy as dspy
|
|
21
21
|
import langwatch.langchain as langchain
|
|
22
|
-
from .prompts.
|
|
22
|
+
from .prompts.prompt_facade import PromptsFacade
|
|
23
23
|
|
|
24
24
|
# Type hint for the prompts service specifically
|
|
25
25
|
# required to get the instance typing correct
|
|
26
|
-
prompts:
|
|
26
|
+
prompts: PromptsFacade
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@module_property
|
|
@@ -61,10 +61,10 @@ def __getattr__(name: str):
|
|
|
61
61
|
globals()[name] = module
|
|
62
62
|
return module
|
|
63
63
|
elif name == "prompts":
|
|
64
|
-
# Special-case: expose a
|
|
65
|
-
from .prompts.
|
|
64
|
+
# Special-case: expose a PromptsFacade instance at langwatch.prompts
|
|
65
|
+
from .prompts.prompt_facade import PromptsFacade
|
|
66
66
|
|
|
67
|
-
svc =
|
|
67
|
+
svc = PromptsFacade.from_global()
|
|
68
68
|
globals()[name] = svc # Cache for subsequent access
|
|
69
69
|
return svc
|
|
70
70
|
|
langwatch/__version__.py
CHANGED
|
@@ -486,17 +486,7 @@ from .post_api_scenario_events_body_type_1_results_type_0_verdict import (
|
|
|
486
486
|
)
|
|
487
487
|
from .post_api_scenario_events_body_type_1_status import PostApiScenarioEventsBodyType1Status
|
|
488
488
|
from .post_api_scenario_events_body_type_2 import PostApiScenarioEventsBodyType2
|
|
489
|
-
from .
|
|
490
|
-
from .post_api_scenario_events_body_type_2_messages_item_type_1 import PostApiScenarioEventsBodyType2MessagesItemType1
|
|
491
|
-
from .post_api_scenario_events_body_type_2_messages_item_type_2 import PostApiScenarioEventsBodyType2MessagesItemType2
|
|
492
|
-
from .post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item import (
|
|
493
|
-
PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItem,
|
|
494
|
-
)
|
|
495
|
-
from .post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item_function import (
|
|
496
|
-
PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItemFunction,
|
|
497
|
-
)
|
|
498
|
-
from .post_api_scenario_events_body_type_2_messages_item_type_3 import PostApiScenarioEventsBodyType2MessagesItemType3
|
|
499
|
-
from .post_api_scenario_events_body_type_2_messages_item_type_4 import PostApiScenarioEventsBodyType2MessagesItemType4
|
|
489
|
+
from .post_api_scenario_events_body_type_2_messages_item import PostApiScenarioEventsBodyType2MessagesItem
|
|
500
490
|
from .post_api_scenario_events_response_201 import PostApiScenarioEventsResponse201
|
|
501
491
|
from .post_api_scenario_events_response_400 import PostApiScenarioEventsResponse400
|
|
502
492
|
from .post_api_scenario_events_response_401 import PostApiScenarioEventsResponse401
|
|
@@ -853,13 +843,7 @@ __all__ = (
|
|
|
853
843
|
"PostApiScenarioEventsBodyType1ResultsType0Verdict",
|
|
854
844
|
"PostApiScenarioEventsBodyType1Status",
|
|
855
845
|
"PostApiScenarioEventsBodyType2",
|
|
856
|
-
"
|
|
857
|
-
"PostApiScenarioEventsBodyType2MessagesItemType1",
|
|
858
|
-
"PostApiScenarioEventsBodyType2MessagesItemType2",
|
|
859
|
-
"PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItem",
|
|
860
|
-
"PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItemFunction",
|
|
861
|
-
"PostApiScenarioEventsBodyType2MessagesItemType3",
|
|
862
|
-
"PostApiScenarioEventsBodyType2MessagesItemType4",
|
|
846
|
+
"PostApiScenarioEventsBodyType2MessagesItem",
|
|
863
847
|
"PostApiScenarioEventsResponse201",
|
|
864
848
|
"PostApiScenarioEventsResponse400",
|
|
865
849
|
"PostApiScenarioEventsResponse401",
|
langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2.py
CHANGED
|
@@ -7,21 +7,7 @@ from attrs import field as _attrs_field
|
|
|
7
7
|
from ..types import UNSET, Unset
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
|
-
from ..models.
|
|
11
|
-
PostApiScenarioEventsBodyType2MessagesItemType0,
|
|
12
|
-
)
|
|
13
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_1 import (
|
|
14
|
-
PostApiScenarioEventsBodyType2MessagesItemType1,
|
|
15
|
-
)
|
|
16
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_2 import (
|
|
17
|
-
PostApiScenarioEventsBodyType2MessagesItemType2,
|
|
18
|
-
)
|
|
19
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_3 import (
|
|
20
|
-
PostApiScenarioEventsBodyType2MessagesItemType3,
|
|
21
|
-
)
|
|
22
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_4 import (
|
|
23
|
-
PostApiScenarioEventsBodyType2MessagesItemType4,
|
|
24
|
-
)
|
|
10
|
+
from ..models.post_api_scenario_events_body_type_2_messages_item import PostApiScenarioEventsBodyType2MessagesItem
|
|
25
11
|
|
|
26
12
|
|
|
27
13
|
T = TypeVar("T", bound="PostApiScenarioEventsBodyType2")
|
|
@@ -33,9 +19,7 @@ class PostApiScenarioEventsBodyType2:
|
|
|
33
19
|
Attributes:
|
|
34
20
|
type_ (Literal['SCENARIO_MESSAGE_SNAPSHOT']):
|
|
35
21
|
timestamp (float):
|
|
36
|
-
messages (list[
|
|
37
|
-
'PostApiScenarioEventsBodyType2MessagesItemType1', 'PostApiScenarioEventsBodyType2MessagesItemType2',
|
|
38
|
-
'PostApiScenarioEventsBodyType2MessagesItemType3', 'PostApiScenarioEventsBodyType2MessagesItemType4']]):
|
|
22
|
+
messages (list['PostApiScenarioEventsBodyType2MessagesItem']):
|
|
39
23
|
batch_run_id (str):
|
|
40
24
|
scenario_id (str):
|
|
41
25
|
scenario_run_id (str):
|
|
@@ -45,15 +29,7 @@ class PostApiScenarioEventsBodyType2:
|
|
|
45
29
|
|
|
46
30
|
type_: Literal["SCENARIO_MESSAGE_SNAPSHOT"]
|
|
47
31
|
timestamp: float
|
|
48
|
-
messages: list[
|
|
49
|
-
Union[
|
|
50
|
-
"PostApiScenarioEventsBodyType2MessagesItemType0",
|
|
51
|
-
"PostApiScenarioEventsBodyType2MessagesItemType1",
|
|
52
|
-
"PostApiScenarioEventsBodyType2MessagesItemType2",
|
|
53
|
-
"PostApiScenarioEventsBodyType2MessagesItemType3",
|
|
54
|
-
"PostApiScenarioEventsBodyType2MessagesItemType4",
|
|
55
|
-
]
|
|
56
|
-
]
|
|
32
|
+
messages: list["PostApiScenarioEventsBodyType2MessagesItem"]
|
|
57
33
|
batch_run_id: str
|
|
58
34
|
scenario_id: str
|
|
59
35
|
scenario_run_id: str
|
|
@@ -62,37 +38,13 @@ class PostApiScenarioEventsBodyType2:
|
|
|
62
38
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
63
39
|
|
|
64
40
|
def to_dict(self) -> dict[str, Any]:
|
|
65
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_0 import (
|
|
66
|
-
PostApiScenarioEventsBodyType2MessagesItemType0,
|
|
67
|
-
)
|
|
68
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_1 import (
|
|
69
|
-
PostApiScenarioEventsBodyType2MessagesItemType1,
|
|
70
|
-
)
|
|
71
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_2 import (
|
|
72
|
-
PostApiScenarioEventsBodyType2MessagesItemType2,
|
|
73
|
-
)
|
|
74
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_3 import (
|
|
75
|
-
PostApiScenarioEventsBodyType2MessagesItemType3,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
41
|
type_ = self.type_
|
|
79
42
|
|
|
80
43
|
timestamp = self.timestamp
|
|
81
44
|
|
|
82
45
|
messages = []
|
|
83
46
|
for messages_item_data in self.messages:
|
|
84
|
-
messages_item
|
|
85
|
-
if isinstance(messages_item_data, PostApiScenarioEventsBodyType2MessagesItemType0):
|
|
86
|
-
messages_item = messages_item_data.to_dict()
|
|
87
|
-
elif isinstance(messages_item_data, PostApiScenarioEventsBodyType2MessagesItemType1):
|
|
88
|
-
messages_item = messages_item_data.to_dict()
|
|
89
|
-
elif isinstance(messages_item_data, PostApiScenarioEventsBodyType2MessagesItemType2):
|
|
90
|
-
messages_item = messages_item_data.to_dict()
|
|
91
|
-
elif isinstance(messages_item_data, PostApiScenarioEventsBodyType2MessagesItemType3):
|
|
92
|
-
messages_item = messages_item_data.to_dict()
|
|
93
|
-
else:
|
|
94
|
-
messages_item = messages_item_data.to_dict()
|
|
95
|
-
|
|
47
|
+
messages_item = messages_item_data.to_dict()
|
|
96
48
|
messages.append(messages_item)
|
|
97
49
|
|
|
98
50
|
batch_run_id = self.batch_run_id
|
|
@@ -126,20 +78,8 @@ class PostApiScenarioEventsBodyType2:
|
|
|
126
78
|
|
|
127
79
|
@classmethod
|
|
128
80
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
129
|
-
from ..models.
|
|
130
|
-
|
|
131
|
-
)
|
|
132
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_1 import (
|
|
133
|
-
PostApiScenarioEventsBodyType2MessagesItemType1,
|
|
134
|
-
)
|
|
135
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_2 import (
|
|
136
|
-
PostApiScenarioEventsBodyType2MessagesItemType2,
|
|
137
|
-
)
|
|
138
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_3 import (
|
|
139
|
-
PostApiScenarioEventsBodyType2MessagesItemType3,
|
|
140
|
-
)
|
|
141
|
-
from ..models.post_api_scenario_events_body_type_2_messages_item_type_4 import (
|
|
142
|
-
PostApiScenarioEventsBodyType2MessagesItemType4,
|
|
81
|
+
from ..models.post_api_scenario_events_body_type_2_messages_item import (
|
|
82
|
+
PostApiScenarioEventsBodyType2MessagesItem,
|
|
143
83
|
)
|
|
144
84
|
|
|
145
85
|
d = dict(src_dict)
|
|
@@ -152,55 +92,7 @@ class PostApiScenarioEventsBodyType2:
|
|
|
152
92
|
messages = []
|
|
153
93
|
_messages = d.pop("messages")
|
|
154
94
|
for messages_item_data in _messages:
|
|
155
|
-
|
|
156
|
-
def _parse_messages_item(
|
|
157
|
-
data: object,
|
|
158
|
-
) -> Union[
|
|
159
|
-
"PostApiScenarioEventsBodyType2MessagesItemType0",
|
|
160
|
-
"PostApiScenarioEventsBodyType2MessagesItemType1",
|
|
161
|
-
"PostApiScenarioEventsBodyType2MessagesItemType2",
|
|
162
|
-
"PostApiScenarioEventsBodyType2MessagesItemType3",
|
|
163
|
-
"PostApiScenarioEventsBodyType2MessagesItemType4",
|
|
164
|
-
]:
|
|
165
|
-
try:
|
|
166
|
-
if not isinstance(data, dict):
|
|
167
|
-
raise TypeError()
|
|
168
|
-
messages_item_type_0 = PostApiScenarioEventsBodyType2MessagesItemType0.from_dict(data)
|
|
169
|
-
|
|
170
|
-
return messages_item_type_0
|
|
171
|
-
except: # noqa: E722
|
|
172
|
-
pass
|
|
173
|
-
try:
|
|
174
|
-
if not isinstance(data, dict):
|
|
175
|
-
raise TypeError()
|
|
176
|
-
messages_item_type_1 = PostApiScenarioEventsBodyType2MessagesItemType1.from_dict(data)
|
|
177
|
-
|
|
178
|
-
return messages_item_type_1
|
|
179
|
-
except: # noqa: E722
|
|
180
|
-
pass
|
|
181
|
-
try:
|
|
182
|
-
if not isinstance(data, dict):
|
|
183
|
-
raise TypeError()
|
|
184
|
-
messages_item_type_2 = PostApiScenarioEventsBodyType2MessagesItemType2.from_dict(data)
|
|
185
|
-
|
|
186
|
-
return messages_item_type_2
|
|
187
|
-
except: # noqa: E722
|
|
188
|
-
pass
|
|
189
|
-
try:
|
|
190
|
-
if not isinstance(data, dict):
|
|
191
|
-
raise TypeError()
|
|
192
|
-
messages_item_type_3 = PostApiScenarioEventsBodyType2MessagesItemType3.from_dict(data)
|
|
193
|
-
|
|
194
|
-
return messages_item_type_3
|
|
195
|
-
except: # noqa: E722
|
|
196
|
-
pass
|
|
197
|
-
if not isinstance(data, dict):
|
|
198
|
-
raise TypeError()
|
|
199
|
-
messages_item_type_4 = PostApiScenarioEventsBodyType2MessagesItemType4.from_dict(data)
|
|
200
|
-
|
|
201
|
-
return messages_item_type_4
|
|
202
|
-
|
|
203
|
-
messages_item = _parse_messages_item(messages_item_data)
|
|
95
|
+
messages_item = PostApiScenarioEventsBodyType2MessagesItem.from_dict(messages_item_data)
|
|
204
96
|
|
|
205
97
|
messages.append(messages_item)
|
|
206
98
|
|
|
@@ -1,54 +1,55 @@
|
|
|
1
1
|
from collections.abc import Mapping
|
|
2
|
-
from typing import Any, TypeVar
|
|
2
|
+
from typing import Any, TypeVar, Union
|
|
3
3
|
|
|
4
4
|
from attrs import define as _attrs_define
|
|
5
5
|
from attrs import field as _attrs_field
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
from ..types import UNSET, Unset
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T", bound="PostApiScenarioEventsBodyType2MessagesItem")
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@_attrs_define
|
|
11
|
-
class
|
|
13
|
+
class PostApiScenarioEventsBodyType2MessagesItem:
|
|
12
14
|
"""
|
|
13
15
|
Attributes:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
id (Union[Unset, str]):
|
|
17
|
+
trace_id (Union[Unset, str]):
|
|
16
18
|
"""
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
id: Union[Unset, str] = UNSET
|
|
21
|
+
trace_id: Union[Unset, str] = UNSET
|
|
20
22
|
additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict)
|
|
21
23
|
|
|
22
24
|
def to_dict(self) -> dict[str, Any]:
|
|
23
|
-
|
|
25
|
+
id = self.id
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
trace_id = self.trace_id
|
|
26
28
|
|
|
27
29
|
field_dict: dict[str, Any] = {}
|
|
28
30
|
field_dict.update(self.additional_properties)
|
|
29
|
-
field_dict.update(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
31
|
+
field_dict.update({})
|
|
32
|
+
if id is not UNSET:
|
|
33
|
+
field_dict["id"] = id
|
|
34
|
+
if trace_id is not UNSET:
|
|
35
|
+
field_dict["trace_id"] = trace_id
|
|
35
36
|
|
|
36
37
|
return field_dict
|
|
37
38
|
|
|
38
39
|
@classmethod
|
|
39
40
|
def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T:
|
|
40
41
|
d = dict(src_dict)
|
|
41
|
-
|
|
42
|
+
id = d.pop("id", UNSET)
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
trace_id = d.pop("trace_id", UNSET)
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
post_api_scenario_events_body_type_2_messages_item = cls(
|
|
47
|
+
id=id,
|
|
48
|
+
trace_id=trace_id,
|
|
48
49
|
)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
return
|
|
51
|
+
post_api_scenario_events_body_type_2_messages_item.additional_properties = d
|
|
52
|
+
return post_api_scenario_events_body_type_2_messages_item
|
|
52
53
|
|
|
53
54
|
@property
|
|
54
55
|
def additional_keys(self) -> list[str]:
|
langwatch/prompts/__init__.py
CHANGED
|
@@ -48,9 +48,11 @@ class PromptServiceTracing:
|
|
|
48
48
|
|
|
49
49
|
span.set_attributes(
|
|
50
50
|
{
|
|
51
|
-
AttributeKey.LangWatchPromptId: result.id,
|
|
52
|
-
AttributeKey.LangWatchPromptVersionId: result.
|
|
53
|
-
|
|
51
|
+
AttributeKey.LangWatchPromptId: result.get("id"),
|
|
52
|
+
AttributeKey.LangWatchPromptVersionId: result.get(
|
|
53
|
+
"version_id"
|
|
54
|
+
),
|
|
55
|
+
AttributeKey.LangWatchPromptHandle: result.get("handle"),
|
|
54
56
|
}
|
|
55
57
|
)
|
|
56
58
|
return result
|
|
@@ -63,7 +65,7 @@ class PromptServiceTracing:
|
|
|
63
65
|
@staticmethod
|
|
64
66
|
def _create_span_name(span_name: str) -> str:
|
|
65
67
|
"""Create a span name for the prompt"""
|
|
66
|
-
return "
|
|
68
|
+
return "PromptApiService" + "." + span_name
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
prompt_service_tracing = PromptServiceTracing()
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local prompt file loader for LangWatch Python SDK.
|
|
3
|
+
|
|
4
|
+
Reads prompts from local files in the CLI format:
|
|
5
|
+
- prompts.json: Configuration file
|
|
6
|
+
- prompts-lock.json: Lock file with materialized paths
|
|
7
|
+
- *.prompt.yaml: Individual prompt files
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Optional, Dict, Any
|
|
15
|
+
import warnings
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from .types import PromptData, MessageDict, ResponseFormatDict
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LocalPromptLoader:
|
|
25
|
+
"""Loads prompts from local files in CLI format."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_path: Optional[Path] = None):
|
|
28
|
+
"""Initialize with base path (defaults to current working directory)."""
|
|
29
|
+
self.base_path = base_path or Path.cwd()
|
|
30
|
+
|
|
31
|
+
def load_prompt(self, prompt_id: str) -> Optional[PromptData]:
|
|
32
|
+
"""
|
|
33
|
+
Load a prompt from local files.
|
|
34
|
+
|
|
35
|
+
Returns None if prompt not found locally.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
# Check if prompts.json exists
|
|
39
|
+
prompts_json_path = self.base_path / "prompts.json"
|
|
40
|
+
if not prompts_json_path.exists():
|
|
41
|
+
logger.debug(
|
|
42
|
+
f"No prompts.json found at {prompts_json_path}, falling back to API"
|
|
43
|
+
)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
# Load prompts.json
|
|
47
|
+
try:
|
|
48
|
+
with open(prompts_json_path, "r") as f:
|
|
49
|
+
prompts_config = json.load(f)
|
|
50
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
51
|
+
warnings.warn(
|
|
52
|
+
f"Failed to read prompts.json at {prompts_json_path}: {e}. "
|
|
53
|
+
f"Falling back to API for prompt '{prompt_id}'.",
|
|
54
|
+
UserWarning,
|
|
55
|
+
)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
# Check if prompt exists in config
|
|
59
|
+
if prompt_id not in prompts_config.get("prompts", {}):
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"Prompt '{prompt_id}' not found in prompts.json, falling back to API"
|
|
62
|
+
)
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# Load prompts-lock.json to get materialized path
|
|
66
|
+
prompts_lock_path = self.base_path / "prompts-lock.json"
|
|
67
|
+
if not prompts_lock_path.exists():
|
|
68
|
+
warnings.warn(
|
|
69
|
+
f"prompts.json exists but prompts-lock.json not found at {prompts_lock_path}. "
|
|
70
|
+
f"Run 'langwatch prompts pull' to sync local prompts. "
|
|
71
|
+
f"Falling back to API for prompt '{prompt_id}'.",
|
|
72
|
+
UserWarning,
|
|
73
|
+
)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
with open(prompts_lock_path, "r") as f:
|
|
78
|
+
prompts_lock = json.load(f)
|
|
79
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
80
|
+
warnings.warn(
|
|
81
|
+
f"Failed to read prompts-lock.json at {prompts_lock_path}: {e}. "
|
|
82
|
+
f"Falling back to API for prompt '{prompt_id}'.",
|
|
83
|
+
UserWarning,
|
|
84
|
+
)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
# Get materialized path
|
|
88
|
+
prompt_info = prompts_lock.get("prompts", {}).get(prompt_id)
|
|
89
|
+
if not prompt_info:
|
|
90
|
+
warnings.warn(
|
|
91
|
+
f"Prompt '{prompt_id}' found in prompts.json but not in prompts-lock.json. "
|
|
92
|
+
f"Run 'langwatch prompts pull' to sync local prompts. "
|
|
93
|
+
f"Falling back to API for prompt '{prompt_id}'.",
|
|
94
|
+
UserWarning,
|
|
95
|
+
)
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
materialized_path = prompt_info.get("materialized")
|
|
99
|
+
if not materialized_path:
|
|
100
|
+
warnings.warn(
|
|
101
|
+
f"Prompt '{prompt_id}' in prompts-lock.json has no materialized path. "
|
|
102
|
+
f"Run 'langwatch prompts pull' to sync local prompts. "
|
|
103
|
+
f"Falling back to API for prompt '{prompt_id}'.",
|
|
104
|
+
UserWarning,
|
|
105
|
+
)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
# Load the actual prompt file
|
|
109
|
+
prompt_file_path = self.base_path / materialized_path
|
|
110
|
+
if not prompt_file_path.exists():
|
|
111
|
+
warnings.warn(
|
|
112
|
+
f"Prompt file not found at {prompt_file_path}. "
|
|
113
|
+
f"Run 'langwatch prompts pull' to sync local prompts. "
|
|
114
|
+
f"Falling back to API for prompt '{prompt_id}'.",
|
|
115
|
+
UserWarning,
|
|
116
|
+
)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
with open(prompt_file_path, "r") as f:
|
|
121
|
+
prompt_data = yaml.safe_load(f)
|
|
122
|
+
except (yaml.YAMLError, OSError) as e:
|
|
123
|
+
warnings.warn(
|
|
124
|
+
f"Failed to parse prompt file at {prompt_file_path}: {e}. "
|
|
125
|
+
f"Falling back to API for prompt '{prompt_id}'.",
|
|
126
|
+
UserWarning,
|
|
127
|
+
)
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
# Build PromptData directly
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Successfully loaded prompt '{prompt_id}' from local file: {prompt_file_path}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Convert messages
|
|
136
|
+
messages = []
|
|
137
|
+
if "messages" in prompt_data:
|
|
138
|
+
messages = [
|
|
139
|
+
MessageDict(role=msg["role"], content=msg["content"])
|
|
140
|
+
for msg in prompt_data["messages"]
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
# Convert response format if present
|
|
144
|
+
response_format = None
|
|
145
|
+
if "response_format" in prompt_data and prompt_data["response_format"]:
|
|
146
|
+
response_format = ResponseFormatDict(
|
|
147
|
+
type="json_schema", json_schema=prompt_data["response_format"]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return PromptData(
|
|
151
|
+
handle=prompt_id, # The prompt_id parameter is the handle
|
|
152
|
+
model=prompt_data["model"], # Required field - let it fail if missing
|
|
153
|
+
messages=messages,
|
|
154
|
+
prompt=prompt_data.get("prompt"),
|
|
155
|
+
temperature=prompt_data.get("temperature"),
|
|
156
|
+
max_tokens=prompt_data.get("max_tokens"),
|
|
157
|
+
response_format=response_format,
|
|
158
|
+
version=prompt_info.get("version"),
|
|
159
|
+
version_id=prompt_info.get("versionId"),
|
|
160
|
+
# id and scope are not available in local files
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
except Exception as e:
|
|
164
|
+
# If any unexpected error occurs, warn and fall back to API
|
|
165
|
+
warnings.warn(
|
|
166
|
+
f"Unexpected error loading prompt '{prompt_id}' from local files: {e}. "
|
|
167
|
+
f"Falling back to API.",
|
|
168
|
+
UserWarning,
|
|
169
|
+
)
|
|
170
|
+
return None
|
langwatch/prompts/prompt.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
from typing import List, Any, Dict, Union, Optional, cast
|
|
2
|
-
|
|
1
|
+
from typing import List, Any, Dict, Union, Optional, cast, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from openai.types.chat import ChatCompletionMessageParam
|
|
5
|
+
|
|
3
6
|
from liquid import Environment, StrictUndefined, Undefined
|
|
4
7
|
from liquid.exceptions import UndefinedError
|
|
5
|
-
from langwatch.generated.langwatch_rest_api_client.models.get_api_prompts_by_id_response_200 import (
|
|
6
|
-
GetApiPromptsByIdResponse200,
|
|
7
|
-
)
|
|
8
|
-
from .formatter import PromptFormatter
|
|
9
8
|
from .decorators.prompt_tracing import prompt_tracing
|
|
10
|
-
from .types import MessageDict
|
|
9
|
+
from .types import PromptData, MessageDict
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
class PromptCompilationError(Exception):
|
|
@@ -34,31 +33,30 @@ class Prompt:
|
|
|
34
33
|
Handles formatting messages with variables using Liquid templating.
|
|
35
34
|
"""
|
|
36
35
|
|
|
37
|
-
def __init__(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
formatter: PromptFormatter = PromptFormatter(),
|
|
41
|
-
):
|
|
42
|
-
self._config = config
|
|
43
|
-
self._formatter = formatter
|
|
36
|
+
def __init__(self, data: PromptData):
|
|
37
|
+
# Store raw data for backward compatibility
|
|
38
|
+
self._data = data.copy()
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return getattr(self._config, name)
|
|
49
|
-
raise AttributeError(
|
|
50
|
-
f"'{self.__class__.__name__}' object has no attribute '{name}'"
|
|
51
|
-
)
|
|
40
|
+
# Assign all fields directly as instance attributes
|
|
41
|
+
for key, value in data.items():
|
|
42
|
+
setattr(self, key, value)
|
|
52
43
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return self._config
|
|
44
|
+
# Set prompt default only if not provided (like TypeScript)
|
|
45
|
+
if not hasattr(self, "prompt") or self.prompt is None:
|
|
46
|
+
self.prompt = self._extract_system_prompt()
|
|
57
47
|
|
|
58
48
|
@property
|
|
59
|
-
def
|
|
60
|
-
"""
|
|
61
|
-
return
|
|
49
|
+
def raw(self) -> PromptData:
|
|
50
|
+
"""Get the raw prompt data"""
|
|
51
|
+
return self._data
|
|
52
|
+
|
|
53
|
+
def _extract_system_prompt(self) -> str:
|
|
54
|
+
"""Extract system prompt from messages, like TypeScript version."""
|
|
55
|
+
if hasattr(self, "messages") and self.messages:
|
|
56
|
+
for message in self.messages:
|
|
57
|
+
if message.get("role") == "system":
|
|
58
|
+
return message.get("content", "")
|
|
59
|
+
return ""
|
|
62
60
|
|
|
63
61
|
def _compile(self, variables: TemplateVariables, strict: bool) -> "CompiledPrompt":
|
|
64
62
|
"""
|
|
@@ -70,19 +68,19 @@ class Prompt:
|
|
|
70
68
|
|
|
71
69
|
# Compile main prompt
|
|
72
70
|
compiled_prompt = ""
|
|
73
|
-
if self.
|
|
74
|
-
template = env.from_string(self.
|
|
71
|
+
if hasattr(self, "prompt") and self.prompt:
|
|
72
|
+
template = env.from_string(self.prompt)
|
|
75
73
|
compiled_prompt = template.render(**variables)
|
|
76
74
|
|
|
77
75
|
# Compile messages
|
|
78
76
|
compiled_messages: List[MessageDict] = []
|
|
79
|
-
if self.
|
|
80
|
-
for message in self.
|
|
81
|
-
content: str = message
|
|
77
|
+
if hasattr(self, "messages") and self.messages:
|
|
78
|
+
for message in self.messages:
|
|
79
|
+
content: str = message["content"]
|
|
82
80
|
template = env.from_string(content)
|
|
83
81
|
compiled_content = template.render(**variables)
|
|
84
82
|
compiled_message = MessageDict(
|
|
85
|
-
role=message
|
|
83
|
+
role=message["role"],
|
|
86
84
|
content=compiled_content,
|
|
87
85
|
)
|
|
88
86
|
compiled_messages.append(compiled_message)
|
|
@@ -96,12 +94,16 @@ class Prompt:
|
|
|
96
94
|
)
|
|
97
95
|
|
|
98
96
|
except UndefinedError as error:
|
|
99
|
-
template_str = self
|
|
97
|
+
template_str = getattr(self, "prompt", "") or str(
|
|
98
|
+
getattr(self, "messages", [])
|
|
99
|
+
)
|
|
100
100
|
raise PromptCompilationError(
|
|
101
101
|
f"Failed to compile prompt template: {str(error)}", template_str, error
|
|
102
102
|
)
|
|
103
103
|
except Exception as error:
|
|
104
|
-
template_str = self
|
|
104
|
+
template_str = getattr(self, "prompt", "") or str(
|
|
105
|
+
getattr(self, "messages", [])
|
|
106
|
+
)
|
|
105
107
|
raise PromptCompilationError(
|
|
106
108
|
f"Failed to compile prompt template: {str(error)}", template_str, error
|
|
107
109
|
)
|
|
@@ -168,21 +170,17 @@ class CompiledPrompt:
|
|
|
168
170
|
self.variables = variables # Store the original compilation variables
|
|
169
171
|
self._compiled_messages = compiled_messages
|
|
170
172
|
|
|
171
|
-
#
|
|
172
|
-
self.id = original_prompt.id
|
|
173
|
-
self.version = original_prompt.version
|
|
174
|
-
self.version_id = original_prompt.version_id
|
|
175
|
-
# ... other properties as needed
|
|
173
|
+
# Properties are delegated via __getattr__ below
|
|
176
174
|
|
|
177
175
|
@property
|
|
178
|
-
def messages(self) -> List[ChatCompletionMessageParam]:
|
|
176
|
+
def messages(self) -> List["ChatCompletionMessageParam"]:
|
|
179
177
|
"""
|
|
180
178
|
Returns the compiled messages as a list of ChatCompletionMessageParam objects.
|
|
181
179
|
This is a convenience method to make the messages accessible as a list of
|
|
182
180
|
ChatCompletionMessageParam objects, which is the format expected by the OpenAI API.
|
|
183
181
|
"""
|
|
184
182
|
messages = [
|
|
185
|
-
cast(ChatCompletionMessageParam, msg) for msg in self._compiled_messages
|
|
183
|
+
cast("ChatCompletionMessageParam", msg) for msg in self._compiled_messages
|
|
186
184
|
]
|
|
187
185
|
|
|
188
186
|
return messages
|