mayankchugh-learning commited on
Commit
393bec4
·
1 Parent(s): 482fce4

Enhance API and Streamlit UI with new timeout handling and telemetry adjustments

Browse files

- Added configurable HTTP read timeout in Streamlit app, allowing users to set a maximum wait time for requests.
- Updated API to disable telemetry for Chroma to prevent errors with posthog capture.
- Introduced legacy query endpoint for backward compatibility with older clients.
- Updated dependencies in pyproject.toml and requirements.txt to include posthog with version constraints.
- Enhanced vector store initialization to disable telemetry and ensure proper client settings.
- Improved user feedback in the Streamlit UI regarding request timeouts and API connection status.

Files changed (7) hide show
  1. api/main.py +6 -0
  2. api/routes/query.py +9 -0
  3. pyproject.toml +2 -0
  4. rag/vector_store.py +6 -2
  5. requirements.txt +1 -0
  6. streamlit_app.py +116 -19
  7. uv.lock +22 -9
api/main.py CHANGED
@@ -1,3 +1,8 @@
 
 
 
 
 
1
  from fastapi import FastAPI
2
 
3
  from api.config import get_settings
@@ -11,6 +16,7 @@ app.include_router(audit.router)
11
  app.include_router(ingest.router)
12
  app.include_router(jobs.router)
13
  app.include_router(query.router)
 
14
 
15
 
16
  @app.on_event("startup")
 
1
+ import os
2
+
3
+ # Before any route imports that touch Chroma: disable product telemetry (avoids posthog capture() errors in logs).
4
+ os.environ.setdefault("ANONYMIZED_TELEMETRY", "FALSE")
5
+
6
  from fastapi import FastAPI
7
 
8
  from api.config import get_settings
 
16
  app.include_router(ingest.router)
17
  app.include_router(jobs.router)
18
  app.include_router(query.router)
19
+ app.include_router(query.legacy_query_router)
20
 
21
 
22
  @app.on_event("startup")
api/routes/query.py CHANGED
@@ -116,3 +116,12 @@ async def summarise_endpoint(payload: SummariseRequest) -> QueryResponse:
116
  except Exception as exc:
117
  raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
118
  return response
 
 
 
 
 
 
 
 
 
 
116
  except Exception as exc:
117
  raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc)) from exc
118
  return response
