feat(graphiti): Implement Graphiti client and related steps for episode ingestion and querying

- Added `graphiti_client.py` to manage the Graphiti client as a singleton.
- Created `ingest_episode_event_step.py` to handle episode ingestion from HTTP webhook events.
- Implemented `ingest_episode_step.py` for validating and enqueuing episode payloads via a POST request.
- Developed `query_graph_step.py` for performing semantic searches in the Graphiti Knowledge-Graph.
- Introduced an `__init__.py` file for the graphiti steps module.
This commit is contained in:
bsiggel
2026-03-30 08:25:49 +00:00
parent 1271e38f2d
commit e255ae1263
8 changed files with 567 additions and 1 deletions

View File

@@ -21,7 +21,7 @@ modules:
config:
port: 3111
host: 0.0.0.0
default_timeout: 30000
default_timeout: 180000
concurrency_request_limit: 1024
cors:
allowed_origins:

View File

@@ -22,5 +22,6 @@ dependencies = [
"langchain>=0.3.0", # LangChain framework
"langchain-xai>=0.2.0", # xAI integration for LangChain
"langchain-core>=0.3.0", # LangChain core
"graphiti-core>=0.28.0", # Graphiti Knowledge-Graph
]

100
services/graphiti_client.py Normal file
View File

@@ -0,0 +1,100 @@
"""Graphiti Knowledge-Graph zentraler Client-Singleton.
Wird von ingest_episode_event_step und query_graph_step genutzt.
Lazy-initialisiert beim ersten Aufruf.
"""
import asyncio
import os
from typing import Any, Optional
from graphiti_core import Graphiti
from graphiti_core.llm_client import LLMConfig
from graphiti_core.llm_client.openai_generic_client import OpenAIGenericClient
from graphiti_core.embedder import OpenAIEmbedder, OpenAIEmbedderConfig
class GraphitiError(Exception):
"""Fehler beim Zugriff auf den Graphiti-Client."""
_graphiti_client: Graphiti | None = None
_graphiti_init_lock = asyncio.Lock()
async def get_graphiti(ctx: Optional[Any] = None) -> Graphiti:
"""Gibt den gecachten Graphiti-Client zurück (Singleton).
Args:
ctx: Optionaler Motia-Context für Logging während der Initialisierung.
Benötigte Umgebungsvariablen:
NEO4J_URI bolt://host:7687
NEO4J_USER neo4j
NEO4J_PASSWORD Pflicht
XAI_API_KEY xAI-Key für LLM (grok)
XAI_BASE_URL optional, Standard: https://api.x.ai/v1
XAI_MODEL optional, Standard: grok-4-1-fast-reasoning
OPENAI_API_KEY OpenAI-Key für Embeddings
GRAPHITI_EMBED_BASE_URL optional, Standard: https://api.openai.com/v1
GRAPHITI_EMBED_MODEL optional, Standard: text-embedding-3-small
"""
global _graphiti_client
if _graphiti_client is None:
async with _graphiti_init_lock:
if _graphiti_client is None:
_log(ctx, "Initialisiere Graphiti-Client...")
try:
_graphiti_client = await _build_graphiti()
_log(ctx, "Graphiti-Client bereit.")
except KeyError as e:
raise GraphitiError(f"Konfigurationsfehler Umgebungsvariable fehlt: {e}") from e
return _graphiti_client
def _log(ctx: Optional[Any], message: str, level: str = "info") -> None:
"""Loggt via ctx.logger falls vorhanden, sonst print."""
if ctx is not None and hasattr(ctx, "logger"):
getattr(ctx.logger, level)(f"[GraphitiClient] {message}")
else:
print(f"[GraphitiClient] {message}")
async def _build_graphiti() -> Graphiti:
neo4j_uri = os.environ["NEO4J_URI"]
neo4j_user = os.environ.get("NEO4J_USER", "neo4j")
neo4j_password = os.environ["NEO4J_PASSWORD"]
xai_base_url = os.environ.get("XAI_BASE_URL", "https://api.x.ai/v1")
xai_api_key = os.environ["XAI_API_KEY"]
xai_model = os.environ.get("XAI_MODEL", "grok-4-1-fast-reasoning")
embed_api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("GRAPHITI_EMBED_API_KEY", xai_api_key)
embed_base_url = os.environ.get("GRAPHITI_EMBED_BASE_URL", "https://api.openai.com/v1")
embed_model = os.environ.get("GRAPHITI_EMBED_MODEL", "text-embedding-3-small")
llm_client = OpenAIGenericClient(
config=LLMConfig(
api_key=xai_api_key,
base_url=xai_base_url,
model=xai_model,
)
)
embedder = OpenAIEmbedder(
config=OpenAIEmbedderConfig(
api_key=embed_api_key,
base_url=embed_base_url,
embedding_model=embed_model,
embedding_dim=1536,
)
)
client = Graphiti(
uri=neo4j_uri,
user=neo4j_user,
password=neo4j_password,
llm_client=llm_client,
embedder=embedder,
)
await client.build_indices_and_constraints()
return client

