arize-phoenix 7.12.3__py3-none-any.whl → 8.0.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.

Potentially problematic release.


This version of arize-phoenix might be problematic. Click here for more details.

Files changed (80) hide show
  1. {arize_phoenix-7.12.3.dist-info → arize_phoenix-8.0.0.dist-info}/METADATA +31 -28
  2. {arize_phoenix-7.12.3.dist-info → arize_phoenix-8.0.0.dist-info}/RECORD +70 -47
  3. phoenix/db/migrations/versions/bc8fea3c2bc8_add_prompt_tables.py +197 -0
  4. phoenix/db/models.py +307 -0
  5. phoenix/db/types/__init__.py +0 -0
  6. phoenix/db/types/identifier.py +7 -0
  7. phoenix/db/types/model_provider.py +8 -0
  8. phoenix/server/api/context.py +2 -0
  9. phoenix/server/api/dataloaders/__init__.py +2 -0
  10. phoenix/server/api/dataloaders/prompt_version_sequence_number.py +35 -0
  11. phoenix/server/api/helpers/jsonschema.py +135 -0
  12. phoenix/server/api/helpers/playground_clients.py +15 -15
  13. phoenix/server/api/helpers/playground_spans.py +9 -0
  14. phoenix/server/api/helpers/prompts/__init__.py +0 -0
  15. phoenix/server/api/helpers/prompts/conversions/__init__.py +0 -0
  16. phoenix/server/api/helpers/prompts/conversions/anthropic.py +87 -0
  17. phoenix/server/api/helpers/prompts/conversions/openai.py +78 -0
  18. phoenix/server/api/helpers/prompts/models.py +575 -0
  19. phoenix/server/api/input_types/ChatCompletionInput.py +9 -4
  20. phoenix/server/api/input_types/PromptTemplateOptions.py +10 -0
  21. phoenix/server/api/input_types/PromptVersionInput.py +133 -0
  22. phoenix/server/api/mutations/__init__.py +6 -0
  23. phoenix/server/api/mutations/chat_mutations.py +18 -16
  24. phoenix/server/api/mutations/prompt_label_mutations.py +191 -0
  25. phoenix/server/api/mutations/prompt_mutations.py +312 -0
  26. phoenix/server/api/mutations/prompt_version_tag_mutations.py +148 -0
  27. phoenix/server/api/mutations/user_mutations.py +7 -6
  28. phoenix/server/api/openapi/schema.py +1 -0
  29. phoenix/server/api/queries.py +84 -31
  30. phoenix/server/api/routers/oauth2.py +3 -2
  31. phoenix/server/api/routers/v1/__init__.py +2 -0
  32. phoenix/server/api/routers/v1/datasets.py +1 -1
  33. phoenix/server/api/routers/v1/experiment_evaluations.py +1 -1
  34. phoenix/server/api/routers/v1/experiment_runs.py +1 -1
  35. phoenix/server/api/routers/v1/experiments.py +1 -1
  36. phoenix/server/api/routers/v1/models.py +45 -0
  37. phoenix/server/api/routers/v1/prompts.py +412 -0
  38. phoenix/server/api/routers/v1/spans.py +1 -1
  39. phoenix/server/api/routers/v1/traces.py +1 -1
  40. phoenix/server/api/routers/v1/utils.py +1 -1
  41. phoenix/server/api/subscriptions.py +21 -24
  42. phoenix/server/api/types/GenerativeProvider.py +4 -4
  43. phoenix/server/api/types/Identifier.py +15 -0
  44. phoenix/server/api/types/Project.py +5 -7
  45. phoenix/server/api/types/Prompt.py +134 -0
  46. phoenix/server/api/types/PromptLabel.py +41 -0
  47. phoenix/server/api/types/PromptVersion.py +148 -0
  48. phoenix/server/api/types/PromptVersionTag.py +27 -0
  49. phoenix/server/api/types/PromptVersionTemplate.py +148 -0
  50. phoenix/server/api/types/ResponseFormat.py +9 -0
  51. phoenix/server/api/types/ToolDefinition.py +9 -0
  52. phoenix/server/app.py +3 -0
  53. phoenix/server/static/.vite/manifest.json +45 -45
  54. phoenix/server/static/assets/components-B-qgPyHv.js +2699 -0
  55. phoenix/server/static/assets/index-D4KO1IcF.js +1125 -0
  56. phoenix/server/static/assets/pages-DdcuL3Rh.js +5634 -0
  57. phoenix/server/static/assets/vendor-DQp7CrDA.js +894 -0
  58. phoenix/server/static/assets/vendor-arizeai-C1nEIEQq.js +657 -0
  59. phoenix/server/static/assets/vendor-codemirror-BZXYUIkP.js +24 -0
  60. phoenix/server/static/assets/vendor-recharts-BUFpwCVD.js +59 -0
  61. phoenix/server/static/assets/{vendor-shiki-Cl9QBraO.js → vendor-shiki-C8L-c9jT.js} +2 -2
  62. phoenix/server/static/assets/{vendor-three-DwGkEfCM.js → vendor-three-C-AGeJYv.js} +1 -1
  63. phoenix/session/client.py +25 -21
  64. phoenix/utilities/client.py +6 -0
  65. phoenix/version.py +1 -1
  66. phoenix/server/api/input_types/TemplateOptions.py +0 -10
  67. phoenix/server/api/routers/v1/pydantic_compat.py +0 -78
  68. phoenix/server/api/types/TemplateLanguage.py +0 -10
  69. phoenix/server/static/assets/components-DckIzNmE.js +0 -2125
  70. phoenix/server/static/assets/index-Bf25Ogon.js +0 -113
  71. phoenix/server/static/assets/pages-DL7J9q9w.js +0 -4463
  72. phoenix/server/static/assets/vendor-DvC8cT4X.js +0 -894
  73. phoenix/server/static/assets/vendor-arizeai-Do1793cv.js +0 -662
  74. phoenix/server/static/assets/vendor-codemirror-BzwZPyJM.js +0 -24
  75. phoenix/server/static/assets/vendor-recharts-_Jb7JjhG.js +0 -59
  76. {arize_phoenix-7.12.3.dist-info → arize_phoenix-8.0.0.dist-info}/WHEEL +0 -0
  77. {arize_phoenix-7.12.3.dist-info → arize_phoenix-8.0.0.dist-info}/entry_points.txt +0 -0
  78. {arize_phoenix-7.12.3.dist-info → arize_phoenix-8.0.0.dist-info}/licenses/IP_NOTICE +0 -0
  79. {arize_phoenix-7.12.3.dist-info → arize_phoenix-8.0.0.dist-info}/licenses/LICENSE +0 -0
  80. /phoenix/server/static/assets/{vendor-DxkFTwjz.css → vendor-Cg6lcjUC.css} +0 -0
