aegra-api 0.1.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.
- aegra_api/__init__.py +3 -0
- aegra_api/api/__init__.py +1 -0
- aegra_api/api/assistants.py +235 -0
- aegra_api/api/runs.py +1110 -0
- aegra_api/api/store.py +200 -0
- aegra_api/api/threads.py +761 -0
- aegra_api/config.py +204 -0
- aegra_api/constants.py +5 -0
- aegra_api/core/__init__.py +0 -0
- aegra_api/core/app_loader.py +91 -0
- aegra_api/core/auth_ctx.py +65 -0
- aegra_api/core/auth_deps.py +186 -0
- aegra_api/core/auth_handlers.py +248 -0
- aegra_api/core/auth_middleware.py +331 -0
- aegra_api/core/database.py +123 -0
- aegra_api/core/health.py +131 -0
- aegra_api/core/orm.py +165 -0
- aegra_api/core/route_merger.py +69 -0
- aegra_api/core/serializers/__init__.py +7 -0
- aegra_api/core/serializers/base.py +22 -0
- aegra_api/core/serializers/general.py +54 -0
- aegra_api/core/serializers/langgraph.py +102 -0
- aegra_api/core/sse.py +178 -0
- aegra_api/main.py +303 -0
- aegra_api/middleware/__init__.py +4 -0
- aegra_api/middleware/double_encoded_json.py +74 -0
- aegra_api/middleware/logger_middleware.py +95 -0
- aegra_api/models/__init__.py +76 -0
- aegra_api/models/assistants.py +81 -0
- aegra_api/models/auth.py +62 -0
- aegra_api/models/enums.py +29 -0
- aegra_api/models/errors.py +29 -0
- aegra_api/models/runs.py +124 -0
- aegra_api/models/store.py +67 -0
- aegra_api/models/threads.py +152 -0
- aegra_api/observability/__init__.py +1 -0
- aegra_api/observability/base.py +88 -0
- aegra_api/observability/otel.py +133 -0
- aegra_api/observability/setup.py +27 -0
- aegra_api/observability/targets/__init__.py +11 -0
- aegra_api/observability/targets/base.py +18 -0
- aegra_api/observability/targets/langfuse.py +33 -0
- aegra_api/observability/targets/otlp.py +38 -0
- aegra_api/observability/targets/phoenix.py +24 -0
- aegra_api/services/__init__.py +0 -0
- aegra_api/services/assistant_service.py +569 -0
- aegra_api/services/base_broker.py +59 -0
- aegra_api/services/broker.py +141 -0
- aegra_api/services/event_converter.py +157 -0
- aegra_api/services/event_store.py +196 -0
- aegra_api/services/graph_streaming.py +433 -0
- aegra_api/services/langgraph_service.py +456 -0
- aegra_api/services/streaming_service.py +362 -0
- aegra_api/services/thread_state_service.py +128 -0
- aegra_api/settings.py +124 -0
- aegra_api/utils/__init__.py +3 -0
- aegra_api/utils/assistants.py +23 -0
- aegra_api/utils/run_utils.py +60 -0
- aegra_api/utils/setup_logging.py +122 -0
- aegra_api/utils/sse_utils.py +26 -0
- aegra_api/utils/status_compat.py +57 -0
- aegra_api-0.1.0.dist-info/METADATA +244 -0
- aegra_api-0.1.0.dist-info/RECORD +64 -0
- aegra_api-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Base observability interface for extensible tracing and monitoring."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ObservabilityProvider(ABC):
|
|
11
|
+
"""Abstract base class for observability providers."""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def get_callbacks(self) -> list[Any]:
|
|
15
|
+
"""Return a list of callbacks for this provider."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_metadata(self, run_id: str, thread_id: str, user_identity: str | None = None) -> dict[str, Any]:
|
|
20
|
+
"""Return metadata to be added to the run configuration."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def is_enabled(self) -> bool:
|
|
25
|
+
"""Check if this provider is enabled."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ObservabilityManager:
|
|
30
|
+
"""Manages multiple observability providers."""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._providers: list[ObservabilityProvider] = []
|
|
34
|
+
|
|
35
|
+
def register_provider(self, provider: ObservabilityProvider) -> None:
|
|
36
|
+
"""Register an observability provider (idempotent for same instance).
|
|
37
|
+
|
|
38
|
+
Only registers enabled providers. Skips if the exact same instance
|
|
39
|
+
is already registered (checked by object identity).
|
|
40
|
+
"""
|
|
41
|
+
if not provider.is_enabled():
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# Check if this exact instance is already registered (by object identity)
|
|
45
|
+
if provider in self._providers:
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
self._providers.append(provider)
|
|
49
|
+
|
|
50
|
+
def get_all_callbacks(self) -> list[Any]:
|
|
51
|
+
"""Get callbacks from all enabled providers."""
|
|
52
|
+
callbacks = []
|
|
53
|
+
for provider in self._providers:
|
|
54
|
+
try:
|
|
55
|
+
callbacks.extend(provider.get_callbacks())
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f"Failed to get callbacks from {provider.__class__.__name__}: {e}")
|
|
58
|
+
return callbacks
|
|
59
|
+
|
|
60
|
+
def get_all_metadata(self, run_id: str, thread_id: str, user_identity: str | None = None) -> dict[str, Any]:
|
|
61
|
+
"""Get metadata from all enabled providers."""
|
|
62
|
+
metadata = {}
|
|
63
|
+
for provider in self._providers:
|
|
64
|
+
try:
|
|
65
|
+
provider_metadata = provider.get_metadata(run_id, thread_id, user_identity)
|
|
66
|
+
metadata.update(provider_metadata)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to get metadata from {provider.__class__.__name__}: {e}")
|
|
69
|
+
return metadata
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Global observability manager instance
|
|
73
|
+
_observability_manager = ObservabilityManager()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_observability_manager() -> ObservabilityManager:
|
|
77
|
+
"""Get the global observability manager."""
|
|
78
|
+
return _observability_manager
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_tracing_callbacks() -> list[Any]:
|
|
82
|
+
"""Get callbacks from all registered observability providers."""
|
|
83
|
+
return _observability_manager.get_all_callbacks()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_tracing_metadata(run_id: str, thread_id: str, user_identity: str | None = None) -> dict[str, Any]:
|
|
87
|
+
"""Get metadata from all registered observability providers."""
|
|
88
|
+
return _observability_manager.get_all_metadata(run_id, thread_id, user_identity)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified OpenTelemetry Provider.
|
|
3
|
+
Orchestrates trace generation and fan-out export to multiple targets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
10
|
+
from opentelemetry import trace
|
|
11
|
+
from opentelemetry.sdk.resources import Resource
|
|
12
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
13
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
14
|
+
|
|
15
|
+
from aegra_api.observability.base import ObservabilityProvider
|
|
16
|
+
from aegra_api.observability.targets import (
|
|
17
|
+
BaseOtelTarget,
|
|
18
|
+
GenericOtelTarget,
|
|
19
|
+
LangfuseTarget,
|
|
20
|
+
PhoenixTarget,
|
|
21
|
+
)
|
|
22
|
+
from aegra_api.settings import settings
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OpenTelemetryProvider(ObservabilityProvider):
|
|
28
|
+
"""
|
|
29
|
+
Main provider that configures the global OpenTelemetry Tracer.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self) -> None:
|
|
33
|
+
self._enabled = False
|
|
34
|
+
self._tracer_provider: TracerProvider | None = None
|
|
35
|
+
|
|
36
|
+
# Defining the list of active targets
|
|
37
|
+
self._active_targets: list[BaseOtelTarget] = self._resolve_targets()
|
|
38
|
+
|
|
39
|
+
if self._active_targets or settings.observability.OTEL_CONSOLE_EXPORT:
|
|
40
|
+
self._enabled = True
|
|
41
|
+
|
|
42
|
+
def is_enabled(self) -> bool:
|
|
43
|
+
return self._enabled
|
|
44
|
+
|
|
45
|
+
def _resolve_targets(self) -> list[BaseOtelTarget]:
|
|
46
|
+
targets: list[BaseOtelTarget] = []
|
|
47
|
+
raw_targets = settings.observability.OTEL_TARGETS
|
|
48
|
+
|
|
49
|
+
if not raw_targets:
|
|
50
|
+
return targets
|
|
51
|
+
|
|
52
|
+
for name in raw_targets.split(","):
|
|
53
|
+
name_clean = name.strip().upper()
|
|
54
|
+
if not name_clean:
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if name_clean == "LANGFUSE":
|
|
58
|
+
targets.append(LangfuseTarget())
|
|
59
|
+
elif name_clean == "PHOENIX":
|
|
60
|
+
targets.append(PhoenixTarget())
|
|
61
|
+
elif name_clean in ("GENERIC", "DEFAULT", "OTLP"):
|
|
62
|
+
targets.append(GenericOtelTarget())
|
|
63
|
+
else:
|
|
64
|
+
logger.warning(f"Unknown OTEL target in settings: {name_clean}")
|
|
65
|
+
|
|
66
|
+
return targets
|
|
67
|
+
|
|
68
|
+
def add_custom_target(self, target: BaseOtelTarget) -> None:
|
|
69
|
+
"""Allow registering custom targets dynamically."""
|
|
70
|
+
self._active_targets.append(target)
|
|
71
|
+
self._enabled = True
|
|
72
|
+
|
|
73
|
+
def setup(self) -> None:
|
|
74
|
+
"""Initializes the Global Tracer Provider. Runs once."""
|
|
75
|
+
if self._tracer_provider:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
# 1. Resource
|
|
79
|
+
resource = Resource.create(
|
|
80
|
+
attributes={
|
|
81
|
+
"service.name": settings.observability.OTEL_SERVICE_NAME,
|
|
82
|
+
"service.version": settings.app.VERSION,
|
|
83
|
+
"deployment.environment": settings.app.ENV_MODE.lower(),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self._tracer_provider = TracerProvider(resource=resource)
|
|
88
|
+
processors_count = 0
|
|
89
|
+
|
|
90
|
+
# 2. Attach Exporters
|
|
91
|
+
for target in self._active_targets:
|
|
92
|
+
try:
|
|
93
|
+
exporter = target.get_exporter()
|
|
94
|
+
if exporter:
|
|
95
|
+
processor = BatchSpanProcessor(exporter)
|
|
96
|
+
self._tracer_provider.add_span_processor(processor)
|
|
97
|
+
processors_count += 1
|
|
98
|
+
logger.info(f"Observability: Attached target '{target.name}'")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Observability: Failed to attach target '{target.name}': {e}")
|
|
101
|
+
|
|
102
|
+
# 3. Console Exporter
|
|
103
|
+
if settings.observability.OTEL_CONSOLE_EXPORT:
|
|
104
|
+
self._tracer_provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
105
|
+
processors_count += 1
|
|
106
|
+
logger.info("Observability: Console export enabled")
|
|
107
|
+
|
|
108
|
+
# 4. Set Global Tracer & Instrument
|
|
109
|
+
if processors_count > 0:
|
|
110
|
+
trace.set_tracer_provider(self._tracer_provider)
|
|
111
|
+
LangChainInstrumentor().instrument(tracer_provider=self._tracer_provider)
|
|
112
|
+
logger.info("Observability: Auto-instrumentation enabled")
|
|
113
|
+
|
|
114
|
+
def get_callbacks(self) -> list[Any]:
|
|
115
|
+
if self.is_enabled():
|
|
116
|
+
self.setup()
|
|
117
|
+
return []
|
|
118
|
+
|
|
119
|
+
def get_metadata(self, run_id: str, thread_id: str, user_identity: str | None = None) -> dict[str, Any]:
|
|
120
|
+
if not self.is_enabled():
|
|
121
|
+
return {}
|
|
122
|
+
|
|
123
|
+
meta = {
|
|
124
|
+
"run_id": run_id,
|
|
125
|
+
"thread_id": thread_id,
|
|
126
|
+
"session_id": thread_id,
|
|
127
|
+
}
|
|
128
|
+
if user_identity:
|
|
129
|
+
meta["user_id"] = user_identity
|
|
130
|
+
return meta
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
otel_provider = OpenTelemetryProvider()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from aegra_api.observability.base import get_observability_manager
|
|
4
|
+
from aegra_api.observability.otel import otel_provider
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_observability() -> None:
|
|
10
|
+
"""
|
|
11
|
+
Registers and initializes the observability subsystem.
|
|
12
|
+
Handles global OpenTelemetry setup if enabled.
|
|
13
|
+
"""
|
|
14
|
+
manager = get_observability_manager()
|
|
15
|
+
|
|
16
|
+
# We are registering our single OTEL provider
|
|
17
|
+
manager.register_provider(otel_provider)
|
|
18
|
+
|
|
19
|
+
# Launching global instrumentation if the provider is active
|
|
20
|
+
if otel_provider.is_enabled():
|
|
21
|
+
try:
|
|
22
|
+
otel_provider.setup()
|
|
23
|
+
logger.info("Observability subsystem initialized successfully.")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.error(f"Failed to initialize observability: {e}")
|
|
26
|
+
else:
|
|
27
|
+
logger.info("Observability is disabled (no targets configured).")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from aegra_api.observability.targets.base import BaseOtelTarget
|
|
2
|
+
from aegra_api.observability.targets.langfuse import LangfuseTarget
|
|
3
|
+
from aegra_api.observability.targets.otlp import GenericOtelTarget
|
|
4
|
+
from aegra_api.observability.targets.phoenix import PhoenixTarget
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"BaseOtelTarget",
|
|
8
|
+
"LangfuseTarget",
|
|
9
|
+
"PhoenixTarget",
|
|
10
|
+
"GenericOtelTarget",
|
|
11
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from opentelemetry.sdk.trace.export import SpanExporter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseOtelTarget(ABC):
|
|
7
|
+
"""Interface for an observability destination."""
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def name(self) -> str:
|
|
12
|
+
"""Friendly name for logging (e.g. 'Langfuse')."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_exporter(self) -> SpanExporter | None:
|
|
17
|
+
"""Returns a configured exporter or None if config is missing."""
|
|
18
|
+
pass
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
5
|
+
from opentelemetry.sdk.trace.export import SpanExporter
|
|
6
|
+
|
|
7
|
+
from aegra_api.observability.targets.base import BaseOtelTarget
|
|
8
|
+
from aegra_api.settings import settings
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LangfuseTarget(BaseOtelTarget):
|
|
14
|
+
@property
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
return "Langfuse"
|
|
17
|
+
|
|
18
|
+
def get_exporter(self) -> SpanExporter | None:
|
|
19
|
+
conf = settings.observability
|
|
20
|
+
pk = conf.LANGFUSE_PUBLIC_KEY
|
|
21
|
+
sk = conf.LANGFUSE_SECRET_KEY
|
|
22
|
+
|
|
23
|
+
if not pk or not sk:
|
|
24
|
+
logger.debug("Langfuse credentials missing.")
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
base_host = conf.LANGFUSE_BASE_URL.rstrip("/")
|
|
28
|
+
endpoint = f"{base_host}/api/public/otel/v1/traces"
|
|
29
|
+
|
|
30
|
+
auth_str = f"{pk}:{sk}"
|
|
31
|
+
auth_b64 = base64.b64encode(auth_str.encode()).decode()
|
|
32
|
+
|
|
33
|
+
return OTLPSpanExporter(endpoint=endpoint, headers={"Authorization": f"Basic {auth_b64}"})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
4
|
+
from opentelemetry.sdk.trace.export import SpanExporter
|
|
5
|
+
|
|
6
|
+
from aegra_api.observability.targets.base import BaseOtelTarget
|
|
7
|
+
from aegra_api.settings import settings
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GenericOtelTarget(BaseOtelTarget):
|
|
13
|
+
@property
|
|
14
|
+
def name(self) -> str:
|
|
15
|
+
return "GenericOTLP"
|
|
16
|
+
|
|
17
|
+
def get_exporter(self) -> SpanExporter | None:
|
|
18
|
+
conf = settings.observability
|
|
19
|
+
if not conf.OTEL_EXPORTER_OTLP_ENDPOINT:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
return OTLPSpanExporter(
|
|
23
|
+
endpoint=conf.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
24
|
+
headers=self._parse_headers(conf.OTEL_EXPORTER_OTLP_HEADERS),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def _parse_headers(self, headers_str: str | None) -> dict[str, str]:
|
|
28
|
+
headers: dict[str, str] = {}
|
|
29
|
+
if not headers_str:
|
|
30
|
+
return headers
|
|
31
|
+
try:
|
|
32
|
+
for item in headers_str.split(","):
|
|
33
|
+
if "=" in item:
|
|
34
|
+
k, v = item.split("=", 1)
|
|
35
|
+
headers[k.strip()] = v.strip()
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.warning(f"Failed to parse OTEL headers: {e}")
|
|
38
|
+
return headers
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
2
|
+
from opentelemetry.sdk.trace.export import SpanExporter
|
|
3
|
+
|
|
4
|
+
from aegra_api.observability.targets.base import BaseOtelTarget
|
|
5
|
+
from aegra_api.settings import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PhoenixTarget(BaseOtelTarget):
|
|
9
|
+
@property
|
|
10
|
+
def name(self) -> str:
|
|
11
|
+
return "Phoenix"
|
|
12
|
+
|
|
13
|
+
def get_exporter(self) -> SpanExporter | None:
|
|
14
|
+
conf = settings.observability
|
|
15
|
+
endpoint = conf.PHOENIX_COLLECTOR_ENDPOINT
|
|
16
|
+
|
|
17
|
+
if not endpoint:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
headers = {}
|
|
21
|
+
if conf.PHOENIX_API_KEY:
|
|
22
|
+
headers["authorization"] = f"Bearer {conf.PHOENIX_API_KEY}"
|
|
23
|
+
|
|
24
|
+
return OTLPSpanExporter(endpoint=endpoint, headers=headers)
|
|
File without changes
|