langwatch 0.2.19__py3-none-any.whl → 0.3.1__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/decorators/prompt_tracing.py +13 -7
  9. langwatch/prompts/local_loader.py +170 -0
  10. langwatch/prompts/prompt.py +41 -43
  11. langwatch/prompts/{service.py → prompt_api_service.py} +23 -33
  12. langwatch/prompts/prompt_facade.py +139 -0
  13. langwatch/prompts/types/__init__.py +27 -0
  14. langwatch/prompts/types/prompt_data.py +93 -0
  15. langwatch/prompts/types/structures.py +37 -0
  16. langwatch/prompts/types.py +16 -24
  17. langwatch/utils/transformation.py +16 -5
  18. {langwatch-0.2.19.dist-info → langwatch-0.3.1.dist-info}/METADATA +1 -1
  19. {langwatch-0.2.19.dist-info → langwatch-0.3.1.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.19.dist-info → langwatch-0.3.1.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.19"
3
+ __version__ = "0.3.1"
@@ -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()
@@ -21,7 +21,7 @@ class PromptTracing:
21
21
  """Internal method to create compile decorators with specified span name"""
22
22
 
23
23
  def decorator(
24
- func: Callable[..., "CompiledPrompt"]
24
+ func: Callable[..., "CompiledPrompt"],
25
25
  ) -> Callable[..., "CompiledPrompt"]:
26
26
  @wraps(func)
27
27
  def wrapper(self: "Prompt", *args: Any, **kwargs: Any) -> "CompiledPrompt":
@@ -31,10 +31,16 @@ class PromptTracing:
31
31
  # Set base prompt
32
32
  span.set_attributes(
33
33
  {
34
- AttributeKey.LangWatchPromptId: self.id,
35
- AttributeKey.LangWatchPromptHandle: self.handle,
36
- AttributeKey.LangWatchPromptVersionId: self.version_id,
37
- AttributeKey.LangWatchPromptVersionNumber: self.version,
34
+ AttributeKey.LangWatchPromptId: getattr(self, "id", None),
35
+ AttributeKey.LangWatchPromptHandle: getattr(
36
+ self, "handle", None
37
+ ),
38
+ AttributeKey.LangWatchPromptVersionId: getattr(
39
+ self, "version_id", None
40
+ ),
41
+ AttributeKey.LangWatchPromptVersionNumber: getattr(
42
+ self, "version", None
43
+ ),
38
44
  }
39
45
  )
40
46
 
@@ -67,14 +73,14 @@ class PromptTracing:
67
73
 
68
74
  @staticmethod
69
75
  def compile(
70
- func: Callable[..., "CompiledPrompt"]
76
+ func: Callable[..., "CompiledPrompt"],
71
77
  ) -> Callable[..., "CompiledPrompt"]:
72
78
  """Decorator for Prompt.compile method with OpenTelemetry tracing"""
73
79
  return PromptTracing._create_compile_decorator("compile")(func)
74
80
 
75
81
  @staticmethod
76
82
  def compile_strict(
77
- func: Callable[..., "CompiledPrompt"]
83
+ func: Callable[..., "CompiledPrompt"],
78
84
  ) -> Callable[..., "CompiledPrompt"]:
79
85
  """Decorator for Prompt.compile_strict method with OpenTelemetry tracing"""
80
86
  return PromptTracing._create_compile_decorator("compile_strict")(func)
@@ -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