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.
@@ -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