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.
Files changed (64) hide show
  1. aegra_api/__init__.py +3 -0
  2. aegra_api/api/__init__.py +1 -0
  3. aegra_api/api/assistants.py +235 -0
  4. aegra_api/api/runs.py +1110 -0
  5. aegra_api/api/store.py +200 -0
  6. aegra_api/api/threads.py +761 -0
  7. aegra_api/config.py +204 -0
  8. aegra_api/constants.py +5 -0
  9. aegra_api/core/__init__.py +0 -0
  10. aegra_api/core/app_loader.py +91 -0
  11. aegra_api/core/auth_ctx.py +65 -0
  12. aegra_api/core/auth_deps.py +186 -0
  13. aegra_api/core/auth_handlers.py +248 -0
  14. aegra_api/core/auth_middleware.py +331 -0
  15. aegra_api/core/database.py +123 -0
  16. aegra_api/core/health.py +131 -0
  17. aegra_api/core/orm.py +165 -0
  18. aegra_api/core/route_merger.py +69 -0
  19. aegra_api/core/serializers/__init__.py +7 -0
  20. aegra_api/core/serializers/base.py +22 -0
  21. aegra_api/core/serializers/general.py +54 -0
  22. aegra_api/core/serializers/langgraph.py +102 -0
  23. aegra_api/core/sse.py +178 -0
  24. aegra_api/main.py +303 -0
  25. aegra_api/middleware/__init__.py +4 -0
  26. aegra_api/middleware/double_encoded_json.py +74 -0
  27. aegra_api/middleware/logger_middleware.py +95 -0
  28. aegra_api/models/__init__.py +76 -0
  29. aegra_api/models/assistants.py +81 -0
  30. aegra_api/models/auth.py +62 -0
  31. aegra_api/models/enums.py +29 -0
  32. aegra_api/models/errors.py +29 -0
  33. aegra_api/models/runs.py +124 -0
  34. aegra_api/models/store.py +67 -0
  35. aegra_api/models/threads.py +152 -0
  36. aegra_api/observability/__init__.py +1 -0
  37. aegra_api/observability/base.py +88 -0
  38. aegra_api/observability/otel.py +133 -0
  39. aegra_api/observability/setup.py +27 -0
  40. aegra_api/observability/targets/__init__.py +11 -0
  41. aegra_api/observability/targets/base.py +18 -0
  42. aegra_api/observability/targets/langfuse.py +33 -0
  43. aegra_api/observability/targets/otlp.py +38 -0
  44. aegra_api/observability/targets/phoenix.py +24 -0
  45. aegra_api/services/__init__.py +0 -0
  46. aegra_api/services/assistant_service.py +569 -0
  47. aegra_api/services/base_broker.py +59 -0
  48. aegra_api/services/broker.py +141 -0
  49. aegra_api/services/event_converter.py +157 -0
  50. aegra_api/services/event_store.py +196 -0
  51. aegra_api/services/graph_streaming.py +433 -0
  52. aegra_api/services/langgraph_service.py +456 -0
  53. aegra_api/services/streaming_service.py +362 -0
  54. aegra_api/services/thread_state_service.py +128 -0
  55. aegra_api/settings.py +124 -0
  56. aegra_api/utils/__init__.py +3 -0
  57. aegra_api/utils/assistants.py +23 -0
  58. aegra_api/utils/run_utils.py +60 -0
  59. aegra_api/utils/setup_logging.py +122 -0
  60. aegra_api/utils/sse_utils.py +26 -0
  61. aegra_api/utils/status_compat.py +57 -0
  62. aegra_api-0.1.0.dist-info/METADATA +244 -0
  63. aegra_api-0.1.0.dist-info/RECORD +64 -0
  64. 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