phoenix/db/models.py CHANGED
@@ -13,6 +13,7 @@ from sqlalchemy import (
13
13
  ForeignKey,
14
14
  Index,
15
15
  MetaData,
16
+ Null,
16
17
  String,
17
18
  TypeDecorator,
18
19
  UniqueConstraint,
@@ -38,6 +39,21 @@ from sqlalchemy.sql import expression
38
39
 
39
40
  from phoenix.config import get_env_database_schema
40
41
  from phoenix.datetime_utils import normalize_datetime
42
+ from phoenix.db.types.identifier import Identifier
43
+ from phoenix.db.types.model_provider import ModelProvider
44
+ from phoenix.server.api.helpers.prompts.models import (
45
+ PromptInvocationParameters,
46
+ PromptInvocationParametersRootModel,
47
+ PromptResponseFormat,
48
+ PromptResponseFormatRootModel,
49
+ PromptTemplate,
50
+ PromptTemplateFormat,
51
+ PromptTemplateRootModel,
52
+ PromptTemplateType,
53
+ PromptTools,
54
+ is_prompt_invocation_parameters,
55
+ is_prompt_template,
56
+ )
41
57
 
42
58
 
43
59
  class AuthMethod(Enum):
@@ -99,6 +115,139 @@ class UtcTimeStamp(TypeDecorator[datetime]):
99
115
  return normalize_datetime(value, timezone.utc)
100
116
 
101
117
 
118
+ class _Identifier(TypeDecorator[Identifier]):
119
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
120
+ cache_ok = True
121
+ impl = String
122
+
123
+ def process_bind_param(self, value: Optional[Identifier], _: Dialect) -> Optional[str]:
124
+ assert isinstance(value, Identifier) or value is None
125
+ return None if value is None else value.root
126
+
127
+ def process_result_value(self, value: Optional[str], _: Dialect) -> Optional[Identifier]:
128
+ return None if value is None else Identifier.model_validate(value)
129
+
130
+
131
+ class _ModelProvider(TypeDecorator[ModelProvider]):
132
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
133
+ cache_ok = True
134
+ impl = String
135
+
136
+ def process_bind_param(self, value: Optional[ModelProvider], _: Dialect) -> Optional[str]:
137
+ if isinstance(value, str):
138
+ return ModelProvider(value).value
139
+ return None if value is None else value.value
140
+
141
+ def process_result_value(self, value: Optional[str], _: Dialect) -> Optional[ModelProvider]:
142
+ return None if value is None else ModelProvider(value)
143
+
144
+
145
+ class _InvocationParameters(TypeDecorator[PromptInvocationParameters]):
146
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
147
+ cache_ok = True
148
+ impl = JSON_
149
+
150
+ def process_bind_param(
151
+ self, value: Optional[PromptInvocationParameters], _: Dialect
152
+ ) -> Optional[dict[str, Any]]:
153
+ assert is_prompt_invocation_parameters(value)
154
+ invocation_parameters = value.model_dump()
155
+ assert isinstance(invocation_parameters, dict)
156
+ return invocation_parameters
157
+
158
+ def process_result_value(
159
+ self, value: Optional[dict[str, Any]], _: Dialect
160
+ ) -> Optional[PromptInvocationParameters]:
161
+ assert isinstance(value, dict)
162
+ return PromptInvocationParametersRootModel.model_validate(value).root
163
+
164
+
165
+ class _PromptTemplate(TypeDecorator[PromptTemplate]):
166
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
167
+ cache_ok = True
168
+ impl = JSON_
169
+
170
+ def process_bind_param(
171
+ self, value: Optional[PromptTemplate], _: Dialect
172
+ ) -> Optional[dict[str, Any]]:
173
+ assert is_prompt_template(value)
174
+ return value.model_dump() if value is not None else None
175
+
176
+ def process_result_value(
177
+ self, value: Optional[dict[str, Any]], _: Dialect
178
+ ) -> Optional[PromptTemplate]:
179
+ assert isinstance(value, dict)
180
+ return PromptTemplateRootModel.model_validate(value).root
181
+
182
+
183
+ class _Tools(TypeDecorator[PromptTools]):
184
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
185
+ cache_ok = True
186
+ impl = JSON_
187
+
188
+ def process_bind_param(
189
+ self, value: Optional[PromptTools], _: Dialect
190
+ ) -> Optional[dict[str, Any]]:
191
+ return value.model_dump() if value is not None else None
192
+
193
+ def process_result_value(
194
+ self, value: Optional[dict[str, Any]], _: Dialect
195
+ ) -> Optional[PromptTools]:
196
+ return PromptTools.model_validate(value) if value is not None else None
197
+
198
+
199
+ class _PromptResponseFormat(TypeDecorator[PromptResponseFormat]):
200
+ # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
201
+ cache_ok = True
202
+ impl = JSON_
203
+
204
+ def process_bind_param(
205
+ self, value: Optional[PromptResponseFormat], _: Dialect
206
+ ) -> Optional[dict[str, Any]]:
207
+ return value.model_dump() if value is not None else None
208
+
209
+ def process_result_value(
210
+ self, value: Optional[dict[str, Any]], _: Dialect
211
+ ) -> Optional[PromptResponseFormat]:
212
+ return (
213
+ PromptResponseFormatRootModel.model_validate(value).root if value is not None else None
214
+ )
215
+
216
+
217
+ class _PromptTemplateType(TypeDecorator[PromptTemplateType]):
218
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
219
+ cache_ok = True
220
+ impl = String
221
+
222
+ def process_bind_param(self, value: Optional[PromptTemplateType], _: Dialect) -> Optional[str]:
223
+ if isinstance(value, str):
224
+ return PromptTemplateType(value).value
225
+ return None if value is None else value.value
226
+
227
+ def process_result_value(
228
+ self, value: Optional[str], _: Dialect
229
+ ) -> Optional[PromptTemplateType]:
230
+ return None if value is None else PromptTemplateType(value)
231
+
232
+
233
+ class _TemplateFormat(TypeDecorator[PromptTemplateFormat]):
234
+ # See # See https://docs.sqlalchemy.org/en/20/core/custom_types.html
235
+ cache_ok = True
236
+ impl = String
237
+
238
+ def process_bind_param(
239
+ self, value: Optional[PromptTemplateFormat], _: Dialect
240
+ ) -> Optional[str]:
241
+ if isinstance(value, str):
242
+ return PromptTemplateFormat(value).value
243
+ return None if value is None else value.value
244
+
245
+ def process_result_value(
246
+ self, value: Optional[str], _: Dialect
247
+ ) -> Optional[PromptTemplateFormat]:
248
+ return None if value is None else PromptTemplateFormat(value)
249
+
250
+
102
251
  class ExperimentRunOutput(TypedDict, total=False):
103
252
  task_output: Any
104
253
 
@@ -805,3 +954,161 @@ class ApiKey(Base):
805
954
  created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
806
955
  expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=True, index=True)
