Merge pull request #3 from run-llama/clelia/add-observability

Adding observability dashboard
This commit is contained in:
Clelia (Astra) Bertelli
2025-06-29 12:11:29 +02:00
committed by GitHub
10 changed files with 708 additions and 12 deletions
+3
View File
@@ -1,3 +1,6 @@
OPENAI_API_KEY="sk-***"
LLAMACLOUD_API_KEY="llx-***"
ELEVENLABS_API_KEY="sk_***"
pgql_db="postgres"
pgql_user="localhost"
pgql_psw="admin"
+21
View File
@@ -0,0 +1,21 @@
name: GitHub Release
on:
push:
tags:
- "v[0-9].[0-9]+.[0-9]+*"
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: true
+6
View File
@@ -40,6 +40,12 @@ uv run tools/create_llama_cloud_index.py
And you're ready to set up the app!
Launch Postgres and Jaeger:
```bash
docker compose up -d
```
Run the **MCP** server:
```bash
+31
View File
@@ -0,0 +1,31 @@
name: instrumentation
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- 16686:16686
- 4317:4317
- 4318:4318
- 9411:9411
environment:
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
postgres:
image: postgres
ports:
- 5432:5432
environment:
POSTGRES_DB: $pgql_db
POSTGRES_USER: $pgql_user
POSTGRES_PASSWORD: $pgql_psw
volumes:
- pgdata:/var/lib/postgresql/data
adminer:
image: adminer
ports:
- "8080:8080"
volumes:
pgdata:
+6 -2
View File
@@ -1,7 +1,7 @@
[project]
name = "notebooklm-clone"
version = "0.1.0"
description = "Add your description here"
version = "0.2.0"
description = "An OSS and LlamaCloud-backed alternative to NotebookLM"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
@@ -15,10 +15,14 @@ dependencies = [
"llama-index-embeddings-openai>=0.3.1",
"llama-index-indices-managed-llama-cloud>=0.6.11",
"llama-index-llms-openai>=0.4.7",
"llama-index-observability-otel>=0.1.1",
"llama-index-tools-mcp>=0.2.5",
"llama-index-workflows>=1.0.1",
"mypy>=1.16.1",
"opentelemetry-exporter-otlp-proto-http>=1.34.1",
"plotly>=6.2.0",
"pre-commit>=4.2.0",
"psycopg2-binary>=2.9.10",
"pydub>=0.25.1",
"pytest>=8.4.1",
"pytest-asyncio>=1.0.0",
+31 -1
View File
@@ -3,12 +3,36 @@ import io
import os
import asyncio
import tempfile as temp
from dotenv import load_dotenv
import time
import streamlit.components.v1 as components
from pathlib import Path
from audio import PODCAST_GEN
from typing import Tuple
from workflow import NotebookLMWorkflow, FileInputEvent, NotebookOutputEvent
from instrumentation import OtelTracesSqlEngine
from llama_index.observability.otel import LlamaIndexOpenTelemetry
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter,
)
load_dotenv()
# define a custom span exporter
span_exporter = OTLPSpanExporter("http://0.0.0.0:4318/v1/traces")
# initialize the instrumentation object
instrumentor = LlamaIndexOpenTelemetry(
service_name_or_resource="agent.traces",
span_exporter=span_exporter,
debug=True,
)
sql_engine = OtelTracesSqlEngine(
engine_url=f"postgresql+psycopg2://{os.getenv('pgql_user')}:{os.getenv('pgql_psw')}@localhost:5432/{os.getenv('pgql_db')}",
table_name="agent_traces",
service_name="agent.traces",
)
WF = NotebookLMWorkflow(timeout=600)
@@ -24,6 +48,7 @@ async def run_workflow(file: io.BytesIO) -> Tuple[str, str, str, str, str]:
content = file.getvalue()
with open(fl.name, "wb") as f:
f.write(content)
st_time = int(time.time() * 1000000)
ev = FileInputEvent(file=fl.name)
result: NotebookOutputEvent = await WF.run(start_event=ev)
q_and_a = ""
@@ -34,7 +59,9 @@ async def run_workflow(file: io.BytesIO) -> Tuple[str, str, str, str, str]:
mind_map = result.mind_map
if Path(mind_map).is_file():
mind_map = read_html_file(mind_map)
os.remove(mind_map)
os.remove(result.mind_map)
end_time = int(time.time() * 1000000)
sql_engine.to_sql_database(start_time=st_time, end_time=end_time)
return result.md_content, result.summary, q_and_a, bullet_points, mind_map
@@ -138,3 +165,6 @@ if file_input is not None:
else:
st.info("Please upload a PDF file to get started.")
if __name__ == "__main__":
instrumentor.start_registering()
+168
View File
@@ -0,0 +1,168 @@
import requests
import time
import csv
import pandas as pd
import tempfile as temp
import os
from sqlalchemy import Engine, create_engine, Connection, Result
from typing import Optional, Dict, Any, List, Literal, Union, cast
class OtelTracesSqlEngine:
def __init__(
self,
engine: Optional[Engine] = None,
engine_url: Optional[str] = None,
table_name: Optional[str] = None,
service_name: Optional[str] = None,
):
self.service_name: str = service_name or "service"
self.table_name: str = table_name or "otel_traces"
self._connection: Optional[Connection] = None
if engine:
self._engine: Engine = engine
elif engine_url:
self._engine = create_engine(url=engine_url)
else:
raise ValueError("One of engine or engine_setup_kwargs must be set")
def _connect(self) -> None:
self._connection = self._engine.connect()
def _export(
self,
start_time: Optional[int] = None,
end_time: Optional[int] = None,
limit: Optional[int] = None,
) -> Dict[str, Any]:
url = "http://localhost:16686/api/traces"
params = {
"service": self.service_name,
"start": start_time
or int(time.time() * 1000000) - (24 * 60 * 60 * 1000000),
"end": end_time or int(time.time() * 1000000),
"limit": limit or 1000,
}
response = requests.get(url, params=params)
print(response.json())
return response.json()
def _to_pandas(self, data: Dict[str, Any]) -> pd.DataFrame:
rows: List[Dict[str, Any]] = []
# Loop over each trace
for trace in data.get("data", []):
trace_id = trace.get("traceID")
service_map = {
pid: proc.get("serviceName")
for pid, proc in trace.get("processes", {}).items()
}
for span in trace.get("spans", []):
span_id = span.get("spanID")
operation = span.get("operationName")
start = span.get("startTime")
duration = span.get("duration")
process_id = span.get("processID")
service = service_map.get(process_id, "")
status = next(
(
tag.get("value")
for tag in span.get("tags", [])
if tag.get("key") == "otel.status_code"
),
"",
)
parent_span_id = None
if span.get("references"):
parent_span_id = span["references"][0].get("spanID")
rows.append(
{
"trace_id": trace_id,
"span_id": span_id,
"parent_span_id": parent_span_id,
"operation_name": operation,
"start_time": start,
"duration": duration,
"status_code": status,
"service_name": service,
}
)
# Define the CSV header
fieldnames = [
"trace_id",
"span_id",
"parent_span_id",
"operation_name",
"start_time",
"duration",
"status_code",
"service_name",
]
fl = temp.NamedTemporaryFile(suffix=".csv", delete=False, delete_on_close=False)
# Write to CSV
with open(fl.name, "w", newline="") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
df = pd.read_csv(fl)
os.remove(fl.name)
return df
def _to_sql(
self,
dataframe: pd.DataFrame,
if_exists_policy: Optional[Literal["fail", "replace", "append"]] = None,
) -> None:
if not self._connection:
self._connect()
dataframe.to_sql(
name=self.table_name,
con=self._connection,
if_exists=if_exists_policy or "append",
)
def to_sql_database(
self,
start_time: Optional[int] = None,
end_time: Optional[int] = None,
limit: Optional[int] = None,
if_exists_policy: Optional[Literal["fail", "replace", "append"]] = None,
) -> None:
data = self._export(start_time=start_time, end_time=end_time, limit=limit)
df = self._to_pandas(data=data)
self._to_sql(dataframe=df, if_exists_policy=if_exists_policy)
def execute(
self,
statement: Any,
parameters: Optional[Any] = None,
execution_options: Optional[Any] = None,
return_pandas: bool = False,
) -> Union[Result, pd.DataFrame]:
if not self._connection:
self._connect()
if not return_pandas:
self._connection = cast(Connection, self._connection)
return self._connection.execute(
statement=statement,
parameters=parameters,
execution_options=execution_options,
)
return pd.read_sql(sql=statement, con=self._connection)
def to_pandas(
self,
) -> pd.DataFrame:
if not self._connection:
self._connect()
return pd.read_sql_table(table_name=self.table_name, con=self._connection)
def disconnect(self) -> None:
if not self._connection:
raise ValueError("Engine was never connected!")
self._engine.dispose(close=True)
@@ -0,0 +1,169 @@
import sys
import os
import pandas as pd
import streamlit as st
import plotly.express as px
import plotly.graph_objects as go
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from dotenv import load_dotenv
from instrumentation import OtelTracesSqlEngine
from sqlalchemy import text
load_dotenv()
sql_engine = OtelTracesSqlEngine(
engine_url=f"postgresql+psycopg2://{os.getenv('pgql_user')}:{os.getenv('pgql_psw')}@localhost:5432/{os.getenv('pgql_db')}",
table_name="agent_traces",
service_name="agent.traces",
)
def display_sql() -> pd.DataFrame:
query = """CREATE TABLE IF NOT EXISTS agent_traces (
trace_id TEXT NOT NULL,
span_id TEXT NOT NULL,
parent_span_id TEXT NULL,
operation_name TEXT NOT NULL,
start_time BIGINT NOT NULL,
duration INTEGER NOT NULL,
status_code TEXT NOT NULL,
service_name TEXT NOT NULL
);"""
sql_engine.execute(text(query))
return sql_engine.to_pandas()
def filter_traces(sql_query: str):
df = sql_engine.execute(text(sql_query), return_pandas=True)
return df
def create_latency_chart(df: pd.DataFrame):
"""Create a line chart showing latency (duration) over time"""
if df.empty:
st.warning("No data available for latency chart")
return
# Convert start_time from nanoseconds to datetime
df_chart = df.copy()
progressive_count = list(range(0, len(df_chart["start_time"])))
df_chart["progressive_count"] = progressive_count
fig = px.line(
df_chart,
x="progressive_count",
y="duration",
title="Latency Overview",
labels={"duration": "Duration (ns)", "timestamp": "Time"},
hover_data=["operation_name", "status_code"],
)
fig.update_layout(
xaxis_title="Time", yaxis_title="Duration (nanoseconds)", hovermode="x unified"
)
st.plotly_chart(fig)
def create_status_pie_chart(df: pd.DataFrame):
"""Create a pie chart showing status code distribution"""
if df.empty:
st.warning("No data available for status code chart")
return
# Count status codes
status_counts = df["status_code"].value_counts()
# Map common status codes to more readable labels
status_labels = {
"OK": "OK",
"ERROR": "ERROR",
"UNSET": "UNSET",
"200": "OK (200)",
"500": "ERROR (500)",
"404": "ERROR (404)",
}
# Create labels and values for the pie chart
labels = [status_labels.get(status, status) for status in status_counts.index]
values = status_counts.values
# Define colors
colors = []
for status in status_counts.index:
if status in ["OK", "200"]:
colors.append("#28a745") # Green for OK
elif status in ["ERROR", "500", "404"]:
colors.append("#dc3545") # Red for ERROR
else:
colors.append("#6c757d") # Gray for others
fig = go.Figure(
data=[go.Pie(labels=labels, values=values, hole=0.3, marker_colors=colors)]
)
fig.update_layout(
title="Status Code Distribution",
annotations=[dict(text="Status", x=0.5, y=0.5, font_size=20, showarrow=False)],
)
st.plotly_chart(fig)
# Streamlit UI
st.set_page_config(page_title="NotebookLlaMa - Observability Dashboard", page_icon="🔍")
st.sidebar.header("Observability Dashboard🔍")
st.sidebar.info("To switch to the other pages, select them from above!🔺")
st.markdown("---")
st.markdown("## NotebookLlaMa - Observability Dashboard🔍")
# Get the data
df_data = display_sql()
# Charts section
st.markdown("## 📊 Analytics Overview")
if not df_data.empty:
col1, col2 = st.columns(2)
with col1:
create_latency_chart(df_data)
with col2:
create_status_pie_chart(df_data)
else:
st.info("No trace data available yet. Charts will appear once data is collected.")
st.markdown("---")
# SQL Query section
st.markdown("### SQL Query")
sql_query = st.text_input(label="")
st.markdown("## Traces Table")
dataframe = st.dataframe(data=df_data)
if st.button("Run SQL query", type="primary"):
if sql_query.strip():
try:
filtered_df = filter_traces(sql_query=sql_query)
st.markdown("### Query Results")
dataframe = st.dataframe(data=filtered_df)
# Update charts with filtered data
if not filtered_df.empty:
st.markdown("### Updated Charts")
col1, col2 = st.columns(2)
with col1:
create_latency_chart(filtered_df)
with col2:
create_status_pie_chart(filtered_df)
except Exception as e:
st.error(f"Error executing query: {str(e)}")
else:
st.warning("Please enter a SQL query")
+84
View File
@@ -0,0 +1,84 @@
import socket
import pytest
import pandas as pd
import os
from dotenv import load_dotenv
from src.notebooklm_clone.instrumentation import OtelTracesSqlEngine
from sqlalchemy import text
ENV = load_dotenv()
def is_port_open(host: str, port: int, timeout: float = 2.0) -> bool:
"""Check if a TCP port is open on a given host."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(timeout)
result = sock.connect_ex((host, port))
return result == 0
@pytest.fixture()
def otel_data() -> pd.DataFrame:
return pd.DataFrame(
{
"trace_id": ["abc123", "abc123", "def456"],
"span_id": ["span1", "span2", "span3"],
"parent_span_id": [None, "span1", "span2"],
"operation_name": [
"ServiceA.handle_request",
"ServiceA.query_db",
"ServiceB.send_email",
],
"start_time": [1750618321000000, 1750618321000100, 1750618321000200],
"duration": [150, 300, 500],
"status_code": ["OK", "OK", "ERROR"],
"service_name": ["service-a", "service-a", "service-b"],
}
)
@pytest.mark.skipif(
condition=not is_port_open(host="localhost", port=5432) and not ENV,
reason="Either Postgres is currently unavailable or you did not set any env variables in a .env file",
)
def test_engine(otel_data: pd.DataFrame) -> None:
engine_url = f"postgresql+psycopg2://{os.getenv('pgql_user')}:{os.getenv('pgql_psw')}@localhost:5432/{os.getenv('pgql_db')}"
sql_engine = OtelTracesSqlEngine(engine_url=engine_url, table_name="test")
res = sql_engine.execute(text("DROP TABLE IF EXISTS test;"))
res.close()
sql_engine._to_sql(dataframe=otel_data)
res1 = sql_engine.execute(
text(
"SELECT span_id, operation_name, duration FROM test WHERE status_code = 'ERROR'"
)
)
res2 = sql_engine.execute(
text(
"SELECT service_name, AVG(duration) AS avg_duration FROM test GROUP BY service_name;"
)
)
res1_data = res1.fetchall()
res2_data = res2.fetchall()
# Compare just the values
assert len(res1_data) == 1
assert res1_data[0].span_id == "span3"
assert res1_data[0].operation_name == "ServiceB.send_email"
assert res1_data[0].duration == 500
assert len(res2_data) == 2
# Sort by service_name for consistent comparison
res2_sorted = sorted(res2_data, key=lambda x: x.service_name)
assert res2_sorted[0].service_name == "service-a"
assert res2_sorted[0].avg_duration == 225.0
assert res2_sorted[1].service_name == "service-b"
assert res2_sorted[1].avg_duration == 500.0
assert isinstance(sql_engine.to_pandas(), pd.DataFrame)
res3 = sql_engine.execute(
text(
"SELECT service_name, AVG(duration) AS avg_duration FROM test GROUP BY service_name;"
),
return_pandas=True,
)
assert isinstance(res3, pd.DataFrame)
Generated
+189 -9
View File
@@ -544,6 +544,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599, upload-time = "2025-01-02T07:32:40.731Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.70.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/39/24/33db22342cf4a2ea27c9955e6713140fedd51e8b141b5ce5260897020f1a/googleapis_common_protos-1.70.0.tar.gz", hash = "sha256:0e1b44e0ea153e6594f9f394fef15193a68aaaea2d843f83e2742717ca753257", size = 145903, upload-time = "2025-04-14T10:17:02.924Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/f1/62a193f0227cf15a920390abe675f386dec35f7ae3ffe6da582d3ade42c7/googleapis_common_protos-1.70.0-py3-none-any.whl", hash = "sha256:b8bfcca8c25a2bb253e0e0b0adaf8c00773e5e6af6fd92397576680b807e0fd8", size = 294530, upload-time = "2025-04-14T10:17:01.271Z" },
]
[[package]]
name = "greenlet"
version = "3.2.3"
@@ -644,6 +656,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "importlib-metadata"
version = "8.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
@@ -912,6 +936,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/e9/391926dad180ced6bb37a62edddb8483fbecde411239bd5e726841bb77b4/llama_index_llms_openai-0.4.7-py3-none-any.whl", hash = "sha256:3b8d9d3c1bcadc2cff09724de70f074f43eafd5b7048a91247c9a41b7cd6216d", size = 25365, upload-time = "2025-06-16T03:38:45.72Z" },
]
[[package]]
name = "llama-index-observability-otel"
version = "0.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "llama-index-core" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-sdk" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "termcolor" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/f2/22c28678fad040c579b9280769ffb79dc5c60d061b8a4b2e0d764787ceb0/llama_index_observability_otel-0.1.1.tar.gz", hash = "sha256:a3f25d0105225d609a198506ecb27c24e420ad28871cd8f2227ee27e55765eda", size = 6391, upload-time = "2025-05-29T04:24:11.04Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/3f/14796f0e0f975378e913240041bb3658216432eceb34d9b02eb39c4dd604/llama_index_observability_otel-0.1.1-py3-none-any.whl", hash = "sha256:d70ccd207c1ad6f31e6697cd8bca093e170c8589042beef17741fd56cef9f115", size = 6311, upload-time = "2025-05-29T04:24:09.826Z" },
]
[[package]]
name = "llama-index-tools-mcp"
version = "0.2.5"
@@ -1155,7 +1195,7 @@ wheels = [
[[package]]
name = "notebooklm-clone"
version = "0.1.0"
version = "0.2.0"
source = { virtual = "." }
dependencies = [
{ name = "audioop-lts" },
@@ -1168,10 +1208,14 @@ dependencies = [
{ name = "llama-index-embeddings-openai" },
{ name = "llama-index-indices-managed-llama-cloud" },
{ name = "llama-index-llms-openai" },
{ name = "llama-index-observability-otel" },
{ name = "llama-index-tools-mcp" },
{ name = "llama-index-workflows" },
{ name = "mypy" },
{ name = "opentelemetry-exporter-otlp-proto-http" },
{ name = "plotly" },
{ name = "pre-commit" },
{ name = "psycopg2-binary" },
{ name = "pydub" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
@@ -1192,10 +1236,14 @@ requires-dist = [
{ name = "llama-index-embeddings-openai", specifier = ">=0.3.1" },
{ name = "llama-index-indices-managed-llama-cloud", specifier = ">=0.6.11" },
{ name = "llama-index-llms-openai", specifier = ">=0.4.7" },
{ name = "llama-index-observability-otel", specifier = ">=0.1.1" },
{ name = "llama-index-tools-mcp", specifier = ">=0.2.5" },
{ name = "llama-index-workflows", specifier = ">=1.0.1" },
{ name = "mypy", specifier = ">=1.16.1" },
{ name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.34.1" },
{ name = "plotly", specifier = ">=6.2.0" },
{ name = "pre-commit", specifier = ">=4.2.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.10" },
{ name = "pydub", specifier = ">=0.25.1" },
{ name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-asyncio", specifier = ">=1.0.0" },
@@ -1265,6 +1313,88 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
]
[[package]]
name = "opentelemetry-api"
version = "1.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4d/5e/94a8cb759e4e409022229418294e098ca7feca00eb3c467bb20cbd329bda/opentelemetry_api-1.34.1.tar.gz", hash = "sha256:64f0bd06d42824843731d05beea88d4d4b6ae59f9fe347ff7dfa2cc14233bbb3", size = 64987, upload-time = "2025-06-10T08:55:19.818Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/3a/2ba85557e8dc024c0842ad22c570418dc02c36cbd1ab4b832a93edf071b8/opentelemetry_api-1.34.1-py3-none-any.whl", hash = "sha256:b7df4cb0830d5a6c29ad0c0691dbae874d8daefa934b8b1d642de48323d32a8c", size = 65767, upload-time = "2025-06-10T08:54:56.717Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-common"
version = "1.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-proto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/f0/ff235936ee40db93360233b62da932d4fd9e8d103cd090c6bcb9afaf5f01/opentelemetry_exporter_otlp_proto_common-1.34.1.tar.gz", hash = "sha256:b59a20a927facd5eac06edaf87a07e49f9e4a13db487b7d8a52b37cb87710f8b", size = 20817, upload-time = "2025-06-10T08:55:22.55Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/e8/8b292a11cc8d8d87ec0c4089ae21b6a58af49ca2e51fa916435bc922fdc7/opentelemetry_exporter_otlp_proto_common-1.34.1-py3-none-any.whl", hash = "sha256:8e2019284bf24d3deebbb6c59c71e6eef3307cd88eff8c633e061abba33f7e87", size = 18834, upload-time = "2025-06-10T08:55:00.806Z" },
]
[[package]]
name = "opentelemetry-exporter-otlp-proto-http"
version = "1.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-common" },
{ name = "opentelemetry-proto" },
{ name = "opentelemetry-sdk" },
{ name = "requests" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/8f/954bc725961cbe425a749d55c0ba1df46832a5999eae764d1a7349ac1c29/opentelemetry_exporter_otlp_proto_http-1.34.1.tar.gz", hash = "sha256:aaac36fdce46a8191e604dcf632e1f9380c7d5b356b27b3e0edb5610d9be28ad", size = 15351, upload-time = "2025-06-10T08:55:24.657Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/54/b05251c04e30c1ac70cf4a7c5653c085dfcf2c8b98af71661d6a252adc39/opentelemetry_exporter_otlp_proto_http-1.34.1-py3-none-any.whl", hash = "sha256:5251f00ca85872ce50d871f6d3cc89fe203b94c3c14c964bbdc3883366c705d8", size = 17744, upload-time = "2025-06-10T08:55:03.802Z" },
]
[[package]]
name = "opentelemetry-proto"
version = "1.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/b3/c3158dd012463bb7c0eb7304a85a6f63baeeb5b4c93a53845cf89f848c7e/opentelemetry_proto-1.34.1.tar.gz", hash = "sha256:16286214e405c211fc774187f3e4bbb1351290b8dfb88e8948af209ce85b719e", size = 34344, upload-time = "2025-06-10T08:55:32.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/ab/4591bfa54e946350ce8b3f28e5c658fe9785e7cd11e9c11b1671a867822b/opentelemetry_proto-1.34.1-py3-none-any.whl", hash = "sha256:eb4bb5ac27f2562df2d6857fc557b3a481b5e298bc04f94cc68041f00cebcbd2", size = 55692, upload-time = "2025-06-10T08:55:14.904Z" },
]
[[package]]
name = "opentelemetry-sdk"
version = "1.34.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "opentelemetry-semantic-conventions" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6f/41/fe20f9036433da8e0fcef568984da4c1d1c771fa072ecd1a4d98779dccdd/opentelemetry_sdk-1.34.1.tar.gz", hash = "sha256:8091db0d763fcd6098d4781bbc80ff0971f94e260739aa6afe6fd379cdf3aa4d", size = 159441, upload-time = "2025-06-10T08:55:33.028Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/1b/def4fe6aa73f483cabf4c748f4c25070d5f7604dcc8b52e962983491b29e/opentelemetry_sdk-1.34.1-py3-none-any.whl", hash = "sha256:308effad4059562f1d92163c61c8141df649da24ce361827812c40abb2a1e96e", size = 118477, upload-time = "2025-06-10T08:55:16.02Z" },
]
[[package]]
name = "opentelemetry-semantic-conventions"
version = "0.55b1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "opentelemetry-api" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5d/f0/f33458486da911f47c4aa6db9bda308bb80f3236c111bf848bd870c16b16/opentelemetry_semantic_conventions-0.55b1.tar.gz", hash = "sha256:ef95b1f009159c28d7a7849f5cbc71c4c34c845bb514d66adfdf1b3fff3598b3", size = 119829, upload-time = "2025-06-10T08:55:33.881Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/89/267b0af1b1d0ba828f0e60642b6a5116ac1fd917cde7fc02821627029bd1/opentelemetry_semantic_conventions-0.55b1-py3-none-any.whl", hash = "sha256:5da81dfdf7d52e3d37f8fe88d5e771e191de924cfff5f550ab0b8f7b2409baed", size = 196223, upload-time = "2025-06-10T08:55:17.638Z" },
]
[[package]]
name = "packaging"
version = "25.0"
@@ -1370,6 +1500,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" },
]
[[package]]
name = "plotly"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "narwhals" },
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/0efc297df362b88b74957a230af61cd6929f531f72f48063e8408702ffba/plotly-6.2.0.tar.gz", hash = "sha256:9dfa23c328000f16c928beb68927444c1ab9eae837d1fe648dbcda5360c7953d", size = 6801941, upload-time = "2025-06-26T16:20:45.765Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/20/f2b7ac96a91cc5f70d81320adad24cc41bf52013508d649b1481db225780/plotly-6.2.0-py3-none-any.whl", hash = "sha256:32c444d4c940887219cb80738317040363deefdfee4f354498cc0b6dab8978bd", size = 9635469, upload-time = "2025-06-26T16:20:40.76Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -1450,16 +1593,35 @@ wheels = [
[[package]]
name = "protobuf"
version = "6.31.1"
version = "5.29.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" }
sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226, upload-time = "2025-05-28T23:51:59.82Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" },
{ url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" },
{ url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" },
{ url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" },
{ url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" },
{ url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963, upload-time = "2025-05-28T23:51:41.204Z" },
{ url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818, upload-time = "2025-05-28T23:51:44.297Z" },
{ url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091, upload-time = "2025-05-28T23:51:45.907Z" },
{ url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824, upload-time = "2025-05-28T23:51:47.545Z" },
{ url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942, upload-time = "2025-05-28T23:51:49.11Z" },
{ url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823, upload-time = "2025-05-28T23:51:58.157Z" },
]
[[package]]
name = "psycopg2-binary"
version = "2.9.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" },
{ url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" },
{ url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" },
{ url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" },
{ url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" },
{ url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" },
{ url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" },
{ url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" },
{ url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" },
{ url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" },
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" },
]
[[package]]
@@ -1947,6 +2109,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
]
[[package]]
name = "termcolor"
version = "3.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
]
[[package]]
name = "tiktoken"
version = "0.9.0"
@@ -2233,3 +2404,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" },
{ url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" },
]
[[package]]
name = "zipp"
version = "3.23.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
]