arize-phoenix 4.14.1__py3-none-any.whl → 4.16.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 (85) hide show
  1. {arize_phoenix-4.14.1.dist-info → arize_phoenix-4.16.0.dist-info}/METADATA +5 -3
  2. {arize_phoenix-4.14.1.dist-info → arize_phoenix-4.16.0.dist-info}/RECORD +81 -71
  3. phoenix/db/bulk_inserter.py +131 -5
  4. phoenix/db/engines.py +2 -1
  5. phoenix/db/helpers.py +23 -1
  6. phoenix/db/insertion/constants.py +2 -0
  7. phoenix/db/insertion/document_annotation.py +157 -0
  8. phoenix/db/insertion/helpers.py +13 -0
  9. phoenix/db/insertion/span_annotation.py +144 -0
  10. phoenix/db/insertion/trace_annotation.py +144 -0
  11. phoenix/db/insertion/types.py +261 -0
  12. phoenix/experiments/functions.py +3 -2
  13. phoenix/experiments/types.py +3 -3
  14. phoenix/server/api/context.py +7 -9
  15. phoenix/server/api/dataloaders/__init__.py +2 -0
  16. phoenix/server/api/dataloaders/average_experiment_run_latency.py +3 -3
  17. phoenix/server/api/dataloaders/dataset_example_revisions.py +2 -4
  18. phoenix/server/api/dataloaders/dataset_example_spans.py +2 -4
  19. phoenix/server/api/dataloaders/document_evaluation_summaries.py +2 -4
  20. phoenix/server/api/dataloaders/document_evaluations.py +2 -4
  21. phoenix/server/api/dataloaders/document_retrieval_metrics.py +2 -4
  22. phoenix/server/api/dataloaders/evaluation_summaries.py +2 -4
  23. phoenix/server/api/dataloaders/experiment_annotation_summaries.py +2 -4
  24. phoenix/server/api/dataloaders/experiment_error_rates.py +2 -4
  25. phoenix/server/api/dataloaders/experiment_run_counts.py +2 -4
  26. phoenix/server/api/dataloaders/experiment_sequence_number.py +2 -4
  27. phoenix/server/api/dataloaders/latency_ms_quantile.py +2 -3
  28. phoenix/server/api/dataloaders/min_start_or_max_end_times.py +2 -4
  29. phoenix/server/api/dataloaders/project_by_name.py +3 -3
  30. phoenix/server/api/dataloaders/record_counts.py +2 -4
  31. phoenix/server/api/dataloaders/span_annotations.py +2 -4
  32. phoenix/server/api/dataloaders/span_dataset_examples.py +36 -0
  33. phoenix/server/api/dataloaders/span_descendants.py +2 -4
  34. phoenix/server/api/dataloaders/span_evaluations.py +2 -4
  35. phoenix/server/api/dataloaders/span_projects.py +3 -3
  36. phoenix/server/api/dataloaders/token_counts.py +2 -4
  37. phoenix/server/api/dataloaders/trace_evaluations.py +2 -4
  38. phoenix/server/api/dataloaders/trace_row_ids.py +2 -4
  39. phoenix/server/api/input_types/SpanAnnotationSort.py +17 -0
  40. phoenix/server/api/input_types/TraceAnnotationSort.py +17 -0
  41. phoenix/server/api/mutations/span_annotations_mutations.py +8 -3
  42. phoenix/server/api/mutations/trace_annotations_mutations.py +8 -3
  43. phoenix/server/api/openapi/main.py +18 -2
  44. phoenix/server/api/openapi/schema.py +12 -12
  45. phoenix/server/api/routers/v1/__init__.py +36 -83
  46. phoenix/server/api/routers/v1/datasets.py +515 -509
  47. phoenix/server/api/routers/v1/evaluations.py +164 -73
  48. phoenix/server/api/routers/v1/experiment_evaluations.py +68 -91
  49. phoenix/server/api/routers/v1/experiment_runs.py +98 -155
  50. phoenix/server/api/routers/v1/experiments.py +132 -181
  51. phoenix/server/api/routers/v1/pydantic_compat.py +78 -0
  52. phoenix/server/api/routers/v1/spans.py +164 -203
  53. phoenix/server/api/routers/v1/traces.py +134 -159
  54. phoenix/server/api/routers/v1/utils.py +95 -0
  55. phoenix/server/api/types/Span.py +27 -3
  56. phoenix/server/api/types/Trace.py +21 -4
  57. phoenix/server/api/utils.py +4 -4
  58. phoenix/server/app.py +172 -192
  59. phoenix/server/grpc_server.py +2 -2
  60. phoenix/server/main.py +5 -9
  61. phoenix/server/static/.vite/manifest.json +31 -31
  62. phoenix/server/static/assets/components-Ci5kMOk5.js +1175 -0
  63. phoenix/server/static/assets/{index-CQgXRwU0.js → index-BQG5WVX7.js} +2 -2
  64. phoenix/server/static/assets/{pages-hdjlFZhO.js → pages-BrevprVW.js} +451 -275
  65. phoenix/server/static/assets/{vendor-DPvSDRn3.js → vendor-CP0b0YG0.js} +2 -2
  66. phoenix/server/static/assets/{vendor-arizeai-CkvPT67c.js → vendor-arizeai-DTbiPGp6.js} +27 -27
  67. phoenix/server/static/assets/vendor-codemirror-DtdPDzrv.js +15 -0
  68. phoenix/server/static/assets/{vendor-recharts-5jlNaZuF.js → vendor-recharts-A0DA1O99.js} +1 -1
  69. phoenix/server/thread_server.py +2 -2
  70. phoenix/server/types.py +18 -0
  71. phoenix/session/client.py +5 -3
  72. phoenix/session/session.py +2 -2
  73. phoenix/trace/dsl/filter.py +2 -6
  74. phoenix/trace/fixtures.py +17 -23
  75. phoenix/trace/utils.py +23 -0
  76. phoenix/utilities/client.py +116 -0
  77. phoenix/utilities/project.py +1 -1
  78. phoenix/version.py +1 -1
  79. phoenix/server/api/routers/v1/dataset_examples.py +0 -178
  80. phoenix/server/openapi/docs.py +0 -221
  81. phoenix/server/static/assets/components-DeS0YEmv.js +0 -1142
  82. phoenix/server/static/assets/vendor-codemirror-Cqwpwlua.js +0 -12
  83. {arize_phoenix-4.14.1.dist-info → arize_phoenix-4.16.0.dist-info}/WHEEL +0 -0
  84. {arize_phoenix-4.14.1.dist-info → arize_phoenix-4.16.0.dist-info}/licenses/IP_NOTICE +0 -0
  85. {arize_phoenix-4.14.1.dist-info → arize_phoenix-4.16.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,9 +1,11 @@
1
- from datetime import timezone
2
- from typing import Any, AsyncIterator, Dict, List
1
+ from datetime import datetime, timezone
2
+ from typing import Any, AsyncIterator, Dict, List, Literal, Optional
3
3
 
4
+ from fastapi import APIRouter, HTTPException, Query
5
+ from pydantic import Field
4
6
  from sqlalchemy import select
5
7
  from starlette.requests import Request
6
- from starlette.responses import JSONResponse, Response, StreamingResponse
8
+ from starlette.responses import Response, StreamingResponse
7
9
  from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
8
10
  from strawberry.relay import GlobalID
9
11
 
@@ -11,94 +13,83 @@ from phoenix.config import DEFAULT_PROJECT_NAME
11
13
  from phoenix.datetime_utils import normalize_datetime
12
14
  from phoenix.db import models
13
15
  from phoenix.db.helpers import SupportedSQLDialect
14
- from phoenix.db.insertion.helpers import insert_on_conflict
15
- from phoenix.server.api.routers.utils import df_to_bytes, from_iso_format
16
- from phoenix.server.api.types.node import from_global_id_with_expected_type
17
- from phoenix.trace.dsl import SpanQuery
16
+ from phoenix.db.insertion.helpers import as_kv, insert_on_conflict
17
+ from phoenix.db.insertion.types import Precursors
18
+ from phoenix.server.api.routers.utils import df_to_bytes
19
+ from phoenix.trace.dsl import SpanQuery as SpanQuery_
20
+
21
+ from .pydantic_compat import V1RoutesBaseModel
22
+ from .utils import RequestBody, ResponseBody, add_errors_to_responses
18
23
 
19
24
  DEFAULT_SPAN_LIMIT = 1000
20
25
 
26
+ router = APIRouter(tags=["traces"], include_in_schema=False)
27
+
28
+
29
+ class SpanQuery(V1RoutesBaseModel):
30
+ select: Optional[Dict[str, Any]] = None
31
+ filter: Optional[Dict[str, Any]] = None
32
+ explode: Optional[Dict[str, Any]] = None
33
+ concat: Optional[Dict[str, Any]] = None
34
+ rename: Optional[Dict[str, Any]] = None
35
+ index: Optional[Dict[str, Any]] = None
36
+
37
+
38
+ class QuerySpansRequestBody(V1RoutesBaseModel):
39
+ queries: List[SpanQuery]
40
+ start_time: Optional[datetime] = None
41
+ end_time: Optional[datetime] = None
42
+ limit: int = DEFAULT_SPAN_LIMIT
43
+ root_spans_only: Optional[bool] = None
44
+ project_name: Optional[str] = Field(
45
+ default=None,
46
+ description=(
47
+ "The name of the project to query. "
48
+ "This parameter has been deprecated, use the project_name query parameter instead."
49
+ ),
50
+ deprecated=True,
51
+ )
52
+ stop_time: Optional[datetime] = Field(
53
+ default=None,
54
+ description=(
55
+ "An upper bound on the time to query for. "
56
+ "This parameter has been deprecated, use the end_time parameter instead."
57
+ ),
58
+ deprecated=True,
59
+ )
60
+
21
61
 
22
62
  # TODO: Add property details to SpanQuery schema
23
- async def query_spans_handler(request: Request) -> Response:
24
- """
25
- summary: Query spans using query DSL
26
- operationId: querySpans
27
- tags:
28
- - private
29
- parameters:
30
- - name: project_name
31
- in: query
32
- schema:
33
- type: string
34
- default: default
35
- description: The project name to get evaluations from
36
- requestBody:
37
- required: true
38
- content:
39
- application/json:
40
- schema:
41
- type: object
42
- properties:
43
- queries:
44
- type: array
45
- items:
46
- type: object
47
- properties:
48
- select:
49
- type: object
50
- filter:
51
- type: object
52
- explode:
53
- type: object
54
- concat:
55
- type: object
56
- rename:
57
- type: object
58
- index:
59
- type: object
60
- start_time:
61
- type: string
62
- format: date-time
63
- end_time:
64
- type: string
65
- format: date-time
66
- nullable: true
67
- limit:
68
- type: integer
69
- nullable: true
70
- default: 1000
71
- root_spans_only:
72
- type: boolean
73
- nullable: true
74
- responses:
75
- 200:
76
- description: Success
77
- 403:
78
- description: Forbidden
79
- 404:
80
- description: Not found
81
- 422:
82
- description: Request body is invalid
83
- """
84
- payload = await request.json()
85
- queries = payload.pop("queries", [])
63
+ @router.post(
64
+ "/spans",
65
+ operation_id="querySpans",
66
+ summary="Query spans with query DSL",
67
+ responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
68
+ )
69
+ async def query_spans_handler(
70
+ request: Request,
71
+ request_body: QuerySpansRequestBody,
72
+ project_name: Optional[str] = Query(
73
+ default=None, description="The project name to get evaluations from"
74
+ ),
75
+ ) -> Response:
76
+ queries = request_body.queries
86
77
  project_name = (
87
- request.query_params.get("project_name")
78
+ project_name
88
79
  or request.query_params.get("project-name") # for backward compatibility
89
80
  or request.headers.get(
90
81
  "project-name"
91
82
  ) # read from headers/payload for backward-compatibility
92
- or payload.get("project_name")
83
+ or request_body.project_name
93
84
  or DEFAULT_PROJECT_NAME
94
85
  )
95
- end_time = payload.get("end_time") or payload.get("stop_time")
86
+ end_time = request_body.end_time or request_body.stop_time
96
87
  try:
97
- span_queries = [SpanQuery.from_dict(query) for query in queries]
88
+ span_queries = [SpanQuery_.from_dict(query.dict()) for query in queries]
98
89
  except Exception as e:
99
- return Response(
90
+ raise HTTPException(
91
+ detail=f"Invalid query: {e}",
100
92
  status_code=HTTP_422_UNPROCESSABLE_ENTITY,
101
- content=f"Invalid query: {e}",
102
93
  )
103
94
  async with request.app.state.db() as session:
104
95
  results = []
@@ -108,19 +99,19 @@ async def query_spans_handler(request: Request) -> Response:
108
99
  query,
109
100
  project_name=project_name,
110
101
  start_time=normalize_datetime(
111
- from_iso_format(payload.get("start_time")),
102
+ request_body.start_time,
112
103
  timezone.utc,
113
104
  ),
114
105
  end_time=normalize_datetime(
115
- from_iso_format(end_time),
106
+ end_time,
116
107
  timezone.utc,
117
108
  ),
118
- limit=payload.get("limit", DEFAULT_SPAN_LIMIT),
119
- root_spans_only=payload.get("root_spans_only"),
109
+ limit=request_body.limit,
110
+ root_spans_only=request_body.root_spans_only,
120
111
  )
121
112
  )
