arize-phoenix 4.10.2rc2__py3-none-any.whl → 4.12.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 (30) hide show
  1. {arize_phoenix-4.10.2rc2.dist-info → arize_phoenix-4.12.0.dist-info}/METADATA +3 -4
  2. {arize_phoenix-4.10.2rc2.dist-info → arize_phoenix-4.12.0.dist-info}/RECORD +29 -29
  3. phoenix/server/api/context.py +7 -3
  4. phoenix/server/api/openapi/main.py +2 -18
  5. phoenix/server/api/openapi/schema.py +12 -12
  6. phoenix/server/api/routers/v1/__init__.py +83 -36
  7. phoenix/server/api/routers/v1/dataset_examples.py +123 -102
  8. phoenix/server/api/routers/v1/datasets.py +507 -389
  9. phoenix/server/api/routers/v1/evaluations.py +66 -73
  10. phoenix/server/api/routers/v1/experiment_evaluations.py +91 -67
  11. phoenix/server/api/routers/v1/experiment_runs.py +155 -97
  12. phoenix/server/api/routers/v1/experiments.py +181 -131
  13. phoenix/server/api/routers/v1/spans.py +173 -143
  14. phoenix/server/api/routers/v1/traces.py +128 -114
  15. phoenix/server/api/types/Span.py +1 -0
  16. phoenix/server/app.py +176 -148
  17. phoenix/server/openapi/docs.py +221 -0
  18. phoenix/server/static/index.js +574 -573
  19. phoenix/server/thread_server.py +2 -2
  20. phoenix/session/client.py +5 -0
  21. phoenix/session/data_extractor.py +20 -1
  22. phoenix/session/session.py +4 -0
  23. phoenix/trace/attributes.py +2 -1
  24. phoenix/trace/schemas.py +1 -0
  25. phoenix/trace/span_json_decoder.py +1 -1
  26. phoenix/version.py +1 -1
  27. phoenix/server/api/routers/v1/utils.py +0 -94
  28. {arize_phoenix-4.10.2rc2.dist-info → arize_phoenix-4.12.0.dist-info}/WHEEL +0 -0
  29. {arize_phoenix-4.10.2rc2.dist-info → arize_phoenix-4.12.0.dist-info}/licenses/IP_NOTICE +0 -0
  30. {arize_phoenix-4.10.2rc2.dist-info → arize_phoenix-4.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,9 @@
1
- from datetime import datetime, timezone
2
- from typing import Any, AsyncIterator, Dict, List, Literal, Optional
1
+ from datetime import timezone
2
+ from typing import Any, AsyncIterator, Dict, List
3
3
 
4
- from fastapi import APIRouter, HTTPException, Query
5
- from pydantic import BaseModel, Field
6
4
  from sqlalchemy import select
7
5
  from starlette.requests import Request
8
- from starlette.responses import Response, StreamingResponse
6
+ from starlette.responses import JSONResponse, Response, StreamingResponse
9
7
  from starlette.status import HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY
10
8
  from strawberry.relay import GlobalID
11
9
 
@@ -14,81 +12,93 @@ from phoenix.datetime_utils import normalize_datetime
14
12
  from phoenix.db import models
15
13
  from phoenix.db.helpers import SupportedSQLDialect
16
14
  from phoenix.db.insertion.helpers import insert_on_conflict
17
- from phoenix.server.api.routers.utils import df_to_bytes
15
+ from phoenix.server.api.routers.utils import df_to_bytes, from_iso_format
18
16
  from phoenix.server.api.types.node import from_global_id_with_expected_type
19
- from phoenix.trace.dsl import SpanQuery as SpanQuery_
20
-
21
- from .utils import RequestBody, ResponseBody, add_errors_to_responses
17
+ from phoenix.trace.dsl import SpanQuery
22
18
 
23
19
  DEFAULT_SPAN_LIMIT = 1000
24
20
 
25
- router = APIRouter(tags=["traces"], include_in_schema=False)
26
-
27
-
28
- class SpanQuery(BaseModel):
29
- select: Optional[Dict[str, Any]] = None
30
- filter: Optional[Dict[str, Any]] = None
31
- explode: Optional[Dict[str, Any]] = None
32
- concat: Optional[Dict[str, Any]] = None
33
- rename: Optional[Dict[str, Any]] = None
34
- index: Optional[Dict[str, Any]] = None
35
-
36
-
37
- class QuerySpansRequestBody(BaseModel):
38
- queries: List[SpanQuery]
39
- start_time: Optional[datetime] = None
40
- end_time: Optional[datetime] = None
41
- limit: int = DEFAULT_SPAN_LIMIT
42
- root_spans_only: Optional[bool] = None
43
- project_name: Optional[str] = Field(
44
- default=None,
45
- description=(
46
- "The name of the project to query. "
47
- "This parameter has been deprecated, use the project_name query parameter instead."
48
- ),
49
- deprecated=True,
50
- )
51
- stop_time: Optional[datetime] = Field(
52
- default=None,
53
- description=(
54
- "An upper bound on the time to query for. "
55
- "This parameter has been deprecated, use the end_time parameter instead."
56
- ),
57
- deprecated=True,
58
- )
59
-
60
21
 
61
22
  # TODO: Add property details to SpanQuery schema
62
- @router.post(
63
- "/spans",
64
- operation_id="querySpans",
65
- summary="Query spans with query DSL",
66
- responses=add_errors_to_responses([HTTP_404_NOT_FOUND, HTTP_422_UNPROCESSABLE_ENTITY]),
67
- )
68
- async def query_spans_handler(
69
- request: Request,
70
- request_body: QuerySpansRequestBody,
71
- project_name: Optional[str] = Query(
72
- default=None, description="The project name to get evaluations from"
73
- ),
74
- ) -> Response:
75
- queries = request_body.queries
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", [])
76
86
  project_name = (
77
- project_name
87
+ request.query_params.get("project_name")
78
88
  or request.query_params.get("project-name") # for backward compatibility
79
89
  or request.headers.get(
80
90
  "project-name"
81
91
  ) # read from headers/payload for backward-compatibility
82
- or request_body.project_name
92
+ or payload.get("project_name")
83
93
  or DEFAULT_PROJECT_NAME
84
94
  )
85
- end_time = request_body.end_time or request_body.stop_time
95
+ end_time = payload.get("end_time") or payload.get("stop_time")
86
96
  try:
87
- span_queries = [SpanQuery_.from_dict(query.dict()) for query in queries]
97
+ span_queries = [SpanQuery.from_dict(query) for query in queries]
88
98
  except Exception as e:
89
- raise HTTPException(
90
- detail=f"Invalid query: {e}",
99
+ return Response(
91
100
  status_code=HTTP_422_UNPROCESSABLE_ENTITY,
101
+ content=f"Invalid query: {e}",
92
102
  )
93
103
  async with request.app.state.db() as session:
94
104
  results = []
@@ -98,19 +108,19 @@ async def query_spans_handler(
98
108
  query,
99
109
  project_name=project_name,
100
110
  start_time=normalize_datetime(
101
- request_body.start_time,
111
+ from_iso_format(payload.get("start_time")),
102
112
  timezone.utc,
103
113
  ),
104
114
  end_time=normalize_datetime(
105
- end_time,
115
+ from_iso_format(end_time),
106
116
  timezone.utc,
107
117
  ),
108
- limit=request_body.limit,
109
- root_spans_only=request_body.root_spans_only,
118
+ limit=payload.get("limit", DEFAULT_SPAN_LIMIT),
119
+ root_spans_only=payload.get("root_spans_only"),
110
120
  )
111
121
  )
112
122
  if not results:
113
- raise HTTPException(status_code=HTTP_404_NOT_FOUND)
123
+ return Response(status_code=HTTP_404_NOT_FOUND)
114
124
 
115
125
  async def content() -> AsyncIterator[bytes]:
116
126
  for result in results:
@@ -122,73 +132,93 @@ async def query_spans_handler(
122
132
  )
123
133
 
124
134
 
125
- @router.get("/spans", include_in_schema=False, deprecated=True)
126
- async def get_spans_handler(
127
- request: Request,
128
- request_body: QuerySpansRequestBody,
129
- project_name: Optional[str] = Query(
130
- default=None, description="The project name to get evaluations from"
131
- ),
132
- ) -> Response:
133
- return await query_spans_handler(request, request_body, project_name)
134
-
135
-
136
- class SpanAnnotationResult(BaseModel):
137
- label: Optional[str] = Field(default=None, description="The label assigned by the annotation")
138
- score: Optional[float] = Field(default=None, description="The score assigned by the annotation")
139
- explanation: Optional[str] = Field(
140
- default=None, description="Explanation of the annotation result"
141
- )
142
-
143
-
144
- class SpanAnnotation(BaseModel):
145
- span_id: str = Field(description="The ID of the span being annotated")
146
- name: str = Field(description="The name of the annotation")
147
- annotator_kind: Literal["LLM", "HUMAN"] = Field(
148
- description="The kind of annotator used for the annotation"
149
- )
150
- result: Optional[SpanAnnotationResult] = Field(
151
- default=None, description="The result of the annotation"
152
- )
153
- metadata: Optional[Dict[str, Any]] = Field(
154
- default=None, description="Metadata for the annotation"
155
- )
156
-
157
-
158
- class AnnotateSpansRequestBody(RequestBody[List[SpanAnnotation]]):
159
- data: List[SpanAnnotation]
160
-
161
-
162
- class InsertedSpanAnnotation(BaseModel):
163
- id: str = Field(description="The ID of the inserted span annotation")
164
-
165
-
166
- class AnnotateSpansResponseBody(ResponseBody[InsertedSpanAnnotation]):
167
- pass
168
-
169
-
170
- @router.post(
171
- "/span_annotations",
172
- operation_id="annotateSpans",
173
- summary="Create or update span annotations",
174
- responses=add_errors_to_responses(
175
- [{"status_code": HTTP_404_NOT_FOUND, "description": "Span not found"}]
176
- ),
177
- response_description="Span annotations inserted successfully",
178
- )
179
- async def annotate_spans(
180
- request: Request, request_body: AnnotateSpansRequestBody
181
- ) -> AnnotateSpansResponseBody:
182
- span_annotations = request_body.data
183
- span_gids = [GlobalID.from_id(annotation.span_id) for annotation in span_annotations]
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]
184
214
 