View File

View File

@@ -0,0 +1,98 @@
"""Graphiti - Episode verarbeiten (Queue: graphiti.ingest_episode)
Empfängt das Event vom HTTP-Webhook und schreibt die Episode asynchron
in den Graphiti Knowledge-Graph (Neo4j + xAI LLM + OpenAI Embeddings).
"""
import json
from datetime import datetime, timezone
from typing import Any
from motia import FlowContext, queue
from services.graphiti_client import get_graphiti, GraphitiError
from graphiti_core.nodes import EpisodeType
config = {
"name": "Graphiti Ingest Episode Worker",
"description": "Verarbeitet eine Episode und schreibt sie in den Knowledge-Graph.",
"flows": ["graphiti-knowledge-graph"],
"triggers": [
queue("graphiti.ingest_episode")
],
"enqueues": [],
}
async def handler(event_data: dict[str, Any], ctx: FlowContext[Any]) -> None:
"""
Schreibt die Episode in Graphiti.
Erwartet im event_data:
rag_akten_id (str) group_id im Graph
rag_dokument_id (str) Episode-Name
chunk_ids (list[str])
content (str) Episoden-Text
source (str) z. B. "Zeugenvernehmung"
valid_at (str | None) ISO-String, None → jetzt
"""
ctx.logger.info("=" * 80)
ctx.logger.info("GRAPHITI INGEST WORKER: Starte Episode-Verarbeitung")
rag_akten_id: str = event_data.get("rag_akten_id", "")
rag_dokument_id: str = event_data.get("rag_dokument_id", "")
chunk_ids: list[str] = event_data.get("chunk_ids") or []
content: str = event_data.get("content", "")
source: str = event_data.get("source", "")
raw_valid_at: str | None = event_data.get("valid_at")
ctx.logger.info(f"Akte: {rag_akten_id}")
ctx.logger.info(f"Dokument: {rag_dokument_id}")
ctx.logger.info(f"Source: {source}")
# valid_at auflösen
if raw_valid_at:
try:
reference_time = datetime.fromisoformat(raw_valid_at)
if reference_time.tzinfo is None:
reference_time = reference_time.replace(tzinfo=timezone.utc)
except (ValueError, TypeError):
ctx.logger.info(f"valid_at '{raw_valid_at}' ungültig verwende jetzt")
reference_time = datetime.now(timezone.utc)
else:
ctx.logger.info("Kein valid_at verwende jetzt als reference_time")
reference_time = datetime.now(timezone.utc)
ctx.logger.info(f"reference_time: {reference_time.isoformat()}")
source_description = (
f"{source} | "
f"rag_dokument_id:{rag_dokument_id} | "
f"chunk_ids:{json.dumps(chunk_ids, ensure_ascii=False)}"
)
try:
graphiti = await get_graphiti(ctx)
result = await graphiti.add_episode(
name=rag_dokument_id,
episode_body=content,
source_description=source_description,
reference_time=reference_time,
source=EpisodeType.text,
group_id=rag_akten_id,
)
episode_id = result.episode.uuid
ctx.logger.info(f"Episode erfolgreich gespeichert: {episode_id}")
ctx.logger.info("=" * 80)
except GraphitiError as e:
ctx.logger.error(f"GraphitiError: {e}")
ctx.logger.error("=" * 80)
raise # Motia retries
except Exception as e:
ctx.logger.error(f"Unerwarteter Fehler: {type(e).__name__}: {e}")
ctx.logger.error("=" * 80)
raise

