trulens-providers-cortex 1.0.1__tar.gz
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.
- trulens_providers_cortex-1.0.1/PKG-INFO +26 -0
- trulens_providers_cortex-1.0.1/README.md +1 -0
- trulens_providers_cortex-1.0.1/pyproject.toml +38 -0
- trulens_providers_cortex-1.0.1/trulens/providers/cortex/__init__.py +21 -0
- trulens_providers_cortex-1.0.1/trulens/providers/cortex/config/cortex_model_costs.json +16 -0
- trulens_providers_cortex-1.0.1/trulens/providers/cortex/endpoint.py +151 -0
- trulens_providers_cortex-1.0.1/trulens/providers/cortex/provider.py +175 -0
- trulens_providers_cortex-1.0.1/trulens/providers/cortex/py.typed +0 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: trulens-providers-cortex
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: A TruLens extension package adding Snowflake Cortex support for LLM App evaluation.
|
|
5
|
+
Home-page: https://trulens.org/
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Snowflake Inc.
|
|
8
|
+
Author-email: ml-observability-wg-dl@snowflake.com
|
|
9
|
+
Requires-Python: >=3.9,<3.12
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Requires-Dist: snowflake-connector-python (>=3.11,<4.0)
|
|
18
|
+
Requires-Dist: snowflake-snowpark-python (>=1.18,<2.0)
|
|
19
|
+
Requires-Dist: trulens-core (>=1.0.0,<2.0.0)
|
|
20
|
+
Requires-Dist: trulens-feedback (>=1.0.0,<2.0.0)
|
|
21
|
+
Project-URL: Documentation, https://trulens.org/trulens/getting_started/
|
|
22
|
+
Project-URL: Repository, https://github.com/truera/trulens
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# trulens-providers-cortex
|
|
26
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# trulens-providers-cortex
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
build-backend = "poetry.core.masonry.api"
|
|
3
|
+
requires = [
|
|
4
|
+
"poetry-core",
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
[tool.poetry]
|
|
8
|
+
name = "trulens-providers-cortex"
|
|
9
|
+
version = "1.0.1"
|
|
10
|
+
description = "A TruLens extension package adding Snowflake Cortex support for LLM App evaluation."
|
|
11
|
+
authors = [
|
|
12
|
+
"Snowflake Inc. <ml-observability-wg-dl@snowflake.com>",
|
|
13
|
+
]
|
|
14
|
+
license = "MIT"
|
|
15
|
+
readme = "README.md"
|
|
16
|
+
packages = [
|
|
17
|
+
{ include = "trulens" },
|
|
18
|
+
]
|
|
19
|
+
homepage = "https://trulens.org/"
|
|
20
|
+
documentation = "https://trulens.org/trulens/getting_started/"
|
|
21
|
+
repository = "https://github.com/truera/trulens"
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.poetry.dependencies]
|
|
30
|
+
python = ">=3.9,<3.12"
|
|
31
|
+
trulens-core = { version = "^1.0.0" }
|
|
32
|
+
trulens-feedback = { version = "^1.0.0" }
|
|
33
|
+
snowflake-connector-python = "^3.11"
|
|
34
|
+
snowflake-snowpark-python = "^1.18"
|
|
35
|
+
|
|
36
|
+
[tool.poetry.group.dev.dependencies]
|
|
37
|
+
trulens-core = { path = "../../core" }
|
|
38
|
+
trulens-feedback = { path = "../../feedback" }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
!!! note "Additional Dependency Required"
|
|
3
|
+
|
|
4
|
+
To use this module, you must have the `trulens-providers-cortex` package installed.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install trulens-providers-cortex
|
|
8
|
+
```
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
|
|
13
|
+
from trulens.core.utils.imports import safe_importlib_package_name
|
|
14
|
+
from trulens.providers.cortex.provider import Cortex
|
|
15
|
+
|
|
16
|
+
__version__ = version(safe_importlib_package_name(__package__ or __name__))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"Cortex",
|
|
21
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"reka-core": 5.5,
|
|
3
|
+
"mistral-large": 5.1,
|
|
4
|
+
"llama3.1-405b": 5,
|
|
5
|
+
"llama3-70b": 1.21,
|
|
6
|
+
"llama3.1-70b": 1.21,
|
|
7
|
+
"snowflake-arctic": 0.84,
|
|
8
|
+
"jamba-instruct": 0.83,
|
|
9
|
+
"llama2-chat-70b": 0.45,
|
|
10
|
+
"reka-flash": 0.45,
|
|
11
|
+
"mixtral-8x7b": 0.22,
|
|
12
|
+
"llama3-8b": 0.19,
|
|
13
|
+
"llama3.1-8b": 0.19,
|
|
14
|
+
"mistral-7b": 0.12,
|
|
15
|
+
"gemma-7b": 0.12
|
|
16
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import pprint
|
|
6
|
+
from typing import Any, Callable, ClassVar, Optional
|
|
7
|
+
|
|
8
|
+
from snowflake.connector.cursor import SnowflakeCursor
|
|
9
|
+
from snowflake.snowpark import DataFrame
|
|
10
|
+
from snowflake.snowpark import Session
|
|
11
|
+
from trulens.core.feedback import Endpoint
|
|
12
|
+
from trulens.core.feedback import EndpointCallback
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
pp = pprint.PrettyPrinter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CortexCallback(EndpointCallback):
|
|
20
|
+
model_config: ClassVar[dict] = dict(arbitrary_types_allowed=True)
|
|
21
|
+
_model_costs: Optional[dict] = None
|
|
22
|
+
# TODO (Daniel): cost tracking for Cortex finetuned models is not yet implemented.
|
|
23
|
+
|
|
24
|
+
def _compute_credits_consumed(
|
|
25
|
+
self, cortex_model_name: str, n_tokens: int
|
|
26
|
+
) -> float:
|
|
27
|
+
try:
|
|
28
|
+
if self._model_costs is None:
|
|
29
|
+
# the credit consumption table needs to be kept up-to-date with
|
|
30
|
+
# the latest cost information https://www.snowflake.com/legal-files/CreditConsumptionTable.pdf#page=9.
|
|
31
|
+
|
|
32
|
+
with open(
|
|
33
|
+
os.path.join(
|
|
34
|
+
os.path.dirname(os.path.realpath(__file__)),
|
|
35
|
+
"config/cortex_model_costs.json",
|
|
36
|
+
),
|
|
37
|
+
"r",
|
|
38
|
+
) as file:
|
|
39
|
+
self._model_costs = json.load(file)
|
|
40
|
+
|
|
41
|
+
if cortex_model_name in self._model_costs:
|
|
42
|
+
return (
|
|
43
|
+
self._model_costs[cortex_model_name] * n_tokens / 1e6
|
|
44
|
+
) # we maintain config per-1M-token cost
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Model {cortex_model_name} not valid or not supported yet for cost estimation."
|
|
48
|
+
)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
logger.error(
|
|
51
|
+
f"Error occurred while computing credits consumed for model {cortex_model_name}: {e}"
|
|
52
|
+
)
|
|
53
|
+
return 0.0
|
|
54
|
+
|
|
55
|
+
def handle_generation(self, response: dict) -> None:
|
|
56
|
+
"""Get the usage information from Cortex LLM function response's usage field."""
|
|
57
|
+
usage = response["usage"]
|
|
58
|
+
|
|
59
|
+
# Increment number of requests.
|
|
60
|
+
super().handle_generation(response)
|
|
61
|
+
|
|
62
|
+
# Assume a response that had usage field was successful. Note at the time of writing 06/12/2024, the usage
|
|
63
|
+
# information from Cortex LLM functions is only available when called via snow SQL. It's not fully supported in
|
|
64
|
+
# Python API such as `from snowflake.cortex import Summarize, Complete, ExtractAnswer, Sentiment, Translate` yet.
|
|
65
|
+
|
|
66
|
+
self.cost.n_successful_requests += 1
|
|
67
|
+
|
|
68
|
+
for cost_field, cortex_field in [
|
|
69
|
+
("n_tokens", "total_tokens"),
|
|
70
|
+
("n_cortext_guardrails_tokens", "guardrails_tokens"),
|
|
71
|
+
("n_prompt_tokens", "prompt_tokens"),
|
|
72
|
+
("n_completion_tokens", "completion_tokens"),
|
|
73
|
+
]:
|
|
74
|
+
setattr(
|
|
75
|
+
self.cost,
|
|
76
|
+
cost_field,
|
|
77
|
+
getattr(self.cost, cost_field, 0) + usage.get(cortex_field, 0),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# compute credits consumed in Snowflake account based on tokens processed
|
|
81
|
+
setattr(
|
|
82
|
+
self.cost,
|
|
83
|
+
"cost",
|
|
84
|
+
getattr(self.cost, "cost", 0)
|
|
85
|
+
+ self._compute_credits_consumed(
|
|
86
|
+
response["model"], usage.get("total_tokens", 0)
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
setattr(self.cost, "cost_currency", "Snowflake credits")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CortexEndpoint(Endpoint):
|
|
94
|
+
"""Snowflake Cortex endpoint."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, *args, **kwargs):
|
|
97
|
+
if hasattr(self, "name"):
|
|
98
|
+
# singleton already made
|
|
99
|
+
if len(kwargs) > 0:
|
|
100
|
+
logger.warning(
|
|
101
|
+
"Ignoring additional kwargs for singleton endpoint %s: %s",
|
|
102
|
+
self.name,
|
|
103
|
+
pp.pformat(kwargs),
|
|
104
|
+
)
|
|
105
|
+
self.warning()
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
kwargs["name"] = "cortex"
|
|
109
|
+
kwargs["callback_class"] = CortexCallback
|
|
110
|
+
|
|
111
|
+
super().__init__(*args, **kwargs)
|
|
112
|
+
|
|
113
|
+
# Instrument various methods for usage/cost tracking.
|
|
114
|
+
self._instrument_class(Session, "sql")
|
|
115
|
+
self._instrument_class(SnowflakeCursor, "fetchall")
|
|
116
|
+
|
|
117
|
+
def __new__(cls, *args, **kwargs):
|
|
118
|
+
return super(Endpoint, cls).__new__(cls, name="cortex")
|
|
119
|
+
|
|
120
|
+
def handle_wrapped_call(
|
|
121
|
+
self,
|
|
122
|
+
func: Callable,
|
|
123
|
+
bindings: inspect.BoundArguments,
|
|
124
|
+
response: Any,
|
|
125
|
+
callback: Optional[EndpointCallback],
|
|
126
|
+
) -> None:
|
|
127
|
+
counted_something = False
|
|
128
|
+
|
|
129
|
+
# response is a snowflake dataframe instance or a list if the response is from cursor.fetchall()
|
|
130
|
+
try:
|
|
131
|
+
if isinstance(response, DataFrame):
|
|
132
|
+
response: dict = json.loads(response.collect()[0][0])
|
|
133
|
+
elif isinstance(response, list):
|
|
134
|
+
response: dict = json.loads(response[0][0])
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"Error occurred while parsing response: {e}")
|
|
137
|
+
raise e
|
|
138
|
+
|
|
139
|
+
if "usage" in response:
|
|
140
|
+
counted_something = True
|
|
141
|
+
|
|
142
|
+
self.global_callback.handle_generation(response=response)
|
|
143
|
+
|
|
144
|
+
if callback is not None:
|
|
145
|
+
callback.handle_generation(response=response)
|
|
146
|
+
|
|
147
|
+
if not counted_something:
|
|
148
|
+
logger.warning(
|
|
149
|
+
"Unrecognized Cortex response format. It did not have usage information:\n%s",
|
|
150
|
+
pp.pformat(response),
|
|
151
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, ClassVar, Dict, Optional, Sequence
|
|
3
|
+
|
|
4
|
+
from trulens.feedback import LLMProvider
|
|
5
|
+
from trulens.providers.cortex.endpoint import CortexEndpoint
|
|
6
|
+
|
|
7
|
+
# If this is set, the provider will use this connection. This is useful for server-side evaluations which are done in a stored procedure and must have a single connection throughout the life of the stored procedure.
|
|
8
|
+
# TODO: This is a bit of a hack to pass the connection to the provider. Explore options on how to improve this.
|
|
9
|
+
_SNOWFLAKE_STORED_PROCEDURE_CONNECTION: Any = None
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Cortex(
|
|
13
|
+
LLMProvider
|
|
14
|
+
): # require `pip install snowflake-snowpark-python` and a active Snowflake account with proper privileges
|
|
15
|
+
# https://docs.snowflake.com/en/user-guide/snowflake-cortex/llm-functions#availability
|
|
16
|
+
|
|
17
|
+
DEFAULT_MODEL_ENGINE: ClassVar[str] = "snowflake-arctic"
|
|
18
|
+
|
|
19
|
+
model_engine: str
|
|
20
|
+
endpoint: CortexEndpoint
|
|
21
|
+
snowflake_conn: Any
|
|
22
|
+
|
|
23
|
+
"""Snowflake's Cortex COMPLETE endpoint. Defaults to `snowflake-arctic`.
|
|
24
|
+
Reference: https://docs.snowflake.com/en/sql-reference/functions/complete-snowflake-cortex
|
|
25
|
+
|
|
26
|
+
!!! example
|
|
27
|
+
=== Connecting with user/password:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
connection_parameters = {
|
|
31
|
+
"account": <account>,
|
|
32
|
+
"user": <user>,
|
|
33
|
+
"password": <password>,
|
|
34
|
+
"role": <role>,
|
|
35
|
+
"database": <database>,
|
|
36
|
+
"schema": <schema>,
|
|
37
|
+
"warehouse": <warehouse>
|
|
38
|
+
}
|
|
39
|
+
provider = Cortex(snowflake.connector.connect(
|
|
40
|
+
**connection_parameters
|
|
41
|
+
))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
=== Connecting with private key:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
connection_parameters = {
|
|
48
|
+
"account": <account>,
|
|
49
|
+
"user": <user>,
|
|
50
|
+
"private_key": <private_key>,
|
|
51
|
+
"role": <role>,
|
|
52
|
+
"database": <database>,
|
|
53
|
+
"schema": <schema>,
|
|
54
|
+
"warehouse": <warehouse>
|
|
55
|
+
}
|
|
56
|
+
provider = Cortex(snowflake.connector.connect(
|
|
57
|
+
**connection_parameters
|
|
58
|
+
))
|
|
59
|
+
|
|
60
|
+
=== Connecting with a private key file:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
connection_parameters = {
|
|
64
|
+
"account": <account>,
|
|
65
|
+
"user": <user>,
|
|
66
|
+
"private_key_file": <private_key_file>,
|
|
67
|
+
"private_key_file_pwd": <private_key_file_pwd>,
|
|
68
|
+
"role": <role>,
|
|
69
|
+
"database": <database>,
|
|
70
|
+
"schema": <schema>,
|
|
71
|
+
"warehouse": <warehouse>
|
|
72
|
+
}
|
|
73
|
+
provider = Cortex(snowflake.connector.connect(
|
|
74
|
+
**connection_parameters
|
|
75
|
+
))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
snowflake_conn (Any): Snowflake connection.
|
|
80
|
+
|
|
81
|
+
model_engine (str, optional): Model engine to use. Defaults to `snowflake-arctic`.
|
|
82
|
+
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
snowflake_conn: Any,
|
|
88
|
+
model_engine: Optional[str] = None,
|
|
89
|
+
*args,
|
|
90
|
+
**kwargs: Dict,
|
|
91
|
+
):
|
|
92
|
+
self_kwargs = dict(kwargs)
|
|
93
|
+
|
|
94
|
+
self_kwargs["model_engine"] = (
|
|
95
|
+
self.DEFAULT_MODEL_ENGINE if model_engine is None else model_engine
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self_kwargs["endpoint"] = CortexEndpoint(*args, **kwargs)
|
|
99
|
+
|
|
100
|
+
# Create a Snowflake connector
|
|
101
|
+
self_kwargs["snowflake_conn"] = _SNOWFLAKE_STORED_PROCEDURE_CONNECTION
|
|
102
|
+
if _SNOWFLAKE_STORED_PROCEDURE_CONNECTION is None:
|
|
103
|
+
self_kwargs["snowflake_conn"] = snowflake_conn
|
|
104
|
+
|
|
105
|
+
super().__init__(**self_kwargs)
|
|
106
|
+
|
|
107
|
+
def _exec_snowsql_complete_command(
|
|
108
|
+
self,
|
|
109
|
+
model: str,
|
|
110
|
+
temperature: float,
|
|
111
|
+
messages: Optional[Sequence[Dict]] = None,
|
|
112
|
+
):
|
|
113
|
+
# Ensure messages are formatted as a JSON array string
|
|
114
|
+
if messages is None:
|
|
115
|
+
messages = []
|
|
116
|
+
|
|
117
|
+
messages_json_str = json.dumps(messages)
|
|
118
|
+
|
|
119
|
+
options = {"temperature": temperature}
|
|
120
|
+
|
|
121
|
+
options_json_str = json.dumps(options)
|
|
122
|
+
|
|
123
|
+
completion_input_str = """
|
|
124
|
+
SELECT SNOWFLAKE.CORTEX.COMPLETE(
|
|
125
|
+
?,
|
|
126
|
+
parse_json(?),
|
|
127
|
+
parse_json(?)
|
|
128
|
+
)
|
|
129
|
+
"""
|
|
130
|
+
if (
|
|
131
|
+
hasattr(self.snowflake_conn, "_paramstyle")
|
|
132
|
+
and self.snowflake_conn._paramstyle == "pyformat"
|
|
133
|
+
):
|
|
134
|
+
completion_input_str = completion_input_str.replace("?", "%s")
|
|
135
|
+
|
|
136
|
+
# Executing Snow SQL command requires an active snow session
|
|
137
|
+
cursor = self.snowflake_conn.cursor()
|
|
138
|
+
try:
|
|
139
|
+
cursor.execute(
|
|
140
|
+
completion_input_str,
|
|
141
|
+
(model, messages_json_str, options_json_str),
|
|
142
|
+
)
|
|
143
|
+
result = cursor.fetchall()
|
|
144
|
+
finally:
|
|
145
|
+
cursor.close()
|
|
146
|
+
|
|
147
|
+
return result
|
|
148
|
+
|
|
149
|
+
def _create_chat_completion(
|
|
150
|
+
self,
|
|
151
|
+
prompt: Optional[str] = None,
|
|
152
|
+
messages: Optional[Sequence[Dict]] = None,
|
|
153
|
+
**kwargs,
|
|
154
|
+
) -> str:
|
|
155
|
+
if "model" not in kwargs:
|
|
156
|
+
kwargs["model"] = self.model_engine
|
|
157
|
+
if "temperature" not in kwargs:
|
|
158
|
+
kwargs["temperature"] = 0.0
|
|
159
|
+
|
|
160
|
+
if messages is not None:
|
|
161
|
+
kwargs["messages"] = messages
|
|
162
|
+
|
|
163
|
+
elif prompt is not None:
|
|
164
|
+
kwargs["messages"] = [{"role": "system", "content": prompt}]
|
|
165
|
+
else:
|
|
166
|
+
raise ValueError("`prompt` or `messages` must be specified.")
|
|
167
|
+
|
|
168
|
+
res = self._exec_snowsql_complete_command(**kwargs)
|
|
169
|
+
|
|
170
|
+
if len(res) == 0 or len(res[0]) == 0:
|
|
171
|
+
raise ValueError("No completion returned from Snowflake Cortex.")
|
|
172
|
+
|
|
173
|
+
completion = json.loads(res[0][0])["choices"][0]["messages"]
|
|
174
|
+
|
|
175
|
+
return completion
|
|
File without changes
|