prompty 0.1.8__tar.gz → 0.1.9__tar.gz
Sign up to get free protection for your applications and to get access to all the features.
- {prompty-0.1.8 → prompty-0.1.9}/PKG-INFO +3 -3
- {prompty-0.1.8 → prompty-0.1.9}/README.md +2 -2
- {prompty-0.1.8 → prompty-0.1.9}/prompty/core.py +32 -2
- {prompty-0.1.8 → prompty-0.1.9}/prompty/executors.py +8 -9
- {prompty-0.1.8 → prompty-0.1.9}/prompty/processors.py +2 -5
- prompty-0.1.9/prompty/tracer.py +200 -0
- {prompty-0.1.8 → prompty-0.1.9}/pyproject.toml +1 -1
- {prompty-0.1.8 → prompty-0.1.9}/tests/__init__.py +4 -2
- {prompty-0.1.8 → prompty-0.1.9}/tests/test_common.py +0 -3
- {prompty-0.1.8 → prompty-0.1.9}/tests/test_execute.py +1 -5
- {prompty-0.1.8 → prompty-0.1.9}/tests/test_factory_invoker.py +1 -3
- {prompty-0.1.8 → prompty-0.1.9}/tests/test_path_exec.py +7 -2
- prompty-0.1.9/tests/test_tracing.py +146 -0
- prompty-0.1.8/prompty/tracer.py +0 -231
- {prompty-0.1.8 → prompty-0.1.9}/LICENSE +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/prompty/__init__.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/prompty/cli.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/prompty/parsers.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/prompty/renderers.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/1contoso.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/2contoso.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/3contoso.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/4contoso.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/basic.prompty.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/camping.jpg +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/context.prompty.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/contoso_multi.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/faithfulness.prompty.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/generated/groundedness.prompty.md +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/hello_world-goodbye_world-hello_again.embedding.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/hello_world.embedding.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/__init__.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/basic.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/basic.prompty.execution.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/basic_json_output.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/camping.jpg +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/chat.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/context.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/context.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/context.prompty.execution.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/embedding.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/embedding.prompty.execution.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/evaluation.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/faithfulness.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/faithfulness.prompty.execution.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/fake.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/funcfile.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/funcfile.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/functions.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/functions.prompty.execution.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/groundedness.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/groundedness.prompty.execution.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/prompty.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/streaming.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/streaming.prompty.execution.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/sub/__init__.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/sub/basic.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/sub/sub/__init__.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/sub/sub/basic.prompty +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/sub/sub/prompty.json +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/sub/sub/test.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompts/test.py +0 -0
- {prompty-0.1.8 → prompty-0.1.9}/tests/prompty.json +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: prompty
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.9
|
4
4
|
Summary: Prompty is a new asset class and format for LLM prompts that aims to provide observability, understandability, and portability for developers. It includes spec, tooling, and a runtime. This Prompty runtime supports Python
|
5
5
|
Author-Email: Seth Juarez <seth.juarez@microsoft.com>
|
6
6
|
License: MIT
|
@@ -15,7 +15,7 @@ Requires-Dist: click>=8.1.7
|
|
15
15
|
Description-Content-Type: text/markdown
|
16
16
|
|
17
17
|
|
18
|
-
Prompty is an asset class and format for LLM prompts designed to enhance observability, understandability, and portability for developers. The primary goal is to accelerate the developer inner loop of prompt engineering and prompt source management in a cross-language and cross-platform
|
18
|
+
Prompty is an asset class and format for LLM prompts designed to enhance observability, understandability, and portability for developers. The primary goal is to accelerate the developer inner loop of prompt engineering and prompt source management in a cross-language and cross-platform implementation.
|
19
19
|
|
20
20
|
The file format has a supporting toolchain with a VS Code extension and runtimes in multiple programming languages to simplify and accelerate your AI application development.
|
21
21
|
|
@@ -133,4 +133,4 @@ prompty -s path/to/prompty/file
|
|
133
133
|
This will execute the prompt and print the response to the console. It also has default tracing enabled.
|
134
134
|
|
135
135
|
## Contributing
|
136
|
-
We welcome contributions to the Prompty project! This community led project is open to all contributors. The project cvan be found on [GitHub](https://github.com/Microsoft/prompty).
|
136
|
+
We welcome contributions to the Prompty project! This community led project is open to all contributors. The project cvan be found on [GitHub](https://github.com/Microsoft/prompty).
|
@@ -1,5 +1,5 @@
|
|
1
1
|
|
2
|
-
Prompty is an asset class and format for LLM prompts designed to enhance observability, understandability, and portability for developers. The primary goal is to accelerate the developer inner loop of prompt engineering and prompt source management in a cross-language and cross-platform
|
2
|
+
Prompty is an asset class and format for LLM prompts designed to enhance observability, understandability, and portability for developers. The primary goal is to accelerate the developer inner loop of prompt engineering and prompt source management in a cross-language and cross-platform implementation.
|
3
3
|
|
4
4
|
The file format has a supporting toolchain with a VS Code extension and runtimes in multiple programming languages to simplify and accelerate your AI application development.
|
5
5
|
|
@@ -117,4 +117,4 @@ prompty -s path/to/prompty/file
|
|
117
117
|
This will execute the prompt and print the response to the console. It also has default tracing enabled.
|
118
118
|
|
119
119
|
## Contributing
|
120
|
-
We welcome contributions to the Prompty project! This community led project is open to all contributors. The project cvan be found on [GitHub](https://github.com/Microsoft/prompty).
|
120
|
+
We welcome contributions to the Prompty project! This community led project is open to all contributors. The project cvan be found on [GitHub](https://github.com/Microsoft/prompty).
|
@@ -6,9 +6,9 @@ import yaml
|
|
6
6
|
import json
|
7
7
|
import abc
|
8
8
|
from pathlib import Path
|
9
|
+
from .tracer import Tracer, trace, to_dict
|
9
10
|
from pydantic import BaseModel, Field, FilePath
|
10
|
-
from typing import List, Literal, Dict, Callable, Set
|
11
|
-
from .tracer import trace
|
11
|
+
from typing import Iterator, List, Literal, Dict, Callable, Set
|
12
12
|
|
13
13
|
|
14
14
|
class PropertySettings(BaseModel):
|
@@ -449,3 +449,33 @@ class Frontmatter:
|
|
449
449
|
"body": body,
|
450
450
|
"frontmatter": fmatter,
|
451
451
|
}
|
452
|
+
|
453
|
+
|
454
|
+
class PromptyStream(Iterator):
|
455
|
+
"""PromptyStream class to iterate over LLM stream.
|
456
|
+
Necessary for Prompty to handle streaming data when tracing."""
|
457
|
+
|
458
|
+
def __init__(self, name: str, iterator: Iterator):
|
459
|
+
self.name = name
|
460
|
+
self.iterator = iterator
|
461
|
+
self.items: List[any] = []
|
462
|
+
self.__name__ = "PromptyStream"
|
463
|
+
|
464
|
+
def __iter__(self):
|
465
|
+
return self
|
466
|
+
|
467
|
+
def __next__(self):
|
468
|
+
try:
|
469
|
+
# enumerate but add to list
|
470
|
+
o = self.iterator.__next__()
|
471
|
+
self.items.append(o)
|
472
|
+
return o
|
473
|
+
|
474
|
+
except StopIteration:
|
475
|
+
# StopIteration is raised
|
476
|
+
# contents are exhausted
|
477
|
+
if len(self.items) > 0:
|
478
|
+
with Tracer.start(f"{self.name}.PromptyStream") as trace:
|
479
|
+
trace("items", [to_dict(s) for s in self.items])
|
480
|
+
|
481
|
+
raise StopIteration
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import azure.identity
|
2
|
-
from .tracer import Trace
|
3
|
-
from openai import AzureOpenAI
|
4
|
-
from .core import Invoker, InvokerFactory, Prompty
|
5
2
|
import importlib.metadata
|
3
|
+
from typing import Iterator
|
4
|
+
from openai import AzureOpenAI
|
5
|
+
from .core import Invoker, InvokerFactory, Prompty, PromptyStream
|
6
6
|
|
7
7
|
VERSION = importlib.metadata.version("prompty")
|
8
8
|
|
@@ -87,9 +87,8 @@ class AzureOpenAIExecutor(Invoker):
|
|
87
87
|
elif self.api == "image":
|
88
88
|
raise NotImplementedError("Azure OpenAI Image API is not implemented yet")
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
return response
|
90
|
+
# stream response
|
91
|
+
if isinstance(response, Iterator):
|
92
|
+
return PromptyStream("AzureOpenAIExecutor", response)
|
93
|
+
else:
|
94
|
+
return response
|
@@ -1,10 +1,8 @@
|
|
1
|
-
from .tracer import Trace
|
2
|
-
from openai import Stream
|
3
1
|
from typing import Iterator
|
4
2
|
from pydantic import BaseModel
|
5
3
|
from openai.types.completion import Completion
|
6
|
-
from .core import Invoker, InvokerFactory, Prompty
|
7
4
|
from openai.types.chat.chat_completion import ChatCompletion
|
5
|
+
from .core import Invoker, InvokerFactory, Prompty, PromptyStream
|
8
6
|
from openai.types.create_embedding_response import CreateEmbeddingResponse
|
9
7
|
|
10
8
|
|
@@ -66,9 +64,8 @@ class OpenAIProcessor(Invoker):
|
|
66
64
|
for chunk in data:
|
67
65
|
if len(chunk.choices) == 1 and chunk.choices[0].delta.content != None:
|
68
66
|
content = chunk.choices[0].delta.content
|
69
|
-
Trace.add("stream", content)
|
70
67
|
yield content
|
71
68
|
|
72
|
-
return generator()
|
69
|
+
return PromptyStream("OpenAIProcessor", generator())
|
73
70
|
else:
|
74
71
|
return data
|
@@ -0,0 +1,200 @@
|
|
1
|
+
import os
|
2
|
+
import json
|
3
|
+
import inspect
|
4
|
+
import contextlib
|
5
|
+
from pathlib import Path
|
6
|
+
from numbers import Number
|
7
|
+
from datetime import datetime
|
8
|
+
from pydantic import BaseModel
|
9
|
+
from functools import wraps, partial
|
10
|
+
from typing import Any, Callable, Dict, Iterator, List
|
11
|
+
|
12
|
+
|
13
|
+
class Tracer:
|
14
|
+
_tracers: Dict[str, Callable[[str], Iterator[Callable[[str, Any], None]]]] = {}
|
15
|
+
|
16
|
+
@classmethod
|
17
|
+
def add(
|
18
|
+
cls, name: str, tracer: Callable[[str], Iterator[Callable[[str, Any], None]]]
|
19
|
+
) -> None:
|
20
|
+
cls._tracers[name] = tracer
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def clear(cls) -> None:
|
24
|
+
cls._tracers = {}
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
@contextlib.contextmanager
|
28
|
+
def start(cls, name: str) -> Iterator[Callable[[str, Any], None]]:
|
29
|
+
with contextlib.ExitStack() as stack:
|
30
|
+
traces = [
|
31
|
+
stack.enter_context(tracer(name)) for tracer in cls._tracers.values()
|
32
|
+
]
|
33
|
+
yield lambda key, value: [trace(key, value) for trace in traces]
|
34
|
+
|
35
|
+
|
36
|
+
def to_dict(obj: Any) -> Dict[str, Any]:
|
37
|
+
# simple json types
|
38
|
+
if isinstance(obj, str) or isinstance(obj, Number) or isinstance(obj, bool):
|
39
|
+
return obj
|
40
|
+
# datetime
|
41
|
+
elif isinstance(obj, datetime):
|
42
|
+
return obj.isoformat()
|
43
|
+
# safe Prompty obj serialization
|
44
|
+
elif type(obj).__name__ == "Prompty":
|
45
|
+
return obj.to_safe_dict()
|
46
|
+
# safe PromptyStream obj serialization
|
47
|
+
elif type(obj).__name__ == "PromptyStream":
|
48
|
+
return "PromptyStream"
|
49
|
+
# pydantic models have their own json serialization
|
50
|
+
elif isinstance(obj, BaseModel):
|
51
|
+
return obj.model_dump()
|
52
|
+
# recursive list and dict
|
53
|
+
elif isinstance(obj, list):
|
54
|
+
return [to_dict(item) for item in obj]
|
55
|
+
elif isinstance(obj, dict):
|
56
|
+
return {k: v if isinstance(v, str) else to_dict(v) for k, v in obj.items()}
|
57
|
+
elif isinstance(obj, Path):
|
58
|
+
return str(obj)
|
59
|
+
# cast to string otherwise...
|
60
|
+
else:
|
61
|
+
return str(obj)
|
62
|
+
|
63
|
+
|
64
|
+
def _name(func: Callable, args):
|
65
|
+
if hasattr(func, "__qualname__"):
|
66
|
+
signature = f"{func.__module__}.{func.__qualname__}"
|
67
|
+
else:
|
68
|
+
signature = f"{func.__module__}.{func.__name__}"
|
69
|
+
|
70
|
+
# core invoker gets special treatment
|
71
|
+
core_invoker = signature == "prompty.core.Invoker.__call__"
|
72
|
+
if core_invoker:
|
73
|
+
name = type(args[0]).__name__
|
74
|
+
signature = f"{args[0].__module__}.{args[0].__class__.__name__}.invoke"
|
75
|
+
else:
|
76
|
+
name = func.__name__
|
77
|
+
|
78
|
+
return name, signature
|
79
|
+
|
80
|
+
|
81
|
+
def _inputs(func: Callable, args, kwargs) -> dict:
|
82
|
+
ba = inspect.signature(func).bind(*args, **kwargs)
|
83
|
+
ba.apply_defaults()
|
84
|
+
|
85
|
+
inputs = {k: to_dict(v) for k, v in ba.arguments.items() if k != "self"}
|
86
|
+
|
87
|
+
return inputs
|
88
|
+
|
89
|
+
|
90
|
+
def _results(result: Any) -> dict:
|
91
|
+
return to_dict(result) if result is not None else "None"
|
92
|
+
|
93
|
+
|
94
|
+
def _trace_sync(func: Callable = None, *, description: str = None) -> Callable:
|
95
|
+
description = description or ""
|
96
|
+
|
97
|
+
@wraps(func)
|
98
|
+
def wrapper(*args, **kwargs):
|
99
|
+
name, signature = _name(func, args)
|
100
|
+
with Tracer.start(name) as trace:
|
101
|
+
trace("signature", signature)
|
102
|
+
if description and description != "":
|
103
|
+
trace("description", description)
|
104
|
+
|
105
|
+
inputs = _inputs(func, args, kwargs)
|
106
|
+
trace("inputs", inputs)
|
107
|
+
|
108
|
+
result = func(*args, **kwargs)
|
109
|
+
trace("result", _results(result))
|
110
|
+
|
111
|
+
return result
|
112
|
+
|
113
|
+
return wrapper
|
114
|
+
|
115
|
+
|
116
|
+
def _trace_async(func: Callable = None, *, description: str = None) -> Callable:
|
117
|
+
description = description or ""
|
118
|
+
|
119
|
+
@wraps(func)
|
120
|
+
async def wrapper(*args, **kwargs):
|
121
|
+
name, signature = _name(func, args)
|
122
|
+
with Tracer.start(name) as trace:
|
123
|
+
trace("signature", signature)
|
124
|
+
if description and description != "":
|
125
|
+
trace("description", description)
|
126
|
+
|
127
|
+
inputs = _inputs(func, args, kwargs)
|
128
|
+
trace("inputs", inputs)
|
129
|
+
|
130
|
+
result = await func(*args, **kwargs)
|
131
|
+
trace("result", _results(result))
|
132
|
+
|
133
|
+
return result
|
134
|
+
|
135
|
+
return wrapper
|
136
|
+
|
137
|
+
|
138
|
+
def trace(func: Callable = None, *, description: str = None) -> Callable:
|
139
|
+
if func is None:
|
140
|
+
return partial(trace, description=description)
|
141
|
+
|
142
|
+
wrapped_method = _trace_async if inspect.iscoroutinefunction(func) else _trace_sync
|
143
|
+
|
144
|
+
return wrapped_method(func, description=description)
|
145
|
+
|
146
|
+
|
147
|
+
class PromptyTracer:
|
148
|
+
def __init__(self, output_dir: str = None) -> None:
|
149
|
+
if output_dir:
|
150
|
+
self.output = Path(output_dir).resolve().absolute()
|
151
|
+
else:
|
152
|
+
self.output = Path(Path(os.getcwd()) / ".runs").resolve().absolute()
|
153
|
+
|
154
|
+
if not self.output.exists():
|
155
|
+
self.output.mkdir(parents=True, exist_ok=True)
|
156
|
+
|
157
|
+
self.stack: List[Dict[str, Any]] = []
|
158
|
+
|
159
|
+
@contextlib.contextmanager
|
160
|
+
def tracer(self, name: str) -> Iterator[Callable[[str, Any], None]]:
|
161
|
+
try:
|
162
|
+
self.stack.append({"name": name})
|
163
|
+
frame = self.stack[-1]
|
164
|
+
|
165
|
+
def add(key: str, value: Any) -> None:
|
166
|
+
if key not in frame:
|
167
|
+
frame[key] = value
|
168
|
+
# multiple values creates list
|
169
|
+
else:
|
170
|
+
if isinstance(frame[key], list):
|
171
|
+
frame[key].append(value)
|
172
|
+
else:
|
173
|
+
frame[key] = [frame[key], value]
|
174
|
+
|
175
|
+
yield add
|
176
|
+
finally:
|
177
|
+
frame = self.stack.pop()
|
178
|
+
# if stack is empty, dump the frame
|
179
|
+
if len(self.stack) == 0:
|
180
|
+
trace_file = (
|
181
|
+
self.output
|
182
|
+
/ f"{frame['name']}.{datetime.now().strftime('%Y%m%d.%H%M%S')}.ptrace"
|
183
|
+
)
|
184
|
+
|
185
|
+
with open(trace_file, "w") as f:
|
186
|
+
json.dump(frame, f, indent=4)
|
187
|
+
# otherwise, append the frame to the parent
|
188
|
+
else:
|
189
|
+
if "__frames" not in self.stack[-1]:
|
190
|
+
self.stack[-1]["__frames"] = []
|
191
|
+
self.stack[-1]["__frames"].append(frame)
|
192
|
+
|
193
|
+
|
194
|
+
@contextlib.contextmanager
|
195
|
+
def console_tracer(name: str) -> Iterator[Callable[[str, Any], None]]:
|
196
|
+
try:
|
197
|
+
print(f"Starting {name}")
|
198
|
+
yield lambda key, value: print(f"{key}:\n{json.dumps(value, indent=4)}")
|
199
|
+
finally:
|
200
|
+
print(f"Ending {name}")
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
from pathlib import Path
|
3
|
+
from prompty.core import PromptyStream
|
3
4
|
from openai.types.chat import ChatCompletionChunk
|
4
5
|
from prompty import Invoker, Prompty, InvokerFactory
|
5
6
|
from openai.types.chat.chat_completion import ChatCompletion
|
@@ -29,11 +30,12 @@ class FakeAzureExecutor(Invoker):
|
|
29
30
|
|
30
31
|
if self.parameters.get("stream", False):
|
31
32
|
items = json.loads(j)
|
33
|
+
|
32
34
|
def generator():
|
33
35
|
for i in range(1, len(items)):
|
34
36
|
yield ChatCompletionChunk.model_validate(items[i])
|
35
|
-
|
36
|
-
return generator()
|
37
|
+
|
38
|
+
return PromptyStream("FakeAzureExecutor", generator())
|
37
39
|
|
38
40
|
elif self.api == "chat":
|
39
41
|
return ChatCompletion.model_validate_json(j)
|
@@ -1,13 +1,8 @@
|
|
1
1
|
import pytest
|
2
2
|
import prompty
|
3
|
-
from pathlib import Path
|
4
|
-
|
5
3
|
from prompty.tracer import trace
|
6
4
|
|
7
5
|
|
8
|
-
BASE_PATH = str(Path(__file__).absolute().parent.as_posix())
|
9
|
-
|
10
|
-
|
11
6
|
@pytest.mark.parametrize(
|
12
7
|
"prompt",
|
13
8
|
[
|
@@ -126,6 +121,7 @@ def test_function_calling():
|
|
126
121
|
)
|
127
122
|
print(result)
|
128
123
|
|
124
|
+
|
129
125
|
# need to add trace attribute to
|
130
126
|
# materialize stream into the function
|
131
127
|
# trace decorator
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import pytest
|
2
1
|
import prompty
|
3
2
|
from pathlib import Path
|
4
3
|
|
@@ -9,24 +8,30 @@ def test_prompty_config_local():
|
|
9
8
|
p = prompty.load(f"{BASE_PATH}/prompts/sub/sub/basic.prompty")
|
10
9
|
assert p.model.configuration["type"] == "TEST_LOCAL"
|
11
10
|
|
11
|
+
|
12
12
|
def test_prompty_config_global():
|
13
13
|
p = prompty.load(f"{BASE_PATH}/prompts/sub/basic.prompty")
|
14
14
|
assert p.model.configuration["type"] == "azure"
|
15
15
|
|
16
16
|
|
17
17
|
def test_prompty_config_headless():
|
18
|
-
p = prompty.headless(
|
18
|
+
p = prompty.headless(
|
19
|
+
"embedding", ["this is the first line", "this is the second line"]
|
20
|
+
)
|
19
21
|
assert p.model.configuration["type"] == "FROM_CONTENT"
|
20
22
|
|
23
|
+
|
21
24
|
# make sure the prompty path is
|
22
25
|
# relative to the current executing file
|
23
26
|
def test_prompty_relative_local():
|
24
27
|
from .prompts.test import run
|
28
|
+
|
25
29
|
p = run()
|
26
30
|
assert p.name == "Basic Prompt"
|
27
31
|
|
28
32
|
|
29
33
|
def test_prompty_relative():
|
30
34
|
from .prompts.sub.sub.test import run
|
35
|
+
|
31
36
|
p = run()
|
32
37
|
assert p.name == "Prompt with complex context"
|
@@ -0,0 +1,146 @@
|
|
1
|
+
import pytest
|
2
|
+
import prompty
|
3
|
+
from prompty.tracer import trace, Tracer, console_tracer, PromptyTracer
|
4
|
+
|
5
|
+
|
6
|
+
@pytest.fixture
|
7
|
+
def setup_tracing():
|
8
|
+
Tracer.add("console", console_tracer)
|
9
|
+
json_tracer = PromptyTracer()
|
10
|
+
Tracer.add("console", json_tracer.tracer)
|
11
|
+
|
12
|
+
|
13
|
+
@pytest.mark.parametrize(
|
14
|
+
"prompt",
|
15
|
+
[
|
16
|
+
"prompts/basic.prompty",
|
17
|
+
"prompts/context.prompty",
|
18
|
+
"prompts/groundedness.prompty",
|
19
|
+
"prompts/faithfulness.prompty",
|
20
|
+
"prompts/embedding.prompty",
|
21
|
+
],
|
22
|
+
)
|
23
|
+
def test_basic_execution(prompt: str, setup_tracing):
|
24
|
+
result = prompty.execute(prompt)
|
25
|
+
print(result)
|
26
|
+
|
27
|
+
|
28
|
+
@trace
|
29
|
+
def get_customer(customerId):
|
30
|
+
return {"id": customerId, "firstName": "Sally", "lastName": "Davis"}
|
31
|
+
|
32
|
+
|
33
|
+
@trace
|
34
|
+
def get_context(search):
|
35
|
+
return [
|
36
|
+
{
|
37
|
+
"id": "17",
|
38
|
+
"name": "RainGuard Hiking Jacket",
|
39
|
+
"price": 110,
|
40
|
+
"category": "Hiking Clothing",
|
41
|
+
"brand": "MountainStyle",
|
42
|
+
"description": "Introducing the MountainStyle RainGuard Hiking Jacket - the ultimate solution for weatherproof comfort during your outdoor undertakings! Designed with waterproof, breathable fabric, this jacket promises an outdoor experience that's as dry as it is comfortable. The rugged construction assures durability, while the adjustable hood provides a customizable fit against wind and rain. Featuring multiple pockets for safe, convenient storage and adjustable cuffs and hem, you can tailor the jacket to suit your needs on-the-go. And, don't worry about overheating during intense activities - it's equipped with ventilation zippers for increased airflow. Reflective details ensure visibility even during low-light conditions, making it perfect for evening treks. With its lightweight, packable design, carrying it inside your backpack requires minimal effort. With options for men and women, the RainGuard Hiking Jacket is perfect for hiking, camping, trekking and countless other outdoor adventures. Don't let the weather stand in your way - embrace the outdoors with MountainStyle RainGuard Hiking Jacket!",
|
43
|
+
},
|
44
|
+
{
|
45
|
+
"id": "3",
|
46
|
+
"name": "Summit Breeze Jacket",
|
47
|
+
"price": 120,
|
48
|
+
"category": "Hiking Clothing",
|
49
|
+
"brand": "MountainStyle",
|
50
|
+
"description": "Discover the joy of hiking with MountainStyle's Summit Breeze Jacket. This lightweight jacket is your perfect companion for outdoor adventures. Sporting a trail-ready, windproof design and a water-resistant fabric, it's ready to withstand any weather. The breathable polyester material and adjustable cuffs keep you comfortable, whether you're ascending a mountain or strolling through a park. And its sleek black color adds style to function. The jacket features a full-zip front closure, adjustable hood, and secure zippered pockets. Experience the comfort of its inner lining and the convenience of its packable design. Crafted for night trekkers too, the jacket has reflective accents for enhanced visibility. Rugged yet chic, the Summit Breeze Jacket is more than a hiking essential, it's the gear that inspires you to reach new heights. Choose adventure, choose the Summit Breeze Jacket.",
|
51
|
+
},
|
52
|
+
{
|
53
|
+
"id": "10",
|
54
|
+
"name": "TrailBlaze Hiking Pants",
|
55
|
+
"price": 75,
|
56
|
+
"category": "Hiking Clothing",
|
57
|
+
"brand": "MountainStyle",
|
58
|
+
"description": "Meet the TrailBlaze Hiking Pants from MountainStyle, the stylish khaki champions of the trails. These are not just pants; they're your passport to outdoor adventure. Crafted from high-quality nylon fabric, these dapper troopers are lightweight and fast-drying, with a water-resistant armor that laughs off light rain. Their breathable design whisks away sweat while their articulated knees grant you the flexibility of a mountain goat. Zippered pockets guard your essentials, making them a hiker's best ally. Designed with durability for all your trekking trials, these pants come with a comfortable, ergonomic fit that will make you forget you're wearing them. Sneak a peek, and you are sure to be tempted by the sleek allure that is the TrailBlaze Hiking Pants. Your outdoors wardrobe wouldn't be quite complete without them.",
|
59
|
+
},
|
60
|
+
]
|
61
|
+
|
62
|
+
|
63
|
+
@trace
|
64
|
+
def get_response(customerId, question, prompt):
|
65
|
+
customer = get_customer(customerId)
|
66
|
+
context = get_context(question)
|
67
|
+
|
68
|
+
result = prompty.execute(
|
69
|
+
prompt,
|
70
|
+
inputs={"question": question, "customer": customer, "documentation": context},
|
71
|
+
)
|
72
|
+
return {"question": question, "answer": result, "context": context}
|
73
|
+
|
74
|
+
|
75
|
+
@trace
|
76
|
+
def test_context_flow(setup_tracing):
|
77
|
+
customerId = 1
|
78
|
+
question = "tell me about your jackets"
|
79
|
+
prompt = "context.prompty"
|
80
|
+
|
81
|
+
response = get_response(customerId, question, f"prompts/{prompt}")
|
82
|
+
print(response)
|
83
|
+
|
84
|
+
|
85
|
+
@trace
|
86
|
+
def evaluate(prompt, evalprompt, customerId, question):
|
87
|
+
response = get_response(customerId, question, prompt)
|
88
|
+
|
89
|
+
result = prompty.execute(
|
90
|
+
evalprompt,
|
91
|
+
inputs=response,
|
92
|
+
)
|
93
|
+
return result
|
94
|
+
|
95
|
+
|
96
|
+
@trace
|
97
|
+
def test_context_groundedness(setup_tracing):
|
98
|
+
result = evaluate(
|
99
|
+
"prompts/context.prompty",
|
100
|
+
"prompts/groundedness.prompty",
|
101
|
+
1,
|
102
|
+
"tell me about your jackets",
|
103
|
+
)
|
104
|
+
print(result)
|
105
|
+
|
106
|
+
|
107
|
+
@trace
|
108
|
+
def test_embedding_headless(setup_tracing):
|
109
|
+
p = prompty.headless(
|
110
|
+
api="embedding",
|
111
|
+
configuration={"type": "azure", "azure_deployment": "text-embedding-ada-002"},
|
112
|
+
content="hello world",
|
113
|
+
)
|
114
|
+
emb = prompty.execute(p)
|
115
|
+
print(emb)
|
116
|
+
|
117
|
+
|
118
|
+
@trace
|
119
|
+
def test_embeddings_headless(setup_tracing):
|
120
|
+
p = prompty.headless(
|
121
|
+
api="embedding",
|
122
|
+
configuration={"type": "azure", "azure_deployment": "text-embedding-ada-002"},
|
123
|
+
content=["hello world", "goodbye world", "hello again"],
|
124
|
+
)
|
125
|
+
emb = prompty.execute(p)
|
126
|
+
print(emb)
|
127
|
+
|
128
|
+
|
129
|
+
@trace
|
130
|
+
def test_function_calling(setup_tracing):
|
131
|
+
result = prompty.execute(
|
132
|
+
"prompts/functions.prompty",
|
133
|
+
)
|
134
|
+
print(result)
|
135
|
+
|
136
|
+
|
137
|
+
# need to add trace attribute to
|
138
|
+
# materialize stream into the function
|
139
|
+
# trace decorator
|
140
|
+
@trace
|
141
|
+
def test_streaming(setup_tracing):
|
142
|
+
result = prompty.execute(
|
143
|
+
"prompts/streaming.prompty",
|
144
|
+
)
|
145
|
+
for item in result:
|
146
|
+
print(item)
|
prompty-0.1.8/prompty/tracer.py
DELETED
@@ -1,231 +0,0 @@
|
|
1
|
-
import abc
|
2
|
-
import json
|
3
|
-
import inspect
|
4
|
-
import datetime
|
5
|
-
from numbers import Number
|
6
|
-
import os
|
7
|
-
from datetime import datetime
|
8
|
-
from pathlib import Path
|
9
|
-
from pydantic import BaseModel
|
10
|
-
from functools import wraps, partial
|
11
|
-
from typing import Any, Callable, Dict, List
|
12
|
-
|
13
|
-
|
14
|
-
class Tracer(abc.ABC):
|
15
|
-
|
16
|
-
@abc.abstractmethod
|
17
|
-
def start(self, name: str) -> None:
|
18
|
-
pass
|
19
|
-
|
20
|
-
@abc.abstractmethod
|
21
|
-
def add(self, key: str, value: Any) -> None:
|
22
|
-
pass
|
23
|
-
|
24
|
-
@abc.abstractmethod
|
25
|
-
def end(self) -> None:
|
26
|
-
pass
|
27
|
-
|
28
|
-
|
29
|
-
class Trace:
|
30
|
-
_tracers: Dict[str, Tracer] = {}
|
31
|
-
|
32
|
-
@classmethod
|
33
|
-
def add_tracer(cls, name: str, tracer: Tracer) -> None:
|
34
|
-
cls._tracers[name] = tracer
|
35
|
-
|
36
|
-
@classmethod
|
37
|
-
def start(cls, name: str) -> None:
|
38
|
-
for tracer in cls._tracers.values():
|
39
|
-
tracer.start(name)
|
40
|
-
|
41
|
-
@classmethod
|
42
|
-
def add(cls, name: str, value: Any) -> None:
|
43
|
-
for tracer in cls._tracers.values():
|
44
|
-
tracer.add(name, value)
|
45
|
-
|
46
|
-
@classmethod
|
47
|
-
def end(cls) -> None:
|
48
|
-
for tracer in cls._tracers.values():
|
49
|
-
tracer.end()
|
50
|
-
|
51
|
-
@classmethod
|
52
|
-
def clear(cls) -> None:
|
53
|
-
cls._tracers = {}
|
54
|
-
|
55
|
-
@classmethod
|
56
|
-
def register(cls, name: str):
|
57
|
-
def inner_wrapper(wrapped_class: Tracer) -> Callable:
|
58
|
-
cls._tracers[name] = wrapped_class()
|
59
|
-
return wrapped_class
|
60
|
-
|
61
|
-
return inner_wrapper
|
62
|
-
|
63
|
-
@classmethod
|
64
|
-
def to_dict(cls, obj: Any) -> Dict[str, Any]:
|
65
|
-
# simple json types
|
66
|
-
if isinstance(obj, str) or isinstance(obj, Number) or isinstance(obj, bool):
|
67
|
-
return obj
|
68
|
-
# datetime
|
69
|
-
elif isinstance(obj, datetime):
|
70
|
-
return obj.isoformat()
|
71
|
-
# safe Prompty obj serialization
|
72
|
-
elif type(obj).__name__ == "Prompty":
|
73
|
-
return obj.to_safe_dict()
|
74
|
-
# pydantic models have their own json serialization
|
75
|
-
elif isinstance(obj, BaseModel):
|
76
|
-
return obj.model_dump()
|
77
|
-
# recursive list and dict
|
78
|
-
elif isinstance(obj, list):
|
79
|
-
return [Trace.to_dict(item) for item in obj]
|
80
|
-
elif isinstance(obj, dict):
|
81
|
-
return {
|
82
|
-
k: v if isinstance(v, str) else Trace.to_dict(v)
|
83
|
-
for k, v in obj.items()
|
84
|
-
}
|
85
|
-
elif isinstance(obj, Path):
|
86
|
-
return str(obj)
|
87
|
-
# cast to string otherwise...
|
88
|
-
else:
|
89
|
-
return str(obj)
|
90
|
-
|
91
|
-
|
92
|
-
def _name(func: Callable, args):
|
93
|
-
if hasattr(func, "__qualname__"):
|
94
|
-
signature = f"{func.__module__}.{func.__qualname__}"
|
95
|
-
else:
|
96
|
-
signature = f"{func.__module__}.{func.__name__}"
|
97
|
-
|
98
|
-
# core invoker gets special treatment
|
99
|
-
core_invoker = signature == "prompty.core.Invoker.__call__"
|
100
|
-
if core_invoker:
|
101
|
-
name = type(args[0]).__name__
|
102
|
-
signature = f"{args[0].__module__}.{args[0].__class__.__name__}.invoke"
|
103
|
-
else:
|
104
|
-
name = func.__name__
|
105
|
-
|
106
|
-
return name, signature
|
107
|
-
|
108
|
-
|
109
|
-
def _inputs(func: Callable, args, kwargs) -> dict:
|
110
|
-
ba = inspect.signature(func).bind(*args, **kwargs)
|
111
|
-
ba.apply_defaults()
|
112
|
-
|
113
|
-
inputs = {k: Trace.to_dict(v) for k, v in ba.arguments.items() if k != "self"}
|
114
|
-
|
115
|
-
return inputs
|
116
|
-
|
117
|
-
def _results(result: Any) -> dict:
|
118
|
-
return {
|
119
|
-
"result": Trace.to_dict(result) if result is not None else "None",
|
120
|
-
}
|
121
|
-
|
122
|
-
def _trace_sync(func: Callable = None, *, description: str = None) -> Callable:
|
123
|
-
description = description or ""
|
124
|
-
|
125
|
-
@wraps(func)
|
126
|
-
def wrapper(*args, **kwargs):
|
127
|
-
name, signature = _name(func, args)
|
128
|
-
Trace.start(name)
|
129
|
-
Trace.add("signature", signature)
|
130
|
-
if description and description != "":
|
131
|
-
Trace.add("description", description)
|
132
|
-
|
133
|
-
inputs = _inputs(func, args, kwargs)
|
134
|
-
Trace.add("inputs", inputs)
|
135
|
-
|
136
|
-
result = func(*args, **kwargs)
|
137
|
-
Trace.add("result", _results(result))
|
138
|
-
|
139
|
-
Trace.end()
|
140
|
-
|
141
|
-
return result
|
142
|
-
|
143
|
-
return wrapper
|
144
|
-
|
145
|
-
def _trace_async(func: Callable = None, *, description: str = None) -> Callable:
|
146
|
-
description = description or ""
|
147
|
-
|
148
|
-
@wraps(func)
|
149
|
-
async def wrapper(*args, **kwargs):
|
150
|
-
name, signature = _name(func, args)
|
151
|
-
Trace.start(name)
|
152
|
-
Trace.add("signature", signature)
|
153
|
-
if description and description != "":
|
154
|
-
Trace.add("description", description)
|
155
|
-
|
156
|
-
inputs = _inputs(func, args, kwargs)
|
157
|
-
Trace.add("inputs", inputs)
|
158
|
-
|
159
|
-
result = await func(*args, **kwargs)
|
160
|
-
Trace.add("result", _results(result))
|
161
|
-
|
162
|
-
Trace.end()
|
163
|
-
|
164
|
-
return result
|
165
|
-
|
166
|
-
return wrapper
|
167
|
-
|
168
|
-
def trace(func: Callable = None, *, description: str = None) -> Callable:
|
169
|
-
if func is None:
|
170
|
-
return partial(trace, description=description)
|
171
|
-
|
172
|
-
wrapped_method = (
|
173
|
-
_trace_async if inspect.iscoroutinefunction(func) else _trace_sync
|
174
|
-
)
|
175
|
-
|
176
|
-
return wrapped_method(func, description=description)
|
177
|
-
|
178
|
-
|
179
|
-
class PromptyTracer(Tracer):
|
180
|
-
_stack: List[Dict[str, Any]] = []
|
181
|
-
_name: str = None
|
182
|
-
|
183
|
-
def __init__(self, output_dir: str = None) -> None:
|
184
|
-
super().__init__()
|
185
|
-
if output_dir:
|
186
|
-
self.root = Path(output_dir).resolve().absolute()
|
187
|
-
else:
|
188
|
-
self.root = Path(Path(os.getcwd()) / ".runs").resolve().absolute()
|
189
|
-
|
190
|
-
if not self.root.exists():
|
191
|
-
self.root.mkdir(parents=True, exist_ok=True)
|
192
|
-
|
193
|
-
def start(self, name: str) -> None:
|
194
|
-
self._stack.append({"name": name})
|
195
|
-
# first entry frame
|
196
|
-
if self._name is None:
|
197
|
-
self._name = name
|
198
|
-
|
199
|
-
def add(self, name: str, value: Any) -> None:
|
200
|
-
frame = self._stack[-1]
|
201
|
-
if name not in frame:
|
202
|
-
frame[name] = value
|
203
|
-
# multiple values creates list
|
204
|
-
else:
|
205
|
-
if isinstance(frame[name], list):
|
206
|
-
frame[name].append(value)
|
207
|
-
else:
|
208
|
-
frame[name] = [frame[name], value]
|
209
|
-
|
210
|
-
|
211
|
-
def end(self) -> None:
|
212
|
-
# pop the current stack
|
213
|
-
frame = self._stack.pop()
|
214
|
-
|
215
|
-
# if stack is empty, dump the frame
|
216
|
-
if len(self._stack) == 0:
|
217
|
-
self.flush(frame)
|
218
|
-
# otherwise, append the frame to the parent
|
219
|
-
else:
|
220
|
-
if "__frames" not in self._stack[-1]:
|
221
|
-
self._stack[-1]["__frames"] = []
|
222
|
-
self._stack[-1]["__frames"].append(frame)
|
223
|
-
|
224
|
-
def flush(self, frame: Dict[str, Any]) -> None:
|
225
|
-
|
226
|
-
trace_file = (
|
227
|
-
self.root / f"{self._name}.{datetime.now().strftime('%Y%m%d.%H%M%S')}.ptrace"
|
228
|
-
)
|
229
|
-
|
230
|
-
with open(trace_file, "w") as f:
|
231
|
-
json.dump(frame, f, indent=4)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|