magic-lab-observer-backend 0.1.1__tar.gz

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.
Files changed (38) hide show
  1. magic_lab_observer_backend-0.1.1/.gitignore +68 -0
  2. magic_lab_observer_backend-0.1.1/Containerfile +19 -0
  3. magic_lab_observer_backend-0.1.1/PKG-INFO +62 -0
  4. magic_lab_observer_backend-0.1.1/README.md +32 -0
  5. magic_lab_observer_backend-0.1.1/alembic/env.py +71 -0
  6. magic_lab_observer_backend-0.1.1/alembic/script.py.mako +28 -0
  7. magic_lab_observer_backend-0.1.1/alembic/versions/20260720_manitos_ingest.py +83 -0
  8. magic_lab_observer_backend-0.1.1/alembic/versions/623c57886e29_initial_schema.py +87 -0
  9. magic_lab_observer_backend-0.1.1/alembic.ini +42 -0
  10. magic_lab_observer_backend-0.1.1/app/__init__.py +1 -0
  11. magic_lab_observer_backend-0.1.1/app/api/__init__.py +5 -0
  12. magic_lab_observer_backend-0.1.1/app/api/analytics.py +389 -0
  13. magic_lab_observer_backend-0.1.1/app/api/evaluations.py +314 -0
  14. magic_lab_observer_backend-0.1.1/app/api/manitos_ingest.py +454 -0
  15. magic_lab_observer_backend-0.1.1/app/api/traces.py +753 -0
  16. magic_lab_observer_backend-0.1.1/app/auth.py +36 -0
  17. magic_lab_observer_backend-0.1.1/app/db/__init__.py +20 -0
  18. magic_lab_observer_backend-0.1.1/app/db/postgres.py +27 -0
  19. magic_lab_observer_backend-0.1.1/app/evaluators/__init__.py +313 -0
  20. magic_lab_observer_backend-0.1.1/app/evaluators/rubric_engine.py +517 -0
  21. magic_lab_observer_backend-0.1.1/app/evaluators/rubrics/default.yaml +218 -0
  22. magic_lab_observer_backend-0.1.1/app/evaluators/rubrics/scoring_levels.json +182 -0
  23. magic_lab_observer_backend-0.1.1/app/main.py +123 -0
  24. magic_lab_observer_backend-0.1.1/app/models/__init__.py +16 -0
  25. magic_lab_observer_backend-0.1.1/app/models/evaluation.py +38 -0
  26. magic_lab_observer_backend-0.1.1/app/models/ingestion.py +27 -0
  27. magic_lab_observer_backend-0.1.1/app/models/trace.py +66 -0
  28. magic_lab_observer_backend-0.1.1/app/ops/__init__.py +1 -0
  29. magic_lab_observer_backend-0.1.1/app/ops/manitos_passive_gate.py +738 -0
  30. magic_lab_observer_backend-0.1.1/app/ratelimit.py +25 -0
  31. magic_lab_observer_backend-0.1.1/app/websocket.py +53 -0
  32. magic_lab_observer_backend-0.1.1/pyproject.toml +59 -0
  33. magic_lab_observer_backend-0.1.1/tests/conftest.py +56 -0
  34. magic_lab_observer_backend-0.1.1/tests/test_analytics_quality_window.py +84 -0
  35. magic_lab_observer_backend-0.1.1/tests/test_api.py +963 -0
  36. magic_lab_observer_backend-0.1.1/tests/test_manitos_ingest.py +338 -0
  37. magic_lab_observer_backend-0.1.1/tests/test_manitos_passive_gate.py +390 -0
  38. magic_lab_observer_backend-0.1.1/tests/test_rubric.py +617 -0
