arize-phoenix 4.35.2__py3-none-any.whl → 5.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.
- {arize_phoenix-4.35.2.dist-info → arize_phoenix-5.0.0.dist-info}/METADATA +10 -12
- {arize_phoenix-4.35.2.dist-info → arize_phoenix-5.0.0.dist-info}/RECORD +92 -79
- phoenix/__init__.py +86 -0
- phoenix/auth.py +275 -14
- phoenix/config.py +369 -27
- phoenix/db/alembic.ini +0 -34
- phoenix/db/engines.py +27 -10
- phoenix/db/enums.py +20 -0
- phoenix/db/facilitator.py +112 -0
- phoenix/db/insertion/dataset.py +0 -1
- phoenix/db/insertion/types.py +1 -1
- phoenix/db/migrate.py +3 -3
- phoenix/db/migrations/env.py +0 -7
- phoenix/db/migrations/versions/cd164e83824f_users_and_tokens.py +157 -0
- phoenix/db/models.py +145 -60
- phoenix/experiments/evaluators/code_evaluators.py +9 -3
- phoenix/experiments/functions.py +1 -4
- phoenix/inferences/fixtures.py +0 -1
- phoenix/inferences/inferences.py +0 -1
- phoenix/logging/__init__.py +3 -0
- phoenix/logging/_config.py +90 -0
- phoenix/logging/_filter.py +6 -0
- phoenix/logging/_formatter.py +69 -0
- phoenix/metrics/__init__.py +0 -1
- phoenix/otel/settings.py +4 -4
- phoenix/server/api/README.md +28 -0
- phoenix/server/api/auth.py +32 -0
- phoenix/server/api/context.py +50 -2
- phoenix/server/api/dataloaders/__init__.py +4 -0
- phoenix/server/api/dataloaders/user_roles.py +30 -0
- phoenix/server/api/dataloaders/users.py +33 -0
- phoenix/server/api/exceptions.py +7 -0
- phoenix/server/api/mutations/__init__.py +0 -2
- phoenix/server/api/mutations/api_key_mutations.py +104 -86
- phoenix/server/api/mutations/dataset_mutations.py +8 -8
- phoenix/server/api/mutations/experiment_mutations.py +2 -2
- phoenix/server/api/mutations/export_events_mutations.py +3 -3
- phoenix/server/api/mutations/project_mutations.py +3 -3
- phoenix/server/api/mutations/span_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/trace_annotations_mutations.py +4 -4
- phoenix/server/api/mutations/user_mutations.py +282 -42
- phoenix/server/api/openapi/schema.py +2 -2
- phoenix/server/api/queries.py +48 -39
- phoenix/server/api/routers/__init__.py +11 -0
- phoenix/server/api/routers/auth.py +284 -0
- phoenix/server/api/routers/embeddings.py +26 -0
- phoenix/server/api/routers/oauth2.py +456 -0
- phoenix/server/api/routers/v1/__init__.py +38 -16
- phoenix/server/api/routers/v1/datasets.py +0 -1
- phoenix/server/api/types/ApiKey.py +11 -0
- phoenix/server/api/types/AuthMethod.py +9 -0
- phoenix/server/api/types/User.py +48 -4
- phoenix/server/api/types/UserApiKey.py +35 -1
- phoenix/server/api/types/UserRole.py +7 -0
- phoenix/server/app.py +105 -34
- phoenix/server/bearer_auth.py +161 -0
- phoenix/server/email/__init__.py +0 -0
- phoenix/server/email/sender.py +26 -0
- phoenix/server/email/templates/__init__.py +0 -0
- phoenix/server/email/templates/password_reset.html +19 -0
- phoenix/server/email/types.py +11 -0
- phoenix/server/grpc_server.py +6 -0
- phoenix/server/jwt_store.py +504 -0
- phoenix/server/main.py +61 -30
- phoenix/server/oauth2.py +51 -0
- phoenix/server/prometheus.py +20 -0
- phoenix/server/rate_limiters.py +191 -0
- phoenix/server/static/.vite/manifest.json +31 -31
- phoenix/server/static/assets/{components-Dte7_KRd.js → components-REunxTt6.js} +348 -286
- phoenix/server/static/assets/index-DAPJxlCw.js +101 -0
- phoenix/server/static/assets/{pages-CnTvEGEN.js → pages-1VrMk2pW.js} +559 -291
- phoenix/server/static/assets/{vendor-BC3OPQuM.js → vendor-B5IC0ivG.js} +5 -5
- phoenix/server/static/assets/{vendor-arizeai-NjB3cZzD.js → vendor-arizeai-aFbT4kl1.js} +2 -2
- phoenix/server/static/assets/{vendor-codemirror-gE_JCOgX.js → vendor-codemirror-BEGorXSV.js} +1 -1
- phoenix/server/static/assets/{vendor-recharts-BXLYwcXF.js → vendor-recharts-6nUU7gU_.js} +1 -1
- phoenix/server/telemetry.py +2 -2
- phoenix/server/templates/index.html +1 -0
- phoenix/server/types.py +157 -1
- phoenix/services.py +0 -1
- phoenix/session/client.py +7 -3
- phoenix/session/evaluation.py +0 -1
- phoenix/session/session.py +0 -1
- phoenix/settings.py +9 -0
- phoenix/trace/exporter.py +0 -1
- phoenix/trace/fixtures.py +0 -2
- phoenix/utilities/client.py +16 -0
- phoenix/utilities/logging.py +9 -1
- phoenix/utilities/re.py +3 -3
- phoenix/version.py +1 -1
- phoenix/db/migrations/future_versions/README.md +0 -4
- phoenix/db/migrations/future_versions/cd164e83824f_users_and_tokens.py +0 -293
- phoenix/db/migrations/versions/.gitignore +0 -1
- phoenix/server/api/mutations/auth.py +0 -18
- phoenix/server/api/mutations/auth_mutations.py +0 -65
- phoenix/server/static/assets/index-fq1-hCK4.js +0 -100
- phoenix/trace/langchain/__init__.py +0 -3
- phoenix/trace/langchain/instrumentor.py +0 -35
- phoenix/trace/llama_index/__init__.py +0 -3
- phoenix/trace/llama_index/callback.py +0 -103
- phoenix/trace/openai/__init__.py +0 -3
- phoenix/trace/openai/instrumentor.py +0 -31
- {arize_phoenix-4.35.2.dist-info → arize_phoenix-5.0.0.dist-info}/WHEEL +0 -0
- {arize_phoenix-4.35.2.dist-info → arize_phoenix-5.0.0.dist-info}/licenses/IP_NOTICE +0 -0
- {arize_phoenix-4.35.2.dist-info → arize_phoenix-5.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import secrets
|
|
5
|
+
from functools import partial
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import (
|
|
8
|
+
distinct,
|
|
9
|
+
insert,
|
|
10
|
+
select,
|
|
11
|
+
)
|
|
12
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
13
|
+
|
|
14
|
+
from phoenix.auth import (
|
|
15
|
+
DEFAULT_ADMIN_EMAIL,
|
|
16
|
+
DEFAULT_ADMIN_PASSWORD,
|
|
17
|
+
DEFAULT_ADMIN_USERNAME,
|
|
18
|
+
DEFAULT_SECRET_LENGTH,
|
|
19
|
+
DEFAULT_SYSTEM_EMAIL,
|
|
20
|
+
DEFAULT_SYSTEM_USERNAME,
|
|
21
|
+
compute_password_hash,
|
|
22
|
+
)
|
|
23
|
+
from phoenix.db import models
|
|
24
|
+
from phoenix.db.enums import COLUMN_ENUMS, UserRole
|
|
25
|
+
from phoenix.server.types import DbSessionFactory
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Facilitator:
|
|
29
|
+
"""
|
|
30
|
+
Facilitates the creation of database records necessary for Phoenix to function. This includes
|
|
31
|
+
ensuring that all enum values are present in their respective tables, ensuring that all user
|
|
32
|
+
roles are present, and ensuring that the admin user has a password hash. These tasks will be
|
|
33
|
+
carried out as callbacks at the very beginning of Starlette's lifespan process.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *, db: DbSessionFactory) -> None:
|
|
37
|
+
self._db = db
|
|
38
|
+
|
|
39
|
+
async def __call__(self) -> None:
|
|
40
|
+
async with self._db() as session:
|
|
41
|
+
for fn in (
|
|
42
|
+
_ensure_enums,
|
|
43
|
+
_ensure_user_roles,
|
|
44
|
+
):
|
|
45
|
+
async with session.begin_nested():
|
|
46
|
+
await fn(session)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def _ensure_enums(session: AsyncSession) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Ensure that all enum values are present in their respective tables. If any values are missing,
|
|
52
|
+
they will be added. If any values are present in the database but not in the enum, an error will
|
|
53
|
+
be raised. This function is idempotent: it will not add duplicate values to the database.
|
|
54
|
+
"""
|
|
55
|
+
for column, enum in COLUMN_ENUMS.items():
|
|
56
|
+
table = column.class_
|
|
57
|
+
existing = set([_ async for _ in await session.stream_scalars(select(distinct(column)))])
|
|
58
|
+
expected = set(e.value for e in enum)
|
|
59
|
+
if unexpected := existing - expected:
|
|
60
|
+
raise ValueError(f"Unexpected values in {table.name}.{column.key}: {unexpected}")
|
|
61
|
+
if not (missing := expected - existing):
|
|
62
|
+
continue
|
|
63
|
+
await session.execute(insert(table), [{column.key: v} for v in missing])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _ensure_user_roles(session: AsyncSession) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Ensure that the system and admin roles are present in the database. If they are not, they will
|
|
69
|
+
be added. The system user will have the email "system@localhost" and the admin user will have
|
|
70
|
+
the email "admin@localhost".
|
|
71
|
+
"""
|
|
72
|
+
role_ids = {
|
|
73
|
+
name: id_
|
|
74
|
+
async for name, id_ in await session.stream(
|
|
75
|
+
select(models.UserRole.name, models.UserRole.id)
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
existing_roles = [
|
|
79
|
+
name
|
|
80
|
+
async for name in await session.stream_scalars(
|
|
81
|
+
select(distinct(models.UserRole.name)).join_from(models.User, models.UserRole)
|
|
82
|
+
)
|
|
83
|
+
]
|
|
84
|
+
if (system_role := UserRole.SYSTEM.value) not in existing_roles and (
|
|
85
|
+
system_role_id := role_ids.get(system_role)
|
|
86
|
+
) is not None:
|
|
87
|
+
system_user = models.User(
|
|
88
|
+
user_role_id=system_role_id,
|
|
89
|
+
username=DEFAULT_SYSTEM_USERNAME,
|
|
90
|
+
email=DEFAULT_SYSTEM_EMAIL,
|
|
91
|
+
reset_password=False,
|
|
92
|
+
password_salt=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
93
|
+
password_hash=secrets.token_bytes(DEFAULT_SECRET_LENGTH),
|
|
94
|
+
)
|
|
95
|
+
session.add(system_user)
|
|
96
|
+
if (admin_role := UserRole.ADMIN.value) not in existing_roles and (
|
|
97
|
+
admin_role_id := role_ids.get(admin_role)
|
|
98
|
+
) is not None:
|
|
99
|
+
salt = secrets.token_bytes(DEFAULT_SECRET_LENGTH)
|
|
100
|
+
compute = partial(compute_password_hash, password=DEFAULT_ADMIN_PASSWORD, salt=salt)
|
|
101
|
+
loop = asyncio.get_running_loop()
|
|
102
|
+
hash_ = await loop.run_in_executor(None, compute)
|
|
103
|
+
admin_user = models.User(
|
|
104
|
+
user_role_id=admin_role_id,
|
|
105
|
+
username=DEFAULT_ADMIN_USERNAME,
|
|
106
|
+
email=DEFAULT_ADMIN_EMAIL,
|
|
107
|
+
password_salt=salt,
|
|
108
|
+
password_hash=hash_,
|
|
109
|
+
reset_password=True,
|
|
110
|
+
)
|
|
111
|
+
session.add(admin_user)
|
|
112
|
+
await session.flush()
|
phoenix/db/insertion/dataset.py
CHANGED
phoenix/db/insertion/types.py
CHANGED
|
@@ -29,7 +29,7 @@ from phoenix.db.insertion.helpers import insert_on_conflict
|
|
|
29
29
|
from phoenix.server.dml_event import DmlEvent
|
|
30
30
|
from phoenix.server.types import DbSessionFactory
|
|
31
31
|
|
|
32
|
-
logger = logging.getLogger(
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class Insertable(Protocol):
|
phoenix/db/migrate.py
CHANGED
|
@@ -14,7 +14,6 @@ from phoenix.exceptions import PhoenixMigrationError
|
|
|
14
14
|
from phoenix.settings import Settings
|
|
15
15
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
|
-
logger.addHandler(logging.NullHandler())
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
def printif(condition: bool, text: str) -> None:
|
|
@@ -48,8 +47,9 @@ def migrate(
|
|
|
48
47
|
alembic_cfg.set_main_option("script_location", scripts_location)
|
|
49
48
|
url = str(engine.url).replace("%", "%%")
|
|
50
49
|
alembic_cfg.set_main_option("sqlalchemy.url", url)
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
with engine.connect() as conn:
|
|
51
|
+
alembic_cfg.attributes["connection"] = conn
|
|
52
|
+
command.upgrade(alembic_cfg, "head")
|
|
53
53
|
engine.dispose()
|
|
54
54
|
printif(log_migrations, "---------------------------")
|
|
55
55
|
printif(log_migrations, "✅ Migrations complete.")
|
phoenix/db/migrations/env.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from logging.config import fileConfig
|
|
3
2
|
|
|
4
3
|
from alembic import context
|
|
5
4
|
from sqlalchemy import Connection, engine_from_config, pool
|
|
@@ -14,14 +13,8 @@ from phoenix.settings import Settings
|
|
|
14
13
|
# access to the values within the .ini file in use.
|
|
15
14
|
config = context.config
|
|
16
15
|
|
|
17
|
-
# Interpret the config file for Python logging.
|
|
18
|
-
# This line sets up loggers basically.
|
|
19
|
-
if config.config_file_name is not None:
|
|
20
|
-
fileConfig(config.config_file_name, disable_existing_loggers=False)
|
|
21
|
-
|
|
22
16
|
# add your model's MetaData object here
|
|
23
17
|
# for 'autogenerate' support
|
|
24
|
-
|
|
25
18
|
target_metadata = Base.metadata
|
|
26
19
|
|
|
27
20
|
# other values from the config, defined by the needs of env.py,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""users and tokens
|
|
2
|
+
|
|
3
|
+
Revision ID: cd164e83824f
|
|
4
|
+
Revises: 10460e46d750
|
|
5
|
+
Create Date: 2024-08-01 18:36:52.157604
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Sequence, Union
|
|
10
|
+
|
|
11
|
+
import sqlalchemy as sa
|
|
12
|
+
from alembic import op
|
|
13
|
+
|
|
14
|
+
# revision identifiers, used by Alembic.
|
|
15
|
+
revision: str = "cd164e83824f"
|
|
16
|
+
down_revision: Union[str, None] = "3be8647b87d8"
|
|
17
|
+
branch_labels: Union[str, Sequence[str], None] = None
|
|
18
|
+
depends_on: Union[str, Sequence[str], None] = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
op.create_table(
|
|
23
|
+
"user_roles",
|
|
24
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
25
|
+
sa.Column(
|
|
26
|
+
"name",
|
|
27
|
+
sa.String,
|
|
28
|
+
nullable=False,
|
|
29
|
+
unique=True,
|
|
30
|
+
index=True,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
op.create_table(
|
|
34
|
+
"users",
|
|
35
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
36
|
+
sa.Column(
|
|
37
|
+
"user_role_id",
|
|
38
|
+
sa.Integer,
|
|
39
|
+
sa.ForeignKey("user_roles.id", ondelete="CASCADE"),
|
|
40
|
+
nullable=False,
|
|
41
|
+
index=True,
|
|
42
|
+
),
|
|
43
|
+
sa.Column("username", sa.String, nullable=False, unique=True, index=True),
|
|
44
|
+
sa.Column("email", sa.String, nullable=False, unique=True, index=True),
|
|
45
|
+
sa.Column("profile_picture_url", sa.String, nullable=True),
|
|
46
|
+
sa.Column("password_hash", sa.LargeBinary, nullable=True),
|
|
47
|
+
sa.Column("password_salt", sa.LargeBinary, nullable=True),
|
|
48
|
+
sa.Column("reset_password", sa.Boolean, nullable=False),
|
|
49
|
+
sa.Column("oauth2_client_id", sa.String, nullable=True, index=True),
|
|
50
|
+
sa.Column("oauth2_user_id", sa.String, nullable=True, index=True),
|
|
51
|
+
sa.Column(
|
|
52
|
+
"created_at",
|
|
53
|
+
sa.TIMESTAMP(timezone=True),
|
|
54
|
+
nullable=False,
|
|
55
|
+
server_default=sa.func.now(),
|
|
56
|
+
),
|
|
57
|
+
sa.Column(
|
|
58
|
+
"updated_at",
|
|
59
|
+
sa.TIMESTAMP(timezone=True),
|
|
60
|
+
nullable=False,
|
|
61
|
+
server_default=sa.func.now(),
|
|
62
|
+
onupdate=sa.func.now(),
|
|
63
|
+
),
|
|
64
|
+
sa.CheckConstraint(
|
|
65
|
+
"(password_hash IS NULL) = (password_salt IS NULL)",
|
|
66
|
+
name="password_hash_and_salt",
|
|
67
|
+
),
|
|
68
|
+
sa.CheckConstraint(
|
|
69
|
+
"(oauth2_client_id IS NULL) = (oauth2_user_id IS NULL)",
|
|
70
|
+
name="oauth2_client_id_and_user_id",
|
|
71
|
+
),
|
|
72
|
+
sa.CheckConstraint(
|
|
73
|
+
"(password_hash IS NULL) != (oauth2_client_id IS NULL)",
|
|
74
|
+
name="exactly_one_auth_method",
|
|
75
|
+
),
|
|
76
|
+
sa.UniqueConstraint(
|
|
77
|
+
"oauth2_client_id",
|
|
78
|
+
"oauth2_user_id",
|
|
79
|
+
),
|
|
80
|
+
sqlite_autoincrement=True,
|
|
81
|
+
)
|
|
82
|
+
op.create_table(
|
|
83
|
+
"password_reset_tokens",
|
|
84
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
85
|
+
sa.Column(
|
|
86
|
+
"user_id",
|
|
87
|
+
sa.Integer,
|
|
88
|
+
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
89
|
+
unique=True,
|
|
90
|
+
index=True,
|
|
91
|
+
),
|
|
92
|
+
sa.Column(
|
|
93
|
+
"created_at",
|
|
94
|
+
sa.TIMESTAMP(timezone=True),
|
|
95
|
+
nullable=False,
|
|
96
|
+
server_default=sa.func.now(),
|
|
97
|
+
),
|
|
98
|
+
sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
|
|
99
|
+
sqlite_autoincrement=True,
|
|
100
|
+
)
|
|
101
|
+
op.create_table(
|
|
102
|
+
"refresh_tokens",
|
|
103
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
104
|
+
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), index=True),
|
|
105
|
+
sa.Column(
|
|
106
|
+
"created_at",
|
|
107
|
+
sa.TIMESTAMP(timezone=True),
|
|
108
|
+
nullable=False,
|
|
109
|
+
server_default=sa.func.now(),
|
|
110
|
+
),
|
|
111
|
+
sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
|
|
112
|
+
sqlite_autoincrement=True,
|
|
113
|
+
)
|
|
114
|
+
op.create_table(
|
|
115
|
+
"access_tokens",
|
|
116
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
117
|
+
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), index=True),
|
|
118
|
+
sa.Column(
|
|
119
|
+
"created_at",
|
|
120
|
+
sa.TIMESTAMP(timezone=True),
|
|
121
|
+
nullable=False,
|
|
122
|
+
server_default=sa.func.now(),
|
|
123
|
+
),
|
|
124
|
+
sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=False, index=True),
|
|
125
|
+
sa.Column(
|
|
126
|
+
"refresh_token_id",
|
|
127
|
+
sa.Integer,
|
|
128
|
+
sa.ForeignKey("refresh_tokens.id", ondelete="CASCADE"),
|
|
129
|
+
index=True,
|
|
130
|
+
unique=True,
|
|
131
|
+
),
|
|
132
|
+
sqlite_autoincrement=True,
|
|
133
|
+
)
|
|
134
|
+
op.create_table(
|
|
135
|
+
"api_keys",
|
|
136
|
+
sa.Column("id", sa.Integer, primary_key=True),
|
|
137
|
+
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id", ondelete="CASCADE"), index=True),
|
|
138
|
+
sa.Column("name", sa.String, nullable=False),
|
|
139
|
+
sa.Column("description", sa.String, nullable=True),
|
|
140
|
+
sa.Column(
|
|
141
|
+
"created_at",
|
|
142
|
+
sa.TIMESTAMP(timezone=True),
|
|
143
|
+
nullable=False,
|
|
144
|
+
server_default=sa.func.now(),
|
|
145
|
+
),
|
|
146
|
+
sa.Column("expires_at", sa.TIMESTAMP(timezone=True), nullable=True, index=True),
|
|
147
|
+
sqlite_autoincrement=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def downgrade() -> None:
|
|
152
|
+
op.drop_table("api_keys")
|
|
153
|
+
op.drop_table("access_tokens")
|
|
154
|
+
op.drop_table("refresh_tokens")
|
|
155
|
+
op.drop_table("password_reset_tokens")
|
|
156
|
+
op.drop_table("users")
|
|
157
|
+
op.drop_table("user_roles")
|
phoenix/db/models.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
|
+
from enum import Enum
|
|
2
3
|
from typing import Any, Dict, List, Optional, TypedDict
|
|
3
4
|
|
|
4
5
|
from sqlalchemy import (
|
|
@@ -18,6 +19,7 @@ from sqlalchemy import (
|
|
|
18
19
|
case,
|
|
19
20
|
func,
|
|
20
21
|
insert,
|
|
22
|
+
not_,
|
|
21
23
|
select,
|
|
22
24
|
text,
|
|
23
25
|
)
|
|
@@ -34,10 +36,15 @@ from sqlalchemy.orm import (
|
|
|
34
36
|
)
|
|
35
37
|
from sqlalchemy.sql import expression
|
|
36
38
|
|
|
37
|
-
from phoenix.config import
|
|
39
|
+
from phoenix.config import get_env_database_schema
|
|
38
40
|
from phoenix.datetime_utils import normalize_datetime
|
|
39
41
|
|
|
40
42
|
|
|
43
|
+
class AuthMethod(Enum):
|
|
44
|
+
LOCAL = "LOCAL"
|
|
45
|
+
OAUTH2 = "OAUTH2"
|
|
46
|
+
|
|
47
|
+
|
|
41
48
|
class JSONB(JSON):
|
|
42
49
|
# See https://docs.sqlalchemy.org/en/20/core/custom_types.html
|
|
43
50
|
__visit_name__ = "JSONB"
|
|
@@ -620,65 +627,143 @@ class ExperimentRunAnnotation(Base):
|
|
|
620
627
|
)
|
|
621
628
|
|
|
622
629
|
|
|
623
|
-
|
|
624
|
-
|
|
630
|
+
class UserRole(Base):
|
|
631
|
+
__tablename__ = "user_roles"
|
|
632
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
633
|
+
name: Mapped[str] = mapped_column(unique=True, index=True)
|
|
634
|
+
users: Mapped[List["User"]] = relationship("User", back_populates="role")
|
|
625
635
|
|
|
626
|
-
class UserRole(Base):
|
|
627
|
-
__tablename__ = "user_roles"
|
|
628
|
-
id: Mapped[int] = mapped_column(primary_key=True)
|
|
629
|
-
name: Mapped[str] = mapped_column(unique=True)
|
|
630
|
-
users: Mapped[List["User"]] = relationship("User", back_populates="role")
|
|
631
636
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
637
|
+
class User(Base):
|
|
638
|
+
__tablename__ = "users"
|
|
639
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
640
|
+
user_role_id: Mapped[int] = mapped_column(
|
|
641
|
+
ForeignKey("user_roles.id", ondelete="CASCADE"),
|
|
642
|
+
index=True,
|
|
643
|
+
)
|
|
644
|
+
role: Mapped["UserRole"] = relationship("UserRole", back_populates="users")
|
|
645
|
+
username: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
|
|
646
|
+
email: Mapped[str] = mapped_column(nullable=False, unique=True, index=True)
|
|
647
|
+
profile_picture_url: Mapped[Optional[str]]
|
|
648
|
+
password_hash: Mapped[Optional[bytes]]
|
|
649
|
+
password_salt: Mapped[Optional[bytes]]
|
|
650
|
+
reset_password: Mapped[bool]
|
|
651
|
+
oauth2_client_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
|
|
652
|
+
oauth2_user_id: Mapped[Optional[str]] = mapped_column(index=True, nullable=True)
|
|
653
|
+
created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
|
|
654
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
655
|
+
UtcTimeStamp, server_default=func.now(), onupdate=func.now()
|
|
656
|
+
)
|
|
657
|
+
password_reset_token: Mapped[Optional["PasswordResetToken"]] = relationship(
|
|
658
|
+
"PasswordResetToken",
|
|
659
|
+
back_populates="user",
|
|
660
|
+
uselist=False,
|
|
661
|
+
)
|
|
662
|
+
access_tokens: Mapped[List["AccessToken"]] = relationship("AccessToken", back_populates="user")
|
|
663
|
+
refresh_tokens: Mapped[List["RefreshToken"]] = relationship(
|
|
664
|
+
"RefreshToken", back_populates="user"
|
|
665
|
+
)
|
|
666
|
+
api_keys: Mapped[List["ApiKey"]] = relationship("ApiKey", back_populates="user")
|
|
667
|
+
|
|
668
|
+
@hybrid_property
|
|
669
|
+
def auth_method(self) -> Optional[str]:
|
|
670
|
+
if self.password_hash is not None:
|
|
671
|
+
return AuthMethod.LOCAL.value
|
|
672
|
+
elif self.oauth2_client_id is not None:
|
|
673
|
+
return AuthMethod.OAUTH2.value
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
@auth_method.inplace.expression
|
|
677
|
+
@classmethod
|
|
678
|
+
def _auth_method_expression(cls) -> ColumnElement[Optional[str]]:
|
|
679
|
+
return case(
|
|
680
|
+
(
|
|
681
|
+
not_(cls.password_hash.is_(None)),
|
|
682
|
+
AuthMethod.LOCAL.value,
|
|
683
|
+
),
|
|
684
|
+
(
|
|
685
|
+
not_(cls.oauth2_client_id.is_(None)),
|
|
686
|
+
AuthMethod.OAUTH2.value,
|
|
687
|
+
),
|
|
688
|
+
else_=None,
|
|
683
689
|
)
|
|
684
|
-
|
|
690
|
+
|
|
691
|
+
__table_args__ = (
|
|
692
|
+
CheckConstraint(
|
|
693
|
+
"(password_hash IS NULL) = (password_salt IS NULL)",
|
|
694
|
+
name="password_hash_and_salt",
|
|
695
|
+
),
|
|
696
|
+
CheckConstraint(
|
|
697
|
+
"(oauth2_client_id IS NULL) = (oauth2_user_id IS NULL)",
|
|
698
|
+
name="oauth2_client_id_and_user_id",
|
|
699
|
+
),
|
|
700
|
+
CheckConstraint(
|
|
701
|
+
"(password_hash IS NULL) != (oauth2_client_id IS NULL)",
|
|
702
|
+
name="exactly_one_auth_method",
|
|
703
|
+
),
|
|
704
|
+
UniqueConstraint(
|
|
705
|
+
"oauth2_client_id",
|
|
706
|
+
"oauth2_user_id",
|
|
707
|
+
),
|
|
708
|
+
dict(sqlite_autoincrement=True),
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class PasswordResetToken(Base):
|
|
713
|
+
__tablename__ = "password_reset_tokens"
|
|
714
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
715
|
+
user_id: Mapped[int] = mapped_column(
|
|
716
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
717
|
+
unique=True,
|
|
718
|
+
index=True,
|
|
719
|
+
)
|
|
720
|
+
user: Mapped["User"] = relationship("User", back_populates="password_reset_token")
|
|
721
|
+
created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
|
|
722
|
+
expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
|
|
723
|
+
__table_args__ = (dict(sqlite_autoincrement=True),)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
class RefreshToken(Base):
|
|
727
|
+
__tablename__ = "refresh_tokens"
|
|
728
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
729
|
+
user_id: Mapped[int] = mapped_column(
|
|
730
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
731
|
+
index=True,
|
|
732
|
+
)
|
|
733
|
+
user: Mapped["User"] = relationship("User", back_populates="refresh_tokens")
|
|
734
|
+
created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
|
|
735
|
+
expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
|
|
736
|
+
__table_args__ = (dict(sqlite_autoincrement=True),)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class AccessToken(Base):
|
|
740
|
+
__tablename__ = "access_tokens"
|
|
741
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
742
|
+
user_id: Mapped[int] = mapped_column(
|
|
743
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
744
|
+
index=True,
|
|
745
|
+
)
|
|
746
|
+
user: Mapped["User"] = relationship("User", back_populates="access_tokens")
|
|
747
|
+
created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
|
|
748
|
+
expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=False, index=True)
|
|
749
|
+
refresh_token_id: Mapped[int] = mapped_column(
|
|
750
|
+
ForeignKey("refresh_tokens.id", ondelete="CASCADE"),
|
|
751
|
+
index=True,
|
|
752
|
+
unique=True,
|
|
753
|
+
)
|
|
754
|
+
__table_args__ = (dict(sqlite_autoincrement=True),)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class ApiKey(Base):
|
|
758
|
+
__tablename__ = "api_keys"
|
|
759
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
760
|
+
user_id: Mapped[int] = mapped_column(
|
|
761
|
+
ForeignKey("users.id", ondelete="CASCADE"),
|
|
762
|
+
index=True,
|
|
763
|
+
)
|
|
764
|
+
user: Mapped["User"] = relationship("User", back_populates="api_keys")
|
|
765
|
+
name: Mapped[str]
|
|
766
|
+
description: Mapped[Optional[str]]
|
|
767
|
+
created_at: Mapped[datetime] = mapped_column(UtcTimeStamp, server_default=func.now())
|
|
768
|
+
expires_at: Mapped[Optional[datetime]] = mapped_column(UtcTimeStamp, nullable=True, index=True)
|
|
769
|
+
__table_args__ = (dict(sqlite_autoincrement=True),)
|
|
@@ -2,7 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import re
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
List,
|
|
8
|
+
Optional,
|
|
9
|
+
Pattern, # import from re module when we drop support for 3.8
|
|
10
|
+
Union,
|
|
11
|
+
)
|
|
6
12
|
|
|
7
13
|
from phoenix.experiments.evaluators.base import CodeEvaluator
|
|
8
14
|
from phoenix.experiments.types import EvaluationResult, TaskOutput
|
|
@@ -144,7 +150,7 @@ class MatchesRegex(CodeEvaluator):
|
|
|
144
150
|
An experiment evaluator that checks if the output of an experiment run matches a regex pattern.
|
|
145
151
|
|
|
146
152
|
Args:
|
|
147
|
-
pattern (Union[str,
|
|
153
|
+
pattern (Union[str, Pattern[str]]): The regex pattern to match the output against.
|
|
148
154
|
name (str, optional): An optional name for the evaluator. Defaults to "matches_({pattern})".
|
|
149
155
|
|
|
150
156
|
Example:
|
|
@@ -157,7 +163,7 @@ class MatchesRegex(CodeEvaluator):
|
|
|
157
163
|
run_experiment(dataset, task, evaluators=[phone_number_evaluator])
|
|
158
164
|
"""
|
|
159
165
|
|
|
160
|
-
def __init__(self, pattern: Union[str,
|
|
166
|
+
def __init__(self, pattern: Union[str, Pattern[str]], name: Optional[str] = None) -> None:
|
|
161
167
|
if isinstance(pattern, str):
|
|
162
168
|
pattern = re.compile(pattern)
|
|
163
169
|
self.pattern = pattern
|
phoenix/experiments/functions.py
CHANGED
|
@@ -41,7 +41,7 @@ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
|
41
41
|
from opentelemetry.trace import Status, StatusCode, Tracer
|
|
42
42
|
from typing_extensions import TypeAlias
|
|
43
43
|
|
|
44
|
-
from phoenix.config import get_base_url
|
|
44
|
+
from phoenix.config import get_base_url
|
|
45
45
|
from phoenix.evals.executors import get_executor_on_sync_context
|
|
46
46
|
from phoenix.evals.models.rate_limiters import RateLimiter
|
|
47
47
|
from phoenix.evals.utils import get_tqdm_progress_bar_formatter
|
|
@@ -77,13 +77,10 @@ from phoenix.utilities.json import jsonify
|
|
|
77
77
|
|
|
78
78
|
|
|
79
79
|
def _phoenix_clients() -> Tuple[httpx.Client, httpx.AsyncClient]:
|
|
80
|
-
headers = get_env_client_headers()
|
|
81
80
|
return VersionedClient(
|
|
82
81
|
base_url=get_base_url(),
|
|
83
|
-
headers=headers,
|
|
84
82
|
), VersionedAsyncClient(
|
|
85
83
|
base_url=get_base_url(),
|
|
86
|
-
headers=headers,
|
|
87
84
|
)
|
|
88
85
|
|
|
89
86
|
|
phoenix/inferences/fixtures.py
CHANGED
phoenix/inferences/inferences.py
CHANGED