idun-agent-engine 0.3.4__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.
- idun_agent_engine/__init__.py +24 -0
- idun_agent_engine/_version.py +3 -0
- idun_agent_engine/agent/__init__.py +10 -0
- idun_agent_engine/agent/adk/__init__.py +5 -0
- idun_agent_engine/agent/adk/adk.py +296 -0
- idun_agent_engine/agent/base.py +112 -0
- idun_agent_engine/agent/haystack/__init__.py +9 -0
- idun_agent_engine/agent/haystack/haystack.py +274 -0
- idun_agent_engine/agent/haystack/haystack_model.py +13 -0
- idun_agent_engine/agent/haystack/utils.py +13 -0
- idun_agent_engine/agent/langgraph/__init__.py +7 -0
- idun_agent_engine/agent/langgraph/langgraph.py +553 -0
- idun_agent_engine/core/__init__.py +11 -0
- idun_agent_engine/core/app_factory.py +73 -0
- idun_agent_engine/core/config_builder.py +657 -0
- idun_agent_engine/core/engine_config.py +21 -0
- idun_agent_engine/core/server_runner.py +145 -0
- idun_agent_engine/guardrails/__init__.py +0 -0
- idun_agent_engine/guardrails/base.py +24 -0
- idun_agent_engine/guardrails/guardrails_hub/guardrails_hub.py +101 -0
- idun_agent_engine/guardrails/guardrails_hub/utils.py +1 -0
- idun_agent_engine/mcp/__init__.py +5 -0
- idun_agent_engine/mcp/helpers.py +97 -0
- idun_agent_engine/mcp/registry.py +109 -0
- idun_agent_engine/observability/__init__.py +17 -0
- idun_agent_engine/observability/base.py +172 -0
- idun_agent_engine/observability/gcp_logging/__init__.py +0 -0
- idun_agent_engine/observability/gcp_logging/gcp_logging_handler.py +52 -0
- idun_agent_engine/observability/gcp_trace/__init__.py +0 -0
- idun_agent_engine/observability/gcp_trace/gcp_trace_handler.py +116 -0
- idun_agent_engine/observability/langfuse/__init__.py +5 -0
- idun_agent_engine/observability/langfuse/langfuse_handler.py +79 -0
- idun_agent_engine/observability/phoenix/__init__.py +5 -0
- idun_agent_engine/observability/phoenix/phoenix_handler.py +65 -0
- idun_agent_engine/observability/phoenix_local/__init__.py +5 -0
- idun_agent_engine/observability/phoenix_local/phoenix_local_handler.py +123 -0
- idun_agent_engine/py.typed +0 -0
- idun_agent_engine/server/__init__.py +5 -0
- idun_agent_engine/server/dependencies.py +52 -0
- idun_agent_engine/server/lifespan.py +106 -0
- idun_agent_engine/server/routers/__init__.py +5 -0
- idun_agent_engine/server/routers/agent.py +204 -0
- idun_agent_engine/server/routers/agui.py +47 -0
- idun_agent_engine/server/routers/base.py +114 -0
- idun_agent_engine/server/server_config.py +8 -0
- idun_agent_engine/templates/__init__.py +1 -0
- idun_agent_engine/templates/correction.py +65 -0
- idun_agent_engine/templates/deep_research.py +40 -0
- idun_agent_engine/templates/translation.py +70 -0
- idun_agent_engine-0.3.4.dist-info/METADATA +335 -0
- idun_agent_engine-0.3.4.dist-info/RECORD +60 -0
- idun_agent_engine-0.3.4.dist-info/WHEEL +4 -0
- idun_agent_engine-0.3.4.dist-info/entry_points.txt +2 -0
- idun_platform_cli/__init__.py +0 -0
- idun_platform_cli/groups/__init__.py +0 -0
- idun_platform_cli/groups/agent/__init__.py +0 -0
- idun_platform_cli/groups/agent/main.py +16 -0
- idun_platform_cli/groups/agent/package.py +70 -0
- idun_platform_cli/groups/agent/serve.py +107 -0
- idun_platform_cli/main.py +14 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""GCP Logging observability handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..base import ObservabilityHandlerBase
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GCPLoggingHandler(ObservabilityHandlerBase):
|
|
14
|
+
"""GCP Logging handler."""
|
|
15
|
+
|
|
16
|
+
provider = "gcp_logging"
|
|
17
|
+
|
|
18
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
19
|
+
"""Initialize handler."""
|
|
20
|
+
super().__init__(options)
|
|
21
|
+
self.options = options or {}
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import google.cloud.logging
|
|
25
|
+
except ImportError as e:
|
|
26
|
+
logger.error("GCP Logging dependencies not found: %s", e)
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"Please install 'google-cloud-logging' to use GCP Logging."
|
|
29
|
+
) from e
|
|
30
|
+
|
|
31
|
+
project_id = self.options.get("project_id")
|
|
32
|
+
# If project_id is explicitly provided, use it, otherwise client will auto-detect
|
|
33
|
+
if project_id:
|
|
34
|
+
client = google.cloud.logging.Client(project=project_id)
|
|
35
|
+
else:
|
|
36
|
+
client = google.cloud.logging.Client()
|
|
37
|
+
|
|
38
|
+
# Get logging configuration options
|
|
39
|
+
log_level = self.options.get("severity", "INFO").upper()
|
|
40
|
+
level = getattr(logging, log_level, logging.INFO)
|
|
41
|
+
|
|
42
|
+
# Setup logging handler
|
|
43
|
+
# This attaches a CloudLoggingHandler to the root python logger
|
|
44
|
+
client.setup_logging(log_level=level)
|
|
45
|
+
|
|
46
|
+
logger.info("GCP Logging initialized for project: %s", client.project)
|
|
47
|
+
|
|
48
|
+
def get_callbacks(self) -> list[Any]:
|
|
49
|
+
"""Return callbacks."""
|
|
50
|
+
# GCP Logging hooks into the standard python logging module,
|
|
51
|
+
# so no explicit LangChain callbacks are needed.
|
|
52
|
+
return []
|
|
File without changes
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""GCP Trace observability handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from opentelemetry import trace
|
|
9
|
+
from opentelemetry.sdk.resources import Resource
|
|
10
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
11
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
12
|
+
from opentelemetry.sdk.trace.sampling import TraceIdRatioBased
|
|
13
|
+
|
|
14
|
+
from ..base import ObservabilityHandlerBase
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GCPTraceHandler(ObservabilityHandlerBase):
|
|
20
|
+
"""GCP Trace handler."""
|
|
21
|
+
|
|
22
|
+
provider = "gcp_trace"
|
|
23
|
+
|
|
24
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
25
|
+
"""Initialize handler."""
|
|
26
|
+
super().__init__(options)
|
|
27
|
+
self.options = options or {}
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
31
|
+
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
|
|
32
|
+
except ImportError as e:
|
|
33
|
+
logger.error("GCP Trace dependencies not found: %s", e)
|
|
34
|
+
raise ImportError(
|
|
35
|
+
"Please install 'opentelemetry-exporter-gcp-trace' and 'openinference-instrumentation-langchain' to use GCP Trace."
|
|
36
|
+
) from e
|
|
37
|
+
|
|
38
|
+
project_id = self.options.get("project_id")
|
|
39
|
+
if not project_id:
|
|
40
|
+
project_id = None
|
|
41
|
+
|
|
42
|
+
# Initialize exporter
|
|
43
|
+
exporter = CloudTraceSpanExporter(
|
|
44
|
+
project_id=project_id,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Initialize sampler
|
|
48
|
+
sampling_rate = float(self.options.get("sampling_rate", 1.0))
|
|
49
|
+
sampler = TraceIdRatioBased(sampling_rate)
|
|
50
|
+
|
|
51
|
+
# Initialize resource
|
|
52
|
+
resource_attributes = {}
|
|
53
|
+
trace_name = self.options.get("trace_name")
|
|
54
|
+
if trace_name:
|
|
55
|
+
resource_attributes["service.name"] = trace_name
|
|
56
|
+
|
|
57
|
+
resource = Resource.create(resource_attributes)
|
|
58
|
+
|
|
59
|
+
# Initialize tracer provider
|
|
60
|
+
tracer_provider = TracerProvider(
|
|
61
|
+
sampler=sampler,
|
|
62
|
+
resource=resource,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Add span processor
|
|
66
|
+
flush_interval = int(self.options.get("flush_interval", 5))
|
|
67
|
+
span_processor = BatchSpanProcessor(
|
|
68
|
+
exporter, schedule_delay_millis=flush_interval * 1000
|
|
69
|
+
)
|
|
70
|
+
tracer_provider.add_span_processor(span_processor)
|
|
71
|
+
|
|
72
|
+
# Set global tracer provider
|
|
73
|
+
trace.set_tracer_provider(tracer_provider)
|
|
74
|
+
|
|
75
|
+
# Instrument LangChain with OpenInference
|
|
76
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
77
|
+
|
|
78
|
+
# Instrument Guardrails
|
|
79
|
+
try:
|
|
80
|
+
from openinference.instrumentation.guardrails import GuardrailsInstrumentor
|
|
81
|
+
|
|
82
|
+
GuardrailsInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
83
|
+
except ImportError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Instrument VertexAI
|
|
87
|
+
try:
|
|
88
|
+
from openinference.instrumentation.vertexai import VertexAIInstrumentor
|
|
89
|
+
|
|
90
|
+
VertexAIInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
91
|
+
except ImportError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
# TODO: GCP GoogleADKInstrumentor is n conflist with langfuse, so we don't need to instrument it here
|
|
95
|
+
# Instrument Google ADK
|
|
96
|
+
# try:
|
|
97
|
+
# from openinference.instrumentation.google_adk import GoogleADKInstrumentor
|
|
98
|
+
|
|
99
|
+
# GoogleADKInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
100
|
+
# except ImportError:
|
|
101
|
+
# pass
|
|
102
|
+
|
|
103
|
+
# Instrument MCP
|
|
104
|
+
try:
|
|
105
|
+
from openinference.instrumentation.mcp import MCPInstrumentor
|
|
106
|
+
|
|
107
|
+
MCPInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
108
|
+
except ImportError:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
logger.info("GCP Trace initialized for project: %s", project_id or "auto-detected")
|
|
112
|
+
|
|
113
|
+
def get_callbacks(self) -> list[Any]:
|
|
114
|
+
"""Return callbacks."""
|
|
115
|
+
# OpenTelemetry instrumentation uses global tracer provider, so no explicit callbacks needed here
|
|
116
|
+
return []
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Langfuse observability handler implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from idun_agent_schema.engine.observability import _resolve_env
|
|
9
|
+
|
|
10
|
+
from ..base import ObservabilityHandlerBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LangfuseHandler(ObservabilityHandlerBase):
|
|
14
|
+
"""Langfuse handler providing LangChain callbacks and client setup."""
|
|
15
|
+
|
|
16
|
+
provider = "langfuse"
|
|
17
|
+
|
|
18
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
19
|
+
"""Initialize handler, resolving env and preparing callbacks."""
|
|
20
|
+
super().__init__(options)
|
|
21
|
+
opts = self.options
|
|
22
|
+
|
|
23
|
+
# Resolve and set env vars as required by Langfuse
|
|
24
|
+
host = self._resolve_env(opts.get("host")) or os.getenv("LANGFUSE_BASE_URL")
|
|
25
|
+
public_key = self._resolve_env(opts.get("public_key")) or os.getenv(
|
|
26
|
+
"LANGFUSE_PUBLIC_KEY"
|
|
27
|
+
)
|
|
28
|
+
secret_key = self._resolve_env(opts.get("secret_key")) or os.getenv(
|
|
29
|
+
"LANGFUSE_SECRET_KEY"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if host:
|
|
33
|
+
os.environ["LANGFUSE_BASE_URL"] = host
|
|
34
|
+
if public_key:
|
|
35
|
+
os.environ["LANGFUSE_PUBLIC_KEY"] = public_key
|
|
36
|
+
if secret_key:
|
|
37
|
+
os.environ["LANGFUSE_SECRET_KEY"] = secret_key
|
|
38
|
+
|
|
39
|
+
# Instantiate callback handler lazily to avoid hard dep if not installed
|
|
40
|
+
self._callbacks: list[Any] = []
|
|
41
|
+
self._langfuse_client = None
|
|
42
|
+
try:
|
|
43
|
+
from langfuse import get_client
|
|
44
|
+
from langfuse.langchain import CallbackHandler
|
|
45
|
+
|
|
46
|
+
# Initialize client for auth check
|
|
47
|
+
self._langfuse_client = get_client()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
if self._langfuse_client.auth_check():
|
|
51
|
+
print("Langfuse client is authenticated and ready!")
|
|
52
|
+
else:
|
|
53
|
+
print(
|
|
54
|
+
"Authentication failed. Please check your credentials and host."
|
|
55
|
+
)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Error during Langfuse client authentication: {e}")
|
|
58
|
+
|
|
59
|
+
# Initialize callback handler
|
|
60
|
+
# We pass the resolved credentials explicitly to ensure they are used
|
|
61
|
+
# even if env vars were not successfully set or read.
|
|
62
|
+
self._callbacks = [
|
|
63
|
+
CallbackHandler()
|
|
64
|
+
]
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(f"Failed to initialize Langfuse callback/client: {e}")
|
|
67
|
+
self._callbacks = []
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _resolve_env(value: str | None) -> str | None:
|
|
71
|
+
return _resolve_env(value)
|
|
72
|
+
|
|
73
|
+
def get_callbacks(self) -> list[Any]:
|
|
74
|
+
"""Return LangChain-compatible callback handlers (if available)."""
|
|
75
|
+
return self._callbacks
|
|
76
|
+
|
|
77
|
+
def get_client(self):
|
|
78
|
+
"""Return underlying Langfuse client instance (if created)."""
|
|
79
|
+
return self._langfuse_client
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Phoenix observability handler implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from idun_agent_schema.engine.observability import _resolve_env
|
|
9
|
+
|
|
10
|
+
from ..base import ObservabilityHandlerBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PhoenixHandler(ObservabilityHandlerBase):
|
|
14
|
+
"""Phoenix handler configuring OpenTelemetry and LangChain instrumentation."""
|
|
15
|
+
|
|
16
|
+
provider = "phoenix"
|
|
17
|
+
|
|
18
|
+
def __init__(self, options: dict[str, Any] | None = None):
|
|
19
|
+
"""Initialize handler, resolving env and setting up instrumentation."""
|
|
20
|
+
super().__init__(options)
|
|
21
|
+
opts = self.options
|
|
22
|
+
|
|
23
|
+
# Resolve and set env vars as required by Phoenix
|
|
24
|
+
api_key = self._resolve_env(opts.get("api_key")) or os.getenv("PHOENIX_API_KEY")
|
|
25
|
+
collector = (
|
|
26
|
+
self._resolve_env(opts.get("collector"))
|
|
27
|
+
or self._resolve_env(opts.get("collector_endpoint"))
|
|
28
|
+
or os.getenv("PHOENIX_COLLECTOR_ENDPOINT")
|
|
29
|
+
)
|
|
30
|
+
self.project_name: str = opts.get("project_name") or "default"
|
|
31
|
+
|
|
32
|
+
if api_key:
|
|
33
|
+
os.environ["PHOENIX_API_KEY"] = api_key
|
|
34
|
+
if collector:
|
|
35
|
+
os.environ["PHOENIX_COLLECTOR_ENDPOINT"] = collector
|
|
36
|
+
|
|
37
|
+
# Some older Phoenix deployments (before 2025-06-24) require setting client headers.
|
|
38
|
+
# If not explicitly provided, set it from API key when available for backward compatibility.
|
|
39
|
+
client_headers = opts.get("client_headers")
|
|
40
|
+
if isinstance(client_headers, str) and client_headers:
|
|
41
|
+
os.environ["PHOENIX_CLIENT_HEADERS"] = client_headers
|
|
42
|
+
elif api_key and not os.getenv("PHOENIX_CLIENT_HEADERS"):
|
|
43
|
+
os.environ["PHOENIX_CLIENT_HEADERS"] = f"api_key={api_key}"
|
|
44
|
+
|
|
45
|
+
# Configure tracer provider using phoenix.otel.register
|
|
46
|
+
self._callbacks: list[Any] = []
|
|
47
|
+
try:
|
|
48
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
49
|
+
from phoenix.otel import register # type: ignore
|
|
50
|
+
|
|
51
|
+
tracer_provider = register(
|
|
52
|
+
project_name=self.project_name, auto_instrument=True
|
|
53
|
+
)
|
|
54
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
55
|
+
except Exception:
|
|
56
|
+
# Silent failure; user may not have phoenix installed
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _resolve_env(value: str | None) -> str | None:
|
|
61
|
+
return _resolve_env(value)
|
|
62
|
+
|
|
63
|
+
def get_callbacks(self) -> list[Any]:
|
|
64
|
+
"""Return callbacks (Phoenix instruments globally; this may be empty)."""
|
|
65
|
+
return self._callbacks
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Phoenix observability handler implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shlex
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from idun_agent_schema.engine.observability import _resolve_env
|
|
12
|
+
|
|
13
|
+
from ..base import ObservabilityHandlerBase
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PhoenixLocalHandler(ObservabilityHandlerBase):
|
|
19
|
+
"""Phoenix handler configuring OpenTelemetry and LangChain instrumentation."""
|
|
20
|
+
|
|
21
|
+
provider = "phoenix-local"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
options: dict[str, Any] | None = None,
|
|
26
|
+
default_endpoint: str = "http://0.0.0.0:6006",
|
|
27
|
+
):
|
|
28
|
+
"""Initialize handler, start Phoenix via CLI, and set up instrumentation.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
options: Configuration options dictionary
|
|
32
|
+
default_endpoint: Default Phoenix collector endpoint URL
|
|
33
|
+
"""
|
|
34
|
+
logger.info("Initializing PhoenixLocalHandler")
|
|
35
|
+
|
|
36
|
+
super().__init__(options)
|
|
37
|
+
opts = self.options or {}
|
|
38
|
+
|
|
39
|
+
# Initialize instance variables
|
|
40
|
+
self._callbacks: list[Any] = []
|
|
41
|
+
self._proc: subprocess.Popen[bytes] | None = None
|
|
42
|
+
self.default_endpoint = default_endpoint
|
|
43
|
+
self.project_name: str = "default"
|
|
44
|
+
|
|
45
|
+
self._configure_collector_endpoint(opts)
|
|
46
|
+
self._start_phoenix_cli()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from openinference.instrumentation.langchain import LangChainInstrumentor
|
|
50
|
+
from phoenix.otel import register
|
|
51
|
+
|
|
52
|
+
logger.debug("Successfully imported Phoenix dependencies")
|
|
53
|
+
|
|
54
|
+
self.project_name = opts.get("project_name") or "default"
|
|
55
|
+
logger.info(f"Using project name: {self.project_name}")
|
|
56
|
+
|
|
57
|
+
tracer_provider = register(
|
|
58
|
+
project_name=self.project_name, auto_instrument=True
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
LangChainInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
62
|
+
|
|
63
|
+
except ImportError as e:
|
|
64
|
+
logger.error(f"Missing required Phoenix dependencies: {e}")
|
|
65
|
+
raise ImportError(f"Phoenix dependencies not found: {e}. ") from e
|
|
66
|
+
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error(f"Failed to set up Phoenix instrumentation: {e}")
|
|
69
|
+
raise RuntimeError(f"Phoenix instrumentation setup failed: {e}") from e
|
|
70
|
+
|
|
71
|
+
logger.info("Phoenix local handler initialized...")
|
|
72
|
+
|
|
73
|
+
def _configure_collector_endpoint(self, opts: dict[str, Any]) -> None:
|
|
74
|
+
"""Configure the Phoenix collector endpoint from various sources."""
|
|
75
|
+
logger.debug("Configuring collector endpoint")
|
|
76
|
+
|
|
77
|
+
collector = (
|
|
78
|
+
self._resolve_env(opts.get("collector"))
|
|
79
|
+
or self._resolve_env(opts.get("collector_endpoint"))
|
|
80
|
+
or os.getenv("PHOENIX_COLLECTOR_ENDPOINT")
|
|
81
|
+
or self.default_endpoint
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
logger.info(f"Setting Phoenix collector endpoint to: {collector}")
|
|
85
|
+
os.environ["PHOENIX_COLLECTOR_ENDPOINT"] = collector
|
|
86
|
+
self.collector_endpoint = collector
|
|
87
|
+
|
|
88
|
+
def _start_phoenix_cli(self) -> None:
|
|
89
|
+
"""Start pheonix subprocess."""
|
|
90
|
+
try:
|
|
91
|
+
cmd = "phoenix serve"
|
|
92
|
+
logger.debug(f"Executing command: {cmd}")
|
|
93
|
+
|
|
94
|
+
self._proc = subprocess.Popen(
|
|
95
|
+
shlex.split(cmd),
|
|
96
|
+
stdout=subprocess.DEVNULL,
|
|
97
|
+
stderr=subprocess.DEVNULL,
|
|
98
|
+
start_new_session=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
logger.info(f"Phoenix server started with PID: {self._proc.pid}")
|
|
102
|
+
|
|
103
|
+
except FileNotFoundError as e:
|
|
104
|
+
logger.error(f"Phoenix CLI not found. Make sure Phoenix is installed : {e}")
|
|
105
|
+
self._proc = None
|
|
106
|
+
|
|
107
|
+
except subprocess.SubprocessError as e:
|
|
108
|
+
logger.error(f"Failed to start Phoenix CLI subprocess: {e}")
|
|
109
|
+
self._proc = None
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.error(f"Unexpected error starting Phoenix CLI: {e}")
|
|
113
|
+
self._proc = None
|
|
114
|
+
|
|
115
|
+
@staticmethod
|
|
116
|
+
def _resolve_env(value: str | None) -> str | None:
|
|
117
|
+
"""Resolve environment variable value."""
|
|
118
|
+
return _resolve_env(value)
|
|
119
|
+
|
|
120
|
+
def get_callbacks(self) -> list[Any]:
|
|
121
|
+
"""Return callbacks (Phoenix instruments globally; this may be empty)."""
|
|
122
|
+
logger.debug("Getting callbacks (Phoenix uses global instrumentation)")
|
|
123
|
+
return self._callbacks
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Dependency injection helpers for FastAPI routes."""
|
|
2
|
+
|
|
3
|
+
from fastapi import HTTPException, Request, status
|
|
4
|
+
|
|
5
|
+
from ..core.config_builder import ConfigBuilder
|
|
6
|
+
from ..mcp import MCPClientRegistry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def get_agent(request: Request):
|
|
10
|
+
"""Return the pre-initialized agent instance from the app state.
|
|
11
|
+
|
|
12
|
+
Falls back to loading from the default config if not present (e.g., tests).
|
|
13
|
+
"""
|
|
14
|
+
if hasattr(request.app.state, "agent"):
|
|
15
|
+
return request.app.state.agent
|
|
16
|
+
else:
|
|
17
|
+
# This is a fallback for cases where the lifespan event did not run,
|
|
18
|
+
# like in some testing scenarios.
|
|
19
|
+
# Consider logging a warning here.
|
|
20
|
+
print("⚠️ Agent not found in app state, initializing fallback agent...")
|
|
21
|
+
|
|
22
|
+
app_config = ConfigBuilder.load_from_file()
|
|
23
|
+
agent = await ConfigBuilder.initialize_agent_from_config(app_config)
|
|
24
|
+
return agent
|
|
25
|
+
|
|
26
|
+
async def get_copilotkit_agent(request: Request):
|
|
27
|
+
"""Return the pre-initialized agent instance from the app state.
|
|
28
|
+
|
|
29
|
+
Falls back to loading from the default config if not present (e.g., tests).
|
|
30
|
+
"""
|
|
31
|
+
if hasattr(request.app.state, "copilotkit_agent"):
|
|
32
|
+
return request.app.state.copilotkit_agent
|
|
33
|
+
else:
|
|
34
|
+
# This is a fallback for cases where the lifespan event did not run,
|
|
35
|
+
# like in some testing scenarios.
|
|
36
|
+
# Consider logging a warning here.
|
|
37
|
+
print("⚠️ CopilotKit agent not found in app state, initializing fallback agent...")
|
|
38
|
+
|
|
39
|
+
app_config = ConfigBuilder.load_from_file()
|
|
40
|
+
copilotkit_agent = await ConfigBuilder.initialize_agent_from_config(app_config)
|
|
41
|
+
return copilotkit_agent
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_mcp_registry(request: Request) -> MCPClientRegistry:
|
|
45
|
+
"""Return the configured MCP registry if available."""
|
|
46
|
+
registry: MCPClientRegistry | None = getattr(request.app.state, "mcp_registry", None)
|
|
47
|
+
if registry is None or not registry.enabled:
|
|
48
|
+
raise HTTPException(
|
|
49
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
50
|
+
detail="MCP servers are not configured for this engine.",
|
|
51
|
+
)
|
|
52
|
+
return registry
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Server lifespan management utilities.
|
|
2
|
+
|
|
3
|
+
Initializes the agent at startup and cleans up resources on shutdown.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI
|
|
11
|
+
|
|
12
|
+
from ..core.config_builder import ConfigBuilder
|
|
13
|
+
from ..mcp import MCPClientRegistry
|
|
14
|
+
|
|
15
|
+
from idun_agent_schema.engine.guardrails import Guardrails, Guardrail
|
|
16
|
+
|
|
17
|
+
from ..guardrails.base import BaseGuardrail
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_guardrails(guardrails_obj: Guardrails) -> Sequence[BaseGuardrail]:
|
|
21
|
+
"""Adds the position of the guardrails (input/output) and returns the lift of updated guardrails."""
|
|
22
|
+
|
|
23
|
+
from ..guardrails.guardrails_hub.guardrails_hub import GuardrailsHubGuard as GHGuard
|
|
24
|
+
|
|
25
|
+
if not guardrails_obj.enabled:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
return [GHGuard(guard, position="input") for guard in guardrails_obj.input] + [
|
|
29
|
+
GHGuard(guard, position="output") for guard in guardrails_obj.output
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def cleanup_agent(app: FastAPI):
|
|
34
|
+
"""Clean up agent resources."""
|
|
35
|
+
agent = getattr(app.state, "agent", None)
|
|
36
|
+
if agent is not None:
|
|
37
|
+
close_fn = getattr(agent, "close", None)
|
|
38
|
+
if callable(close_fn):
|
|
39
|
+
result = close_fn()
|
|
40
|
+
if inspect.isawaitable(result):
|
|
41
|
+
await result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def configure_app(app: FastAPI, engine_config):
|
|
45
|
+
"""Initialize the agent, MCP registry, guardrails, and app state with the given engine config."""
|
|
46
|
+
guardrails_obj = engine_config.guardrails
|
|
47
|
+
guardrails = _parse_guardrails(guardrails_obj) if guardrails_obj else []
|
|
48
|
+
|
|
49
|
+
print("guardrails: ", guardrails)
|
|
50
|
+
|
|
51
|
+
# # Initialize MCP Registry first
|
|
52
|
+
# mcp_registry = MCPClientRegistry(engine_config.mcp_servers)
|
|
53
|
+
# app.state.mcp_registry = mcp_registry
|
|
54
|
+
|
|
55
|
+
# Use ConfigBuilder's centralized agent initialization, passing the registry
|
|
56
|
+
try:
|
|
57
|
+
agent_instance = await ConfigBuilder.initialize_agent_from_config(engine_config)
|
|
58
|
+
except Exception as e:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Error retrieving agent instance from ConfigBuilder: {e}"
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
app.state.agent = agent_instance
|
|
64
|
+
app.state.config = engine_config
|
|
65
|
+
app.state.engine_config = engine_config
|
|
66
|
+
|
|
67
|
+
app.state.guardrails = guardrails
|
|
68
|
+
agent_name = getattr(agent_instance, "name", "Unknown")
|
|
69
|
+
print(f"✅ Agent '{agent_name}' initialized and ready to serve!")
|
|
70
|
+
|
|
71
|
+
# Setup AGUI routes if the agent is a LangGraph agent
|
|
72
|
+
from ..agent.langgraph.langgraph import LanggraphAgent
|
|
73
|
+
from ..agent.adk.adk import AdkAgent
|
|
74
|
+
# from ..server.routers.agui import setup_agui_router
|
|
75
|
+
|
|
76
|
+
if isinstance(agent_instance, (LanggraphAgent, AdkAgent)):
|
|
77
|
+
try:
|
|
78
|
+
# compiled_graph = getattr(agent_instance, "agent_instance")
|
|
79
|
+
# app.state.copilotkit_agent = setup_agui_router(app, agent_instance) # TODO: agent_instance is a compiled graph (duplicate agent_instance name not clear)
|
|
80
|
+
app.state.copilotkit_agent = agent_instance.copilotkit_agent_instance
|
|
81
|
+
except Exception as e:
|
|
82
|
+
print(f"⚠️ Warning: Failed to setup AGUI routes: {e}")
|
|
83
|
+
# Continue even if AGUI setup fails
|
|
84
|
+
|
|
85
|
+
# if app.state.mcp_registry.enabled:
|
|
86
|
+
# servers = ", ".join(app.state.mcp_registry.available_servers())
|
|
87
|
+
# print(f"🔌 MCP servers ready: {servers}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@asynccontextmanager
|
|
91
|
+
async def lifespan(app: FastAPI):
|
|
92
|
+
"""FastAPI lifespan context to initialize and teardown the agent."""
|
|
93
|
+
|
|
94
|
+
# Load config and initialize agent on startup
|
|
95
|
+
print("Server starting up...")
|
|
96
|
+
if not app.state.engine_config:
|
|
97
|
+
raise ValueError("Error: No Engine configuration found.")
|
|
98
|
+
|
|
99
|
+
await configure_app(app, app.state.engine_config)
|
|
100
|
+
|
|
101
|
+
yield
|
|
102
|
+
|
|
103
|
+
# Clean up on shutdown
|
|
104
|
+
print("🔄 Idun Agent Engine shutting down...")
|
|
105
|
+
await cleanup_agent(app)
|
|
106
|
+
print("✅ Agent resources cleaned up successfully.")
|