ignis-observability 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. ignis_observability/__init__.py +34 -0
  2. ignis_observability/config.py +99 -0
  3. ignis_observability/database.py +135 -0
  4. ignis_observability/engine.py +787 -0
  5. ignis_observability/providers/__init__.py +1 -0
  6. ignis_observability/providers/anthropic.py +138 -0
  7. ignis_observability/providers/gemini.py +138 -0
  8. ignis_observability/providers/ingestion/__init__.py +23 -0
  9. ignis_observability/providers/ingestion/arize.py +375 -0
  10. ignis_observability/providers/ingestion/base.py +14 -0
  11. ignis_observability/providers/ingestion/field_mapping.py +27 -0
  12. ignis_observability/providers/ingestion/laminar.py +47 -0
  13. ignis_observability/providers/ingestion/langfuse.py +46 -0
  14. ignis_observability/providers/ingestion/langsmith.py +641 -0
  15. ignis_observability/providers/ingestion/langwatch.py +294 -0
  16. ignis_observability/providers/ingestion/otel.py +232 -0
  17. ignis_observability/providers/openai.py +139 -0
  18. ignis_observability/providers/sync/__init__.py +1 -0
  19. ignis_observability/providers/sync/__main__.py +4 -0
  20. ignis_observability/providers/sync/arize_adapter.py +149 -0
  21. ignis_observability/providers/sync/base_adapter.py +130 -0
  22. ignis_observability/providers/sync/db_writer.py +319 -0
  23. ignis_observability/providers/sync/field_mapping.py +116 -0
  24. ignis_observability/providers/sync/langsmith_adapter.py +197 -0
  25. ignis_observability/providers/sync/langsmith_sync.py +304 -0
  26. ignis_observability/providers/sync/langwatch_adapter.py +259 -0
  27. ignis_observability/providers/sync/pull_from_tools.py +1186 -0
  28. ignis_observability/providers/sync/run_pull_scheduler.py +28 -0
  29. ignis_observability/py.typed +0 -0
  30. ignis_observability/trace_builder.py +240 -0
  31. ignis_observability-0.2.0.dist-info/METADATA +549 -0
  32. ignis_observability-0.2.0.dist-info/RECORD +35 -0
  33. ignis_observability-0.2.0.dist-info/WHEEL +5 -0
  34. ignis_observability-0.2.0.dist-info/licenses/LICENSE +21 -0
  35. ignis_observability-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,34 @@