122
113
  if not results:
123
- return Response(status_code=HTTP_404_NOT_FOUND)
114
+ raise HTTPException(status_code=HTTP_404_NOT_FOUND)
124
115
 
125
116
  async def content() -> AsyncIterator[bytes]:
126
117
  for result in results:
@@ -132,134 +123,104 @@ async def query_spans_handler(request: Request) -> Response:
132
123
  )
133
124
 
134
125
 
135
- async def get_spans_handler(request: Request) -> Response:
136
- return await query_spans_handler(request)
137
-
138
-
139
- async def annotate_spans(request: Request) -> Response:
140
- """
141
- summary: Upsert annotations for spans
142
- operationId: annotateSpans
143
- tags:
144
- - private
145
- requestBody:
146
- description: List of span annotations to be inserted
147
- required: true
148
- content:
149
- application/json:
150
- schema:
151
- type: object
152
- properties:
153
- data:
154
- type: array
155
- items:
156
- type: object
157
- properties:
158
- span_id:
159
- type: string
160
- description: The ID of the span being annotated
161
- name:
162
- type: string
163
- description: The name of the annotation
164
- annotator_kind:
165
- type: string
166
- description: The kind of annotator used for the annotation ("LLM" or "HUMAN")
167
- result:
168
- type: object
169
- description: The result of the annotation
170
- properties:
171
- label:
172
- type: string
173
- description: The label assigned by the annotation
174
- score:
175
- type: number
176
- format: float
177
- description: The score assigned by the annotation
178
- explanation:
179
- type: string
180
- description: Explanation of the annotation result
181
- error:
182
- type: string
183
- description: Optional error message if the annotation encountered an error
184
- metadata:
185
- type: object
186
- description: Metadata for the annotation
187
- additionalProperties:
188
- type: string
189
- required:
190
- - span_id
191
- - name
192
- - annotator_kind
193
- responses:
194
- 200:
195
- description: Span annotations inserted successfully
196
- content:
197
- application/json:
198
- schema:
199
- type: object
200
- properties:
201
- data:
202
- type: array
203
- items:
204
- type: object
205
- properties:
206
- id:
207
- type: string
208
- description: The ID of the inserted span annotation
209
- 404:
210
- description: Span not found
211
- """
212
- payload: List[Dict[str, Any]] = (await request.json()).get("data", [])
213
- span_gids = [GlobalID.from_id(annotation["span_id"]) for annotation in payload]
214
-
215
- resolved_span_ids = []
216
- for span_gid in span_gids:
217
- try:
218
- resolved_span_ids.append(from_global_id_with_expected_type(span_gid, "Span"))
219
- except ValueError:
220
- return Response(
221
- content="Span with ID {span_gid} does not exist",
222
- status_code=HTTP_404_NOT_FOUND,
223
- )
126
+ @router.get("/spans", include_in_schema=False, deprecated=True)
127
+ async def get_spans_handler(
128
+ request: Request,
129
+ request_body: QuerySpansRequestBody,
130
+ project_name: Optional[str] = Query(
131
+ default=None, description="The project name to get evaluations from"
132
+ ),
133
+ ) -> Response:
134
+ return await query_spans_handler(request, request_body, project_name)
224
135
 