807
956
  __table_args__ = (dict(sqlite_autoincrement=True),)
957
+
958
+
959
+ class PromptLabel(Base):
960
+ __tablename__ = "prompt_labels"
961
+
962
+ id: Mapped[int] = mapped_column(primary_key=True)
963
+ name: Mapped[str] = mapped_column(String, unique=True, index=True, nullable=False)
964
+ description: Mapped[Optional[str]]
965
+ color: Mapped[str] = mapped_column(String, nullable=True)
966
+
967
+ prompts_prompt_labels: Mapped[list["PromptPromptLabel"]] = relationship(
968
+ "PromptPromptLabel",
969
+ back_populates="prompt_label",
970
+ cascade="all, delete-orphan",
971
+ uselist=True,
972
+ )
973
+
974
+
975
+ class Prompt(Base):
976
+ __tablename__ = "prompts"
977
+
978
+ id: Mapped[int] = mapped_column(primary_key=True)
979
+ source_prompt_id: Mapped[Optional[int]] = mapped_column(
980
+ ForeignKey("prompts.id", ondelete="SET NULL"),
981
+ index=True,
982
+ nullable=True,
983
+ )
984
+ name: Mapped[Identifier] = mapped_column(_Identifier, unique=True, index=True, nullable=False)
985
+ description: Mapped[Optional[str]]
986
+ metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
987
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
988
+ updated_at: Mapped[datetime] = mapped_column(
989
+ UtcTimeStamp, server_default=func.now(), onupdate=func.now()
990
+ )
991
+
992
+ prompts_prompt_labels: Mapped[list["PromptPromptLabel"]] = relationship(
993
+ "PromptPromptLabel",
994
+ back_populates="prompt",
995
+ cascade="all, delete-orphan",
996
+ uselist=True,
997
+ )
998
+
999
+ prompt_versions: Mapped[list["PromptVersion"]] = relationship(
1000
+ "PromptVersion",
1001
+ back_populates="prompt",
1002
+ cascade="all, delete-orphan",
1003
+ uselist=True,
1004
+ )
1005
+
1006
+ prompt_version_tags: Mapped[list["PromptVersionTag"]] = relationship(
1007
+ "PromptVersionTag",
1008
+ back_populates="prompt",
1009
+ cascade="all, delete-orphan",
1010
+ uselist=True,
1011
+ )
1012
+
1013
+
1014
+ class PromptPromptLabel(Base):
1015
+ __tablename__ = "prompts_prompt_labels"
1016
+
1017
+ id: Mapped[int] = mapped_column(primary_key=True)
1018
+ prompt_label_id: Mapped[int] = mapped_column(
1019
+ ForeignKey("prompt_labels.id", ondelete="CASCADE"),
1020
+ index=True,
1021
+ nullable=False,
1022
+ )
1023
+ prompt_id: Mapped[int] = mapped_column(
1024
+ ForeignKey("prompts.id", ondelete="CASCADE"),
1025
+ index=True,
1026
+ nullable=False,
1027
+ )
1028
+
1029
+ prompt_label: Mapped["PromptLabel"] = relationship(
1030
+ "PromptLabel", back_populates="prompts_prompt_labels"
1031
+ )
1032
+ prompt: Mapped["Prompt"] = relationship("Prompt", back_populates="prompts_prompt_labels")
1033
+
1034
+ __table_args__ = (UniqueConstraint("prompt_label_id", "prompt_id"),)
1035
+
1036
+
1037
+ class PromptVersion(Base):
1038
+ __tablename__ = "prompt_versions"
1039
+
1040
+ id: Mapped[int] = mapped_column(primary_key=True)
1041
+ prompt_id: Mapped[int] = mapped_column(
1042
+ ForeignKey("prompts.id", ondelete="CASCADE"),
1043
+ index=True,
1044
+ nullable=False,
1045
+ )
1046
+ description: Mapped[Optional[str]] = mapped_column(String, nullable=True)
1047
+ user_id: Mapped[Optional[int]] = mapped_column(
1048
+ ForeignKey("users.id", ondelete="SET NULL"),
1049
+ index=True,
1050
+ nullable=True,
1051
+ )
1052
+ template_type: Mapped[PromptTemplateType] = mapped_column(
1053
+ _PromptTemplateType,
1054
+ CheckConstraint("template_type IN ('CHAT', 'STR')", name="template_type"),
1055
+ nullable=False,
1056
+ )
1057
+ template_format: Mapped[PromptTemplateFormat] = mapped_column(
1058
+ _TemplateFormat,
1059
+ CheckConstraint(
1060
+ "template_format IN ('F_STRING', 'MUSTACHE', 'NONE')", name="template_format"
1061
+ ),
1062
+ nullable=False,
1063
+ )
1064
+ template: Mapped[PromptTemplate] = mapped_column(_PromptTemplate, nullable=False)
1065
+ invocation_parameters: Mapped[PromptInvocationParameters] = mapped_column(
1066
+ _InvocationParameters, nullable=False
1067
+ )
1068
+ tools: Mapped[Optional[PromptTools]] = mapped_column(_Tools, default=Null(), nullable=True)
1069
+ response_format: Mapped[Optional[PromptResponseFormat]] = mapped_column(
1070
+ _PromptResponseFormat, default=Null(), nullable=True
1071
+ )
1072
+ model_provider: Mapped[ModelProvider] = mapped_column(_ModelProvider)
1073
+ model_name: Mapped[str]
1074
+ metadata_: Mapped[dict[str, Any]] = mapped_column("metadata")
1075
+ created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
1076
+
1077
+ prompt: Mapped["Prompt"] = relationship("Prompt", back_populates="prompt_versions")
1078
+
1079
+ prompt_version_tags: Mapped[list["PromptVersionTag"]] = relationship(
1080
+ "PromptVersionTag",
1081
+ back_populates="prompt_version",
1082
+ cascade="all, delete-orphan",
1083
+ uselist=True,
1084
+ )
1085
+
1086
+
1087
+ class PromptVersionTag(Base):
1088
+ __tablename__ = "prompt_version_tags"
1089
+
1090
+ id: Mapped[int] = mapped_column(primary_key=True)
1091
+ name: Mapped[Identifier] = mapped_column(_Identifier, nullable=False)
1092
+ description: Mapped[Optional[str]] = mapped_column(String, nullable=True)
1093
+ prompt_id: Mapped[int] = mapped_column(
1094
+ ForeignKey("prompts.id", ondelete="CASCADE"),
1095
+ index=True,
1096
+ nullable=False,
1097
+ )
1098
+ prompt_version_id: Mapped[int] = mapped_column(
1099
+ ForeignKey("prompt_versions.id", ondelete="CASCADE"),
1100
+ index=True,
1101
+ nullable=False,
1102
+ )
1103
+ user_id: Mapped[Optional[int]] = mapped_column(
1104
+ ForeignKey("users.id", ondelete="SET NULL"),
1105
+ index=True,
1106
+ nullable=True,
1107
+ )
1108
+
1109
+ prompt: Mapped["Prompt"] = relationship("Prompt", back_populates="prompt_version_tags")
1110
+ prompt_version: Mapped["PromptVersion"] = relationship(
1111
+ "PromptVersion", back_populates="prompt_version_tags"
1112
+ )
1113
+
1114
+ __table_args__ = (UniqueConstraint("name", "prompt_id"),)
File without changes
@@ -0,0 +1,7 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import Field, RootModel
4
+
5
+
6
+ class Identifier(RootModel[str]):
7
+ root: Annotated[str, Field(pattern=r"^[a-z0-9]([_a-z0-9-]*[a-z0-9])?$")]
@@ -0,0 +1,8 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ModelProvider(Enum):
5
+ OPENAI = "OPENAI"
6
+ AZURE_OPENAI = "AZURE_OPENAI"
7
+ ANTHROPIC = "ANTHROPIC"
8
+ GOOGLE = "GOOGLE"
@@ -30,6 +30,7 @@ from phoenix.server.api.dataloaders import (
30
30
  LatencyMsQuantileDataLoader,
31
31
  MinStartOrMaxEndTimeDataLoader,
32
32
  ProjectByNameDataLoader,
33
+ PromptVersionSequenceNumberDataLoader,
33
34
  RecordCountDataLoader,
34
35
  SessionIODataLoader,
35
36
  SessionNumTracesDataLoader,
@@ -73,6 +74,7 @@ class DataLoaders:
73
74
  experiment_sequence_number: ExperimentSequenceNumberDataLoader
74
75
  latency_ms_quantile: LatencyMsQuantileDataLoader
75
76
  min_start_or_max_end_times: MinStartOrMaxEndTimeDataLoader
77
+ prompt_version_sequence_number: PromptVersionSequenceNumberDataLoader
76
78
  record_counts: RecordCountDataLoader
77
79
  session_first_inputs: SessionIODataLoader
78
80
  session_last_outputs: SessionIODataLoader
@@ -18,6 +18,7 @@ from .experiment_sequence_number import ExperimentSequenceNumberDataLoader
18
18
  from .latency_ms_quantile import LatencyMsQuantileCache, LatencyMsQuantileDataLoader
19
19
  from .min_start_or_max_end_times import MinStartOrMaxEndTimeCache, MinStartOrMaxEndTimeDataLoader
20
20
  from .project_by_name import ProjectByNameDataLoader
21
+ from .prompt_version_sequence_number import PromptVersionSequenceNumberDataLoader
21
22
  from .record_counts import RecordCountCache, RecordCountDataLoader
22
23
  from .session_io import SessionIODataLoader
23
24
  from .session_num_traces import SessionNumTracesDataLoader
@@ -50,6 +51,7 @@ __all__ = [
50
51
  "ExperimentSequenceNumberDataLoader",
51
52
  "LatencyMsQuantileDataLoader",
52
53
  "MinStartOrMaxEndTimeDataLoader",
54
+ "PromptVersionSequenceNumberDataLoader",
53
55
  "RecordCountDataLoader",
54
56
  "SessionIODataLoader",
55
57
  "SessionNumTracesDataLoader",
@@ -0,0 +1,35 @@
1
+ from typing import Optional
2
+
3
+ from sqlalchemy import func, select
4
+ from strawberry.dataloader import DataLoader
5
+ from typing_extensions import TypeAlias
6
+
7
+ from phoenix.db import models
8
+ from phoenix.server.types import DbSessionFactory
9
+
10
+ PromptVersionId: TypeAlias = int
11
+ Key: TypeAlias = PromptVersionId
12
+ Result: TypeAlias = Optional[int]
13
+
14
+
15
+ class PromptVersionSequenceNumberDataLoader(DataLoader[Key, Result]):
16
+ def __init__(self, db: DbSessionFactory) -> None:
17
+ super().__init__(load_fn=self._load_fn)
18
+ self._db = db
19
+
20
+ async def _load_fn(self, keys: list[Key]) -> list[Result]:
21
+ prompt_version_ids = keys
22
+ row_number = (
23
+ func.row_number().over(
24
+ partition_by=models.PromptVersion.prompt_id,
25
+ order_by=models.PromptVersion.id,
26
+ )
27
+ ).label("sequence_number")
28
+ subq = select(models.PromptVersion.id.label("prompt_version_id"), row_number).subquery()
29
+ stmt = select(subq).where(subq.c.prompt_version_id.in_(prompt_version_ids))
30
+ async with self._db() as session:
31
+ result = {
32
+ prompt_version_id: seq_number
33
+ async for prompt_version_id, seq_number in await session.stream(stmt)
34
+ }
35
+ return [result.get(prompt_version_id) for prompt_version_id in keys]
@@ -0,0 +1,135 @@
1
+ from typing import Annotated, Any, Literal, Union
2
+
3
+ from jsonschema import Draft7Validator, ValidationError
4
+ from pydantic import AfterValidator, BaseModel, Field
5
+ from typing_extensions import TypeAlias
6
+
7
+ # This meta-schema describes valid JSON schemas according to the JSON Schema Draft 7 specification.
8
+ # It is copied from https://json-schema.org/draft-07/schema#
9
+ JSON_SCHEMA_DRAFT_7_META_SCHEMA = {
10
+ "$schema": "http://json-schema.org/draft-07/schema#",
11
+ "$id": "http://json-schema.org/draft-07/schema#",
12
+ "title": "Core schema meta-schema",
13
+ "definitions": {
14
+ "schemaArray": {"type": "array", "minItems": 1, "items": {"$ref": "#"}},
15
+ "nonNegativeInteger": {"type": "integer", "minimum": 0},
16
+ "nonNegativeIntegerDefault0": {
17
+ "allOf": [{"$ref": "#/definitions/nonNegativeInteger"}, {"default": 0}]
18
+ },
19
+ "simpleTypes": {
20
+ "enum": ["array", "boolean", "integer", "null", "number", "object", "string"]
21
+ },
22
+ "stringArray": {
23
+ "type": "array",
24
+ "items": {"type": "string"},
25
+ "uniqueItems": True,
26
+ "default": [],
27
+ },
28
+ },
29
+ "type": ["object", "boolean"],
30
+ "properties": {
31
+ "$id": {"type": "string", "format": "uri-reference"},
32
+ "$schema": {"type": "string", "format": "uri"},
33
+ "$ref": {"type": "string", "format": "uri-reference"},
34
+ "$comment": {"type": "string"},
35
+ "title": {"type": "string"},
36
+ "description": {"type": "string"},
37
+ "default": True,
38
+ "readOnly": {"type": "boolean", "default": False},
39
+ "writeOnly": {"type": "boolean", "default": False},
40
+ "examples": {"type": "array", "items": True},
41
+ "multipleOf": {"type": "number", "exclusiveMinimum": 0},
42
+ "maximum": {"type": "number"},
43
+ "exclusiveMaximum": {"type": "number"},
44
+ "minimum": {"type": "number"},
45
+ "exclusiveMinimum": {"type": "number"},
46
+ "maxLength": {"$ref": "#/definitions/nonNegativeInteger"},
47
+ "minLength": {"$ref": "#/definitions/nonNegativeIntegerDefault0"},
48
+ "pattern": {"type": "string", "format": "regex"},
49
+ "additionalItems": {"$ref": "#"},
50
+ "items": {"anyOf": [{"$ref": "#"}, {"$ref": "#/definitions/schemaArray"}], "default": True},
51
+ "maxItems": {"$ref": "#/definitions/nonNegativeInteger"},
52
+ "minItems": {"$ref": "#/definitions/nonNegativeIntegerDefault0"},
53
+ "uniqueItems": {"type": "boolean", "default": False},
54
+ "contains": {"$ref": "#"},
55
+ "maxProperties": {"$ref": "#/definitions/nonNegativeInteger"},
56
+ "minProperties": {"$ref": "#/definitions/nonNegativeIntegerDefault0"},
57
+ "required": {"$ref": "#/definitions/stringArray"},
58
+ "additionalProperties": {"$ref": "#"},
59
+ "definitions": {"type": "object", "additionalProperties": {"$ref": "#"}, "default": {}},
60
+ "properties": {"type": "object", "additionalProperties": {"$ref": "#"}, "default": {}},
61
+ "patternProperties": {
62
+ "type": "object",
63
+ "additionalProperties": {"$ref": "#"},
64
+ "propertyNames": {"format": "regex"},
65
+ "default": {},
66
+ },
67
+ "dependencies": {
68
+ "type": "object",
69
+ "additionalProperties": {
70
+ "anyOf": [{"$ref": "#"}, {"$ref": "#/definitions/stringArray"}]
71
+ },
72
+ },
73
+ "propertyNames": {"$ref": "#"},
74
+ "const": True,
75
+ "enum": {"type": "array", "items": True, "minItems": 1, "uniqueItems": True},
76
+ "type": {
77
+ "anyOf": [
78
+ {"$ref": "#/definitions/simpleTypes"},
79
+ {
80
+ "type": "array",
81
+ "items": {"$ref": "#/definitions/simpleTypes"},
82
+ "minItems": 1,
83
+ "uniqueItems": True,
84
+ },
85
+ ]
86
+ },
87
+ "format": {"type": "string"},
88
+ "contentMediaType": {"type": "string"},
89
+ "contentEncoding": {"type": "string"},
90
+ "if": {"$ref": "#"},
91
+ "then": {"$ref": "#"},
92
+ "else": {"$ref": "#"},
93
+ "allOf": {"$ref": "#/definitions/schemaArray"},
94
+ "anyOf": {"$ref": "#/definitions/schemaArray"},
95
+ "oneOf": {"$ref": "#/definitions/schemaArray"},
96
+ "not": {"$ref": "#"},
97
+ },
98
+ "default": True,
99
+ }
100
+ Draft7Validator.check_schema(JSON_SCHEMA_DRAFT_7_META_SCHEMA) # ensure the schema is valid
101
+ JSON_SCHEMA_DRAFT_7_VALIDATOR = Draft7Validator(JSON_SCHEMA_DRAFT_7_META_SCHEMA)
102
+
103
+
104
+ def validate_json_schema_draft_7_object(schema: dict[str, Any]) -> dict[str, Any]:
105
+ """
106
+ Validates that a dictionary is a valid JSON schema according to the JSON
107
+ Schema Draft 7 specification.
108
+ """
109
+ try:
110
+ JSON_SCHEMA_DRAFT_7_VALIDATOR.validate(schema)
111
+ except ValidationError as error:
112
+ raise ValueError(str(error))
113
+ if schema.get("type") != "object":
114
+ raise ValueError("The 'type' property must be 'object'")
115
+ return schema
116
+
117
+
118
+ JSONSchemaDraft7ObjectSchemaContent: TypeAlias = Annotated[
119
+ dict[str, Any],
120
+ AfterValidator(validate_json_schema_draft_7_object),
121
+ ]
122
+
123
+
124
+ class JSONSchemaDraft7ObjectSchema(BaseModel):
125
+ type: Literal["json-schema-draft-7-object-schema"]
126
+ json_: JSONSchemaDraft7ObjectSchemaContent = Field(
127
+ ...,
128
+ alias="json", # avoid conflict with pydantic json method
129
+ )
130
+
131
+
132
+ JSONSchemaObjectSchema: TypeAlias = Annotated[
133
+ Union[JSONSchemaDraft7ObjectSchema],
134
+ Field(discriminator="type"),
135
+ ]
@@ -856,7 +856,7 @@ class AnthropicStreamingClient(PlaygroundStreamingClient):
856
856
 
857
857
 
858
858
  @register_llm_client(
859
- provider_key=GenerativeProviderKey.GEMINI,
859
+ provider_key=GenerativeProviderKey.GOOGLE,
860
860
  model_names=[
861
861
  PROVIDER_DEFAULT,
862
862
  "gemini-2.0-flash-exp",
@@ -866,7 +866,7 @@ class AnthropicStreamingClient(PlaygroundStreamingClient):
866
866
  "gemini-1.0-pro",
867
867
  ],
868
868
  )
869
- class GeminiStreamingClient(PlaygroundStreamingClient):
869
+ class GoogleStreamingClient(PlaygroundStreamingClient):
870
870
  def __init__(
871
871
  self,
872
872
  model: GenerativeModelInput,
@@ -941,7 +941,7 @@ class GeminiStreamingClient(PlaygroundStreamingClient):
941
941
  ) -> AsyncIterator[ChatCompletionChunk]:
942
942
  import google.generativeai as google_genai
943
943
 
944
- gemini_message_history, current_message, system_prompt = self._build_gemini_messages(
944
+ google_message_history, current_message, system_prompt = self._build_google_messages(
945
945
  messages
946
946
  )
947
947
 
@@ -950,17 +950,17 @@ class GeminiStreamingClient(PlaygroundStreamingClient):
950
950
  model_args["system_instruction"] = system_prompt
951
951
  client = google_genai.GenerativeModel(**model_args)
952
952
 
953
- gemini_config = google_genai.GenerationConfig(
953
+ google_config = google_genai.GenerationConfig(
954
954
  **invocation_parameters,
955
955
  )
956
- gemini_params = {
956
+ google_params = {
957
957
  "content": current_message,
958
- "generation_config": gemini_config,
958
+ "generation_config": google_config,
959
959
  "stream": True,
960
960
  }
961
961
 
962
- chat = client.start_chat(history=gemini_message_history)
963
- stream = await chat.send_message_async(**gemini_params)
962
+ chat = client.start_chat(history=google_message_history)
963
+ stream = await chat.send_message_async(**google_params)
964
964
  async for event in stream:
965
965
  self._attributes.update(
966
966
  {
@@ -971,29 +971,29 @@ class GeminiStreamingClient(PlaygroundStreamingClient):
971
971
  )
972
972
  yield TextChunk(content=event.text)
973
973
 
974
- def _build_gemini_messages(
974
+ def _build_google_messages(
975
975
  self,
976
976
  messages: list[tuple[ChatCompletionMessageRole, str, Optional[str], Optional[list[str]]]],
977
977
  ) -> tuple[list["ContentType"], str, str]:
978
- gemini_message_history: list["ContentType"] = []
978
+ google_message_history: list["ContentType"] = []
979
979
  system_prompts = []
980
980
  for role, content, _tool_call_id, _tool_calls in messages:
981
981
  if role == ChatCompletionMessageRole.USER:
982
- gemini_message_history.append({"role": "user", "parts": content})
982
+ google_message_history.append({"role": "user", "parts": content})
983
983
  elif role == ChatCompletionMessageRole.AI:
984
- gemini_message_history.append({"role": "model", "parts": content})
984
+ google_message_history.append({"role": "model", "parts": content})
985
985
  elif role == ChatCompletionMessageRole.SYSTEM:
986
986
  system_prompts.append(content)
987
987
  elif role == ChatCompletionMessageRole.TOOL:
988
988
  raise NotImplementedError
989
989
  else:
990
990
  assert_never(role)
991
- if gemini_message_history:
992
- prompt = gemini_message_history.pop()["parts"]
991
+ if google_message_history:
992
+ prompt = google_message_history.pop()["parts"]
993
993
  else:
994
994
  prompt = ""
995
995
 
996
- return gemini_message_history, prompt, "\n".join(system_prompts)
996
+ return google_message_history, prompt, "\n".join(system_prompts)
997
997
 
998
998
 
999
999
  def initialize_playground_clients() -> None:
@@ -41,6 +41,7 @@ from phoenix.server.api.types.ChatCompletionSubscriptionPayload import (
41
41
  TextChunk,
42
42
  ToolCallChunk,
43
43
  )
44
+ from phoenix.server.api.types.Identifier import Identifier
44
45
  from phoenix.trace.attributes import get_attribute_value, unflatten
45
46
  from phoenix.trace.schemas import (
46
47
  SpanEvent,
@@ -70,6 +71,8 @@ class streaming_llm_span:
70
71
  ) -> None:
71
72
  self._input = input
72
73
  self._attributes: dict[str, Any] = attributes if attributes is not None else {}
74
+ self._attributes.update(dict(prompt_metadata(input.prompt_name)))
75
+
73
76
  self._attributes.update(
74
77
  chain(
75
78
  llm_span_kind(),
@@ -264,6 +267,11 @@ def input_value_and_mime_type(
264
267
  yield INPUT_VALUE, safe_json_dumps(input_data)
265
268
 
266
269
 
270
+ def prompt_metadata(prompt_name: Optional[Identifier]) -> Iterator[tuple[str, Any]]:
271
+ if prompt_name:
272
+ yield METADATA, {"phoenix_prompt_id": prompt_name}
273
+
274
+
267
275
  def _merge_tool_call_chunks(
268
276
  chunks_by_id: defaultdict[str, list[ToolCallChunk]],
269
277
  ) -> list[dict[str, Any]]:
@@ -442,6 +450,7 @@ LLM_INVOCATION_PARAMETERS = SpanAttributes.LLM_INVOCATION_PARAMETERS
442
450
  LLM_TOOLS = SpanAttributes.LLM_TOOLS
443
451
  LLM_TOKEN_COUNT_PROMPT = SpanAttributes.LLM_TOKEN_COUNT_PROMPT
444
452
  LLM_TOKEN_COUNT_COMPLETION = SpanAttributes.LLM_TOKEN_COUNT_COMPLETION
453
+ METADATA = SpanAttributes.METADATA
445
454
 
446
455
  MESSAGE_CONTENT = MessageAttributes.MESSAGE_CONTENT
447
456
  MESSAGE_ROLE = MessageAttributes.MESSAGE_ROLE
File without changes