1
+ """
2
+ ignis_observability — Non-Blocking AI Observability Hub for multi-model LLM applications.
3
+
4
+ Quick start::
5
+
6
+ from ignis_observability import ObsHub, DBManager, settings
7
+
8
+ db = DBManager(settings.DATABASE_URL)
9
+ await db.init_db()
10
+ await db.seed_prices()
11
+
12
+ hub = ObsHub(langsmith_api_key=settings.LANGCHAIN_API_KEY, db_manager=db)
13
+
14
+ @hub.observe(name="my_llm_call")
15
+ async def call_llm(prompt: str):
16
+ ...
17
+ """
18
+
19
+ from .config import settings
20
+ from .database import DBManager, UnifiedTrace, ModelPrice
21
+ from .engine import ObsHub
22
+ from .trace_builder import TraceBuilder
23
+
24
+ __version__ = "0.2.0"
25
+
26
+ __all__ = [
27
+ "ObsHub",
28
+ "DBManager",
29
+ "UnifiedTrace",
30
+ "ModelPrice",
31
+ "TraceBuilder",
32
+ "settings",
33
+ "__version__",
34
+ ]
@@ -0,0 +1,99 @@
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+
3
+
4
+ class Settings(BaseSettings):
5
+ #APPLICATION NAME
6
+ APPLICATION_NAME: str = "ignis"
7
+ USER_ID: str = ""
8
+
9
+ # OpenAI Configuration
10
+ OPENAI_API_KEY: str = ""
11
+ # Other LLM providers (optional)
12
+ ANTHROPIC_API_KEY: str = ""
13
+ GEMINI_API_KEY: str = ""
14
+
15
+ # LangSmith Configuration
16
+ LANGCHAIN_API_KEY: str = ""
17
+ LANGCHAIN_ENDPOINT: str = "https://api.smith.langchain.com"
18
+ LANGCHAIN_PROJECT: str = "default"
19
+ LANGCHAIN_PROJECT_ID: str = ""
20
+ LANGCHAIN_TRACING_V2: bool = True
21
+
22
+ # Database Configuration
23
+ DATABASE_URL: str = "sqlite+aiosqlite:///./obs_hub.db"
24
+
25
+ # Sync Configuration
26
+ SYNC_INTERVAL: int = 60 # Sync every 60 seconds
27
+
28
+ # Logging
29
+ LOG_LEVEL: str = "info"
30
+
31
+ # ------------------------------------------------------------------
32
+ # Environment & Request Context
33
+ # ------------------------------------------------------------------
34
+ ENVIRONMENT: str = "development" # development, staging, production
35
+ SERVICE_NAME: str = "llm-app"
36
+ SERVICE_VERSION: str = "1.0.0"
37
+ DEFAULT_USER_ID: str = "" # Can be overridden per request
38
+ DEFAULT_SESSION_ID: str = "" # Can be overridden per request
39
+
40
+ # ------------------------------------------------------------------
41
+ # Advanced Tracing Configuration (Optional)
42
+ # ------------------------------------------------------------------
43
+ # Master toggle for advanced tracing features
44
+ ADVANCED_TRACING_ENABLED: bool = True
45
+
46
+ # Individual feature toggles (only apply when ADVANCED_TRACING_ENABLED=True)
47
+ CAPTURE_REQUEST_ID: bool = True # Unique trace ID per request
48
+ CAPTURE_USER_SESSION: bool = True # User ID & Session ID tracking
49
+ CAPTURE_LLM_PARAMS: bool = True # temperature, top_p, max_tokens, etc.
50
+ CAPTURE_STATUS_ERROR: bool = True # Status & error information
51
+ CAPTURE_TAGS: bool = True # Custom tags for organization
52
+ CAPTURE_ENVIRONMENT: bool = True # Environment context
53
+
54
+ # ------------------------------------------------------------------
55
+ # Additional Observability Platform Configuration
56
+ # ------------------------------------------------------------------
57
+ # LangWatch
58
+ LANGWATCH_API_KEY: str = ""
59
+ LANGWATCH_PROJECT: str = ""
60
+ LANGWATCH_ENDPOINT: str = ""
61
+ LANGWATCH_ENABLED: bool = False
62
+
63
+ # LangFuse
64
+ LANGFUSE_API_KEY: str = ""
65
+ LANGFUSE_DATASET: str = ""
66
+ LANGFUSE_ENDPOINT: str = ""
67
+ LANGFUSE_ENABLED: bool = False
68
+
69
+ # Arize Cloud
70
+ ARIZE_API_KEY: str = ""
71
+ ARIZE_SPACE_KEY: str = ""
72
+ ARIZE_MODEL_NAME: str = ""
73
+ ARIZE_ENDPOINT: str = ""
74
+ ARIZE_COLLECTOR_ENDPOINT: str = "https://otlp.arize.com"
75
+ ARIZE_PROJECT: str = ""
76
+ ARIZE_ENABLED: bool = False
77
+
78
+ # Arize Phoenix (open-source observability)
79
+ PHOENIX_API_KEY: str = ""
80
+ PHOENIX_COLLECTOR_ENDPOINT: str = "https://app.phoenix.arize.com"
81
+
82
+ # Laminar
83
+ LAMINAR_API_KEY: str = ""
84
+ LAMINAR_PROJECT: str = ""
85
+ LAMINAR_ENDPOINT: str = ""
86
+ LAMINAR_ENABLED: bool = False
87
+
88
+ # OpenTelemetry (OTel)
89
+ OTEL_ENABLED: bool = False
90
+ OTEL_EXPORTER_OTLP_ENDPOINT: str = "http://localhost:4317"
91
+ OTEL_SERVICE_NAME: str = "llm-app"
92
+ OTEL_ENVIRONMENT: str = "development"
93
+
94
+ model_config = SettingsConfigDict(
95
+ env_file=".env", env_file_encoding="utf-8", case_sensitive=True, extra="ignore"
96
+ )
97
+
98
+
99
+ settings = Settings()
@@ -0,0 +1,135 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from sqlalchemy import JSON, DateTime, Float, Integer, String, func, select
5
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
6
+ from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
7
+
8
+
9
+ class Base(DeclarativeBase):
10
+ """Base class for all database models"""
11
+
12
+
13
+ class ModelPrice(Base):
14
+ __tablename__ = "model_prices"
15
+ model: Mapped[str] = mapped_column(String, primary_key=True)
16
+ input_1k_price: Mapped[float] = mapped_column(Float) # Price per 1k tokens
17
+ output_1k_price: Mapped[float] = mapped_column(Float)
18
+
19
+
20
+ class UnifiedTrace(Base):
21
+ __tablename__ = "llm_traces"
22
+
23
+ id: Mapped[str] = mapped_column(String, primary_key=True)
24
+ timestamp: Mapped[datetime] = mapped_column(DateTime, default=func.now())
25
+ name: Mapped[str] = mapped_column(String)
26
+ model: Mapped[str] = mapped_column(String)
27
+ latency_ms: Mapped[float] = mapped_column(Float)
28
+ prompt_tokens: Mapped[int] = mapped_column(Integer, default=0)
29
+ completion_tokens: Mapped[int] = mapped_column(Integer, default=0)
30
+ total_cost: Mapped[float] = mapped_column(Float, default=0.0)
31
+ prompt: Mapped[Optional[str]] = mapped_column(String, nullable=True)
32
+ completion: Mapped[Optional[str]] = mapped_column(String, nullable=True)
33
+
34
+ # Advanced tracing fields
35
+ request_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
36
+ session_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
37
+ user_id: Mapped[Optional[str]] = mapped_column(String, nullable=True)
38
+ status: Mapped[str] = mapped_column(
39
+ String, default="success"
40
+ ) # success, error, timeout
41
+ error: Mapped[Optional[str]] = mapped_column(String, nullable=True)
42
+ tags: Mapped[Optional[str]] = mapped_column(
43
+ String, nullable=True
44
+ ) # comma-separated
45
+ environment: Mapped[str] = mapped_column(String, default="development")
46
+ llm_params_json: Mapped[Optional[dict]] = mapped_column(
47
+ JSON, nullable=True
48
+ ) # temperature, max_tokens, etc.
49
+
50
+ metadata_json: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True)
51
+
52
+
53
+ class DBManager:
54
+ def __init__(self, db_url: str = "sqlite+aiosqlite:///./obs_hub.db"):
55
+ self.engine = create_async_engine(db_url)
56
+ self.session_factory = async_sessionmaker(self.engine, expire_on_commit=False)
57
+
58
+ async def init_db(self):
59
+ async with self.engine.begin() as conn:
60
+ await conn.run_sync(Base.metadata.create_all)
61
+
62
+ async def save_trace(self, data: dict):
63
+ """Save a trace, filtering to only database fields and converting types"""
64
+ async with self.session_factory() as session:
65
+ # Extract only fields that exist in UnifiedTrace model
66
+ db_fields = {
67
+ "id",
68
+ "timestamp",
69
+ "name",
70
+ "model",
71
+ "latency_ms",
72
+ "prompt_tokens",
73
+ "completion_tokens",
74
+ "total_cost",
75
+ "prompt",
76
+ "completion",
77
+ "request_id",
78
+ "session_id",
79
+ "user_id",
80
+ "status",
81
+ "error",
82
+ "tags",
83
+ "environment",
84
+ "llm_params_json",
85
+ "metadata_json",
86
+ }
87
+
88
+ filtered_data = {k: v for k, v in data.items() if k in db_fields}
89
+
90
+ # Convert tags list to comma-separated string if needed
91
+ if "tags" in filtered_data and isinstance(filtered_data["tags"], list):
92
+ filtered_data["tags"] = ",".join(filtered_data["tags"])
93
+
94
+ try:
95
+ # Create ORM object and add to session (don't await ORM methods)
96
+ trace = UnifiedTrace(**filtered_data)
97
+ session.add(trace)
98
+ await session.commit()
99
+ except Exception as e:
100
+ await session.rollback()
101
+ print(f"Error saving trace: {e}")
102
+
103
+ async def seed_prices(self):
104
+ async with self.session_factory() as session:
105
+ # 2026 Estimated Pricing (based on current trends)
106
+ prices = [
107
+ ModelPrice(
108
+ model="gpt-3.5-turbo", input_1k_price=0.0005, output_1k_price=0.0015
109
+ ),
110
+ ModelPrice(
111
+ model="gpt-4o", input_1k_price=0.0025, output_1k_price=0.010
112
+ ),
113
+ ModelPrice(
114
+ model="gpt-4o-mini", input_1k_price=0.00015, output_1k_price=0.0006
115
+ ),
116
+ ]
117
+ for p in prices:
118
+ await session.merge(p)
119
+ await session.commit()
120
+
121
+ async def calculate_and_save(self, data: dict):
122
+ async with self.session_factory() as session:
123
+ # Fetch rates for the model
124
+ res = await session.execute(
125
+ select(ModelPrice).where(ModelPrice.model == data["model"])
126
+ )
127
+ price = res.scalar_one_or_none()
128
+
129
+ if price:
130
+ input_cost = (data["prompt_tokens"] / 1000) * price.input_1k_price
131
+ output_cost = (data["completion_tokens"] / 1000) * price.output_1k_price
132
+ data["total_cost"] = input_cost + output_cost
133
+
134
+ await session.merge(UnifiedTrace(**data))
135
+ await session.commit()