arize-phoenix 4.26.0__py3-none-any.whl → 4.28.1__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.

phoenix/server/app.py CHANGED
@@ -2,6 +2,7 @@ import asyncio
2
2
  import contextlib
3
3
  import json
4
4
  import logging
5
+ from datetime import datetime, timedelta, timezone
5
6
  from functools import cached_property
6
7
  from pathlib import Path
7
8
  from types import MethodType
@@ -10,12 +11,14 @@ from typing import (
10
11
  Any,
11
12
  AsyncContextManager,
12
13
  AsyncIterator,
14
+ Awaitable,
13
15
  Callable,
14
16
  Dict,
15
17
  Iterable,
16
18
  List,
17
19
  NamedTuple,
18
20
  Optional,
21
+ Set,
19
22
  Tuple,
20
23
  Union,
21
24
  cast,
@@ -26,11 +29,8 @@ from fastapi import APIRouter, FastAPI
26
29
  from fastapi.middleware.gzip import GZipMiddleware
27
30
  from fastapi.responses import FileResponse
28
31
  from fastapi.utils import is_body_allowed_for_status_code
29
- from sqlalchemy.ext.asyncio import (
30
- AsyncEngine,
31
- AsyncSession,
32
- async_sessionmaker,
33
- )
32
+ from sqlalchemy import select
33
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
34
34
  from starlette.datastructures import State as StarletteState
35
35
  from starlette.exceptions import HTTPException
36
36
  from starlette.middleware import Middleware
@@ -46,12 +46,9 @@ from typing_extensions import TypeAlias
46
46
 
47
47
  import phoenix
48
48
  import phoenix.trace.v1 as pb
49
- from phoenix.config import (
50
- DEFAULT_PROJECT_NAME,
51
- SERVER_DIR,
52
- server_instrumentation_is_enabled,
53
- )
49
+ from phoenix.config import DEFAULT_PROJECT_NAME, SERVER_DIR, server_instrumentation_is_enabled
54
50
  from phoenix.core.model_schema import Model
51
+ from phoenix.db import models
55
52
  from phoenix.db.bulk_inserter import BulkInserter
56
53
  from phoenix.db.engines import create_engine
57
54
  from phoenix.db.helpers import SupportedSQLDialect
@@ -82,7 +79,6 @@ from phoenix.server.api.dataloaders import (
82
79
  TokenCountDataLoader,
83
80
  TraceRowIdsDataLoader,
84
81
  )
85
- from phoenix.server.api.routers.auth import router as auth_router
86
82
  from phoenix.server.api.routers.v1 import REST_API_VERSION
87
83
  from phoenix.server.api.routers.v1 import router as v1_router
88
84
  from phoenix.server.api.schema import schema
@@ -93,9 +89,17 @@ from phoenix.server.telemetry import initialize_opentelemetry_tracer_provider
93
89
  from phoenix.server.types import (
94
90
  CanGetLastUpdatedAt,
95
91
  CanPutItem,
92
+ DaemonTask,
96
93
  DbSessionFactory,
97
94
  LastUpdatedAt,
98
95
  )
96
+ from phoenix.trace.fixtures import (
97
+ get_evals_from_fixture,
98
+ get_trace_fixture_by_name,
99
+ load_example_traces,
100
+ reset_fixture_span_ids_and_timestamps,
101
+ )
102
+ from phoenix.trace.otel import decode_otlp_span, encode_span_to_otlp
99
103
  from phoenix.trace.schemas import Span
100
104
  from phoenix.utilities.client import PHOENIX_SERVER_VERSION_HEADER
101
105
 
@@ -103,12 +107,23 @@ if TYPE_CHECKING:
103
107
  from opentelemetry.trace import TracerProvider
104
108
 
105
109
  logger = logging.getLogger(__name__)
110
+ logger.setLevel(logging.INFO)
106
111
  logger.addHandler(logging.NullHandler())
107
112
 
108
113
  router = APIRouter(include_in_schema=False)
109
114
 
110
115
  templates = Jinja2Templates(directory=SERVER_DIR / "templates")
111
116
 
117
+ """
118
+ Threshold (in minutes) to determine if database is booted up for the first time.
119
+
120
+ Used to assess whether the `default` project was created recently.
121
+ If so, demo data is automatically ingested upon initial boot up to populate the database.
122
+ """
123
+ NEW_DB_AGE_THRESHOLD_MINUTES = 2
124
+
125
+ ProjectName: TypeAlias = str
126
+
112
127
 
113
128
  class AppConfig(NamedTuple):
114
129
  has_inferences: bool
@@ -226,9 +241,99 @@ def _db(engine: AsyncEngine) -> Callable[[], AsyncContextManager[AsyncSession]]:
226
241
  return factory
227
242
 
228
243
 
244
+ class Scaffolder(DaemonTask):
245
+ def __init__(
246
+ self,
247
+ db: DbSessionFactory,
248
+ queue_span: Callable[[Span, ProjectName], Awaitable[None]],
249
+ queue_evaluation: Callable[[pb.Evaluation], Awaitable[None]],
250
+ tracing_fixture_names: Set[str] = set(),
251
+ force_fixture_ingestion: bool = False,
252
+ ) -> None:
253
+ super().__init__()
254
+ self._db = db
255
+ self._queue_span = queue_span
256
+ self._queue_evaluation = queue_evaluation
257
+ self._tracing_fixtures = set(
258
+ get_trace_fixture_by_name(name) for name in tracing_fixture_names
259
+ )
260
+ self._force_fixture_ingestion = force_fixture_ingestion
261
+
262
+ async def __aenter__(self) -> None:
263
+ await self.start()
264
+
265
+ async def __aexit__(self, *args: Any, **kwargs: Any) -> None:
266
+ await self.stop()
267
+
268
+ async def _run(self) -> None:
269
+ """
270
+ Main entry point for Scaffolder.
271
+ Determines whether to load fixtures and handles them.
272
+ """
273
+ if await self._should_load_fixtures():
274
+ logger.info("Loading trace fixtures.")
275
+ await self._handle_tracing_fixtures()
276
+ logger.info("Finished loading fixtures.")
277
+ else:
278
+ logger.info("DB is not new, avoid loading demo fixtures.")
279
+
280
+ async def _should_load_fixtures(self) -> bool:
281
+ if self._force_fixture_ingestion:
282
+ return True
283
+
284
+ async with self._db() as session:
285
+ created_at = await session.scalar(
286
+ select(models.Project.created_at).where(models.Project.name == "default")
287
+ )
288
+ if created_at is None:
289
+ return False
290
+
291
+ is_new_db = datetime.now(timezone.utc) - created_at < timedelta(
292
+ minutes=NEW_DB_AGE_THRESHOLD_MINUTES
293
+ )
294
+ return is_new_db
295
+
296
+ async def _handle_tracing_fixtures(self) -> None:
297
+ """
298
+ Main handler for processing trace fixtures. Process each fixture by
299
+ loading its trace dataframe, gettting and processings its
300
+ spans and evals, and queuing.
301
+ """
302
+ loop = asyncio.get_running_loop()
303
+ for fixture in self._tracing_fixtures:
304
+ try:
305
+ trace_ds = await loop.run_in_executor(None, load_example_traces, fixture.name)
306
+
307
+ fixture_spans, fixture_evals = await loop.run_in_executor(
308
+ None,
309
+ reset_fixture_span_ids_and_timestamps,
310
+ (
311
+ # Apply `encode` here because legacy jsonl files contains UUIDs as strings.
312
+ # `encode` removes the hyphens in the UUIDs.
313
+ decode_otlp_span(encode_span_to_otlp(span))
314
+ for span in trace_ds.to_spans()
315
+ ),
316
+ get_evals_from_fixture(fixture.name),
317
+ )
318
+
319
+ project_name = fixture.project_name or fixture.name
320
+ logger.info(f"Loading '{project_name}' fixtures...")
321
+ for span in fixture_spans:
322
+ await self._queue_span(span, project_name)
323
+ for evaluation in fixture_evals:
324
+ await self._queue_evaluation(evaluation)
325
+
326
+ except FileNotFoundError:
327
+ logger.warning(f"Fixture file not found for '{fixture.name}'")
328
+ except ValueError as e:
329
+ logger.error(f"Error processing fixture '{fixture.name}': {e}")
330
+ except Exception as e:
331
+ logger.error(f"Unexpected error processing fixture '{fixture.name}': {e}")
332
+
333
+
229
334
  def _lifespan(
230
335
  *,
231
- dialect: SupportedSQLDialect,
336
+ db: DbSessionFactory,
232
337
  bulk_inserter: BulkInserter,
233
338
  dml_event_handler: DmlEventHandler,
234
339
  tracer_provider: Optional["TracerProvider"] = None,
@@ -236,11 +341,13 @@ def _lifespan(
236
341
  startup_callbacks: Iterable[Callable[[], None]] = (),
237
342
  shutdown_callbacks: Iterable[Callable[[], None]] = (),
238
343
  read_only: bool = False,
344
+ tracing_fixture_names: Set[str] = set(),
345
+ force_fixture_ingestion: bool = False,
239
346
  ) -> StatefulLifespan[FastAPI]:
240
347
  @contextlib.asynccontextmanager
241
348
  async def lifespan(_: FastAPI) -> AsyncIterator[Dict[str, Any]]:
242
349
  global DB_MUTEX
243
- DB_MUTEX = asyncio.Lock() if dialect is SupportedSQLDialect.SQLITE else None
350
+ DB_MUTEX = asyncio.Lock() if db.dialect is SupportedSQLDialect.SQLITE else None
244
351
  async with bulk_inserter as (
245
352
  enqueue,
246
353
  queue_span,
@@ -251,7 +358,13 @@ def _lifespan(
251
358
  disabled=read_only,
252
359
  tracer_provider=tracer_provider,
253
360
  enable_prometheus=enable_prometheus,
254
- ), dml_event_handler:
361
+ ), dml_event_handler, Scaffolder(
362
+ db=db,
363
+ queue_span=queue_span,
364
+ queue_evaluation=queue_evaluation,
365
+ tracing_fixture_names=tracing_fixture_names,
366
+ force_fixture_ingestion=force_fixture_ingestion,
367
+ ):
255
368
  for callback in startup_callbacks:
256
369
  callback()
257
370
  yield {
@@ -317,17 +430,19 @@ def create_graphql_router(
317
430
  dataset_example_spans=DatasetExampleSpansDataLoader(db),
318
431
  document_evaluation_summaries=DocumentEvaluationSummaryDataLoader(
319
432
  db,
320
- cache_map=cache_for_dataloaders.document_evaluation_summary
321
- if cache_for_dataloaders
322
- else None,
433
+ cache_map=(
434
+ cache_for_dataloaders.document_evaluation_summary
435
+ if cache_for_dataloaders
436
+ else None
437
+ ),
323
438
  ),
324
439
  document_evaluations=DocumentEvaluationsDataLoader(db),
325
440
  document_retrieval_metrics=DocumentRetrievalMetricsDataLoader(db),
326
441
  annotation_summaries=AnnotationSummaryDataLoader(
327
442
  db,
328
- cache_map=cache_for_dataloaders.annotation_summary
329
- if cache_for_dataloaders
330
- else None,
443
+ cache_map=(
444
+ cache_for_dataloaders.annotation_summary if cache_for_dataloaders else None
445
+ ),
331
446
  ),
332
447
  experiment_annotation_summaries=ExperimentAnnotationSummaryDataLoader(db),
333
448
  experiment_error_rates=ExperimentErrorRatesDataLoader(db),
@@ -335,15 +450,17 @@ def create_graphql_router(
335
450
  experiment_sequence_number=ExperimentSequenceNumberDataLoader(db),
336
451
  latency_ms_quantile=LatencyMsQuantileDataLoader(
337
452
  db,
338
- cache_map=cache_for_dataloaders.latency_ms_quantile
339
- if cache_for_dataloaders
340
- else None,
453
+ cache_map=(
454
+ cache_for_dataloaders.latency_ms_quantile if cache_for_dataloaders else None
455
+ ),
341
456
  ),
342
457
  min_start_or_max_end_times=MinStartOrMaxEndTimeDataLoader(
343
458
  db,
344
- cache_map=cache_for_dataloaders.min_start_or_max_end_time
345
- if cache_for_dataloaders
346
- else None,
459
+ cache_map=(
460
+ cache_for_dataloaders.min_start_or_max_end_time
461
+ if cache_for_dataloaders
462
+ else None
463
+ ),
347
464
  ),
348
465
  record_counts=RecordCountDataLoader(
349
466
  db,
@@ -438,6 +555,8 @@ def create_app(
438
555
  startup_callbacks: Iterable[Callable[[], None]] = (),
439
556
  shutdown_callbacks: Iterable[Callable[[], None]] = (),
440
557
  secret: Optional[str] = None,
558
+ tracing_fixture_names: Set[str] = set(),
559
+ force_fixture_ingestion: bool = False,
441
560
  ) -> FastAPI:
442
561
  startup_callbacks_list: List[Callable[[], None]] = list(startup_callbacks)
443
562
  shutdown_callbacks_list: List[Callable[[], None]] = list(shutdown_callbacks)
@@ -510,11 +629,12 @@ def create_app(
510
629
  prometheus_middlewares = [Middleware(PrometheusMiddleware)]
511
630
  else:
512
631
  prometheus_middlewares = []
632
+
513
633
  app = FastAPI(
514
634
  title="Arize-Phoenix REST API",
515
635
  version=REST_API_VERSION,
516
636
  lifespan=_lifespan(
517
- dialect=db.dialect,
637
+ db=db,
518
638
  read_only=read_only,
519
639
  bulk_inserter=bulk_inserter,
520
640
  dml_event_handler=dml_event_handler,
@@ -522,6 +642,8 @@ def create_app(
522
642
  enable_prometheus=enable_prometheus,
523
643
  shutdown_callbacks=shutdown_callbacks_list,
524
644
  startup_callbacks=startup_callbacks_list,
645
+ tracing_fixture_names=tracing_fixture_names,
646
+ force_fixture_ingestion=force_fixture_ingestion,
525
647
  ),
526
648
  middleware=[
527
649
  Middleware(HeadersMiddleware),
@@ -539,8 +661,6 @@ def create_app(
539
661
  app.include_router(router)
540
662
  app.include_router(graphql_router)
541
663
  app.add_middleware(GZipMiddleware)
542
- if authentication_enabled:
543
- app.include_router(auth_router)
544
664
  if serve_ui:
545
665
  app.mount(
546
666
  "/",
phoenix/server/main.py CHANGED
@@ -48,6 +48,7 @@ from phoenix.trace.fixtures import (
48
48
  TRACES_FIXTURES,
49
49
  get_dataset_fixtures,
50
50
  get_evals_from_fixture,
51
+ get_trace_fixtures_by_project_name,
51
52
  load_example_traces,
52
53
  reset_fixture_span_ids_and_timestamps,
53
54
  send_dataset_fixtures,
@@ -135,7 +136,7 @@ if __name__ == "__main__":
135
136
  parser.add_argument("--export_path")
136
137
  parser.add_argument("--host", type=str, required=False)
137
138
  parser.add_argument("--port", type=int, required=False)
138
- parser.add_argument("--read-only", type=bool, default=False)
139
+ parser.add_argument("--read-only", action="store_true", required=False) # Default is False
139
140
  parser.add_argument("--no-internet", action="store_true")
140
141
  parser.add_argument("--umap_params", type=str, required=False, default=DEFAULT_UMAP_PARAMS_STR)
141
142
  parser.add_argument("--debug", action="store_true")
@@ -144,6 +145,43 @@ if __name__ == "__main__":
144
145
  parser.add_argument("--no-ui", action="store_true")
145
146
  subparsers = parser.add_subparsers(dest="command", required=True)
146
147
  serve_parser = subparsers.add_parser("serve")
148
+ serve_parser.add_argument(
149
+ "--with-fixture",
150
+ type=str,
151
+ required=False,
152
+ default="",
153
+ help=("Name of an inference fixture. Example: 'fixture1'"),
154
+ )
155
+ serve_parser.add_argument(
156
+ "--with-trace-fixtures",
157
+ type=str,
158
+ required=False,
159
+ default="",
160
+ help=(
161
+ "Comma separated list of tracing fixture names (spaces are ignored). "
162
+ "Example: 'fixture1, fixture2'"
163
+ ),
164
+ )
165
+ serve_parser.add_argument(
166
+ "--with-projects",
167
+ type=str,
168
+ required=False,
169
+ default="",
170
+ help=(
171
+ "Comma separated list of project names (spaces are ignored). "
172
+ "Example: 'project1, project2'"
173
+ ),
174
+ )
175
+ serve_parser.add_argument(
176
+ "--force-fixture-ingestion",
177
+ action="store_true", # default is False
178
+ required=False,
179
+ help=(
180
+ "Whether or not to check the database age before adding the fixtures. "
181
+ "Default is False, i.e., fixtures will only be added if the "
182
+ "database is new."
183
+ ),
184
+ )
147
185
  datasets_parser = subparsers.add_parser("datasets")
148
186
  datasets_parser.add_argument("--primary", type=str, required=True)
149
187
  datasets_parser.add_argument("--reference", type=str, required=False)
@@ -151,12 +189,14 @@ if __name__ == "__main__":
151
189
  datasets_parser.add_argument("--trace", type=str, required=False)
152
190
  fixture_parser = subparsers.add_parser("fixture")
153
191
  fixture_parser.add_argument("fixture", type=str, choices=[fixture.name for fixture in FIXTURES])
154
- fixture_parser.add_argument("--primary-only", type=bool)
192
+ fixture_parser.add_argument("--primary-only", action="store_true") # Default is False
155
193
  trace_fixture_parser = subparsers.add_parser("trace-fixture")
156
194
  trace_fixture_parser.add_argument(
157
195
  "fixture", type=str, choices=[fixture.name for fixture in TRACES_FIXTURES]
158
196
  )
159
- trace_fixture_parser.add_argument("--simulate-streaming", type=bool)
197
+ trace_fixture_parser.add_argument(
198
+ "--simulate-streaming", action="store_true"
199
+ ) # Default is False
160
200
  demo_parser = subparsers.add_parser("demo")
161
201
  demo_parser.add_argument("fixture", type=str, choices=[fixture.name for fixture in FIXTURES])
162
202
  demo_parser.add_argument(
@@ -164,10 +204,12 @@ if __name__ == "__main__":
164
204
  )
165
205
  demo_parser.add_argument("--simulate-streaming", action="store_true")
166
206
  args = parser.parse_args()
207
+
167
208
  db_connection_str = (
168
209
  args.database_url if args.database_url else get_env_database_connection_str()
169
210
  )
170
211
  export_path = Path(args.export_path) if args.export_path else EXPORT_DIR
212
+ force_fixture_ingestion = False
171
213
  if args.command == "datasets":
172
214
  primary_inferences_name = args.primary
173
215
  reference_inferences_name = args.reference
@@ -202,7 +244,26 @@ if __name__ == "__main__":
202
244
  )
203
245
  trace_dataset_name = args.trace_fixture
204
246
  simulate_streaming = args.simulate_streaming
205
-
247
+ elif args.command == "serve":
248
+ # We use sets to avoid duplicates
249
+ tracing_fixture_names = set()
250
+ if args.with_fixture:
251
+ primary_inferences, reference_inferences, corpus_inferences = get_inferences(
252
+ str(args.with_fixture),
253
+ args.no_internet,
254
+ )
255
+ if args.with_trace_fixtures:
256
+ tracing_fixture_names.update(
257
+ [name.strip() for name in args.with_trace_fixtures.split(",")]
258
+ )
259
+ if args.with_projects:
260
+ project_names = [name.strip() for name in args.with_projects.split(",")]
261
+ tracing_fixture_names.update(
262
+ fixture.name
263
+ for name in project_names
264
+ for fixture in get_trace_fixtures_by_project_name(name)
265
+ )
266
+ force_fixture_ingestion = args.force_fixture_ingestion
206
267
  host: Optional[str] = args.host or get_env_host()
207
268
  display_host = host or "localhost"
208
269
  # If the host is "::", the convention is to bind to all interfaces. However, uvicorn
@@ -260,6 +321,9 @@ if __name__ == "__main__":
260
321
  engine = create_engine_and_run_migrations(db_connection_str)
261
322
  instrumentation_cleanups = instrument_engine_if_enabled(engine)
262
323
  factory = DbSessionFactory(db=_db(engine), dialect=engine.dialect.name)
324
+ corpus_model = (
325
+ None if corpus_inferences is None else create_model_from_inferences(corpus_inferences)
326
+ )
263
327
  # Print information about the server
264
328
  msg = _WELCOME_MESSAGE.format(
265
329
  version=version("arize-phoenix"),
@@ -278,9 +342,7 @@ if __name__ == "__main__":
278
342
  model=model,
279
343
  authentication_enabled=authentication_enabled,
280
344
  umap_params=umap_params,
281
- corpus=None
282
- if corpus_inferences is None
283
- else create_model_from_inferences(corpus_inferences),
345
+ corpus=corpus_model,
284
346
  debug=args.debug,
285
347
  dev=args.dev,
286
348
  serve_ui=not args.no_ui,
@@ -291,6 +353,8 @@ if __name__ == "__main__":
291
353
  startup_callbacks=[lambda: print(msg)],
292
354
  shutdown_callbacks=instrumentation_cleanups,
293
355
  secret=secret,
356
+ tracing_fixture_names=tracing_fixture_names,
357
+ force_fixture_ingestion=force_fixture_ingestion,
294
358
  )
295
359
  server = Server(config=Config(app, host=host, port=port, root_path=host_root_path)) # type: ignore
296
360
  Thread(target=_write_pid_file_when_ready, args=(server,), daemon=True).start()
@@ -1,21 +1,21 @@
1
1
  {
2
- "_components-1Ahruijo.js": {
3
- "file": "assets/components-1Ahruijo.js",
2
+ "_components-BYH03rjA.js": {
3
+ "file": "assets/components-BYH03rjA.js",
4
4
  "name": "components",
5
5
  "imports": [
6
6
  "_vendor-aSQri0vz.js",
7
7
  "_vendor-arizeai-CsdcB1NH.js",
8
- "_pages-CFS6mPnW.js",
8
+ "_pages-DnbxgoTK.js",
9
9
  "_vendor-three-DwGkEfCM.js",
10
10
  "_vendor-codemirror-CYHkhs7D.js"
11
11
  ]
12
12
  },
13
- "_pages-CFS6mPnW.js": {
14
- "file": "assets/pages-CFS6mPnW.js",
13
+ "_pages-DnbxgoTK.js": {
14
+ "file": "assets/pages-DnbxgoTK.js",
15
15
  "name": "pages",
16
16
  "imports": [
17
17
  "_vendor-aSQri0vz.js",
18
- "_components-1Ahruijo.js",
18
+ "_components-BYH03rjA.js",
19
19
  "_vendor-arizeai-CsdcB1NH.js",
20
20
  "_vendor-recharts-B0sannek.js",
21
21
  "_vendor-codemirror-CYHkhs7D.js"
@@ -61,15 +61,15 @@
61
61
  "name": "vendor-three"
62
62
  },
63
63
  "index.tsx": {
64
- "file": "assets/index-BEE_RWJx.js",
64
+ "file": "assets/index-fqdjNpYm.js",
65
65
  "name": "index",
66
66
  "src": "index.tsx",
67
67
  "isEntry": true,
68
68
  "imports": [
69
69
  "_vendor-aSQri0vz.js",
70
70
  "_vendor-arizeai-CsdcB1NH.js",
71
- "_pages-CFS6mPnW.js",
72
- "_components-1Ahruijo.js",
71
+ "_pages-DnbxgoTK.js",
72
+ "_components-BYH03rjA.js",
73
73
  "_vendor-three-DwGkEfCM.js",
74
74
  "_vendor-recharts-B0sannek.js",
75
75
  "_vendor-codemirror-CYHkhs7D.js"