View File

@@ -0,0 +1,87 @@
"""Graphiti - Episode ingestieren (POST /ingest_episode)
Dünner HTTP-Webhook: validiert den Payload und enqueued ihn sofort.
Die eigentliche Graphiti-Arbeit passiert in ingest_episode_event_step.py.
"""
from datetime import datetime, timezone
from typing import Any
from motia import FlowContext, http, ApiRequest, ApiResponse
config = {
"name": "Graphiti Ingest Episode",
"description": "Nimmt Episode-Payload entgegen, validiert und enqueued ihn.",
"flows": ["graphiti-knowledge-graph"],
"triggers": [
http("POST", "/ingest_episode")
],
"enqueues": ["graphiti.ingest_episode"],
}
def _parse_valid_at(raw: Any) -> str | None:
"""ISO-String validieren; gibt None zurück wenn ungültig/fehlend."""
if not raw or not isinstance(raw, str):
return None
try:
dt = datetime.fromisoformat(raw)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.isoformat()
except (ValueError, TypeError):
return None
async def handler(request: ApiRequest, ctx: FlowContext[Any]) -> ApiResponse:
"""
Validiert den Ingest-Payload und enqueued ein graphiti.ingest_episode-Event.
Payload (JSON):
rag_akten_id (str, Pflicht)
rag_dokument_id (str, Pflicht)
chunk_ids (list[str], optional)
content (str, Pflicht)
source (str, Pflicht)
valid_at (ISO-String | null, optional)
Returns:
202 { status: "accepted", group_id, rag_dokument_id }
400 bei fehlendem Pflichtfeld
"""
ctx.logger.info("=" * 80)
ctx.logger.info("GRAPHITI INGEST WEBHOOK: Payload empfangen")
body: dict = request.body or {}
ctx.logger.info(f"Payload-Keys: {list(body.keys())}")
# Pflichtfelder
missing = [f for f in ("rag_akten_id", "rag_dokument_id", "content", "source") if not body.get(f)]
if missing:
ctx.logger.error(f"Fehlende Pflichtfelder: {missing}")
return ApiResponse(status=400, body={"error": f"Fehlende Pflichtfelder: {missing}"})
valid_at = _parse_valid_at(body.get("valid_at"))
event_data = {
"rag_akten_id": body["rag_akten_id"],
"rag_dokument_id": body["rag_dokument_id"],
"chunk_ids": body.get("chunk_ids") or [],
"content": body["content"],
"source": body["source"],
"valid_at": valid_at, # None → Queue-Step löst CRM-Fallback aus
}
await ctx.enqueue({"topic": "graphiti.ingest_episode", "data": event_data})
ctx.logger.info(f"Event 'graphiti.ingest_episode' enqueued für {body['rag_akten_id']}/{body['rag_dokument_id']}")
ctx.logger.info("=" * 80)
return ApiResponse(
status=202,
body={
"status": "accepted",
"group_id": body["rag_akten_id"],
"rag_dokument_id": body["rag_dokument_id"],
},
)

View File