185
215
  resolved_span_ids = []
186
216
  for span_gid in span_gids:
187
217
  try:
188
218
  resolved_span_ids.append(from_global_id_with_expected_type(span_gid, "Span"))
189
219
  except ValueError:
190
- raise HTTPException(
191
- detail="Span with ID {span_gid} does not exist",
220
+ return Response(
221
+ content="Span with ID {span_gid} does not exist",
192
222
  status_code=HTTP_404_NOT_FOUND,
193
223
  )
194
224
 
@@ -203,22 +233,22 @@ async def annotate_spans(
203
233
  missing_span_gids = [
204
234
  str(GlobalID("Span", str(span_gid))) for span_gid in missing_span_ids
205
235
  ]
206
- raise HTTPException(
207
- detail=f"Spans with IDs {', '.join(missing_span_gids)} do not exist.",
236
+ return Response(
237
+ content=f"Spans with IDs {', '.join(missing_span_gids)} do not exist.",
208
238
  status_code=HTTP_404_NOT_FOUND,
209
239
  )
210
240
 
211
241
  inserted_annotations = []
212
- for annotation in span_annotations:
213
- span_gid = GlobalID.from_id(annotation.span_id)
242
+ for annotation in payload:
243
+ span_gid = GlobalID.from_id(annotation["span_id"])
214
244
  span_id = from_global_id_with_expected_type(span_gid, "Span")
215
- name = annotation.name
216
- annotator_kind = annotation.annotator_kind
217
- result = annotation.result
218
- label = result.label if result else None
219
- score = result.score if result else None
220
- explanation = result.explanation if result else None
221
- metadata = annotation.metadata or {}
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 {}
222
252
 
223
253
  values = dict(
224
254
  span_rowid=span_id,
@@ -239,7 +269,7 @@ async def annotate_spans(
239
269
  ).returning(models.SpanAnnotation.id)
240
270
  )
241
271
  inserted_annotations.append(
242
- InsertedSpanAnnotation(id=str(GlobalID("SpanAnnotation", str(span_annotation_id))))
272
+ {"id": str(GlobalID("SpanAnnotation", str(span_annotation_id)))}
243
273
  )
244
274
 
245
- return AnnotateSpansResponseBody(data=inserted_annotations)
275
+ return JSONResponse(content={"data": inserted_annotations})