@@ -0,0 +1,68 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ .venv/
25
+ venv/
26
+ ENV/
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+ *~
34
+
35
+ # Testing
36
+ .coverage
37
+ htmlcov/
38
+ .pytest_cache/
39
+ .mypy_cache/
40
+
41
+ # Node.js
42
+ node_modules/
43
+ npm-debug.log*
44
+ yarn-debug.log*
45
+ yarn-error.log*
46
+
47
+ # Build
48
+ dist/
49
+ build/
50
+ *.tsbuildinfo
51
+
52
+ # Environment
53
+ .env
54
+ .env.local
55
+ .env.*.local
56
+
57
+ # Podman
58
+ podman-compose.override.yml
59
+
60
+ # Database
61
+ *.db
62
+ *.sqlite3
63
+ .observer-state/
64
+
65
+ # OS
66
+ .DS_Store
67
+ Thumbs.db
68
+
@@ -0,0 +1,19 @@
1
+ FROM python:3.13-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y --no-install-recommends \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Install the complete application as an immutable production package.
11
+ COPY . .
12
+ RUN pip install --no-cache-dir .
13
+
14
+ # Expose port
15
+ EXPOSE 8000
16
+
17
+ # Migrations must run against the configured runtime database, not against a
18
+ # throwaway SQLite file while the image is being built.
19
+ CMD ["sh", "-c", "alembic upgrade head && exec uvicorn app.main:app --host 0.0.0.0 --port 8000"]
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: magic-lab-observer-backend
3
+ Version: 0.1.1
4
+ Summary: Backend API for LLM Observatory
5
+ Project-URL: Homepage, https://github.com/Magic-Lab-Studio/observer
6
+ Project-URL: Repository, https://github.com/Magic-Lab-Studio/observer.git
7
+ Project-URL: Issues, https://github.com/Magic-Lab-Studio/observer/issues
8
+ License-Expression: Apache-2.0
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: alembic>=1.13.0
11
+ Requires-Dist: asyncpg>=0.29.0
12
+ Requires-Dist: fastapi>=0.111.0
13
+ Requires-Dist: httpx>=0.27.0
14
+ Requires-Dist: pydantic-settings>=2.0.0
15
+ Requires-Dist: pydantic>=2.0.0
16
+ Requires-Dist: python-dotenv>=1.0.0
17
+ Requires-Dist: pyyaml>=6.0
18
+ Requires-Dist: slowapi>=0.1.9
19
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.0
20
+ Requires-Dist: uvicorn[standard]>=0.30.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: aiosqlite>=0.20.0; extra == 'dev'
23
+ Requires-Dist: mypy>=1.10; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
25
+ Requires-Dist: pytest>=8.0; extra == 'dev'
26
+ Requires-Dist: ruff>=0.5; extra == 'dev'
27
+ Provides-Extra: sqlite
28
+ Requires-Dist: aiosqlite>=0.20.0; extra == 'sqlite'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # LLM Observatory Backend
32
+
33
+ FastAPI backend for LLM Observatory - Open-source LLM observability.
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ pip install -e ".[dev]"
39
+ ```
40
+
41
+ ## Running
42
+
43
+ ```bash
44
+ # Start PostgreSQL
45
+ podman run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=observatory postgres:16
46
+
47
+ # Run migrations
48
+ alembic upgrade head
49
+
50
+ # Start server
51
+ uvicorn app.main:app --reload
52
+ ```
53
+
54
+ ## API Documentation
55
+
56
+ Once running, visit:
57
+ - Swagger UI: http://localhost:8000/docs
58
+ - ReDoc: http://localhost:8000/redoc
59
+
60
+ ## License
61
+
62
+ Apache License 2.0
@@ -0,0 +1,32 @@
1
+ # LLM Observatory Backend
2
+
3
+ FastAPI backend for LLM Observatory - Open-source LLM observability.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install -e ".[dev]"
9
+ ```
10
+
11
+ ## Running
12
+
13
+ ```bash
14
+ # Start PostgreSQL
15
+ podman run -d --name postgres -p 5432:5432 -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=observatory postgres:16
16
+
17
+ # Run migrations
18
+ alembic upgrade head
19
+
20
+ # Start server
21
+ uvicorn app.main:app --reload
22
+ ```
23
+
24
+ ## API Documentation
25
+
26
+ Once running, visit:
27
+ - Swagger UI: http://localhost:8000/docs
28
+ - ReDoc: http://localhost:8000/redoc
29
+
30
+ ## License
31
+
32
+ Apache License 2.0
@@ -0,0 +1,71 @@
1
+ """Alembic environment configuration."""
2
+
3
+ import asyncio
4
+ import os
5
+ from logging.config import fileConfig
6
+
7
+ from sqlalchemy import pool
8
+ from sqlalchemy.engine import Connection
9
+ from sqlalchemy.ext.asyncio import async_engine_from_config
10
+
11
+ from alembic import context
12
+ from app.models import Base
13
+
14
+ # Alembic Config object
15
+ config = context.config
16
+ if database_url := os.getenv("DATABASE_URL"):
17
+ config.set_main_option("sqlalchemy.url", database_url.replace("%", "%%"))
18
+
19
+ # Interpret the config file for Python logging
20
+ if config.config_file_name is not None:
21
+ fileConfig(config.config_file_name)
22
+
23
+ # Add model's MetaData object for autogenerate support
24
+ target_metadata = Base.metadata
25
+
26
+
27
+ def run_migrations_offline() -> None:
28
+ """Run migrations in 'offline' mode."""
29
+ url = config.get_main_option("sqlalchemy.url")
30
+ context.configure(
31
+ url=url,
32
+ target_metadata=target_metadata,
33
+ literal_binds=True,
34
+ dialect_opts={"paramstyle": "named"},
35
+ )
36
+
37
+ with context.begin_transaction():
38
+ context.run_migrations()
39
+
40
+
41
+ def do_run_migrations(connection: Connection) -> None:
42
+ """Run migrations with a connection."""
43
+ context.configure(connection=connection, target_metadata=target_metadata)
44
+
45
+ with context.begin_transaction():
46
+ context.run_migrations()
47
+
48
+
49
+ async def run_async_migrations() -> None:
50
+ """Run migrations in 'online' mode."""
51
+ connectable = async_engine_from_config(
52
+ config.get_section(config.config_ini_section, {}),
53
+ prefix="sqlalchemy.",
54
+ poolclass=pool.NullPool,
55
+ )
56
+
57
+ async with connectable.connect() as connection:
58
+ await connection.run_sync(do_run_migrations)
59
+
60
+ await connectable.dispose()
61
+
62
+
63
+ def run_migrations_online() -> None:
64
+ """Run migrations in 'online' mode."""
65
+ asyncio.run(run_async_migrations())
66
+
67
+
68
+ if context.is_offline_mode():
69
+ run_migrations_offline()
70
+ else:
71
+ run_migrations_online()
@@ -0,0 +1,28 @@
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+
7
+ """
8
+ from typing import Sequence, Union
9
+
10
+ from alembic import op
11
+ import sqlalchemy as sa
12
+ ${imports if imports else ""}
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = ${repr(up_revision)}
16
+ down_revision: Union[str, None] = ${repr(down_revision)}
17
+ branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18
+ depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ ${upgrades if upgrades else "pass"}
24
+
25
+
26
+ def downgrade() -> None:
27
+ """Downgrade schema."""
28
+ ${downgrades if downgrades else "pass"}
@@ -0,0 +1,83 @@
1
+ """add versioned ManitOS ingestion contract
2
+
3
+ Revision ID: 20260720_manitos
4
+ Revises: 623c57886e29
5
+ Create Date: 2026-07-20
6
+ """
7
+
8
+ from collections.abc import Sequence
9
+
10
+ import sqlalchemy as sa
11
+
12
+ from alembic import op
13
+
14
+ revision: str = "20260720_manitos"
15
+ down_revision: str | None = "623c57886e29"
16
+ branch_labels: str | Sequence[str] | None = None
17
+ depends_on: str | Sequence[str] | None = None
18
+
19
+
20
+ def upgrade() -> None:
21
+ """Add ManitOS correlation fields and retry receipts."""
22
+
23
+ with op.batch_alter_table("traces") as batch_op:
24
+ batch_op.alter_column(
25
+ "session_id",
26
+ existing_type=sa.String(length=36),
27
+ type_=sa.String(length=255),
28
+ existing_nullable=True,
29
+ )
30
+ batch_op.add_column(sa.Column("turn_id", sa.String(length=128), nullable=True))
31
+ batch_op.add_column(sa.Column("project_id", sa.String(length=128), nullable=True))
32
+ batch_op.add_column(sa.Column("environment", sa.String(length=64), nullable=True))
33
+ batch_op.add_column(sa.Column("service_instance_id", sa.String(length=128), nullable=True))
34
+ batch_op.add_column(sa.Column("actor_id_hash", sa.String(length=128), nullable=True))
35
+ batch_op.add_column(
36
+ sa.Column(
37
+ "schema_version",
38
+ sa.String(length=64),
39
+ nullable=False,
40
+ server_default="observer.trace.v1",
41
+ )
42
+ )
43
+ batch_op.create_index("ix_traces_turn_id", ["turn_id"], unique=False)
44
+ batch_op.create_index("ix_traces_project_id", ["project_id"], unique=False)
45
+
46
+ op.create_table(
47
+ "ingestion_receipts",
48
+ sa.Column("id", sa.String(length=36), nullable=False),
49
+ sa.Column("project_id", sa.String(length=128), nullable=False),
50
+ sa.Column("idempotency_key", sa.String(length=255), nullable=False),
51
+ sa.Column("payload_sha256", sa.String(length=64), nullable=False),
52
+ sa.Column("trace_id", sa.String(length=36), nullable=False),
53
+ sa.Column("response", sa.JSON(), nullable=True),
54
+ sa.Column("created_at", sa.DateTime(timezone=True), nullable=True),
55
+ sa.PrimaryKeyConstraint("id"),
56
+ sa.UniqueConstraint("project_id", "idempotency_key", name="uq_ingestion_project_key"),
57
+ )
58
+ op.create_index("ix_ingestion_receipts_project_id", "ingestion_receipts", ["project_id"])
59
+ op.create_index("ix_ingestion_receipts_trace_id", "ingestion_receipts", ["trace_id"])
60
+
61
+
62
+ def downgrade() -> None:
63
+ """Remove ManitOS ingestion state."""
64
+
65
+ op.drop_index("ix_ingestion_receipts_trace_id", table_name="ingestion_receipts")
66
+ op.drop_index("ix_ingestion_receipts_project_id", table_name="ingestion_receipts")
67
+ op.drop_table("ingestion_receipts")
68
+
69
+ with op.batch_alter_table("traces") as batch_op:
70
+ batch_op.drop_index("ix_traces_project_id")
71
+ batch_op.drop_index("ix_traces_turn_id")
72
+ batch_op.drop_column("schema_version")
73
+ batch_op.drop_column("actor_id_hash")
74
+ batch_op.drop_column("service_instance_id")
75
+ batch_op.drop_column("environment")
76
+ batch_op.drop_column("project_id")
77
+ batch_op.drop_column("turn_id")
78
+ batch_op.alter_column(
79
+ "session_id",
80
+ existing_type=sa.String(length=255),
81
+ type_=sa.String(length=36),
82
+ existing_nullable=True,
83
+ )
@@ -0,0 +1,87 @@
1
+ """initial schema
2
+
3
+ Revision ID: 623c57886e29
4
+ Revises:
5
+ Create Date: 2026-07-12 20:58:35.221373
6
+
7
+ """
8
+ from collections.abc import Sequence
9
+
10
+ import sqlalchemy as sa
11
+
12
+ from alembic import op
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision: str = '623c57886e29'
16
+ down_revision: str | None = None
17
+ branch_labels: str | Sequence[str] | None = None
18
+ depends_on: str | Sequence[str] | None = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ """Upgrade schema."""
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table('traces',
25
+ sa.Column('id', sa.String(length=36), nullable=False),
26
+ sa.Column('name', sa.String(length=255), nullable=False),
27
+ sa.Column('session_id', sa.String(length=36), nullable=True),
28
+ sa.Column('start_time', sa.DateTime(timezone=True), nullable=False),
29
+ sa.Column('end_time', sa.DateTime(timezone=True), nullable=True),
30
+ sa.Column('status', sa.String(length=20), nullable=False),
31
+ sa.Column('metadata', sa.JSON(), nullable=True),
32
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
33
+ sa.PrimaryKeyConstraint('id')
34
+ )
35
+ op.create_index(op.f('ix_traces_session_id'), 'traces', ['session_id'], unique=False)
36
+ op.create_table('spans',
37
+ sa.Column('id', sa.String(length=36), nullable=False),
38
+ sa.Column('trace_id', sa.String(length=36), nullable=False),
39
+ sa.Column('parent_span_id', sa.String(length=36), nullable=True),
40
+ sa.Column('name', sa.String(length=255), nullable=False),
41
+ sa.Column('span_type', sa.String(length=50), nullable=False),
42
+ sa.Column('start_time', sa.DateTime(timezone=True), nullable=False),
43
+ sa.Column('end_time', sa.DateTime(timezone=True), nullable=True),
44
+ sa.Column('status', sa.String(length=20), nullable=False),
45
+ sa.Column('input', sa.JSON(), nullable=True),
46
+ sa.Column('output', sa.JSON(), nullable=True),
47
+ sa.Column('tokens_input', sa.Integer(), nullable=True),
48
+ sa.Column('tokens_output', sa.Integer(), nullable=True),
49
+ sa.Column('cost_usd', sa.Numeric(precision=10, scale=6), nullable=True),
50
+ sa.Column('metadata', sa.JSON(), nullable=True),
51
+ sa.Column('attributes', sa.JSON(), nullable=True),
52
+ sa.ForeignKeyConstraint(['parent_span_id'], ['spans.id'], ),
53
+ sa.ForeignKeyConstraint(['trace_id'], ['traces.id'], ondelete='CASCADE'),
54
+ sa.PrimaryKeyConstraint('id')
55
+ )
56
+ op.create_index(op.f('ix_spans_parent_span_id'), 'spans', ['parent_span_id'], unique=False)
57
+ op.create_index(op.f('ix_spans_trace_id'), 'spans', ['trace_id'], unique=False)
58
+ op.create_table('evaluations',
59
+ sa.Column('id', sa.String(length=36), nullable=False),
60
+ sa.Column('trace_id', sa.String(length=36), nullable=False),
61
+ sa.Column('span_id', sa.String(length=36), nullable=True),
62
+ sa.Column('evaluator_type', sa.String(length=20), nullable=False),
63
+ sa.Column('score', sa.Numeric(precision=5, scale=4), nullable=True),
64
+ sa.Column('criteria', sa.JSON(), nullable=True),
65
+ sa.Column('result', sa.JSON(), nullable=True),
66
+ sa.Column('created_at', sa.DateTime(timezone=True), nullable=True),
67
+ sa.ForeignKeyConstraint(['span_id'], ['spans.id'], ondelete='CASCADE'),
68
+ sa.ForeignKeyConstraint(['trace_id'], ['traces.id'], ondelete='CASCADE'),
69
+ sa.PrimaryKeyConstraint('id')
70
+ )
71
+ op.create_index(op.f('ix_evaluations_span_id'), 'evaluations', ['span_id'], unique=False)
72
+ op.create_index(op.f('ix_evaluations_trace_id'), 'evaluations', ['trace_id'], unique=False)
73
+ # ### end Alembic commands ###
74
+
75
+
76
+ def downgrade() -> None:
77
+ """Downgrade schema."""
78
+ # ### commands auto generated by Alembic - please adjust! ###
79
+ op.drop_index(op.f('ix_evaluations_trace_id'), table_name='evaluations')
80
+ op.drop_index(op.f('ix_evaluations_span_id'), table_name='evaluations')
81
+ op.drop_table('evaluations')
82
+ op.drop_index(op.f('ix_spans_trace_id'), table_name='spans')
83
+ op.drop_index(op.f('ix_spans_parent_span_id'), table_name='spans')
84
+ op.drop_table('spans')
85
+ op.drop_index(op.f('ix_traces_session_id'), table_name='traces')
86
+ op.drop_table('traces')
87
+ # ### end Alembic commands ###
@@ -0,0 +1,42 @@
1
+ # Alembic Configuration File
2
+ # https://alembic.sqlalchemy.org/en/latest/tutorial.html
3
+
4
+ [alembic]
5
+ script_location = alembic
6
+ prepend_sys_path = .
7
+ # Default to SQLite for local development, override with DATABASE_URL env var
8
+ sqlalchemy.url = sqlite+aiosqlite:///./observatory.db
9
+
10
+ [loggers]
11
+ keys = root,sqlalchemy,alembic
12
+
13
+ [handlers]
14
+ keys = console
15
+
16
+ [formatters]
17
+ keys = generic
18
+
19
+ [logger_root]
20
+ level = WARN
21
+ handlers = console
22
+ qualname =
23
+
24
+ [logger_sqlalchemy]
25
+ level = WARN
26
+ handlers =
27
+ qualname = sqlalchemy.engine
28
+
29
+ [logger_alembic]
30
+ level = INFO
31
+ handlers =
32
+ qualname = alembic
33
+
34
+ [handler_console]
35
+ class = StreamHandler
36
+ args = (sys.stderr,)
37
+ level = NOTSET
38
+ formatter = generic
39
+
40
+ [formatter_generic]
41
+ format = %(levelname)-5.5s [%(name)s] %(message)s
42
+ datefmt = %H:%M:%S
@@ -0,0 +1 @@
1
+ """LLM Observatory Backend - FastAPI application."""
@@ -0,0 +1,5 @@
1
+ """API routers for LLM Observatory."""
2
+
3
+ from app.api import analytics, evaluations, traces
4
+
5
+ __all__ = ["traces", "evaluations", "analytics"]