@@ -0,0 +1,149 @@
"""Graphiti - Graph abfragen (POST /query_graph)
Führt eine semantische Suche im Graphiti Knowledge-Graph durch,
gefiltert nach group_id (= rag_akten_id). Optionaler time_point
schränkt das Ergebnis auf zum Zeitpunkt gültige Kanten ein.
"""
from datetime import datetime, timezone
from typing import Any
from motia import FlowContext, http, ApiRequest, ApiResponse
from graphiti_core.search.search_filters import DateFilter, ComparisonOperator, SearchFilters
from services.graphiti_client import get_graphiti, GraphitiError
config = {
"name": "Graphiti Query Graph",
"description": "Führt eine semantische Suche im Graphiti Knowledge-Graph durch.",
"flows": ["graphiti-knowledge-graph"],
"triggers": [
http("POST", "/query_graph")
],
"enqueues": [],
}
def _parse_dt(raw: Any) -> datetime | None:
"""ISO-String → timezone-aware datetime, oder None bei Fehler."""
if not raw or not isinstance(raw, str):
return None
try:
dt = datetime.fromisoformat(raw)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt
except (ValueError, TypeError):
return None
def _serialize_edge(edge: Any) -> dict:
"""Serialisiert eine EntityEdge in ein dict für die JSON-Antwort."""
return {
"uuid": edge.uuid,
"name": edge.name,
"fact": edge.fact,
"valid_at": edge.valid_at.isoformat() if edge.valid_at else None,
"invalid_at": edge.invalid_at.isoformat() if edge.invalid_at else None,
"created_at": edge.created_at.isoformat() if edge.created_at else None,
"source_node_uuid": edge.source_node_uuid,
"target_node_uuid": edge.target_node_uuid,
}
# ---------------------------------------------------------------------------
# HTTP-Handler
# ---------------------------------------------------------------------------
async def handler(request: ApiRequest, ctx: FlowContext[Any]) -> ApiResponse:
"""
Sucht im Graphiti Knowledge-Graph nach relevanten Fakten.
Payload (JSON):
rag_akten_id (str, Pflicht) filtert auf group_id
query (str, Pflicht) Suchanfrage
time_point (str, optional) ISO-Datum; filtert auf zum
Zeitpunkt gültige Kanten
Returns:
200 { status, group_id, query, time_point, num_results, results[] }
400 bei fehlendem Pflichtfeld
500 bei internem Fehler
"""
ctx.logger.info("=" * 80)
ctx.logger.info("GRAPHITI QUERY: Starte Graph-Suche")
body: dict = request.body or {}
ctx.logger.info(f"Payload-Keys: {list(body.keys())}")
# --- Pflichtfelder prüfen ---
missing = [f for f in ("rag_akten_id", "query") if not body.get(f)]
if missing:
ctx.logger.error(f"Fehlende Pflichtfelder: {missing}")
return ApiResponse(status=400, body={"error": f"Fehlende Pflichtfelder: {missing}"})
rag_akten_id: str = body["rag_akten_id"]
query: str = body["query"]
raw_time_point: Any = body.get("time_point")
time_point = _parse_dt(raw_time_point)
if raw_time_point and time_point is None:
ctx.logger.warning(
f"time_point konnte nicht geparst werden ({raw_time_point!r}), wird ignoriert"
)
ctx.logger.info(f"group_id (rag_akten_id): {rag_akten_id}")
ctx.logger.info(f"query: {query!r}")
ctx.logger.info(f"time_point: {time_point.isoformat() if time_point else 'keiner'}")
try:
graphiti = await get_graphiti(ctx)
# Zeitfilter aufbauen: Kanten die zum time_point noch gültig waren
# (valid_at <= time_point AND (invalid_at IS NULL OR invalid_at > time_point))
search_filter: SearchFilters | None = None
if time_point is not None:
search_filter = SearchFilters(
valid_at=[[DateFilter(
date=time_point,
comparison_operator=ComparisonOperator.less_than_equal,
)]],
invalid_at=[[DateFilter(
comparison_operator=ComparisonOperator.is_null,
)], [DateFilter(
date=time_point,
comparison_operator=ComparisonOperator.greater_than,
)]],
)
edges = await graphiti.search(
query=query,
group_ids=[rag_akten_id],
num_results=20,
search_filter=search_filter,
)
ctx.logger.info(f"Gefundene Kanten: {len(edges)}")
ctx.logger.info("=" * 80)
return ApiResponse(
status=200,
body={
"status": "success",
"group_id": rag_akten_id,
"query": query,
"time_point": time_point.isoformat() if time_point else None,
"num_results": len(edges),
"results": [_serialize_edge(e) for e in edges],
},
)
except GraphitiError as e:
ctx.logger.error(f"GraphitiError: {e}")
ctx.logger.error("=" * 80)
return ApiResponse(status=500, body={"error": str(e)})
except Exception as e:
ctx.logger.error(f"Fehler bei der Graph-Suche: {type(e).__name__}: {e}")
ctx.logger.error("=" * 80)
return ApiResponse(status=500, body={"error": "Interner Fehler", "details": str(e)})

131
uv.lock generated
View File

