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.
Files changed (27) hide show
  1. langwatch/__init__.py +5 -5
  2. langwatch/__version__.py +1 -1
  3. langwatch/generated/langwatch_rest_api_client/models/__init__.py +2 -18
  4. langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2.py +7 -115
  5. 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
  6. langwatch/prompts/__init__.py +2 -2
  7. langwatch/prompts/decorators/prompt_service_tracing.py +6 -4
  8. langwatch/prompts/local_loader.py +170 -0
  9. langwatch/prompts/prompt.py +41 -43
  10. langwatch/prompts/{service.py → prompt_api_service.py} +23 -33
  11. langwatch/prompts/prompt_facade.py +139 -0
  12. langwatch/prompts/types/__init__.py +27 -0
  13. langwatch/prompts/types/prompt_data.py +93 -0
  14. langwatch/prompts/types/structures.py +37 -0
  15. langwatch/prompts/types.py +16 -24
  16. langwatch/utils/initialization.py +8 -2
  17. langwatch/utils/transformation.py +16 -5
  18. {langwatch-0.2.18.dist-info → langwatch-0.3.0.dist-info}/METADATA +1 -1
  19. {langwatch-0.2.18.dist-info → langwatch-0.3.0.dist-info}/RECORD +20 -22
  20. langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_0.py +0 -88
  21. langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_1.py +0 -88
  22. langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_2.py +0 -120
  23. langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item.py +0 -87
  24. langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_3.py +0 -88
  25. langwatch/generated/langwatch_rest_api_client/models/post_api_scenario_events_body_type_2_messages_item_type_4.py +0 -85
  26. langwatch/prompts/formatter.py +0 -31
  27. {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.service import PromptService
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: PromptService
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 PromptService instance at langwatch.prompts
65
- from .prompts.service import PromptService
64
+ # Special-case: expose a PromptsFacade instance at langwatch.prompts
65
+ from .prompts.prompt_facade import PromptsFacade
66
66
 
67
- svc = PromptService.from_global()
67
+ svc = PromptsFacade.from_global()
68
68
  globals()[name] = svc # Cache for subsequent access
69
69
  return svc
70
70
 
langwatch/__version__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Version information for LangWatch."""
2
2
 
3
- __version__ = "0.2.18"
3
+ __version__ = "0.2.19"
@@ -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 .post_api_scenario_events_body_type_2_messages_item_type_0 import PostApiScenarioEventsBodyType2MessagesItemType0
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
- "PostApiScenarioEventsBodyType2MessagesItemType0",
857
- "PostApiScenarioEventsBodyType2MessagesItemType1",
858
- "PostApiScenarioEventsBodyType2MessagesItemType2",
859
- "PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItem",
860
- "PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItemFunction",
861
- "PostApiScenarioEventsBodyType2MessagesItemType3",
862
- "PostApiScenarioEventsBodyType2MessagesItemType4",
846
+ "PostApiScenarioEventsBodyType2MessagesItem",
863
847
  "PostApiScenarioEventsResponse201",
864
848
  "PostApiScenarioEventsResponse400",
865
849
  "PostApiScenarioEventsResponse401",
@@ -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.post_api_scenario_events_body_type_2_messages_item_type_0 import (
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[Union['PostApiScenarioEventsBodyType2MessagesItemType0',
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: dict[str, Any]
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.post_api_scenario_events_body_type_2_messages_item_type_0 import (
130
- PostApiScenarioEventsBodyType2MessagesItemType0,
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
- T = TypeVar("T", bound="PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItemFunction")
7
+ from ..types import UNSET, Unset
8
+
9
+ T = TypeVar("T", bound="PostApiScenarioEventsBodyType2MessagesItem")
8
10
 
9
11
 
10
12
  @_attrs_define
11
- class PostApiScenarioEventsBodyType2MessagesItemType2ToolCallsItemFunction:
13
+ class PostApiScenarioEventsBodyType2MessagesItem:
12
14
  """
13
15
  Attributes:
14
- name (str):
15
- arguments (str):
16
+ id (Union[Unset, str]):
17
+ trace_id (Union[Unset, str]):
16
18
  """
17
19
 
18
- name: str
19
- arguments: str
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
- name = self.name
25
+ id = self.id
24
26
 
25
- arguments = self.arguments
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
- "name": name,
32
- "arguments": arguments,
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
- name = d.pop("name")
42
+ id = d.pop("id", UNSET)
42
43
 
43
- arguments = d.pop("arguments")
44
+ trace_id = d.pop("trace_id", UNSET)
44
45
 
45
- post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item_function = cls(
46
- name=name,
47
- arguments=arguments,
46
+ post_api_scenario_events_body_type_2_messages_item = cls(
47
+ id=id,
48
+ trace_id=trace_id,
48
49
  )
49
50
 
50
- post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item_function.additional_properties = d
51
- return post_api_scenario_events_body_type_2_messages_item_type_2_tool_calls_item_function
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]:
@@ -1,5 +1,5 @@
1
- from .service import PromptService
1
+ from .prompt_facade import PromptsFacade
2
2
 
3
3
  __all__ = [
4
- "PromptService",
4
+ "PromptsFacade",
5
5
  ]
@@ -48,9 +48,11 @@ class PromptServiceTracing:
48
48
 
49
49
  span.set_attributes(
50
50
  {
51
- AttributeKey.LangWatchPromptId: result.id,
52
- AttributeKey.LangWatchPromptVersionId: result.version_id,
53
- AttributeKey.LangWatchPromptHandle: result.handle,
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 "PromptService" + "." + span_name
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
@@ -1,13 +1,12 @@
1
- from typing import List, Any, Dict, Union, Optional, cast
2
- from openai.types.chat import ChatCompletionMessageParam
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
- self,
39
- config: GetApiPromptsByIdResponse200,
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
- def __getattr__(self, name: str) -> Any:
46
- """Delegate attribute access to the underlying config object"""
47
- if hasattr(self._config, name):
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
- @property
54
- def raw(self) -> Any:
55
- """Get the raw prompt data from the API"""
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 version_number(self) -> int:
60
- """Returns the version number of the prompt."""
61
- return int(self._config.version)
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._config.prompt:
74
- template = env.from_string(self._config.prompt)
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._config.messages:
80
- for message in self._config.messages:
81
- content: str = message.content
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.role.value,
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._config.prompt or str(self._config.messages or [])
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._config.prompt or str(self._config.messages or [])
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
- # Expose original prompt properties for convenience
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