119
+
120
+
121
+ legacy_query_router = APIRouter(tags=["query"])
122
+
123
+
124
+ @legacy_query_router.post("/query", response_model=QueryResponse)
125
+ async def query_post_compat(payload: QueryRequest) -> QueryResponse:
126
+ """Same behavior as POST /query/ask; kept for older clients and docs that used POST /query."""
127
+ return await ask_endpoint(payload)
pyproject.toml CHANGED
@@ -14,6 +14,8 @@ dependencies = [
14
  "langchain-anthropic==0.1.15",
15
  "langchain-ollama==0.1.3",
16
  "chromadb==0.5.0",
 
 
17
  "openai==1.30.1",
18
  "anthropic==0.28.1",
19
  "pydantic-settings==2.3.4",
 
14
  "langchain-anthropic==0.1.15",
15
  "langchain-ollama==0.1.3",
16
  "chromadb==0.5.0",
17
+ # Chroma 0.5 calls posthog.capture(distinct_id, event, props); posthog 6+ removed that API (breaks telemetry + spams stderr).
18
+ "posthog>=3.7.0,<4",
19
  "openai==1.30.1",
20
  "anthropic==0.28.1",
21
  "pydantic-settings==2.3.4",
rag/vector_store.py CHANGED
@@ -2,10 +2,13 @@ from pathlib import Path
2
  from uuid import uuid4
3
 
4
  import chromadb
 
5
  from langchain_chroma import Chroma
6
  from langchain_core.documents import Document
7
  from langchain_core.embeddings import Embeddings
8
 
 
 
9
 
10
  def get_vector_store(
11
  persist_directory: str,
@@ -17,6 +20,7 @@ def get_vector_store(
17
  collection_name=collection_name,
18
  embedding_function=embedding_function,
19
  persist_directory=persist_directory,
 
20
  )
21
 
22
 
@@ -28,12 +32,12 @@ def add_documents(vector_store: Chroma, chunks: list[Document]) -> list[str]:
28
 
29
  def list_collection_names(persist_directory: str) -> list[str]:
30
  Path(persist_directory).mkdir(parents=True, exist_ok=True)
31
- client = chromadb.PersistentClient(path=persist_directory)
32
  return sorted(c.name for c in client.list_collections())
33
 
34
 
35
  def delete_collection(persist_directory: str, collection_name: str) -> None:
36
  Path(persist_directory).mkdir(parents=True, exist_ok=True)
37
- client = chromadb.PersistentClient(path=persist_directory)
38
  client.delete_collection(name=collection_name)
39
 
 
2
  from uuid import uuid4
3
 
4
  import chromadb
5
+ from chromadb.config import Settings
6
  from langchain_chroma import Chroma
7
  from langchain_core.documents import Document
8
  from langchain_core.embeddings import Embeddings
9
 
10
+ _CHROMA_CLIENT_SETTINGS = Settings(anonymized_telemetry=False)
11
+
12
 
13
  def get_vector_store(
14
  persist_directory: str,
 
20
  collection_name=collection_name,
21
  embedding_function=embedding_function,
22
  persist_directory=persist_directory,
23
+ client_settings=_CHROMA_CLIENT_SETTINGS,
24
  )
25
 
26
 
 
32
 
33
  def list_collection_names(persist_directory: str) -> list[str]:
34
  Path(persist_directory).mkdir(parents=True, exist_ok=True)
35
+ client = chromadb.PersistentClient(path=persist_directory, settings=_CHROMA_CLIENT_SETTINGS)
36
  return sorted(c.name for c in client.list_collections())
37
 
38
 
39
  def delete_collection(persist_directory: str, collection_name: str) -> None:
40
  Path(persist_directory).mkdir(parents=True, exist_ok=True)
41
+ client = chromadb.PersistentClient(path=persist_directory, settings=_CHROMA_CLIENT_SETTINGS)
42
  client.delete_collection(name=collection_name)
43
 
requirements.txt CHANGED
@@ -9,6 +9,7 @@ langchain-text-splitters==0.2.0
9
  langchain-anthropic==0.1.15
10
  langchain-ollama==0.1.3
11
  chromadb==0.5.0
 
12
  openai==1.30.1
13
  anthropic==0.28.1
14
  pymupdf==1.24.3
 
9
  langchain-anthropic==0.1.15
10
  langchain-ollama==0.1.3
11
  chromadb==0.5.0
12
+ posthog>=3.7.0,<4
13
  openai==1.30.1
14
  anthropic==0.28.1
15
  pymupdf==1.24.3
streamlit_app.py CHANGED
@@ -10,15 +10,44 @@ import httpx
10
  import streamlit as st
11
 
12
  DEFAULT_API_BASE = os.environ.get("DOC_AUDI_API_BASE", "http://127.0.0.1:8000")
13
- HTTP_TIMEOUT = httpx.Timeout(120.0, connect=10.0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
 
16
  def _api_base() -> str:
17
- return (st.session_state.get("api_base") or DEFAULT_API_BASE).rstrip("/")
 
 
 
 
 
 
 
18
 
19
 
20
  def _client() -> httpx.Client:
21
- return httpx.Client(base_url=_api_base(), timeout=HTTP_TIMEOUT)
22
 
23
 
24
  def _fmt_api_error(exc: httpx.HTTPStatusError) -> str:
@@ -42,6 +71,30 @@ def _fmt_api_error(exc: httpx.HTTPStatusError) -> str:
42
  return f"HTTP {exc.response.status_code}"
43
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  def _health_check() -> tuple[bool, str]:
46
  try:
47
  with _client() as c:
@@ -51,6 +104,8 @@ def _health_check() -> tuple[bool, str]:
51
  return True, str(data)
52
  except httpx.ConnectError as e:
53
  return False, f"Cannot connect to {_api_base()}: {e}"
 
 
54
  except httpx.HTTPStatusError as e:
55
  return False, _fmt_api_error(e)
56
  except Exception as e:
@@ -59,15 +114,25 @@ def _health_check() -> tuple[bool, str]:
59
 
60
  def main() -> None:
61
  st.set_page_config(page_title="doc-audi-ai", layout="wide")
62
- st.title("doc-audi-ai")
63
- st.caption("Ingest, query, and audit via the FastAPI backend.")
64
-
65
  if "api_base" not in st.session_state:
66
  st.session_state.api_base = DEFAULT_API_BASE
67
 
 
 
 
 
68
  with st.sidebar:
69
  st.subheader("Backend")
70
- st.text_input("API base URL", key="api_base")
 
 
 
 
 
 
 
 
 
71
  if st.button("Test connection"):
72
  ok, msg = _health_check()
73
  if ok:
@@ -254,18 +319,36 @@ def main() -> None:
254
  st.warning("Enter a question.")
255
  else:
256
  try:
257
- with _client() as c:
258
- r = c.post("/query/ask", json={"question": question.strip(), "collection_name": q_col})
259
- r.raise_for_status()
260
- ans = r.json()
261
- st.success(ans.get("message", ""))
 
 
 
 
 
 
 
 
 
262
  if ans.get("answer"):
263
  st.markdown("### Answer")
264
  st.markdown(ans["answer"])
 
 
 
 
 
265
  src = ans.get("sources") or []
266
  if src:
267
  with st.expander(f"Sources ({len(src)})"):
268
  st.json(src)
 
 
 
 
269
  except httpx.HTTPStatusError as e:
270
  st.error(_fmt_api_error(e))
271
  except httpx.ConnectError as e:
@@ -282,22 +365,32 @@ def main() -> None:
282
  body: dict[str, Any] = {"collection_name": s_col}
283
  if focus.strip():
284
  body["focus"] = focus.strip()
285
- with _client() as c:
286
- r = c.post("/query/summarise", json=body)
287
- r.raise_for_status()
288
- ans = r.json()
289
- st.success(ans.get("message", ""))
 
 
290
  if ans.get("answer"):
291
  st.markdown("### Summary")
292
  st.markdown(ans["answer"])
 
 
293
  src = ans.get("sources") or []
294
  if src:
295
  with st.expander(f"Sources ({len(src)})"):
296
  st.json(src)
 
 
297
  except httpx.HTTPStatusError as e:
298
  st.error(_fmt_api_error(e))
299
  except httpx.ConnectError as e:
300
  st.error(f"Connection failed: {e}")
 
 
 
 
301
  except Exception as e:
302
  st.exception(e)
303
 
@@ -311,7 +404,11 @@ def main() -> None:
311
  if st.button("List audit events", key="btn_audit_list"):
312
  try:
313
  with _client() as c:
314
- r = c.get("/audit/logs", params={"limit": int(a_limit), "offset": int(a_offset)})
 
 
 
 
315
  r.raise_for_status()
316
  payload = r.json()
317
  events = payload.get("events", [])
@@ -340,7 +437,7 @@ def main() -> None:
340
  if st.button("Load detail", key="btn_audit_detail") and ev_id:
341
  try:
342
  with _client() as c:
343
- r = c.get(f"/audit/logs/{ev_id}")
344
  r.raise_for_status()
345
  st.json(r.json())
346
  except httpx.HTTPStatusError as e:
 
10
  import streamlit as st
11
 
12
  DEFAULT_API_BASE = os.environ.get("DOC_AUDI_API_BASE", "http://127.0.0.1:8000")
13
+
14
+
15
+ def _http_read_timeout_seconds() -> float:
16
+ raw = os.environ.get("DOC_AUDI_HTTP_READ_TIMEOUT", "600")
17
+ try:
18
+ read_s = float(raw)
19
+ except ValueError:
20
+ read_s = 600.0
21
+ return max(60.0, min(read_s, 3600.0))
22
+
23
+
24
+ def _http_timeout() -> httpx.Timeout:
25
+ """LLM + embeddings can exceed a few minutes on CPU or cold Ollama; Streamlit uses this, not Uvicorn."""
26
+ read_s = _http_read_timeout_seconds()
27
+ return httpx.Timeout(connect=20.0, read=read_s, write=120.0, pool=30.0)
28
+
29
+
30
+ def _fmt_timeout_hint() -> str:
31
+ cap = int(_http_read_timeout_seconds())
32
+ return (
33
+ f"The UI stops waiting after **{cap}s** per request (set **DOC_AUDI_HTTP_READ_TIMEOUT** to raise it, max 3600). "
34
+ "Ensure `ollama serve` is running; cold models or CPU inference can exceed a few minutes."
35
+ )
36
 
37
 
38
  def _api_base() -> str:
39
+ """Resolve API base URL. Whitespace-only sidebar input must not win over default (breaks httpx)."""
40
+ raw = st.session_state.get("api_base")
41
+ if raw is None:
42
+ return DEFAULT_API_BASE.rstrip("/")
43
+ s = str(raw).strip()
44
+ if not s:
45
+ return DEFAULT_API_BASE.rstrip("/")
46
+ return s.rstrip("/")
47
 
48
 
49
  def _client() -> httpx.Client:
50
+ return httpx.Client(base_url=_api_base(), timeout=_http_timeout())
51
 
52
 
53
  def _fmt_api_error(exc: httpx.HTTPStatusError) -> str:
 
71
  return f"HTTP {exc.response.status_code}"
72
 
73
 
74
+ def _post_query_ask(client: httpx.Client, *, question: str, collection_name: str) -> httpx.Response:
75
+ """Milestone 8 uses POST /query/ask; older servers only expose POST /query."""
76
+ body = {"question": question.strip(), "collection_name": collection_name}
77
+ r = client.post("/query/ask", json=body)
78
+ if r.status_code == 404:
79
+ r = client.post("/query", json=body)
80
+ return r
81
+
82
+
83
+ def _get_audit_logs(client: httpx.Client, *, limit: int, offset: int) -> httpx.Response:
84
+ params = {"limit": limit, "offset": offset}
85
+ r = client.get("/audit/logs", params=params)
86
+ if r.status_code == 404:
87
+ r = client.get("/audit", params=params)
88
+ return r
89
+
90
+
91
+ def _get_audit_event_detail(client: httpx.Client, event_id: str) -> httpx.Response:
92
+ r = client.get(f"/audit/logs/{event_id}")
93
+ if r.status_code == 404:
94
+ r = client.get(f"/audit/{event_id}")
95
+ return r
96
+
97
+
98
  def _health_check() -> tuple[bool, str]:
99
  try:
100
  with _client() as c:
 
104
  return True, str(data)
105
  except httpx.ConnectError as e:
106
  return False, f"Cannot connect to {_api_base()}: {e}"
107
+ except httpx.TimeoutException as e:
108
+ return False, f"Timed out: {e}. {_fmt_timeout_hint()}"
109
  except httpx.HTTPStatusError as e:
110
  return False, _fmt_api_error(e)
111
  except Exception as e:
 
114
 
115
  def main() -> None:
116
  st.set_page_config(page_title="doc-audi-ai", layout="wide")
 
 
 
117
  if "api_base" not in st.session_state:
118
  st.session_state.api_base = DEFAULT_API_BASE
119
 
120
+ st.title("doc-audi-ai")
121
+ st.caption("Ingest, query, and audit via the FastAPI backend.")
122
+ st.caption(f"Requests go to: `{_api_base()}`")
123
+
124
  with st.sidebar:
125
  st.subheader("Backend")
126
+ st.text_input(
127
+ "API base URL",
128
+ key="api_base",
129
+ placeholder=DEFAULT_API_BASE,
130
+ help=f"Default: {DEFAULT_API_BASE}. Clear the field to use the default.",
131
+ )
132
+ st.caption(
133
+ f"Ask/Summarise wait up to **{int(_http_read_timeout_seconds())}s** per request "
134
+ "(env `DOC_AUDI_HTTP_READ_TIMEOUT`, range 60–3600)."
135
+ )
136
  if st.button("Test connection"):
137
  ok, msg = _health_check()
138
  if ok:
 
319
  st.warning("Enter a question.")
320
  else:
321
  try:
322
+ with st.spinner(
323
+ "Calling the API (embeddings + LLM can take several minutes on a slow machine; "
324
+ "ensure Ollama is running). Timeout is controlled by DOC_AUDI_HTTP_READ_TIMEOUT…"
325
+ ):
326
+ with _client() as c:
327
+ r = _post_query_ask(
328
+ c,
329
+ question=question,
330
+ collection_name=q_col,
331
+ )
332
+ r.raise_for_status()
333
+ ans = r.json()
334
+ msg = ans.get("message") or ""
335
+ st.success(msg if msg else "Request completed.")
336
  if ans.get("answer"):
337
  st.markdown("### Answer")
338
  st.markdown(ans["answer"])
339
+ else:
340
+ st.warning(
341
+ "The API returned no **answer** text. "
342
+ "Check the collection has ingested chunks, LLM env, and expand **Raw response** below."
343
+ )
344
  src = ans.get("sources") or []
345
  if src:
346
  with st.expander(f"Sources ({len(src)})"):
347
  st.json(src)
348
+ else:
349
+ st.caption("No sources in this response (empty retrieval or model returned nothing).")
350
+ with st.expander("Raw response (debug)"):
351
+ st.json(ans)
352
  except httpx.HTTPStatusError as e:
353
  st.error(_fmt_api_error(e))
354
  except httpx.ConnectError as e:
 
365
  body: dict[str, Any] = {"collection_name": s_col}
366
  if focus.strip():
367
  body["focus"] = focus.strip()
368
+ with st.spinner("Calling summarise (can take 1–2 minutes on a cold model)…"):
369
+ with _client() as c:
370
+ r = c.post("/query/summarise", json=body)
371
+ r.raise_for_status()
372
+ ans = r.json()
373
+ msg = ans.get("message") or ""
374
+ st.success(msg if msg else "Request completed.")
375
  if ans.get("answer"):
376
  st.markdown("### Summary")
377
  st.markdown(ans["answer"])
378
+ else:
379
+ st.warning("No summary text in the response; see **Raw response** below.")
380
  src = ans.get("sources") or []
381
  if src:
382
  with st.expander(f"Sources ({len(src)})"):
383
  st.json(src)
384
+ with st.expander("Raw response (debug)"):
385
+ st.json(ans)
386
  except httpx.HTTPStatusError as e:
387
  st.error(_fmt_api_error(e))
388
  except httpx.ConnectError as e:
389
  st.error(f"Connection failed: {e}")
390
+ except httpx.ReadTimeout as e:
391
+ st.error(f"Read timed out: {e}\n\n{_fmt_timeout_hint()}")
392
+ except httpx.TimeoutException as e:
393
+ st.error(f"Request timed out: {e}\n\n{_fmt_timeout_hint()}")
394
  except Exception as e:
395
  st.exception(e)
396
 
 
404
  if st.button("List audit events", key="btn_audit_list"):
405
  try:
406
  with _client() as c:
407
+ r = _get_audit_logs(
408
+ c,
409
+ limit=int(a_limit),
410
+ offset=int(a_offset),
411
+ )
412
  r.raise_for_status()
413
  payload = r.json()
414
  events = payload.get("events", [])
 
437
  if st.button("Load detail", key="btn_audit_detail") and ev_id:
438
  try:
439
  with _client() as c:
440
+ r = _get_audit_event_detail(c, ev_id)
441
  r.raise_for_status()
442
  st.json(r.json())
443
  except httpx.HTTPStatusError as e:
uv.lock CHANGED
@@ -585,6 +585,7 @@ dependencies = [
585
  { name = "langchain-text-splitters" },
586
  { name = "onnxruntime", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" },
587
  { name = "openai" },
 
588
  { name = "pydantic-settings" },
589
  { name = "pymupdf" },
590
  { name = "python-multipart" },
@@ -611,6 +612,7 @@ requires-dist = [
611
  { name = "langchain-text-splitters", specifier = "==0.2.0" },
612
  { name = "onnxruntime", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "==1.23.2" },
613
  { name = "openai", specifier = "==1.30.1" },
 
614
  { name = "pydantic-settings", specifier = "==2.3.4" },
615
  { name = "pymupdf", specifier = "==1.24.3" },
616
  { name = "python-multipart", specifier = "==0.0.9" },
@@ -1655,6 +1657,15 @@ wheels = [
1655
  { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" },
1656
  ]
1657
 
 
 
 
 
 
 
 
 
 
1658
  [[package]]
1659
  name = "mpmath"
1660
  version = "1.3.0"
@@ -1869,7 +1880,7 @@ name = "nvidia-cudnn-cu12"
1869
  version = "8.9.2.26"
1870
  source = { registry = "https://pypi.org/simple" }
1871
  dependencies = [
1872
- { name = "nvidia-cublas-cu12" },
1873
  ]
1874
  wheels = [
1875
  { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872, upload-time = "2023-06-01T19:24:57.328Z" },
@@ -1896,9 +1907,9 @@ name = "nvidia-cusolver-cu12"
1896
  version = "11.4.5.107"
1897
  source = { registry = "https://pypi.org/simple" }
1898
  dependencies = [
1899
- { name = "nvidia-cublas-cu12" },
1900
- { name = "nvidia-cusparse-cu12" },
1901
- { name = "nvidia-nvjitlink-cu12" },
1902
  ]
1903
  wheels = [
1904
  { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928, upload-time = "2023-04-19T15:51:25.781Z" },
@@ -1909,7 +1920,7 @@ name = "nvidia-cusparse-cu12"
1909
  version = "12.1.0.106"
1910
  source = { registry = "https://pypi.org/simple" }
1911
  dependencies = [
1912
- { name = "nvidia-nvjitlink-cu12" },
1913
  ]
1914
  wheels = [
1915
  { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278, upload-time = "2023-04-19T15:51:49.939Z" },
@@ -2378,17 +2389,19 @@ wheels = [
2378
 
2379
  [[package]]
2380
  name = "posthog"
2381
- version = "7.13.2"
2382
  source = { registry = "https://pypi.org/simple" }
2383
  dependencies = [
2384
  { name = "backoff" },
2385
  { name = "distro" },
 
 
2386
  { name = "requests" },
2387
- { name = "typing-extensions" },
2388
  ]
2389
- sdist = { url = "https://files.pythonhosted.org/packages/77/54/9c117beaa96519077cae355263712d705e560e8ee7ca4a35373438d1f4d2/posthog-7.13.2.tar.gz", hash = "sha256:a9fc322518bc7f42e24501a0df1072aa60bad6fba759cbe388d167b7e054b052", size = 195555, upload-time = "2026-04-30T09:01:26.08Z" }
2390
  wheels = [
2391
- { url = "https://files.pythonhosted.org/packages/e9/8b/ec789af64ef3d85816a5c4ac8fe1e3c74d6e3bb9675c1325e3b1de4ddb14/posthog-7.13.2-py3-none-any.whl", hash = "sha256:e8ea9a7fa740b453533a76c0f3300b16120a3c27c19ab417cf0e69bb8c210fb6", size = 229762, upload-time = "2026-04-30T09:01:24.144Z" },
2392
  ]
2393
 
2394
  [[package]]
 
585
  { name = "langchain-text-splitters" },
586
  { name = "onnxruntime", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" },
587
  { name = "openai" },
588
+ { name = "posthog" },
589
  { name = "pydantic-settings" },
590
  { name = "pymupdf" },
591
  { name = "python-multipart" },
 
612
  { name = "langchain-text-splitters", specifier = "==0.2.0" },
613
  { name = "onnxruntime", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "==1.23.2" },
614
  { name = "openai", specifier = "==1.30.1" },
615
+ { name = "posthog", specifier = ">=3.7.0,<4" },
616
  { name = "pydantic-settings", specifier = "==2.3.4" },
617
  { name = "pymupdf", specifier = "==1.24.3" },
618
  { name = "python-multipart", specifier = "==0.0.9" },
 
1657
  { url = "https://files.pythonhosted.org/packages/a0/0f/59204bf136d1201f8d7884cfbaf7498c5b4674e87a4c693f9bde63741ce1/mmh3-5.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dfd51b4c56b673dfbc43d7d27ef857dd91124801e2806c69bb45585ce0fa019b", size = 40391, upload-time = "2026-03-05T15:55:56.697Z" },
1658
  ]
1659
 
1660
+ [[package]]
1661
+ name = "monotonic"
1662
+ version = "1.6"
1663
+ source = { registry = "https://pypi.org/simple" }
1664
+ sdist = { url = "https://files.pythonhosted.org/packages/ea/ca/8e91948b782ddfbd194f323e7e7d9ba12e5877addf04fb2bf8fca38e86ac/monotonic-1.6.tar.gz", hash = "sha256:3a55207bcfed53ddd5c5bae174524062935efed17792e9de2ad0205ce9ad63f7", size = 7615, upload-time = "2021-08-11T14:37:28.79Z" }
1665
+ wheels = [
1666
+ { url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154, upload-time = "2021-04-09T21:58:05.122Z" },
1667
+ ]
1668
+
1669
  [[package]]
1670
  name = "mpmath"
1671
  version = "1.3.0"
 
1880
  version = "8.9.2.26"
1881
  source = { registry = "https://pypi.org/simple" }
1882
  dependencies = [
1883
+ { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
1884
  ]
1885
  wheels = [
1886
  { url = "https://files.pythonhosted.org/packages/ff/74/a2e2be7fb83aaedec84f391f082cf765dfb635e7caa9b49065f73e4835d8/nvidia_cudnn_cu12-8.9.2.26-py3-none-manylinux1_x86_64.whl", hash = "sha256:5ccb288774fdfb07a7e7025ffec286971c06d8d7b4fb162525334616d7629ff9", size = 731725872, upload-time = "2023-06-01T19:24:57.328Z" },
 
1907
  version = "11.4.5.107"
1908
  source = { registry = "https://pypi.org/simple" }
1909
  dependencies = [
1910
+ { name = "nvidia-cublas-cu12", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
1911
+ { name = "nvidia-cusparse-cu12", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
1912
+ { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
1913
  ]
1914
  wheels = [
1915
  { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928, upload-time = "2023-04-19T15:51:25.781Z" },
 
1920
  version = "12.1.0.106"
1921
  source = { registry = "https://pypi.org/simple" }
1922
  dependencies = [
1923
+ { name = "nvidia-nvjitlink-cu12", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
1924
  ]
1925
  wheels = [
1926
  { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278, upload-time = "2023-04-19T15:51:49.939Z" },
 
2389
 
2390
  [[package]]
2391
  name = "posthog"
2392
+ version = "3.25.0"
2393
  source = { registry = "https://pypi.org/simple" }
2394
  dependencies = [
2395
  { name = "backoff" },
2396
  { name = "distro" },
2397
+ { name = "monotonic" },
2398
+ { name = "python-dateutil" },
2399
  { name = "requests" },
2400
+ { name = "six" },
2401
  ]
2402
+ sdist = { url = "https://files.pythonhosted.org/packages/85/a9/ec3bbc23b6f3c23c52e0b5795b1357cca74aa5cfb254213f1e471fef9b4d/posthog-3.25.0.tar.gz", hash = "sha256:9168f3e7a0a5571b6b1065c41b3c171fbc68bfe72c3ac0bfd6e3d2fcdb7df2ca", size = 75968, upload-time = "2025-04-15T21:15:45.552Z" }
2403
  wheels = [
2404
+ { url = "https://files.pythonhosted.org/packages/54/e2/c158366e621562ef224f132e75c1d1c1fce6b078a19f7d8060451a12d4b9/posthog-3.25.0-py2.py3-none-any.whl", hash = "sha256:85db78c13d1ecb11aed06fad53759c4e8fb3633442c2f3d0336bc0ce8a585d30", size = 89115, upload-time = "2025-04-15T21:15:43.934Z" },
2405
  ]
2406
 
2407
  [[package]]