@@ -556,6 +556,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
]
[[package]]
name = "graphiti-core"
version = "0.28.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "neo4j" },
{ name = "numpy" },
{ name = "openai" },
{ name = "posthog" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "tenacity" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/6d/81b78aec2030bff0030bbabb8b227a6fa3c5fb49fb11e8501e7d0f39f3fe/graphiti_core-0.28.2.tar.gz", hash = "sha256:9b2a72f117827e015a21b610eb2c3acbe05310b79736abef7372e81247578e9d", size = 6846195, upload-time = "2026-03-11T16:20:02.736Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/cd/e6203f1fee0a8e2a797f2d5f9a867513e0c63af4e19fdd1c5a7e14a47670/graphiti_core-0.28.2-py3-none-any.whl", hash = "sha256:4e1c19b7bc70a73a612a473144ed4b3fe615ac6d4c5d6b10f48e206a858bcb53", size = 314919, upload-time = "2026-03-11T16:20:01.037Z" },
]
[[package]]
name = "grpcio"
version = "1.78.0"
@@ -937,6 +955,7 @@ dependencies = [
{ name = "backoff" },
{ name = "google-api-python-client" },
{ name = "google-auth" },
{ name = "graphiti-core" },
{ name = "iii-sdk" },
{ name = "langchain" },
{ name = "langchain-core" },
@@ -957,6 +976,7 @@ requires-dist = [
{ name = "backoff", specifier = ">=2.2.1" },
{ name = "google-api-python-client", specifier = ">=2.100.0" },
{ name = "google-auth", specifier = ">=2.23.0" },
{ name = "graphiti-core", specifier = ">=0.28.0" },
{ name = "iii-sdk", specifier = "==0.2.0" },
{ name = "langchain", specifier = ">=0.3.0" },
{ name = "langchain-core", specifier = ">=0.3.0" },
@@ -1069,6 +1089,79 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
]
[[package]]
name = "neo4j"
version = "6.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytz" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/01/d6ce65e4647f6cb2b9cca3b813978f7329b54b4e36660aaec1ddf0ccce7a/neo4j-6.1.0.tar.gz", hash = "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84", size = 239629, upload-time = "2026-01-12T11:27:34.777Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/5c/ee71e2dd955045425ef44283f40ba1da67673cf06404916ca2950ac0cd39/neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", size = 325326, upload-time = "2026-01-12T11:27:33.196Z" },
]
[[package]]
name = "numpy"
version = "2.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
{ url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
{ url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
{ url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
{ url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
{ url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
{ url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
{ url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
{ url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
{ url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
{ url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
{ url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
{ url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
{ url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
{ url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
{ url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
{ url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
{ url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
{ url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
{ url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
{ url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
{ url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
{ url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
{ url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
{ url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
{ url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
{ url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
{ url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
{ url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
{ url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
{ url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
{ url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
{ url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
{ url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
{ url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
{ url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
{ url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
{ url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
{ url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
{ url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
{ url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
{ url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
{ url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
{ url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
{ url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
{ url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
{ url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
{ url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
{ url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
{ url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
]
[[package]]
name = "openai"
version = "2.26.0"
@@ -1302,6 +1395,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "posthog"
version = "7.9.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backoff" },
{ name = "distro" },
{ name = "python-dateutil" },
{ name = "requests" },
{ name = "six" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1c/a7/2865487853061fbd62383492237b546d2d8f7c1846272350d2b9e14138cd/posthog-7.9.12.tar.gz", hash = "sha256:ebabf2eb2e1c1fbf22b0759df4644623fa43cc6c9dcbe9fd429b7937d14251ec", size = 176828, upload-time = "2026-03-12T09:01:15.184Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/65/a9/7a803aed5a5649cf78ea7b31e90d0080181ba21f739243e1741a1e607f1f/posthog-7.9.12-py3-none-any.whl", hash = "sha256:7175bd1698a566bfea98a016c64e3456399f8046aeeca8f1d04ae5bf6c5a38d0", size = 202469, upload-time = "2026-03-12T09:01:13.38Z" },
]
[[package]]
name = "propcache"
version = "0.4.1"
@@ -1538,6 +1648,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.2"
@@ -1751,6 +1873,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"