225
- async with request.app.state.db() as session:
226
- spans = await session.execute(
227
- select(models.Span).filter(models.Span.id.in_(resolved_span_ids))
136
+
137
+ class SpanAnnotationResult(V1RoutesBaseModel):
138
+ label: Optional[str] = Field(default=None, description="The label assigned by the annotation")
139
+ score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
140
+ explanation: Optional[str] = Field(
141
+ default=None, description="Explanation of the annotation result"
142
+ )
143
+
144
+
145
+ class SpanAnnotation(V1RoutesBaseModel):
146
+ span_id: str = Field(description="OpenTelemetry Span ID (hex format w/o 0x prefix)")
147
+ name: str = Field(description="The name of the annotation")
148
+ annotator_kind: Literal["LLM", "HUMAN"] = Field(
149
+ description="The kind of annotator used for the annotation"
150
+ )
151
+ result: Optional[SpanAnnotationResult] = Field(
152
+ default=None, description="The result of the annotation"
153
+ )
154
+ metadata: Optional[Dict[str, Any]] = Field(
155
+ default=None, description="Metadata for the annotation"
156
+ )
157
+
158
+ def as_precursor(self) -> Precursors.SpanAnnotation:
159
+ return Precursors.SpanAnnotation(
160
+ self.span_id,
161
+ models.SpanAnnotation(
162
+ name=self.name,
163
+ annotator_kind=self.annotator_kind,
164
+ score=self.result.score if self.result else None,
165
+ label=self.result.label if self.result else None,
166
+ explanation=self.result.explanation if self.result else None,
167
+ metadata_=self.metadata or {},
168
+ ),
228
169
  )
229
- existing_span_ids = {span.id for span in spans.scalars()}
230
170
 
231
- missing_span_ids = set(resolved_span_ids) - existing_span_ids
171
+
172
+ class AnnotateSpansRequestBody(RequestBody[List[SpanAnnotation]]):
173
+ data: List[SpanAnnotation]
174
+
175
+
176
+ class InsertedSpanAnnotation(V1RoutesBaseModel):
177
+ id: str = Field(description="The ID of the inserted span annotation")
178
+
179
+
180
+ class AnnotateSpansResponseBody(ResponseBody[List[InsertedSpanAnnotation]]):
181
+ pass
182
+
183
+
184
+ @router.post(
185
+ "/span_annotations",
186
+ operation_id="annotateSpans",
187
+ summary="Create or update span annotations",
188
+ responses=add_errors_to_responses(
189
+ [{"status_code": HTTP_404_NOT_FOUND, "description": "Span not found"}]
190
+ ),
191
+ response_description="Span annotations inserted successfully",
192
+ )
193
+ async def annotate_spans(
194
+ request: Request,
195
+ request_body: AnnotateSpansRequestBody,
196
+ sync: bool = Query(default=True, description="If true, fulfill request synchronously."),
197
+ ) -> AnnotateSpansResponseBody:
198
+ precursors = [d.as_precursor() for d in request_body.data]
199
+ if not sync:
200
+ await request.state.enqueue(*precursors)
201
+ return AnnotateSpansResponseBody(data=[])
202
+
203
+ span_ids = {p.span_id for p in precursors}
204
+ async with request.app.state.db() as session:
205
+ existing_spans = {
206
+ span.span_id: span.id
207
+ async for span in await session.stream_scalars(
208
+ select(models.Span).filter(models.Span.span_id.in_(span_ids))
209
+ )
210
+ }
211
+
212
+ missing_span_ids = span_ids - set(existing_spans.keys())
232
213
  if missing_span_ids:
233
- missing_span_gids = [
234
- str(GlobalID("Span", str(span_gid))) for span_gid in missing_span_ids
235
- ]
236
- return Response(
237
- content=f"Spans with IDs {', '.join(missing_span_gids)} do not exist.",
214
+ raise HTTPException(
215
+ detail=f"Spans with IDs {', '.join(missing_span_ids)} do not exist.",
238
216
  status_code=HTTP_404_NOT_FOUND,
239
217
  )
240
218
 
241
219
  inserted_annotations = []
242
- for annotation in payload:
243
- span_gid = GlobalID.from_id(annotation["span_id"])
244
- span_id = from_global_id_with_expected_type(span_gid, "Span")
245
- name = annotation["name"]
246
- annotator_kind = annotation["annotator_kind"]
247
- result = annotation.get("result")
248
- label = result.get("label") if result else None
249
- score = result.get("score") if result else None
250
- explanation = result.get("explanation") if result else None
251
- metadata = annotation.get("metadata") or {}
252
-
253
- values = dict(
254
- span_rowid=span_id,
255
- name=name,
256
- label=label,
257
- score=score,
258
- explanation=explanation,
259
- annotator_kind=annotator_kind,
260
- metadata_=metadata,
261
- )
262
- dialect = SupportedSQLDialect(session.bind.dialect.name)
220
+
221
+ dialect = SupportedSQLDialect(session.bind.dialect.name)
222
+ for p in precursors:
223
+ values = dict(as_kv(p.as_insertable(existing_spans[p.span_id]).row))
263
224
  span_annotation_id = await session.scalar(
264
225
  insert_on_conflict(
265
226
  values,
@@ -269,7 +230,7 @@ async def annotate_spans(request: Request) -> Response:
269
230
  ).returning(models.SpanAnnotation.id)
270
231
  )
271
232
  inserted_annotations.append(
272
- {"id": str(GlobalID("SpanAnnotation", str(span_annotation_id)))}
233
+ InsertedSpanAnnotation(id=str(GlobalID("SpanAnnotation", str(span_annotation_id))))
273
234
  )
274
235
 
275
- return JSONResponse(content={"data": inserted_annotations})
236
+ return AnnotateSpansResponseBody(data=inserted_annotations)